"""确定性规则引擎:按官方 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