chinatown_1 / ahsim /engine.py
Torther's picture
Core research: deterministic simulator, evolutionary strategy, genetic algorithm
cdf3b47 unverified
Raw
History Blame Contribute Delete
39.8 kB
"""确定性规则引擎:按官方 resolution order 解析一个 Tick。
约定:
- plan: {"core": (action_type, kwargs) | None, "units": {uid: (action_type, kwargs)}}
- 每玩家每 Tick 一个 plan。
- 事件为 list[dict],全局顺序记录(模拟器不区分玩家私有事件)。
- cargo 堆(死 Worker 掉落的资源)持久保存在 Engine 实例中。
- Beacon 状态贯穿整个 resolve:("ground", x, y) | ("carried", uid)。
"""
from .config import (
EMPTY, OBSTACLE, RESOURCE, DIRECTIONS, CORE_MIGRATION_TICKS,
CELL_CAPACITY, RANGER_RANGE, UNIT_STATS, CORE_HP,
CORE_SHIELD, CORE_SHIELD_CAP_BEACON,
unit_cost, storage_capacity,
)
from .entities import Core, Unit
from .vision import is_shot_line, shot_intermediate_cells
class Engine:
def __init__(self, world):
self.world = world
self.cargo = {} # (x, y) -> amount(死 Worker 掉落)
self._beacon = None
self._uid_owner = {} # uid -> player_id(本 Tick 有效)
# =====================================================================
# 主入口
# =====================================================================
def resolve(self, players, plans, beacon, tick):
"""players: {player_id: Player}; plans: {player_id: plan}。
返回 (beacon, events)。
"""
self._beacon = beacon
self._beacon_dropped_this_tick = False # 掉落当 Tick 冷却(不可拾取)
self._uid_owner = {}
self._players = players
for p in players.values():
if p.core is not None:
self._uid_owner[p.core.uid] = p.player_id
for u in p.units.values():
self._uid_owner[u.uid] = p.player_id
events = []
actives = [p for p in players.values() if p.core is not None]
# 上 Tick 已存在的单位恢复行动能力(SPAWN 当 Tick 由 step12 重新标记)
for p in players.values():
for u in p.units.values():
u.just_spawned = False
self._step2_unit_self_destruct(actives, plans, events)
self._step4_movement(actives, plans, events)
self._step5_start_move(actives, plans, events)
self._step6_beacon(actives, plans, events)
self._step7_worker_actions(actives, plans, events)
dead_cores = self._step8_9_combat(actives, plans, events)
# 战斗死亡导致人口下降 → 立即销毁超容量库存(官方 v0.6:
# population falls → excess inventory destroyed immediately),
# 这样后续治疗/生产只能花销毁后的资源
self._enforce_capacity(players, events)
self._step10_core_self_destruct(actives, plans, dead_cores, events)
self._step11_unit_heal(actives, plans, events)
self._step12_core_actions(actives, plans, events)
self._step13_respawn(players, dead_cores, events, tick)
self.world.replenish_if_due(tick, lambda x, y: self._occupied_by_core(players, x, y))
# 丢失 Beacon 的玩家盾值钳制回 5(官方:Lose the Beacon and anything
# above 5 is clamped straight back down to 5)
for p in players.values():
if p.core is not None:
has = self._beacon[0] == "carried" and \
self._uid_owner.get(self._beacon[1]) == p.player_id
if not has and p.core.shield > CORE_SHIELD:
p.core.shield = CORE_SHIELD
# 兜底清算(覆盖任意其余人口下降来源)
self._enforce_capacity(players, events)
# Beacon 持有 Tick 统计(resolution 结束时仍持有才计入)
if self._beacon[0] == "carried":
owner_id = self._uid_owner.get(self._beacon[1])
if owner_id is not None and owner_id in players:
players[owner_id].stats["beacon_ticks"] += 1
return self._beacon, events
# =====================================================================
# Beacon 掉落辅助
# =====================================================================
def _drop_beacon_at(self, pos):
self._beacon = ("ground", pos[0], pos[1])
self._beacon_dropped_this_tick = True
# =====================================================================
# 人口变化后的资源容量清算
# =====================================================================
def _enforce_capacity(self, players, events):
"""人口下降导致容量超限:销毁超出资源(CORE_RESOURCE_OVERFLOW_DESTROYED)。"""
for p in players.values():
if p.core is not None:
cap = storage_capacity(p.population)
if p.core.resources > cap:
destroyed = p.core.resources - cap
p.core.resources = cap
p.stats["overflow_destroyed"] += destroyed
events.append({"type": "CORE_RESOURCE_OVERFLOW_DESTROYED",
"player": p.player_id, "amount": destroyed,
"capacity": cap})
# =====================================================================
# Step 2: Unit SELF_DESTRUCT
# =====================================================================
def _step2_unit_self_destruct(self, actives, plans, events):
for p in actives:
plan = plans.get(p.player_id)
if not plan:
continue
for uid, (atype, _args) in plan["units"].items():
if atype != "SELF_DESTRUCT":
continue
u = p.units.get(uid)
if u is None:
continue
self._remove_unit(p, u, events, "UNIT_SELF_DESTRUCTED")
# =====================================================================
# Step 3: 移动(单位 MOVE + Core 迁移第 4 Tick)
# =====================================================================
def _step4_movement(self, actives, plans, events):
movers = [] # [(obj, (dx, dy))]
for p in actives:
plan = plans.get(p.player_id)
if plan:
for uid, (atype, args) in plan["units"].items():
if atype == "MOVE" and uid in p.units:
d = DIRECTIONS[args["direction"]]
movers.append((p.units[uid], d))
core = p.core
if core is not None and core.migration is not None:
direction, progress = core.migration
progress += 1
if progress >= CORE_MIGRATION_TICKS:
core.migration = None # 尝试真实移动;失败即归零
movers.append((core, DIRECTIONS[direction]))
else:
core.migration = (direction, progress)
self._resolve_movement_graph(actives, movers, events)
def _resolve_movement_graph(self, actives, movers, events):
if not movers:
return
finals = {}
for p in actives:
if p.core is not None:
finals[p.core] = p.core.pos
for u in p.units.values():
finals[u] = u.pos
intents = {}
for obj, delta in movers:
x, y = obj.pos
intents[obj] = (x + delta[0], y + delta[1])
failed = {} # obj -> reason(移动失败原因,供策略按 reason 分类处理)
settled = set() # 已成功移动(finals 已更新到 intents)
# 反复扫描:等待者可被后续轮次解决(链式);全部等待时做环检测
while True:
progressed = False
for obj in sorted(intents, key=lambda o: o.uid):
if obj in failed or obj in settled:
continue
dest = intents[obj]
if not self._passable(obj, dest):
failed[obj] = "MOVE_BLOCKED_TERRAIN"
progressed = True
continue
occupants = [o for o in finals if finals[o] == dest and o is not obj]
leaving = [o for o in occupants if o in intents and o not in failed]
if leaving:
continue # 等它们先移动(链式)
stayers = [o for o in occupants if o not in intents or o in failed]
# 不同玩家的对象不能同格终局
if any(o.owner != obj.owner for o in stayers):
failed[obj] = "MOVE_DESTINATION_OCCUPIED"
progressed = True
continue
comps = [o for o in intents
if o not in failed and o not in settled
and o is not obj and intents[o] == dest]
# 跨玩家竞争同一格 → 全部失败
if any(o.owner != obj.owner for o in comps):
failed[obj] = "MOVE_CONTESTED"
for o in comps:
failed[o] = "MOVE_CONTESTED"
progressed = True
continue
slots = CELL_CAPACITY - len(stayers)
if slots <= 0:
failed[obj] = "CELL_UNIT_LIMIT"
progressed = True
continue
candidates = sorted(comps + [obj], key=lambda o: o.uid)
winners = candidates[:slots]
if obj not in winners:
failed[obj] = "CELL_UNIT_LIMIT"
else:
finals[obj] = dest
settled.add(obj)
for o in candidates[slots:]:
failed[o] = "CELL_UNIT_LIMIT"
progressed = True
if progressed:
continue
# 无进展:剩余未决者全部在“等待” → 检测依赖环
pending = [o for o in intents
if o not in failed and o not in settled]
if not pending:
break
handled = False
for obj in list(pending):
cycle = self._find_cycle(obj, intents, finals, pending)
if cycle is None:
continue
if self._cycle_ok(cycle):
for o in cycle:
finals[o] = intents[o]
settled.add(o)
else:
for o in cycle:
failed[o] = "MOVE_SWAP_BLOCKED"
handled = True
if not handled:
break # 无环(孤立等待者,理论上不会出现)
# 收尾:仍未移动者失败(链式依赖未成功 → DEPENDENCY),然后一轮传播阻塞
for obj in list(intents):
if obj not in failed and obj not in settled:
failed[obj] = "MOVE_DEPENDENCY_FAILED"
for obj in sorted(intents, key=lambda o: o.uid):
if obj in failed or obj in settled:
continue
dest = intents[obj]
if not self._passable(obj, dest):
failed[obj] = "MOVE_BLOCKED_TERRAIN"
continue
occupants = [o for o in finals if finals[o] == dest and o is not obj]
if any(o.owner != obj.owner for o in occupants):
failed[obj] = "MOVE_DESTINATION_OCCUPIED"
continue
comps = [o for o in intents if o not in failed and o not in settled
and o is not obj and intents[o] == dest]
if any(o.owner != obj.owner for o in comps):
failed[obj] = "MOVE_CONTESTED"
continue
slots = CELL_CAPACITY - len(occupants)
if slots <= 0:
failed[obj] = "CELL_UNIT_LIMIT"
continue
candidates = sorted(comps + [obj], key=lambda o: o.uid)
if obj in candidates[:slots]:
finals[obj] = dest
settled.add(obj)
for obj, pos in finals.items():
if pos != obj.pos:
events.append({"type": "MOVED", "obj_id": obj.uid, "to": pos})
obj.pos = pos
# 有移动意图但最终未移动 → 阻塞反馈(供策略感知真实障碍/冲突,
# 带官方 reason_code:TERRAIN/CONTESTED/OCCUPIED/DEPENDENCY/CELL_UNIT_LIMIT/SWAP)
for obj in sorted(intents, key=lambda o: o.uid):
if finals[obj] == obj.pos:
dx = intents[obj][0] - obj.pos[0]
dy = intents[obj][1] - obj.pos[1]
for name, (ndx, ndy) in DIRECTIONS.items():
if (ndx, ndy) == (dx, dy):
events.append({"type": "MOVE_BLOCKED", "obj_id": obj.uid,
"direction": name,
"reason": failed.get(
obj, "MOVE_DEPENDENCY_FAILED")})
break
def _find_cycle(self, start, intents, finals, pending):
"""沿依赖链(obj 的 dest 内仍未决的占据者)找环;返回环成员列表或 None。"""
seen = {}
chain = []
cur = start
while cur is not None:
if cur in seen:
idx = seen[cur]
cycle = chain[idx:]
return cycle if len(cycle) >= 2 else None
seen[cur] = len(chain)
chain.append(cur)
dest = intents[cur]
deps = [o for o in finals
if finals[o] == dest and o in pending and o is not cur]
cur = deps[0] if deps else None
return None
@staticmethod
def _cycle_ok(cycle):
"""2 环仅同玩家可交换;3+ 环(卡迪纳网格上即 4 格环)可成功。"""
if len(cycle) >= 3:
return True
a, b = cycle
return a.owner == b.owner
# =====================================================================
# Step 5: 验证新 START_MOVE
# =====================================================================
def _step5_start_move(self, actives, plans, events):
for p in actives:
plan = plans.get(p.player_id)
if not plan or not plan["core"]:
continue
atype, args = plan["core"]
core = p.core
if core is None:
continue
if atype == "START_MOVE" and core.migration is None:
if args["direction"] not in DIRECTIONS:
continue
core.migration = (args["direction"], 1)
elif atype == "CANCEL_MOVE" and core.migration is not None:
# 取消迁移并清零进度
core.migration = None
events.append({"type": "CORE_MOVE_CANCELLED",
"player": p.player_id})
# =====================================================================
# Step 6: Beacon 拾取 / 放下
# =====================================================================
def _step6_beacon(self, actives, plans, events):
pickupers = [] # (obj, player)
droppers = []
for p in actives:
plan = plans.get(p.player_id)
if not plan:
continue
core = p.core
if plan["core"]:
atype = plan["core"][0]
if atype == "PICKUP_BEACON" and core is not None and core.migration is None:
pickupers.append((core, p))
elif atype == "DROP_BEACON" and core is not None and core.migration is None:
droppers.append((core, p))
for uid, (atype, _args) in plan["units"].items():
u = p.units.get(uid)
if u is None:
continue
if atype == "PICKUP_BEACON":
pickupers.append((u, p))
elif atype == "DROP_BEACON":
droppers.append((u, p))
for obj, p in droppers:
if obj.carries_beacon:
obj.carries_beacon = False
self._drop_beacon_at(obj.pos)
self._beacon_dropped_this_tick = True
events.append({"type": "BEACON_DROPPED", "obj_id": obj.uid,
"player": p.player_id, "at": obj.pos})
# 掉落当 Tick 不可再被拾取(官方冷却:阻止同 Tick 接力传递)
if self._beacon[0] == "ground" and not self._beacon_dropped_this_tick:
_, gx, gy = self._beacon
eligible = [o for (o, _p) in pickupers
if o.pos == (gx, gy) and not o.carries_beacon]
if eligible:
winner = min(eligible, key=lambda o: o.uid)
winner.carries_beacon = True
self._beacon = ("carried", winner.uid)
events.append({"type": "BEACON_PICKED_UP", "obj_id": winner.uid,
"player": winner.owner})
# =====================================================================
# Step 7: Worker 采集 / 存放
# =====================================================================
def _step7_worker_actions(self, actives, plans, events):
# HARVEST:同格同源竞争,最低 uid 优先
by_cell = {}
for p in actives:
plan = plans.get(p.player_id)
if not plan:
continue
for uid, (atype, _args) in plan["units"].items():
u = p.units.get(uid)
if u is None or u.utype != "WORKER" or atype != "HARVEST":
continue
if u.cargo > 0:
continue # 只允许空货采集
x, y = u.pos
has_cargo = (x, y) in self.cargo
has_natural = (x, y) in self.world.resources
if not has_cargo and not has_natural:
events.append({"type": "HARVEST_FAILED", "unit": u.uid,
"reason": "NO_RESOURCE"})
continue
by_cell.setdefault((x, y), []).append((u, has_cargo, has_natural))
for (x, y), cands in sorted(by_cell.items()):
cands.sort(key=lambda c: c[0].uid)
succeeded = False # 同源竞争:只有最低 uid 的 Worker 成功
for u, has_cargo, has_natural in cands:
if succeeded:
events.append({"type": "HARVEST_FAILED", "unit": u.uid,
"reason": "RESOURCE_DEPLETED"})
continue
owner_has_beacon = self._player_has_beacon(u.owner)
if has_cargo and (x, y) in self.cargo:
amt = self.cargo[(x, y)]
gain = min(2 if owner_has_beacon else 1, amt)
u.cargo = gain
if amt == gain:
del self.cargo[(x, y)]
else:
self.cargo[(x, y)] = amt - gain
succeeded = True
events.append({"type": "HARVESTED", "unit": u.uid,
"from": "cargo", "amount": gain, "at": (x, y)})
elif has_natural and (x, y) in self.world.resources:
self.world.consume_resource(x, y)
gain = 2 if owner_has_beacon else 1
u.cargo = gain
p = self._player_of(u.owner)
if p is not None:
p.stats["harvested"] += gain
succeeded = True
events.append({"type": "HARVESTED", "unit": u.uid,
"from": "natural", "amount": gain, "at": (x, y)})
else:
events.append({"type": "HARVEST_FAILED", "unit": u.uid,
"reason": "RESOURCE_DEPLETED"})
# DEPOSIT
for p in actives:
plan = plans.get(p.player_id)
if not plan:
continue
core = p.core
if core is None:
continue
for uid, (atype, _args) in plan["units"].items():
u = p.units.get(uid)
if u is None or u.utype != "WORKER" or atype != "DEPOSIT":
continue
if u.pos != core.pos:
continue
if core.migration is not None:
events.append({"type": "DEPOSIT_FAILED", "unit": u.uid,
"reason": "CORE_MIGRATING"})
continue
cap = storage_capacity(p.population)
room = cap - core.resources
if room <= 0:
events.append({"type": "DEPOSIT_FAILED", "unit": u.uid,
"reason": "CORE_RESOURCE_FULL"})
continue
moved = min(u.cargo, room)
u.cargo -= moved
core.resources += moved
p.stats["deposited"] = p.stats.get("deposited", 0) + moved
events.append({"type": "DEPOSITED", "unit": u.uid, "amount": moved})
def _player_has_beacon(self, player_id):
if self._beacon[0] != "carried":
return False
return self._uid_owner.get(self._beacon[1]) == player_id
def _player_of(self, player_id):
return self._players.get(player_id)
# =====================================================================
# Step 8+9: 战斗(快照、累积、同时结算)
# =====================================================================
def _step8_9_combat(self, actives, plans, events):
pos_units = {}
for u in [u for p in actives for u in p.units.values()]:
pos_units.setdefault(u.pos, []).append(u)
pos_cores = {c.pos: c for c in [p.core for p in actives if p.core is not None]}
damage = {} # target_uid -> {attacker_uid: dmg}
atk_records = [] # (target_uid, attacker_uid, dmg)
for p in actives:
plan = plans.get(p.player_id)
if not plan:
continue
for uid, (atype, args) in plan["units"].items():
u = p.units.get(uid)
if u is None or u.just_spawned:
continue
if atype == "SWEEP" and u.utype == "VANGUARD":
dx, dy = DIRECTIONS[args["direction"]]
tx, ty = u.pos[0] + dx, u.pos[1] + dy
for t in pos_units.get((tx, ty), []):
if t.owner != p.player_id:
self._add_damage(damage, atk_records, t.uid, u.uid, 1)
c = pos_cores.get((tx, ty))
if c is not None and c.owner != p.player_id:
self._add_damage(damage, atk_records, c.uid, u.uid, 1)
elif atype == "SHOOT" and u.utype == "RANGER":
target_cell = tuple(args["expected_cell"])
x0, y0 = u.pos
if not is_shot_line(x0, y0, target_cell[0], target_cell[1], RANGER_RANGE):
events.append({"type": "SHOT_MISSED", "unit": u.uid,
"reason": "NOT_ALIGNED"})
continue
blocked = False
for cx, cy in shot_intermediate_cells(x0, y0,
target_cell[0], target_cell[1]):
if self.world.is_obstacle(cx, cy):
blocked = True
break
if blocked:
events.append({"type": "SHOT_MISSED", "unit": u.uid,
"reason": "OBSTACLE"})
continue
hostiles = [t for t in pos_units.get(target_cell, [])
if t.owner != p.player_id]
c = pos_cores.get(target_cell)
if c is not None and c.owner != p.player_id:
hostiles.append(c)
if not hostiles:
events.append({"type": "SHOT_MISSED", "unit": u.uid,
"reason": "EMPTY"})
continue
target = min(hostiles, key=lambda t: (t.hp, t.uid))
self._add_damage(damage, atk_records, target.uid, u.uid, 1)
# 同时应用伤害
dead_units = set()
dead_cores = []
for target_uid, attackers in damage.items():
total = sum(attackers.values())
owner = self._owner_of(actives, target_uid)
if owner is None:
continue
t = owner.core if owner.core is not None and owner.core.uid == target_uid \
else owner.units.get(target_uid)
if t is None:
continue
for atk_uid, dmg in attackers.items():
atk_owner = self._owner_of(actives, atk_uid)
if atk_owner is not None:
atk_owner.stats["damage_dealt"] += dmg
if hasattr(t, "shield"):
absorbed = min(t.shield, total)
t.shield -= absorbed
total -= absorbed
t.hp -= total
events.append({"type": "CORE_DAMAGED" if hasattr(t, "shield")
else "UNIT_DAMAGED", "target_id": t.uid,
"player": owner.player_id, "damage": total})
if t.hp <= 0:
if hasattr(t, "shield"):
dead_cores.append(owner)
else:
dead_units.add(t)
# 移除死亡单位
for u in dead_units:
p = self._owner_of(actives, u.uid)
if p is not None:
self._remove_unit(p, u, events, "COMBAT_KILLED")
# 移除被毁 Core:舰队全部移除
destroyed_ids = {p.player_id for p in dead_cores}
for p in dead_cores:
events.append({"type": "CORE_DESTROYED", "player": p.player_id,
"reason": "ATTACK"})
core = p.core
if core.carries_beacon:
core.carries_beacon = False
self._drop_beacon_at(core.pos)
for u in list(p.units.values()):
if u.carries_beacon:
u.carries_beacon = False
self._drop_beacon_at(u.pos)
if u.cargo > 0:
self._drop_cargo(u)
p.stats["units_lost"] += 1 # 舰队随 Core 摧毁计入损失
del p.units[u.uid]
p.respawn_count += 1
# 库存转移(受害者按 player_id 顺序)
for p in sorted(dead_cores, key=lambda p: p.player_id):
victim = p
attackers = {}
for target_uid, atk_uid, _dmg in atk_records:
if target_uid == victim.core.uid:
ao = self._owner_of(actives, atk_uid)
if ao is not None:
attackers[ao] = attackers.get(ao, 0) + _dmg
if attackers:
winner = max(attackers, key=lambda ao: (attackers[ao], -ao.player_id))
if winner.core is None or winner.player_id in destroyed_ids:
victim.stats["resources_lost"] += victim.core.resources
events.append({"type": "CORE_RESOURCES_DESTROYED",
"victim": victim.player_id,
"amount": victim.core.resources})
else:
cap = storage_capacity(winner.population)
room = max(0, cap - winner.core.resources)
avail = victim.core.resources
moved = min(room, avail)
winner.core.resources += moved
victim.stats["resources_lost"] += avail
events.append({"type": "CORE_RESOURCES_CAPTURED",
"winner": winner.player_id, "amount": moved,
"available": avail, "destroyed": avail - moved,
"capacity": cap})
victim.core = None
return dead_cores
@staticmethod
def _add_damage(damage, atk_records, target_uid, attacker_uid, dmg):
inner = damage.setdefault(target_uid, {})
inner[attacker_uid] = inner.get(attacker_uid, 0) + dmg
atk_records.append((target_uid, attacker_uid, dmg))
def _owner_of(self, actives, uid):
for p in actives:
if uid in p.units or (p.core is not None and p.core.uid == uid):
return p
return None
# =====================================================================
# Step 10: Core SELF_DESTRUCT(战斗后幸存者)
# =====================================================================
def _step10_core_self_destruct(self, actives, plans, dead_cores, events):
dead_ids = {p.player_id for p in dead_cores}
for p in actives:
if p.player_id in dead_ids or p.core is None:
continue
plan = plans.get(p.player_id)
if not plan or not plan["core"] or plan["core"][0] != "SELF_DESTRUCT":
continue
core = p.core
# Core self-destruction removes the whole fleet, just like combat
# destruction. Keeping these entries after ``core = None`` would
# create ghost units that survive forever and block respawn.
for u in p.units.values():
if u.carries_beacon:
u.carries_beacon = False
self._drop_beacon_at(u.pos)
if u.cargo > 0:
self._drop_cargo(u)
if core.carries_beacon:
core.carries_beacon = False
self._drop_beacon_at(core.pos)
p.respawn_count += 1
p.stats["units_lost"] += p.population
events.append({"type": "CORE_DESTROYED", "player": p.player_id,
"reason": "SELF_DESTRUCT"})
p.units.clear()
p.core = None
# v0.14 respawn is immediate whenever a legal placement exists.
dead_cores.append(p)
# =====================================================================
# Step 11: Unit HEAL(战斗后,UUID 顺序)
# =====================================================================
def _step11_unit_heal(self, actives, plans, events):
for p in actives:
core = p.core
if core is None or core.migration is not None:
continue
plan = plans.get(p.player_id)
if not plan:
continue
healers = [u for uid, (atype, _a) in plan["units"].items()
if atype == "HEAL" and (u := p.units.get(uid)) is not None
and u.pos == core.pos]
healers.sort(key=lambda u: u.uid)
for u in healers:
max_hp = UNIT_STATS[u.utype]["hp"]
while u.hp < max_hp and core.resources > 0:
u.hp += 1
core.resources -= 1
p.stats["heal_cost"] += 1
events.append({"type": "UNIT_HEALED", "unit": u.uid})
# =====================================================================
# Step 12: Core 动作(HEAL / REPAIR_SHIELD / SPAWN)
# =====================================================================
def _step12_core_actions(self, actives, plans, events):
for p in sorted(actives, key=lambda p: p.player_id):
core = p.core
if core is None or core.migration is not None:
continue
plan = plans.get(p.player_id)
if not plan or not plan["core"]:
continue
atype, args = plan["core"]
if atype == "HEAL":
while core.hp < CORE_HP and core.resources > 0:
core.hp += 1
core.resources -= 1
p.stats["heal_cost"] += 1
elif atype == "REPAIR_SHIELD":
cap = CORE_SHIELD_CAP_BEACON if self._player_has_beacon(p.player_id) \
else CORE_SHIELD
if core.shield < cap and core.resources >= 1:
core.shield += 1
core.resources -= 1
p.stats["repair_cost"] += 1
elif atype == "SPAWN":
utype = args["unit_type"]
# 动态单位价格(rules v0.14):N = 当前存活单位数
# (同 Tick 自毁/战斗死亡已先结算)
cost = unit_cost(UNIT_STATS[utype]["cost"], p.population)
if core.resources < cost:
events.append({"type": "SPAWN_FAILED", "player": p.player_id,
"reason": "INSUFFICIENT_RESOURCES",
"required": cost})
continue
same = [u for u in p.units.values() if u.pos == core.pos]
if len(same) >= 1:
events.append({"type": "SPAWN_FAILED", "player": p.player_id,
"reason": "CELL_UNIT_LIMIT"})
continue
u = Unit(p.player_id, utype, core.pos)
p.units[u.uid] = u
core.resources -= cost
p.stats["spawn_cost"] += cost
events.append({"type": "UNIT_SPAWNED", "player": p.player_id,
"unit": u.uid, "utype": utype, "cost": cost})
# =====================================================================
# Step 13: 重生
# =====================================================================
def _step13_respawn(self, players, dead_cores, events, tick):
dead_ids = {p.player_id for p in dead_cores}
candidates = [p for p in players.values()
if p.core is None and (p.player_id in dead_ids or p.respawning)]
for p in sorted(candidates, key=lambda p: p.player_id):
if p.core is not None:
continue
pos = self._find_spawn(players, p.player_id)
if pos is None:
p.respawning = True
events.append({"type": "RESPAWN_FAILED", "player": p.player_id})
continue
p.respawning = False
core = Core(p.player_id, pos)
p.core = core
w = Unit(p.player_id, "WORKER", pos)
p.units[w.uid] = w
events.append({"type": "CORE_RESPAWNED", "player": p.player_id, "at": pos})
def _find_spawn(self, players, me, center=(0, 0), fixed=False):
"""在距离最近活 Core 20-30 曼哈顿的环带内找合法位置(优先实体少)。
center: 出生基准点(无其他玩家时使用;默认 [0,0],线上复现场景
传远离原点的坐标)。
fixed: True 时强制以 center 为基准(忽略其他玩家位置)——
用于预发育档案:老玩家固定在各自出生中心,不会被"距最近
活 Core 20-30 环"拉回同一区域(否则全员挤成一团)。
"""
others = [p.core for p in players.values()
if p.core is not None and p.player_id != me]
if fixed:
base = center
else:
base = min(others, key=lambda c: self._mdist(c.pos, center)).pos if others else center
candidates = set()
for d in range(20, 31):
for x in range(-d, d + 1):
candidates.add((base[0] + x, base[1] + (d - abs(x))))
candidates.add((base[0] + x, base[1] - (d - abs(x))))
best = None
best_score = None
for cand in candidates:
if not self._valid_spawn_cell(players, me, cand):
continue
# 优先实体少 + 地形开阔(5×5 窗口障碍少),避免出生在死角
score = (self._nearby_entities(players, cand, 5),
self._openness(cand),
cand[0], cand[1])
if best_score is None or score < best_score:
best_score = score
best = cand
return best
def _openness(self, cand):
"""cand 周围 5×5 窗口内的障碍格数(少 = 开阔)。"""
x, y = cand
n = 0
for dy in range(-2, 3):
for dx in range(-2, 3):
if self.world.is_obstacle(x + dx, y + dy):
n += 1
return n
def _valid_spawn_cell(self, players, me, cand):
x, y = cand
if not self.world.in_bounds(x, y):
return False
if self.world.terrain_kind(x, y) != EMPTY:
return False
n = 0
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
if self.world.in_bounds(x + dx, y + dy) and \
self.world.terrain_kind(x + dx, y + dy) != OBSTACLE:
n += 1
if n < 2:
return False
for p in players.values():
if p.core is not None and p.player_id != me:
if self._mdist(p.core.pos, cand) < 20:
return False
for p in players.values():
if p.core is not None and p.core.pos == cand:
return False
for u in p.units.values():
if u.pos == cand:
return False
return True
def _nearby_entities(self, players, pos, radius):
n = 0
for p in players.values():
if p.core is not None and self._mdist(p.core.pos, pos) <= radius:
n += 1
for u in p.units.values():
if self._mdist(u.pos, pos) <= radius:
n += 1
return n
# =====================================================================
# 工具
# =====================================================================
def _remove_unit(self, p, u, events, reason):
"""移除单位:掉落 cargo 与 Beacon。"""
if u.cargo > 0:
self._drop_cargo(u)
if u.carries_beacon:
u.carries_beacon = False
self._drop_beacon_at(u.pos)
del p.units[u.uid]
p.stats["units_lost"] += 1
events.append({"type": "UNIT_REMOVED", "unit": u.uid, "reason": reason,
"player": p.player_id})
def _drop_cargo(self, u):
x, y = u.pos
self.cargo[(x, y)] = self.cargo.get((x, y), 0) + u.cargo
u.cargo = 0
@staticmethod
def _mdist(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def _passable(self, obj, dest):
x, y = dest
kind = self.world.terrain_kind(x, y)
if kind == OBSTACLE:
return False
if isinstance(obj, Core):
# 迁移中的 Core 只能进 EMPTY:资源点/货堆格不可入
if (x, y) in self.cargo:
return False
return kind == EMPTY
return kind in (EMPTY, RESOURCE)
def _occupied_by_core(self, players, x, y):
for p in players.values():
if p.core is not None and p.core.pos == (x, y):
return True
return False