Design & Implementation

Creep 模块 —— 设计

🤖️ Creep 模块对外暴露的功能应当是:

  • 申请特定型号的 Creep

  • 归还特定型号的 Creep

  • 设计特定型号的 Creep

🤖️ Creep 模块对内管理的内容应当是:

  • 管理已有的 Creep (申请时的分配、归属房间控制等)

  • 申请生产新的 Creep (数量控制、生的位置选择等)

这样,一个利用 🤖️ Creep 模块的 🔄 流程就是:提前设计好特定型号的 Creep => 进程运行时,动态申请 Creep => Creep 若有,则返回 Id。否则阻塞,直到有相应的 Creep 再唤醒。 => 如果进程运行时错误,则进程释放资源,从头开始;否则运行到结束,释放 Creep。

⚙️ 对外接口依赖

这样的一个 🤖️ Creep 模块,外部的依赖的接口就是 生新的 Creep 了。该函数输入指定房间、生Creep的参数、以及完成后的 callback。callback 的主要作用是返回新生产出来的 Creep Id 到 🤖️ Creep 模块进行管理。

/**
 * 外部接口依赖
 */
interface CreepModuleContext {
    /**
     * 申请生产新的 Creep.
     * 注意: 这个 API 保证成功, 即错误处理应当在 spawn 模块内部.
     *
     * @param roomName 生产 Creep 的房间.
     * @param callback 在 Creep 成功生产后, 执行的回调函数.
     * @param body An array describing the new creep’s body. Should contain 1 to 50 elements with one of these constants:
     *  * WORK
     *  * MOVE
     *  * CARRY
     *  * ATTACK
     *  * RANGED_ATTACK
     *  * HEAL
     *  * TOUGH
     *  * CLAIM
     * @param name The name of a new creep. It must be a unique creep name, i.e. the Game.creeps object should not contain another creep with the same name (hash key).
     * @param memory Memory of the new creep. If provided, it will be immediately stored into Memory.creeps[name].
     * @param workPos Creep 预计的工作地点. 可以根据该信息优化生产 Creep 所选用的 Spawn.
     */
    spawnCreep(roomName: string, callback: (id: Id<Creep>) => void, body: BodyPartConstant[], name: string, memory?: CreepMemory, workPos?: RoomPosition): void
}

☠️ Creep 消亡处理

对称的,除了新生产的 Creep 加入到 🤖️ Creep 模块 中进行管理之外,我们也还要处理旧的 Creep 的消亡。一种做法是每 tick 刚开始,都检查是否有 Creep 消亡,消亡的话,取消管理并释放资源。但是其实我们可以 Lazy 一些,就是只有在需要特定型号的 Creep 的时候,去检查目前管理的 Creep 存不存在;不存在,再执行消亡。除此之外,我们还可以定期 (例如 1500 ticks) 的清除一下死亡的 Creep 的 Memory,防止有些型号一直没有申请,然后以前的占用的资源得不到释放。

📃 具体实现

/**
 * 🤖️ Creep 管理模块
 */

import { Apollo as A } from "@/framework/apollo"
import { assertWithMsg, generate_random_hex, largest_less_than } from "@/utils"

/**
 * 外部接口依赖
 */
interface CreepModuleContext {
    /**
     * 申请生产新的 Creep.
     * 注意: 这个 API 保证成功, 即错误处理应当在 spawn 模块内部.
     *
     * @param roomName 生产 Creep 的房间 (必须在控制内).
     * @param callback 在 Creep 成功生产后, 执行的回调函数.
     * @param body An array describing the new creep’s body. Should contain 1 to 50 elements with one of these constants:
     *  * WORK
     *  * MOVE
     *  * CARRY
     *  * ATTACK
     *  * RANGED_ATTACK
     *  * HEAL
     *  * TOUGH
     *  * CLAIM
     * @param name The name of a new creep. It must be a unique creep name, i.e. the Game.creeps object should not contain another creep with the same name (hash key).
     * @param priority 特权级别, 数字越低, 特权越高.
     * @param memory Memory of the new creep. If provided, it will be immediately stored into Memory.creeps[name].
     * @param workPos Creep 预计的工作地点. 可以根据该信息优化生产 Creep 所选用的 Spawn.
     */
    spawnCreep(roomName: string, callback: (name: string) => void, body: BodyPartConstant[], name: string, priority: number, memory?: CreepMemory, workPos?: RoomPosition): void
}

