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)
}
}
注意:为了防止出现进程饥饿现象,即申请 Creep 永远申请不到,除了必要的定期消亡及消亡忙碌 Creep 时补充 Creep 数量外,健康的进程应当满足 (1) 不用 Creep 立即释放; (2) 使用 Creep 时, 出现错误, 立即释放资源, 即主动进行消亡;否则, 无法判定是否需要进行数量补充。
Last updated