const PRIORITY_CRITICAL = 0
const PRIORITY_IMPORTANT = 1
const PRIORITY_NORMAL = 2
const PRIORITY_CASUAL = 3

type CreepTypeDescriptor = {
    /** 体型设计 */
    body: {
        /** 按照 Controller 等级划分. 达到特定 Controller 等级, 发生变化. */
        [controllerLevel: number]: BodyPartConstant[]
    } | BodyPartConstant[], 
    /** 数量设计 */
    amount?: {
        /** 按照 Controller 等级划分. 达到特定 Controller 等级, 发生变化. */
        [controllerLevel: number]: number | 'auto'
    } | number | 'auto', 
    /** 特权级别 */
    priority?: number
}

class CreepModule {
    /** 生产 Creep 特权级别 —— 危急! */
    PRIORITY_CRITICAL = PRIORITY_CRITICAL
    /** 生产 Creep 特权级别 —— 重要 */
    PRIORITY_IMPORTANT = PRIORITY_IMPORTANT
    /** 生产 Creep 特权级别 —— 正常 */
    PRIORITY_NORMAL = PRIORITY_NORMAL
    /** 生产 Creep 特权级别 —— 随意 */
    PRIORITY_CASUAL = PRIORITY_CASUAL
    #emaBeta: number = 0.9
    #context: CreepModuleContext
    #types: { [type: string]: { [controllerLevel: string]: {body: BodyPartConstant[], amount: number | 'auto', priority: number} } } = {}
    /**
     * 设计特定型号的 Creep
     * @param type 型号名称
     * @param descriptor 型号描述
     */
    design(type: string, descriptor: CreepTypeDescriptor): void {
        assertWithMsg(!(type in this.#types), `无法再次注册已有的 Creep 型号 '${type}'`)
        if ( Array.isArray(descriptor.body) ) descriptor.body = { 1: descriptor.body }
        if ( descriptor.amount === undefined) descriptor.amount = 'auto'
        if ( typeof descriptor.amount !== 'object' ) descriptor.amount = { 1: descriptor.amount }
        if ( descriptor.priority === undefined ) descriptor.priority = PRIORITY_NORMAL
        
        this.#types[type] = {}
        // 特定型号的 Creep 按照 Controller 等级划分体型
        for (const level of Object.keys(CONTROLLER_LEVELS))
            // 为了方便指定, 我们在输入的时候, 不一定需要指明所有的
            // Controller 等级对应的体型和数量. 而是输入的 Controller
            // 等级对应于体型, 数量发生变化.
            this.#types[type][level] = {
                body: descriptor.body[largest_less_than(Object.keys(descriptor.body), level)], 
                amount: descriptor.amount[largest_less_than(Object.keys(descriptor.amount), level)], 
                priority: descriptor.priority, 
            }
    }
    #repo: { [type: string]: {
        [roomName: string]: {
            /** 就绪 (闲置) 的 Creep name 序列 */
            ready: string[], 
            /** 匆忙 (已被占用) 的 Creep name 序列 */
            busy: string[], 
            /** 正在生成的 Creep 数量 (已经加入到生成队列中) */
            spawning: number, 
            /** 排队需求 Creep 的数量 */
            waiting: number, 
            /** 数量控制信号量 Id (本质上是对应的 就绪序列 长度) */
            signalId: string, 
            /** 最后一次 Check 是否有消亡的 tick (防止同一 tick 多次申请引发多次无效 Check) */
            lastCheckTick: number, 
            /** 请求该型号和管辖房间的数量 (EMA). 用于自动数量控制. */
            requestEMA: number
        }
    } }
    #getRepo(type: string, roomName: string) {
        if ( !(type in this.#repo) ) this.#repo[type] = {}
        if ( !(roomName in this.#repo[type]) ) this.#repo[type][roomName] = { ready: [], busy: [], spawning: 0, waiting: 0, signalId: A.proc.signal.createSignal(0), lastCheckTick: Game.time, requestEMA: null }
        return this.#repo[type][roomName]
    }
    /**
     * 申请特定型号的 Creep
     * 
     * @atom
     * @param type 型号名称
     * @param roomName 申请的房间名称 (必须在控制内)
     * @param callback 申请到后执行的回调函数
     * @param workPos Creep 预计的工作地点
     */
    acquire(type: string, roomName: string, callback: (name: string) => void, workPos?: RoomPosition) {
        const repo = this.#getRepo(type, roomName)
        if ( repo.lastCheckTick < Game.time ) {
            // 惰性检测: 是否有 Creep 消亡
            _.forEach([...repo.ready], name => !(name in Game.creeps) && this.cancel(name))
            _.forEach([...repo.busy], name => !(name in Game.creeps) && this.cancel(name))
            repo.lastCheckTick = Game.time

            // 消亡后判定: 是否需要生产新的 Creep
            this.#replenish(type, roomName)
        }
        
        if ( repo.ready.length > 0 ) {
            // 此时有可用的 Creep
            const name = _.sortBy( repo.ready, creepName => Game.creeps[creepName].pos.roomName !== roomName? Infinity : ( workPos? Game.creeps[creepName].pos.getRangeTo(workPos) : 0 ) )[0]
            assertWithMsg( A.proc.signal.Swait({ signalId: repo.signalId, lowerbound: 1, request: 1 }) === OK, `申请 模块型号 '${type}', 管辖房间 '${roomName}' 的 Creep 时, 管理闲置数量的信号量数值与闲置数量不匹配` )
            callback(name)
            return OK
        } else 
            return A.proc.signal.Swait({ signalId: repo.signalId, lowerbound: 1, request: 1 })
    }
    /**
     * 归还特定的 Creep
     */
    release(name: string): typeof OK {
        assertWithMsg(name in Game.creeps, `无法找到 Creep '${name}' 以归还`)
        const creep = Game.creeps[name]

        assertWithMsg(creep.memory.spawnType in this.#repo, `无法找到 Creep '${name}' 的型号 '${creep.memory.spawnType}' 以归还`)
        const repo = this.#getRepo(creep.memory.spawnType, creep.memory.spawnRoomName)

        assertWithMsg(_.includes(repo.busy, name), `Creep 模块型号 '${creep.memory.spawnType}' 的管辖房间 '${creep.memory.spawnRoomName}'内无法找到正在被占用的 Creep '${name}'`)

        // 从忙碌队列中删去, 并添加到闲置队列中
        _.pull(repo.busy, name)
        repo.ready.push(name)
        // 更新信号量
        A.proc.signal.Ssignal({ signalId: repo.signalId, request: 1 })

        return OK
    }

    /**
     * 申请生产特定型号新的 Creep
     * 注意: 本函数不进行数量控制
     */
    #issue(type: string, roomName: string, workPos?: RoomPosition) {
        this.#getRepo(type, roomName).spawning += 1
        const controllerLevel = Game.rooms[roomName].controller.level
        const prototype = this.#types[type][controllerLevel]
        let name = null
        while ( !name || name in Game.creeps )
            name = `${type}-${generate_random_hex(4)}`
        const { spawnCreep } = this.#context
        
        return spawnCreep(roomName, 
            (name: string) => {
                const repo = this.#getRepo(type, roomName)
                repo.spawning -= 1
                this.#register(name)
            }, 
            prototype.body, name, prototype.priority, { spawnType: type, spawnRoomName: roomName }, workPos, 
        )
    }

    /** 获得当前期望的最佳数量 (综合考虑设计时的数量设置, 当前拥有的数量, 以及期望的数量) */
    #getExpectedAmount(type: string, roomName: string): number {
        const controllerLevel = Game.rooms[roomName].controller.level
        const descriptor = this.#types[type][controllerLevel]
        const repo = this.#getRepo(type, roomName)
        const currentAmount = repo.ready.length + repo.busy.length + repo.spawning
        const currentRequest = repo.busy.length + repo.waiting

        if ( typeof descriptor.amount === "number" ) {
            // 限定最大数量 ( 限定数量 与 请求之间的最大值 )
            return Math.min(currentRequest, descriptor.amount)
        } else if ( descriptor.amount === "auto" ) {
            let amount = null
            if ( repo.requestEMA === null ) amount = currentRequest
            else amount = repo.requestEMA
            // 自动数量控制
            // Log 函数 - 经过 (0, 0), (1, 1)
            const expectedAmount = Math.ceil(Math.log((Math.E - 1) * amount + 1))
            return Math.min(currentRequest, expectedAmount)
        }
    }

    /** 补充特定型号的 Creep 数量 (不检查是否有 Creep 应当消亡) */
    #replenish(type: string, roomName: string) {
        const repo = this.#getRepo(type, roomName)
        const currentAmount = repo.ready.length + repo.busy.length + repo.spawning
        const expectedAmount = this.#getExpectedAmount(type, roomName)

        for ( let i = 0; i < expectedAmount - currentAmount; ++i ) this.#issue(type, roomName)
    }
    
    /**
     * 注册特定的 Creep 进入本模块进行管理
     * 
     * @param roomName 申请的房间名称 (必须在控制内)
     */
    #register(name: string) {
        assertWithMsg(name in Game.creeps, `无法找到 Creep '${name}' 以注册`)
        const creep = Game.creeps[name]

        assertWithMsg(creep.memory.spawnType in this.#types, `无法找到 Creep '${name}' 的型号 '${creep.memory.spawnType}' 以注册`)

        const repo = this.#getRepo(creep.memory.spawnType, creep.memory.spawnRoomName)
        assertWithMsg(!_.includes(repo.ready, name) && !_.includes(repo.busy, name), `Creep '${name}' (型号 '${creep.memory.spawnType}') 已经被注册, 无法再次注册`)
        repo.ready.push(name)
        A.proc.signal.Ssignal({ signalId: repo.signalId, request: 1 })
    }

    /**
     * 注销特定的 Creep
     * 注意: 注销时, 并不考虑再次 Spawn 以弥补空位的问题
     */
    cancel(name: string) {
        assertWithMsg(name in Memory.creeps, `无法找到想要消亡的 Creep '${name}' 的 Memory`)
        if ( Memory.creeps[name].spawnType in this.#types ) {
            const repo = this.#getRepo(Memory.creeps[name].spawnType, Memory.creeps[name].spawnRoomName)

            if ( _.includes(repo.ready, name) ) {
                // 闲置时注销, 此时可能是周期性消亡, 或者是申请 Creep
                // 时发现.
                // 此时, 信号量需要发生改变.
                // 注意: 闲置注销, 注销应当不会引起进程饥饿.
                _.pull(repo.ready, name)
                assertWithMsg(A.proc.signal.Swait({ signalId: repo.signalId, lowerbound: 1, request: 1 }) === OK, `注销闲置 Creep 时, 信号量应该一定大于 0, 但是不是`)
            } else if ( _.includes(repo.busy, name) ) {
                // 被占用时注销, 可能是周期性消亡, 也可能是占用的进程
                // 在执行时出现错误, 发现 Creep 死亡.
                // 此时, 信号量不需要发生改变.
                // 注意: 为了防止原本占用 Creep 的进程不再申请 Creep, 
                // 导致出现最终无 Creep 可用, 却有申请等待的进程, 即
                // 进程饥饿现象, 在消亡时, 进行数量检查并进行补充.
                _.pull(repo.busy, name)
                this.#replenish(Memory.creeps[name].spawnType, Memory.creeps[name].spawnRoomName)
            }
        }
        delete Memory.creeps[name]
    }

    constructor(context: CreepModuleContext) {
        this.#context = context

        // 注册周期性消亡 Creep 功能
        const period = CREEP_LIFE_TIME
        // 避免在重启时, 所有周期任务都堆叠在一起
        A.timer.add(Game.time + 1 + Math.floor(Math.random() * period), () => {
            // 释放 Creep 资源
            for (const name in Memory.creeps)
                if ( !(name in Game.creeps) )
                    this.cancel(name)
        }, [], period)

        // 注册已有的 Creep
        for (const name in Game.creeps)
            this.#register(name)
        
        // 注册不同型号, 不同归属房间请求数量计算 EMA
        A.timer.add(Game.time + 1, () => {
            for (const type in this.#repo)
                for (const roomName in this.#repo[type]) {
                    const currentRequest = this.#repo[type][roomName].busy.length + this.#repo[type][roomName].waiting
                    if ( this.#repo[type][roomName].requestEMA === null ) this.#repo[type][roomName].requestEMA = currentRequest
                    else this.#repo[type][roomName].requestEMA = this.#repo[type][roomName].requestEMA * this.#emaBeta + currentRequest * (1 - this.#emaBeta)
                }
        }, [], 1)
    }
}

Last updated