From 33ced41bc03bc20288d8fe643d7bb4f4d06328a7 Mon Sep 17 00:00:00 2001 From: zuchaoli Date: Wed, 18 Dec 2024 21:30:53 +0800 Subject: [PATCH] sumbit --- __init__.py | 0 __pycache__/__init__.cpython-310.pyc | Bin 0 -> 144 bytes __pycache__/yes_cmdr_env.cpython-310.pyc | Bin 0 -> 32579 bytes __pycache__/yes_cmdr_utils.cpython-310.pyc | Bin 0 -> 1545 bytes common/__pycache__/agent.cpython-310.pyc | Bin 0 -> 19658 bytes common/__pycache__/agent.cpython-39.pyc | Bin 0 -> 19785 bytes common/__pycache__/renderer.cpython-310.pyc | Bin 0 -> 149 bytes common/__pycache__/renderer.cpython-39.pyc | Bin 0 -> 5488 bytes common/__pycache__/utils.cpython-310.pyc | Bin 0 -> 8633 bytes common/__pycache__/utils.cpython-39.pyc | Bin 0 -> 8665 bytes common/agent.py | 540 ++++ common/renderer.py | 155 + common/utils.py | 301 ++ data/test/AgentsInfo.json | 603 ++++ data/test/MapInfo.json | 2802 +++++++++++++++++ .../test_yes_cmdr_env.cpython-310.pyc | Bin 0 -> 2551 bytes .../test_yes_cmdr_env.cpython-311.pyc | Bin 0 -> 6489 bytes .../test_yes_cmdr_env.cpython-39.pyc | Bin 0 -> 2677 bytes tests/test_yes_cmdr_env.py | 89 + yes_cmdr_env.py | 1267 ++++++++ 20 files changed, 5757 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-310.pyc create mode 100644 __pycache__/yes_cmdr_env.cpython-310.pyc create mode 100644 __pycache__/yes_cmdr_utils.cpython-310.pyc create mode 100644 common/__pycache__/agent.cpython-310.pyc create mode 100644 common/__pycache__/agent.cpython-39.pyc create mode 100644 common/__pycache__/renderer.cpython-310.pyc create mode 100644 common/__pycache__/renderer.cpython-39.pyc create mode 100644 common/__pycache__/utils.cpython-310.pyc create mode 100644 common/__pycache__/utils.cpython-39.pyc create mode 100644 common/agent.py create mode 100644 common/renderer.py create mode 100644 common/utils.py create mode 100644 data/test/AgentsInfo.json create mode 100644 data/test/MapInfo.json create mode 100644 tests/__pycache__/test_yes_cmdr_env.cpython-310.pyc create mode 100644 tests/__pycache__/test_yes_cmdr_env.cpython-311.pyc create mode 100644 tests/__pycache__/test_yes_cmdr_env.cpython-39.pyc create mode 100644 tests/test_yes_cmdr_env.py create mode 100644 yes_cmdr_env.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90ac4dbfa7f5155e2755bff993b866133ff2a2cc GIT binary patch literal 144 zcmd1j<>g`kf(?`2rh(|kAOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yUTJ{qp>x?BasN z|2y`1s7c%#!$cy@JYH95%W6DWy57 Nb|AxwnScZf0|2z?AnO1C literal 0 HcmV?d00001 diff --git a/__pycache__/yes_cmdr_env.cpython-310.pyc b/__pycache__/yes_cmdr_env.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fe8ef946f5c95f857a9e8cc97e69dec0be5738e GIT binary patch literal 32579 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¥j2rq zVyLoC&tE}89>K1{uipexkdp4vl5kowpsaL&I;9KLEj^%K;U85>Z)&f#MIO^mBTx)9 zjU%&NQ!SEI>LN}DW~~P_PIfY$2vZ&O$;sL0nujEY~JT;~P>WN;iDtYT4g5 zelO3qlO0t7R23kpBEjH4Ch7uAf4|+jnX4Om5cgzDOSLOitJz9N#kJP3Ncy@plA8A* zdglzc`^~|~1e_;XQgH48kgfp_?Igf zKzwTJ$^kGXIU+W^bV3E2u#)Oc*n^NyATN&~JEwHwmhN?DLJL$pm|gAg&7G%*ud`D}Y@=yD$gVeig1C4!;DK!U)F6pZ89t z2T$)D&%WDx`e6U`!QJ_z-{-&HJ~_VsXU=?lLC3#@16K0bOT7y%zif#yo*w literal 0 HcmV?d00001 diff --git a/common/__pycache__/agent.cpython-310.pyc b/common/__pycache__/agent.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9946c06918c44d4227abe356bcf783ec0ba0f5b GIT binary patch literal 19658 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 literal 0 HcmV?d00001 diff --git a/common/__pycache__/agent.cpython-39.pyc b/common/__pycache__/agent.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e4b049c4126c621f7575d7ffb81043fd79f43a2 GIT binary patch literal 19785 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 Km@Q-q*8d0WdueF^ literal 0 HcmV?d00001 diff --git a/common/__pycache__/renderer.cpython-310.pyc b/common/__pycache__/renderer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cf8acfcb80d3aac590b1dad07ad988dbaee9174 GIT binary patch literal 149 zcmd1j<>g`kf@?M6>HB3F7#@Q-$bb>ZaRB0C79f$r5X_*-=(m!g2qcUkep%?3=NDxc z7bGU9>KCULm8BNx=T??v=$Dpc<`nB!rWVI1=cW|tC+Fwp=I7}brRJri7Nr*H6;$5h Uu*uC&Da}c>1DRON1SD7(0GtXVi2wiq literal 0 HcmV?d00001 diff --git a/common/__pycache__/renderer.cpython-39.pyc b/common/__pycache__/renderer.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e04bc98eb9d33cd05b8637bcdcbe64c5eb4dcb3f GIT binary patch literal 5488 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 literal 0 HcmV?d00001 diff --git a/common/__pycache__/utils.cpython-310.pyc b/common/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df5976090250a7a6673f6f6323e242ff75896966 GIT binary patch literal 8633 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 dict: + from .utils import axial_to_cube_dict + return { + "curOrderedUnitID": self.agent_id, + "targetUnitID": self.target_id, + "destination": axial_to_cube_dict((self.des[0]-1000, self.des[1]-1000)), + "commandType": self.action_type.value + } + + def __str__(self): + if self.action_type == ActionType.MOVE: + return f"{self.agent_id} moves to {self.des}" + elif self.action_type == ActionType.ATTACK: + return f"{self.agent_id} attacks {self.target_id}" + elif self.action_type == ActionType.INTERACT: + return f"Interact {self.agent_id} with {self.target_id}" + elif self.action_type == ActionType.RELEASE: + return f"Release {self.target_id} from {self.agent_id} to {self.des}" + elif self.action_type == ActionType.END_OF_TURN: + return f"End of turn" + elif self.action_type == ActionType.SWITCH_WEAPON: + return f"{self.agent_id} switches to {self.weapon_name}" + else: + return "Unknown action" + + def to_dict(self): + return { + "agent_id": self.agent_id, + "target_id": self.target_id, + "des": self.des, + "action_type": self.action_type.name, + "weapon_id": self.weapon_id, + "weapon_name": self.weapon_name, + "start_time": self.start_time, + "end_type": self.end_type + } + +class Command(Action): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def description(self) -> List[str]: + if self.command.action_type == ActionType.MOVE: + return [ + f"Agent {self.agent_id} started to move to {self.command.des} at time {self.start_time}.", + f"Agent {self.agent_id} arrived at {self.command.des} at time {self.end_time}." + ] + elif self.command.action_type == ActionType.ATTACK: + return [ + f"Agent {self.agent_id} started to attack {self.command.des} at time {self.start_time}.", + f"Agent {self.agent_id} attacked {self.command.des} at time {self.end_time}." + ] + elif self.command.action_type == ActionType.SUPPLY: + return [ + f"Agent {self.agent_id} started to get supply from {self.command.des} at time {self.start_time}.", + f"Agent {self.agent_id} got supply from {self.command.des} at time {self.end_time}." + ] + elif self.command.action_type == ActionType.SWITCH_WEAPON: + return [ + f"Agent {self.agent_id} started to switch weapon to {self.command.weapon_name} at time {self.start_time}.", + f"Agent {self.agent_id} switched weapon to {self.command.weapon_name} at time {self.end_time}." + ] + else: + raise NotImplementedError(f"Unsupported command type: {self.command.action_type}") + +class Agent: + def __init__(self, + agent_id: str, + agent_type: AgentType, + team_id: int, + faction_id: int, + move_type: MoveType, + pos: Tuple[int, int], + info_level: int, stealth_level: int, + max_endurance: int, defense: int, + max_fuel: float, mobility: float, + switchable_weapons = [], + modules = [], + is_key: bool = False): + self.agent_id = agent_id + self.agent_type = agent_type + self.team_id = team_id + self.faction_id = faction_id + self.move_type = MoveType(move_type) + self.init_pos = pos + self.info_level = info_level + self.stealth_level = stealth_level + self.max_endurance = max_endurance + self.defense = defense + self.endurance = max_endurance + self.max_fuel = max_fuel + self.mobility = mobility + self.switchable_weapons = switchable_weapons + self.modules = modules + self.is_key = is_key + + # Get action space + from .utils import range_to_count + max_attack_range = max(weapon.attack_range for weapon in self.switchable_weapons) if self.switchable_weapons else 0 + max_capacity = max(module.capacity if hasattr(module, "capacity") else 0 for module in self.modules) if self.modules else 0 + # max_capacity = 6 if self.modules and any(hasattr(module, "parked_agents") for module in self.modules) else 0 + self._action_space = gym.spaces.Dict( + { + action_type: space + for action_type, space in { + ActionType.MOVE: gym.spaces.Discrete(range_to_count(int(self.mobility))) \ + if self.mobility > 0 else None, + ActionType.ATTACK: gym.spaces.Discrete(range_to_count(max_attack_range)) \ + if max_attack_range > 0 else None, + ActionType.SWITCH_WEAPON: gym.spaces.Discrete(len(self.switchable_weapons)) \ + if len(self.switchable_weapons) >= 1 else None, + ActionType.INTERACT: gym.spaces.Discrete(range_to_count(1)), + ActionType.RELEASE: gym.spaces.Discrete(max_capacity) \ + if max_capacity > 0 else None, + }.items() + if space is not None # 过滤掉值为 None 的键值对 + } + ) + + self._has_supply = False + self._available_types = [] + self._parked_agents = [] + self._capacity = max_capacity # 实际上应该最多只有一个运输单元 + + for module in self.modules: + if module.module_type == ModuleType.SUPPLY: + self._has_supply = True + self.supply = module + elif module.module_type == ModuleType.HANGER: + self._parked_agents = module.parked_agents + elif module.module_type == ModuleType.TRANSPORT: + self._parked_agents = module.parked_agents + self._available_types.extend(module.available_types) + + self.reset() + + def reset(self): + self.pos = self.init_pos + self.endurance = self.max_endurance + self.fuel = self.max_fuel + if self.switchable_weapons: + for weapon in self.switchable_weapons: + weapon.reset() + self.weapon = self.switchable_weapons[0] + else: + self.weapon = None + self.commenced_action = False + self.cmd_todo = [] + self.todo = [] + self.is_carried = False + + def __lt__(self, other): + return self.agent_id < other.agent_id + + def __eq__(self, other): + return self.agent_id == other.agent_id + + def __hash__(self): + return hash(self.agent_id) + + def __str__(self): + return f"{self.agent_id} ({self.move_type.name}) at {self.pos}" + + def __repr__(self): + return f"{self.agent_id} ({self.move_type.name}) at {self.pos}" + + def to_dict(self): + return { + "agent_id": self.agent_id, + "agent_type": self.agent_type.name, + "team_id": self.team_id, + "faction_id": self.faction_id, + "move_type": self.move_type.name, + # "info_level": self.info_level, + # "stealth_level": self.stealth_level, + "defense": self.defense, + "max_endurance": self.max_endurance, + "max_fuel": self.max_fuel, + "switchable_weapons": [weapon.to_dict() for weapon in self.switchable_weapons], + "modules": [module.to_dict() for module in self.modules], + # 以下为可变属性 + "pos": list(self.pos), + "endurance": self.endurance, + "fuel": self.fuel, + "mobility": self.mobility, + "weapon": self.weapon.to_dict() if self.weapon is not None else None + } + + def plan_to_dict(self): + state = self.todo[-1].state if self.todo else self.state + return { + "agent_id": self.agent_id, + "agent_type": self.agent_type.name, + "move_type": self.move_type.name, + "defense": self.defense, + "max_endurance": self.max_endurance, + "max_fuel": self.max_fuel, + "switchable_weapons": [weapon.to_dict() for weapon in self.switchable_weapons], + "modules": [module.to_dict() for module in self.modules], + "pos": list(state.pos), + "endurance": self.endurance, + "fuel": self.fuel, + "mobility": self.mobility, + "weapon": self.weapon.to_dict() if self.weapon is not None else None, + } + + @property + def alive(self): + return self.endurance > 0 + + @property + def attack_range(self): + return self.weapon.attack_range if self.weapon is not None else 0 + + @property + def strike_types(self): + return self.weapon.strike_types if self.weapon is not None else [] + + @property + def damage(self): + return self.weapon.damage if self.weapon is not None else 0 + + @property + def ammo(self): + return self.weapon.ammo if self.weapon is not None else 0 + + @property + def action_space(self): + return self._action_space + + @property + def has_supply(self): + return self._has_supply + + @property + def available_types(self): + return self._available_types + + @property + def parked_agents(self): + return self._parked_agents + + @property + def capacity(self): + return self._capacity + + @property + def state(self): + return AgentState(self.pos, self.endurance, self.fuel, self.weapon, self.commenced_action, self.cmd_todo, self.todo) + + def update(self, state: 'AgentState'): + self.pos = state.pos + self.endurance = state.endurance + self.fuel = state.fuel + self.commenced_action = state.commenced_action + self.cmd_todo = copy.deepcopy(state.cmd_todo) + self.todo = copy.deepcopy(state.todo) + self.weapon = copy.deepcopy(state.weapon) + +class AgentState: + def __init__(self, pos: Tuple[int, int], endurance: int, fuel: float, weapon: Weapon = None, commenced_action: bool = False, cmd_todo: List[Action] = [], todo: List[Action] = []): + self.pos = pos + self.endurance = endurance + self.fuel = fuel + self.commenced_action = commenced_action + self.weapon = copy.deepcopy(weapon) if weapon is not None else None + self.cmd_todo = copy.deepcopy(cmd_todo) + self.todo = copy.deepcopy(todo) + +class Team: + def __init__(self, team_id: int, faction_id: int, agents: dict[str, Agent] = {}): + self.team_id = team_id + self.faction_id = faction_id + self.agents = agents + self.reset() + + def __lt__(self, other): + return self.team_id < other.team_id + + def __eq__(self, other): + return self.team_id == other.team_id + + def __hash__(self): + return hash(self.team_id) + + def __str__(self): + return f"Team {self.team_id}" + + def __repr__(self): + return f"Team {self.team_id}" + + def add_agent(self, agent: Agent): + self.agents.append(agent) + + def remove_agent(self, agent_id: str): + self.agents.pop(agent_id) + + def reset(self): + for agent in self.agents.values(): + agent.reset() + + @property + def alive_count(self): + return sum(1 for agent in self.agents.values() if agent.alive) + + @property + def alive_ids(self): + return [agent.agent_id for agent in self.agents.values() if agent.alive] + +class TileNode: + def __init__(self, pos: Tuple[int, int], terrain_type: TerrainType, agent_id: str = None, is_city: bool = False): + self.pos = pos + self.terrain_type = TerrainType(terrain_type) + self.agent_id = agent_id + self.is_city = is_city + self.team_id = -1 + + def reset(self): + self.chaotic_value = 0 + self.occupy_value = 0 + self.occupied_by = None + self.agent_id = None + self.team_id = -1 + + def __lt__(self, other): + return self.pos < other.pos + + def __eq__(self, other): + return self.pos == other.pos + + def __hash__(self): + return hash(self.pos) + + def __str__(self): + return f"{self.pos} ({self.terrain_type.name})" + + def __repr__(self): + return f"{self.pos} ({self.terrain_type.name})" + + +class Map: + def __init__(self, nodes: dict[Tuple[int, int], TileNode]): + self.width = max(pos[0] for pos in nodes.keys()) + 1 + self.height = max(pos[1] for pos in nodes.keys()) + 1 + self.nodes = nodes + +class Module(ABC): + def __init__(self, module_type: ModuleType, add_endurance: int, add_ammo: int, add_fuel: float, available_types: List[AgentType] = None, capacity: int = 0): + self.module_type = module_type + self.add_endurance = add_endurance + self.add_ammo = add_ammo + self.add_fuel = add_fuel + self.available_types = available_types if available_types is not None else [] + self.capacity = capacity + + def to_dict(self): + return { + "module_type": self.module_type.name, + "add_endurance": self.add_endurance, + "add_ammo": self.add_ammo, + "add_fuel": self.add_fuel, + "available_types": [agent_type.name for agent_type in self.available_types] + } + +class Hanger(Module): + def __init__(self, available_types: List[AgentType] = None, capacity: int = 6): + super().__init__(module_type=ModuleType.HANGER, + add_endurance=4, + add_ammo=-1, + add_fuel=-1, + available_types=available_types, + capacity=capacity) + self.parked_agents = [] + + def reset(self): + self.parked_agents = [] + +class Supply(Module): + def __init__(self, available_types: List[AgentType] = None, capacity: int = 0): + super().__init__(module_type=ModuleType.SUPPLY, + add_endurance=2, + add_ammo=2, + add_fuel=-1, + available_types=available_types, + capacity=capacity) + +class Transport(Module): + def __init__(self, available_types: List[AgentType] = None, capacity: int = 6): + super().__init__(module_type=ModuleType.TRANSPORT, + add_endurance=0, + add_ammo=0, + add_fuel=0, + available_types=available_types, + capacity=capacity) + self.parked_agents = [] + + def reset(self): + self.parked_agents = [] + diff --git a/common/renderer.py b/common/renderer.py new file mode 100644 index 0000000..0d045e2 --- /dev/null +++ b/common/renderer.py @@ -0,0 +1,155 @@ +import numpy as np +import pygame +import math +from .utils import * +from .agent import TileNode, Agent, AgentType + +terrain_colors = [ + (245, 245, 220), # 米色 Plain + (210, 180, 140), # 褐色 Road + (224, 255, 255), # 青色 Water + (173, 216, 230) # 蓝色 Subwater +] + +team_colors = [ + (255, 0, 0), # 红色 + (0, 0, 255), # 蓝色 + (0, 255, 0), # 绿色 + (255, 255, 0), # 黄色 + (255, 165, 0), # 橙色 + (128, 0, 128), # 紫色 + (0, 255, 255), # 青色 + (255, 192, 203) # 粉色 +] + +icons_path = "./tianqiong/envs/icons" + +unit_icons: Dict[str, Dict[int, pygame.Surface]] = { + AgentType.AntiAir: {0: pygame.image.load(f"{icons_path}/AntiAir_b.png"), 1: pygame.image.load(f"{icons_path}/AntiAir_r.png")}, + AgentType.Airport: {0: pygame.image.load(f"{icons_path}/Airport_b.png"), 1: pygame.image.load(f"{icons_path}/Airport_r.png")}, + AgentType.Artillery: {0: pygame.image.load(f"{icons_path}/Artillery_b.png"), 1: pygame.image.load(f"{icons_path}/Artillery_r.png")}, + AgentType.Bomber: {0: pygame.image.load(f"{icons_path}/Bomber_b.png"), 1: pygame.image.load(f"{icons_path}/Bomber_r.png")}, + AgentType.Carrier: {0: pygame.image.load(f"{icons_path}/Carrier_b.png"), 1: pygame.image.load(f"{icons_path}/Carrier_r.png")}, + AgentType.Fighter: {0: pygame.image.load(f"{icons_path}/Fighter_b.png"), 1: pygame.image.load(f"{icons_path}/Fighter_r.png")}, + AgentType.Helicopter: {0: pygame.image.load(f"{icons_path}/Helicopter_b.png"), 1: pygame.image.load(f"{icons_path}/Helicopter_r.png")}, + AgentType.Infantry: {0: pygame.image.load(f"{icons_path}/Infantry_b.png"), 1: pygame.image.load(f"{icons_path}/Infantry_r.png")}, + AgentType.Tank: {0: pygame.image.load(f"{icons_path}/Tank_b.png"), 1: pygame.image.load(f"{icons_path}/Tank_r.png")}, + AgentType.TransportHelicopter: {0: pygame.image.load(f"{icons_path}/TransportHelicopter_b.png"), 1: pygame.image.load(f"{icons_path}/TransportHelicopter_r.png")}, + AgentType.TransportShip: {0: pygame.image.load(f"{icons_path}/TransportShip_b.png"), 1: pygame.image.load(f"{icons_path}/TransportShip_r.png")}, + AgentType.CombatShip: {0: pygame.image.load(f"{icons_path}/CombatShip_b.png"), 1: pygame.image.load(f"{icons_path}/CombatShip_r.png")}, +} + +class Renderer: + def __init__(self, nodes: List[TileNode], hex_size=20): + self.nodes = nodes + self.hex_size = hex_size + self.window_size, (self.offset_x, self.offset_y) = get_window_size_and_offsets(nodes, hex_size) + + def render(self, player_agents: List[Agent], spotted_enemy_agents: List[Agent], attack_agent: Agent=None, defend_agent: Agent=None) -> np.ndarray: + screen = pygame.Surface(self.window_size) + offset_x = self.offset_x + offset_y = self.offset_y + hex_size = self.hex_size + nodes = self.nodes + draw_hex_grid(screen, offset_x, offset_y, hex_size, nodes) + for agent in player_agents: + pos = agent.pos + alpha = agent.endurance / agent.max_endurance * 255 + draw_unit(screen, offset_x, offset_y, hex_size, axial_to_pixel(pos, hex_size), alpha=alpha, agent_type=agent.agent_type, team_id=agent.team_id) + for agent in spotted_enemy_agents: + pos = agent.pos + alpha = agent.endurance / agent.max_endurance * 255 + draw_unit(screen, offset_x, offset_y, hex_size, axial_to_pixel(pos, hex_size), alpha=alpha, agent_type=agent.agent_type, team_id=agent.team_id) + + if attack_agent and defend_agent: + start_pos = axial_to_pixel(attack_agent.pos, hex_size) + end_pos = axial_to_pixel(defend_agent.pos, hex_size) + draw_hexagon(screen, hex_size, (start_pos[0] + offset_x, start_pos[1] + offset_y), team_colors[attack_agent.team_id], True, 5) + draw_hexagon(screen, hex_size, (end_pos[0] + offset_x, end_pos[1] + offset_y), team_colors[attack_agent.team_id], True, 5) + + rgb_array = pygame.surfarray.array3d(screen) + + return np.transpose(rgb_array, (1, 0, 2)) + +# 轴坐标系到像素坐标的转换函数(尖角朝上) +def axial_to_pixel(pos: Tuple[int, int], hex_size): + q, r = pos + x = hex_size * math.sqrt(3) * q + y = hex_size * 3/2 * r + x += hex_size * math.sqrt(3) / 2 * r + return x, y + +# 绘制六边形的函数 +def draw_hexagon(surface, hex_size, center, color=(255, 255, 255), only_frame=False, line_width=1): + line_color = (169, 169, 169) # 灰色 + angle_offset = math.pi / 6 # 顶点指向上方 + points = [ + ( + center[0] + hex_size * math.cos(angle_offset + math.pi / 3 * i), + center[1] + hex_size * math.sin(angle_offset + math.pi / 3 * i) + ) + for i in range(6) + ] + if not only_frame: + pygame.draw.polygon(surface, color, points) + pygame.draw.polygon(surface, line_color, points, width=1) # 绘制六边形边框 + else: + pygame.draw.polygon(surface, color, points, width=line_width) # 绘制六边形边框 + +def get_window_size_and_offsets(nodes: List[TileNode], hex_size: int) -> Tuple[Tuple[int, int], Tuple[int, int]]: + min_x = min_y = float('inf') + max_x = max_y = float('-inf') + offset_x = offset_y = float('-inf') + + for node in nodes: + q, r = node.pos + x, y = axial_to_pixel((q, r), hex_size) + min_x = min(min_x, x) + max_x = max(max_x, x) + min_y = min(min_y, y) + max_y = max(max_y, y) + offset_x = max(offset_x, -x) + offset_y = max(offset_y, -y) + + # 加一些边距 + margin = hex_size * 2 + offset_x += margin + offset_y += margin + width = int(max_x - min_x + 2 * margin) + height = int(max_y - min_y + 2 * margin) + + width = (width - 15) // 16 * 16 + 16 + height = (height - 15) // 16 * 16 + 16 + + return (width, height), (int(offset_x), int(offset_y)) + +# 绘制单位的函数,带透明度 +def draw_unit(surface, offset_x, offset_y, hex_size, center, alpha=128, agent_type=None, team_id=None): + # print(agent_type, team_id) + if agent_type in unit_icons and team_id in unit_icons[agent_type]: + # 如果有图标,用图标表示 + icon = unit_icons[agent_type][team_id] + icon_size = hex_size * 1.2 + icon = pygame.transform.scale(icon, (icon_size, icon_size)) # 缩放图标至合适尺寸 + icon.set_alpha(alpha) # 设置透明度 + surface.blit(icon, (center[0] + offset_x - icon_size // 2, center[1] + offset_y - icon_size // 2)) + else: + # 如果没有图标,用圆圈表示单位 + unit_surface = pygame.Surface((hex_size, hex_size), pygame.SRCALPHA) + unit_surface.set_alpha(alpha) # 设置透明度 + color = team_colors[team_id] + pygame.draw.circle(unit_surface, color, (hex_size // 2, hex_size // 2), hex_size // 3) + surface.blit(unit_surface, (center[0] + offset_x - hex_size // 2, center[1] + offset_y - hex_size // 2)) + +# 主绘制功能 +def draw_hex_grid(surface, offset_x, offset_y, hex_size, nodes: List[TileNode]): + background_color = (255, 255, 255) # 白色 + surface.fill(background_color) + + for node in nodes: + q, r = node.pos + color = terrain_colors[node.terrain_type.value] + pixel_x, pixel_y = axial_to_pixel((q, r), hex_size) + center = (pixel_x + offset_x, pixel_y + offset_y) + draw_hexagon(surface, hex_size, center, color) + diff --git a/common/utils.py b/common/utils.py new file mode 100644 index 0000000..41864ea --- /dev/null +++ b/common/utils.py @@ -0,0 +1,301 @@ +import heapq +from .agent import Agent, Weapon, Action, TileNode, Module, Supply, Hanger, Transport +from .agent import MoveType, TerrainType, ActionType, AgentType, ModuleType +from typing import Tuple, List, Dict, Union +import numpy as np + +# Define the cost matrix for agent types and terrain types +cost_matrix = [ + # Plain Road Sea Hill + [ 1, 1, 1, 1], # Air + [ 1.5, 1, 0, 2], # Ground + [ 0, 0, 1, 0], # Sea + [ 0, 0, 1, 0], # Subwater +] + +def get_cost(move_type: MoveType, terrain_type: TerrainType): + return cost_matrix[move_type.value][terrain_type.value] + +def get_axial_dis(pos1: Tuple[int, int], pos2: Tuple[int, int] = (0, 0)): + q1, r1 = pos1 + q2, r2 = pos2 + return (abs(q1 - q2) + abs(r1 - r2) + abs((q1 + r1) - (q2 + r2))) / 2 + +def astar_search(map_data: Dict[Tuple[int, int], object], + move_type: MoveType, + start: Tuple[int, int], + goal: Tuple[int, int] = None, + limit: float = float('inf'), + return_cost: bool = False) -> Union[List[Tuple[int, int]], Tuple[float, List[Tuple[int, int]]]]: + """ + A* search algorithm to find the shortest path from start to goal. + map_data: a dictionary of nodes, where the keys are the node coordinates and the values are the node objects + move_type: the type of agent to consider (0 for air, 1 for ground, 2 for sea, 3 for subwater) + start: the starting node + goal: the ending node + """ + if isinstance(start, tuple): + start = [start] + multi_source = False + elif isinstance(start, list): + multi_source = True + else: + raise ValueError(f"Invalid start type. Found{type(start)} but expected tuple or list.") + + g = {} + f = {} + parent = {} + open_list = [] + cand_parent_count = {} + + for pos in start: + g[pos] = 0 + f[pos] = 0 + parent[pos] = None + cand_parent_count[pos] = 1 + open_list.append((0.0, pos)) + + in_open_list = set(start) + heapq.heapify(open_list) + closed_list = set() + + directions = [(1, 0), (1, -1), (0, -1), (-1, 0), (-1, 1), (0, 1)] + + while open_list: + _, cur = heapq.heappop(open_list) + in_open_list.remove(cur) + + if goal and cur == goal: + path = [] + while cur not in start: + path.append(cur) + cur = parent[cur] + if multi_source: + if path: + path.pop(0) # 弹出实际上当前所在位置 + path.append(cur) # 确定多源最短路的终点 + else: + path.reverse() + return (cost, path) if return_cost else path + + closed_list.add(cur) + + for dq, dr in directions: + neighbor = (cur[0] + dq, cur[1] + dr) + if neighbor in map_data and neighbor not in closed_list: + cost = get_cost(move_type, map_data[neighbor].terrain_type) \ + if not multi_source \ + else get_cost(move_type, map_data[cur].terrain_type) + if cost == 0: + continue + tentative_g = g[cur] + cost + if neighbor not in g or tentative_g < g[neighbor]: + g[neighbor] = tentative_g + f[neighbor] = tentative_g + (get_axial_dis(neighbor, goal) if goal else 0) + if f[neighbor] > limit: + continue + parent[neighbor] = cur + cand_parent_count[neighbor] = 1 + if neighbor not in in_open_list: + heapq.heappush(open_list, (f[neighbor], neighbor)) + in_open_list.add(neighbor) + elif neighbor in g and tentative_g == g[neighbor] and f[neighbor] <= limit: + cand_parent_count[neighbor] += 1 + if np.random.random() < 1 / cand_parent_count[neighbor]: # reservior sampling + parent[neighbor] = cur + + if goal is not None: + raise ValueError("No path found") + + return list(closed_list) + +def get_path(agent, map_data, start, des: Union[Tuple[int, int], List[Tuple[int, int]]], limit=float('inf')) -> List[Tuple[int, int]]: + """ + Returns a list of move actions to move the agent from its current position to the destination. + """ + if isinstance(des, tuple): + raw_path = astar_search(map_data=map_data, move_type=agent.move_type, start=start, goal=des, limit=limit) + elif isinstance(des, list): # multi-source + raw_path = astar_search(map_data=map_data, move_type=agent.move_type, start=des, goal=start, limit=limit) + else: + raise ValueError(f"Invalid destination type. Found{type(des)} but expected tuple or list.") + + if not raw_path: + raise ValueError("No path found") + + # Create the frame path by breaking the path into smaller moves + path = [] + sum = 0 + raw_path = [start] + raw_path + + # print(agent.pos, des, raw_path) + + for parent, node in zip(raw_path[:-1], raw_path[1:]): + cost = get_cost(agent.move_type, map_data[node].terrain_type) + sum += cost + if sum > agent.fuel: + raise ValueError(f"Not enough fuel to reach {des}") + if sum > agent.mobility: + sum = cost + path.append(parent) + path.append(node) + return path + +def axial_to_cube_dict(a): + q, r = a + return { + "x": q, + "y": -(q + r), + "z": r + } + +def cube_dict_to_axial(c): + return (c["x"], c["z"]) + +def axial_to_cube(a): + q, r = a + return (q, -(q + r), r) + +def cube_to_axial(c): + return (c[0], c[2]) + +def encode_axial(pos: Tuple[int, int]) -> int: + q, r = pos + if q == 0 and r == 0: + return 0 + + x, y, z = axial_to_cube(pos) + d = get_axial_dis(pos) + + base_number = 3 * d * (d - 1) + 1 + + if x > 0 and y < 0 and z >= 0: # 0 + return base_number + z + elif x <= 0 and y < 0 and z > 0: # 1 + return base_number + d - x + elif x < 0 and y >= 0 and z > 0: # 2 + return base_number + d * 2 + y + elif x < 0 and y > 0 and z <= 0: # 3 + return base_number + d * 3 - z + elif x >= 0 and y > 0 and z < 0: # 4 + return base_number + d * 4 + x + else: # x > 0 and y <= 0 and z < 0 # 5 + return base_number + d * 5 - y + +def decode_axial(num: int) -> Tuple[int, int]: + if num == 0: + return (0, 0) + + l, r = 1, num + while l < r: + mid = (l + r) // 2 + if range_to_count(mid) + 1 > num: + r = mid + else: + l = mid + 1 + d = l + base_number = range_to_count(d - 1) + 1 + # base_number = 1 + # d = 1 + # while base_number + 6 * d <= num: + # base_number += 6 * d + # d += 1 + + k = (num - base_number) // d + mod = (num - base_number) % d + + if k == 0: + q = d - mod + r = mod + elif k == 1: + q = -mod + r = d + elif k == 2: + q = -d + r = d - mod + elif k == 3: + q = -(d - mod) + r = -mod + elif k == 4: + q = mod + r = -d + else: # k == 5 + q = d + r = -(d - mod) + + return (q, r) + +def get_adj_pos(pos: Tuple[int, int], max_dis: int) -> List[Tuple[int, int]]: + """ + Returns a list of adjacent positons within the given range. + """ + d = [decode_axial(num) for num in range(1, range_to_count(int(max_dis)) + 1)] + return [(pos[0] + dq, pos[1] + dr) for dq, dr in d] + +def range_to_count(max_dis: int) -> int: # not including 0 + return 3 * max_dis * (max_dis + 1) + +def dict_to_action(action_dict: dict) -> Action: + return Action( + action_type=ActionType.from_input(action_dict["action_type"]), + agent_id=action_dict["agent_id"], + target_id=action_dict.get("target_id", ""), + des=action_dict.get("des", (-1, -1)), + weapon_id=action_dict.get("weapon_id", -1), + weapon_name=action_dict.get("weapon_name", ""), + start_time=action_dict.get("start_time", 0), + end_type=action_dict.get("end_type", None), + attack_count=action_dict.get("attack_count", 0), + ) + +def dict_to_node(tile_dict: dict) -> TileNode: + return TileNode( + pos=(tile_dict["pos"]["q"], tile_dict["pos"]["r"]), + terrain_type=TerrainType.from_input(tile_dict["terrain_type"]), + # agent_id=tile_dict.get("agent_id", None), + # is_city=tile_dict.get("is_city", False), + # chaotic_value=tile_dict.get("chaotic_value", 0), + # occupy_value=tile_dict.get("occupy_value", 0), + # occupied_by=tile_dict.get("occupied_by", None) + ) + +def dict_to_weapon(weapon_dict: dict) -> Weapon: + return Weapon( + name=weapon_dict["name"], + attack_range=weapon_dict["attack_range"], + damage=weapon_dict["damage"], + max_ammo=weapon_dict["max_ammo"], + strike_types=[MoveType.from_input(strike_type) for strike_type in weapon_dict["strike_types"]] + ) + +def dict_to_module(module_dict: dict) -> Module: + module_type = ModuleType.from_input(module_dict["module_id"]) + available_types = [AgentType.from_input(agent_type) for agent_type in module_dict.get("available_types", [])] \ + if module_dict.get("available_types", []) is not None else [] + capacity = module_dict.get("capacity", 0) + if module_type == ModuleType.HANGER: + return Hanger(available_types=available_types, capacity=capacity) + elif module_type == ModuleType.SUPPLY: + return Supply(available_types=available_types, capacity=capacity) + elif module_type == ModuleType.TRANSPORT: + return Transport(available_types=available_types, capacity=capacity) + else: + raise ValueError(f"Invalid module type: {module_type}") + +def dict_to_agent(agent_dict: dict) -> Agent: + return Agent( + agent_id=agent_dict["agent_id"], + agent_type=AgentType.from_input(agent_dict["type"]), + team_id=agent_dict["team_id"], + faction_id=agent_dict["faction_id"], + move_type=MoveType.from_input(agent_dict["move_type"]), + pos=(agent_dict["pos"]["q"], agent_dict["pos"]["r"]), + info_level=agent_dict.get("info_level", 0), + stealth_level=agent_dict.get("stealth_level", 0), + max_endurance=agent_dict["max_endurance"], + defense=agent_dict["defense"], + max_fuel=agent_dict["max_fuel"], + mobility=agent_dict["mobility"], + switchable_weapons=[dict_to_weapon(weapon_dict) for weapon_dict in agent_dict.get("switchable_weapons", [])], + modules=[dict_to_module(module_dict) for module_dict in agent_dict.get("modules", [])] if agent_dict.get("modules", []) is not None else [] + ) \ No newline at end of file diff --git a/data/test/AgentsInfo.json b/data/test/AgentsInfo.json new file mode 100644 index 0000000..2b5126b --- /dev/null +++ b/data/test/AgentsInfo.json @@ -0,0 +1,603 @@ +[ + { + "agent_id": "879a", + "pos": { + "q": 10, + "r": 0 + }, + "max_endurance": 10.0, + "max_fuel": 0.0, + "type": 16, + "team_id": 0, + "faction_id": 0, + "move_type": 1, + "defense": 5.0, + "mobility": 0.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [], + "modules": [ + { + "module_id": 0, + "capacity": 6, + "available_types": [ + 4, + 5, + 6, + 19, + 20 + ] + }, + { + "module_id": 1 + } + ] + }, + { + "agent_id": "9f19", + "pos": { + "q": 11, + "r": 0 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 0, + "faction_id": 0, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 6, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "a947", + "pos": { + "q": 12, + "r": 0 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 0, + "faction_id": 0, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 50, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "cc4c", + "pos": { + "q": 13, + "r": 0 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 1, + "faction_id": 0, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 6, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "c865", + "pos": { + "q": 14, + "r": 0 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 1, + "faction_id": 0, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 6, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "4189", + "pos": { + "q": 22, + "r": 0 + }, + "max_endurance": 10.0, + "max_fuel": 120.0, + "type": 1, + "team_id": 1, + "faction_id": 0, + "move_type": 1, + "defense": 3.0, + "mobility": 4.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [] + }, + { + "agent_id": "0cd5", + "pos": { + "q": 23, + "r": 0 + }, + "max_endurance": 10.0, + "max_fuel": 120.0, + "type": 1, + "team_id": 1, + "faction_id": 0, + "move_type": 1, + "defense": 3.0, + "mobility": 4.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [] + }, + { + "agent_id": "2602", + "pos": { + "q": 24, + "r": 0 + }, + "max_endurance": 3.0, + "max_fuel": 80.0, + "type": 12, + "team_id": 2, + "faction_id": 1, + "move_type": 0, + "defense": 0.0, + "mobility": 5.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [], + "switchable_weaponsNames": [] + }, + { + "agent_id": "c53f", + "pos": { + "q": 2, + "r": 14 + }, + "max_endurance": 10.0, + "max_fuel": 0.0, + "type": 16, + "team_id": 2, + "faction_id": 1, + "defense": 5.0, + "mobility": 0.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [], + "switchable_weaponsNames": [] + }, + { + "agent_id": "95f3", + "pos": { + "q": 3, + "r": 14 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 2, + "faction_id": 1, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 6, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "7772", + "pos": { + "q": 4, + "r": 14 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 2, + "faction_id": 1, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 6, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "6f9d", + "pos": { + "q": 5, + "r": 14 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 3, + "faction_id": 2, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 6, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "2477", + "pos": { + "q": 6, + "r": 14 + }, + "max_endurance": 4.0, + "max_fuel": 120.0, + "type": 5, + "team_id": 3, + "faction_id": 2, + "move_type": 0, + "defense": 1.0, + "mobility": 6.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [ + { + "name": "AKD-21\u53cd\u5766\u514b\u5bfc\u5f39", + "attack_range": 6, + "damage": 8.0, + "max_ammo": 6, + "strike_types": [ + 1 + ] + }, + { + "name": "LS-6\u5236\u5bfc\u70b8\u5f39", + "attack_range": 5, + "damage": 15.0, + "max_ammo": 1, + "strike_types": [ + 1 + ] + }, + { + "name": "PL-15\u7a7a\u7a7a\u5bfc\u5f39", + "attack_range": 8, + "damage": 7.0, + "max_ammo": 4, + "strike_types": [ + 0 + ] + }, + { + "name": "YJ-91\u7a7a\u8230\u5bfc\u5f39", + "attack_range": 6, + "damage": 15.0, + "max_ammo": 2, + "strike_types": [ + 2 + ] + } + ] + }, + { + "agent_id": "9183", + "pos": { + "q": 7, + "r": 14 + }, + "max_endurance": 10.0, + "max_fuel": 120.0, + "type": 1, + "team_id": 3, + "faction_id": 2, + "move_type": 1, + "defense": 3.0, + "mobility": 4.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [] + }, + { + "agent_id": "45e5", + "pos": { + "q": 18, + "r": 14 + }, + "max_endurance": 10.0, + "max_fuel": 120.0, + "type": 1, + "team_id": 3, + "faction_id": 2, + "move_type": 1, + "defense": 3.0, + "mobility": 4.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [] + }, + { + "agent_id": "7e23", + "pos": { + "q": 19, + "r": 14 + }, + "max_endurance": 3.0, + "max_fuel": 80.0, + "type": 12, + "team_id": 4, + "faction_id": 3, + "move_type": 0, + "defense": 0.0, + "mobility": 5.0, + "info_level": 6, + "stealth_level": 0.0, + "switchable_weapons": [] + } +] \ No newline at end of file diff --git a/data/test/MapInfo.json b/data/test/MapInfo.json new file mode 100644 index 0000000..61f0f64 --- /dev/null +++ b/data/test/MapInfo.json @@ -0,0 +1,2802 @@ +[ + { + "pos": { + "q": 10, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 10, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 11, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 12, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 13, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 14, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 15, + "r": 0 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 16, + "r": 0 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 17, + "r": 0 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 18, + "r": 0 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 19, + "r": 0 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 20, + "r": 0 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 21, + "r": 0 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 22, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 23, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 24, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 25, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 26, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 27, + "r": 0 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 28, + "r": 0 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 9, + "r": 1 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 10, + "r": 1 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 11, + "r": 1 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 12, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 14, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 15, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 16, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 17, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 18, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 19, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 20, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 21, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 22, + "r": 1 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 23, + "r": 1 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 24, + "r": 1 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 25, + "r": 1 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 26, + "r": 1 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 27, + "r": 1 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 28, + "r": 1 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 8, + "r": 2 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 9, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 14, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 15, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 16, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 17, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 18, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 19, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 20, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 21, + "r": 2 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 22, + "r": 2 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 23, + "r": 2 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 24, + "r": 2 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 25, + "r": 2 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 26, + "r": 2 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 27, + "r": 2 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 8, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 14, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 15, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 16, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 17, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 18, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 19, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 20, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 21, + "r": 3 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 22, + "r": 3 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 23, + "r": 3 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 24, + "r": 3 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 25, + "r": 3 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 26, + "r": 3 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 27, + "r": 3 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 7, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 14, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 15, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 16, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 17, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 18, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 19, + "r": 4 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 20, + "r": 4 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 21, + "r": 4 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 22, + "r": 4 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 23, + "r": 4 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 24, + "r": 4 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 25, + "r": 4 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 26, + "r": 4 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 7, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 14, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 15, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 16, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 17, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 18, + "r": 5 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 19, + "r": 5 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 20, + "r": 5 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 21, + "r": 5 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 22, + "r": 5 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 23, + "r": 5 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 24, + "r": 5 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 25, + "r": 5 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 26, + "r": 5 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 6, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 7, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 14, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 15, + "r": 6 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 16, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 17, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 18, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 19, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 20, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 21, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 22, + "r": 6 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 23, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 24, + "r": 6 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 25, + "r": 6 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 6, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 7, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 14, + "r": 7 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 15, + "r": 7 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 16, + "r": 7 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 17, + "r": 7 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 18, + "r": 7 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 19, + "r": 7 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 20, + "r": 7 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 21, + "r": 7 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 22, + "r": 7 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 23, + "r": 7 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 24, + "r": 7 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 25, + "r": 7 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 5, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 6, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 7, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 8 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 13, + "r": 8 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 14, + "r": 8 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 15, + "r": 8 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 16, + "r": 8 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 17, + "r": 8 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 18, + "r": 8 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 8 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 8 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 21, + "r": 8 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 22, + "r": 8 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 23, + "r": 8 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 24, + "r": 8 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 5, + "r": 9 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 6, + "r": 9 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 7, + "r": 9 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 9 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 9 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 9 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 11, + "r": 9 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 12, + "r": 9 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 13, + "r": 9 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 14, + "r": 9 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 15, + "r": 9 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 16, + "r": 9 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 17, + "r": 9 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 9 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 9 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 9 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 21, + "r": 9 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 22, + "r": 9 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 23, + "r": 9 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 24, + "r": 9 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 4, + "r": 10 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 5, + "r": 10 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 6, + "r": 10 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 7, + "r": 10 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 10 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 10 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 10 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 11, + "r": 10 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 12, + "r": 10 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 13, + "r": 10 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 14, + "r": 10 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 15, + "r": 10 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 10 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 10 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 10 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 10 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 10 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 21, + "r": 10 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 22, + "r": 10 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 23, + "r": 10 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 4, + "r": 11 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 5, + "r": 11 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 6, + "r": 11 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 7, + "r": 11 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 8, + "r": 11 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 9, + "r": 11 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 10, + "r": 11 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 11, + "r": 11 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 12, + "r": 11 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 13, + "r": 11 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 14, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 21, + "r": 11 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 22, + "r": 11 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 23, + "r": 11 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 3, + "r": 12 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 4, + "r": 12 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 5, + "r": 12 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 6, + "r": 12 + }, + "terrain_type": 2 + }, + { + "pos": { + "q": 7, + "r": 12 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 8, + "r": 12 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 9, + "r": 12 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 10, + "r": 12 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 11, + "r": 12 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 12, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 12 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 21, + "r": 12 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 22, + "r": 12 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 3, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 4, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 5, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 6, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 7, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 8, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 9, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 10, + "r": 13 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 11, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 12, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 13 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 20, + "r": 13 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 21, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 22, + "r": 13 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 2, + "r": 14 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 3, + "r": 14 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 4, + "r": 14 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 5, + "r": 14 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 6, + "r": 14 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 7, + "r": 14 + }, + "terrain_type": 0 + }, + { + "pos": { + "q": 8, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 9, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 10, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 11, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 12, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 14 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 19, + "r": 14 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 20, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 21, + "r": 14 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 2, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 3, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 4, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 5, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 6, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 7, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 8, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 9, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 10, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 11, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 12, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 15 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 18, + "r": 15 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 19, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 21, + "r": 15 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 1, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 2, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 3, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 4, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 5, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 6, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 7, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 8, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 9, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 10, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 11, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 12, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 16 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 17, + "r": 16 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 18, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 16 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 1, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 2, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 3, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 4, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 5, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 6, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 7, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 8, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 9, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 10, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 11, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 12, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 17 + }, + "terrain_type": 1 + }, + { + "pos": { + "q": 17, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 20, + "r": 17 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 0, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 1, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 2, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 3, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 4, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 5, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 6, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 7, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 8, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 9, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 10, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 11, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 12, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 18 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 0, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 1, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 2, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 3, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 4, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 5, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 6, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 7, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 8, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 9, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 10, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 11, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 12, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 13, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 14, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 15, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 16, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 17, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 18, + "r": 19 + }, + "terrain_type": 3 + }, + { + "pos": { + "q": 19, + "r": 19 + }, + "terrain_type": 0 + } +] \ No newline at end of file diff --git a/tests/__pycache__/test_yes_cmdr_env.cpython-310.pyc b/tests/__pycache__/test_yes_cmdr_env.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f44bc2b2885e999d4347fe8855335edee853c6d GIT binary patch literal 2551 zcmb7G%Wu?1829+Kw%5CPkQYIv2KBX7n^k!T5g|kfAhne^KrW4_EaRC3Z`Sr^JPBlF zOBM7=rK(4Sls$6kpU`{%0S-R4r-)lQREmV?_lns3J6eE#Np{$|rg!%^VT zew|(ay{;&K;>*!TgO_VCvY&uZg_5qasxYNPc2%kl2tl6K)na{BmutOi#OA6Q*H&w> zwQ2#+pyrRts!eOug4v-qbzrX3I(1=oS%Wq`ZyQP)pQYfM{DQnAV90v(4or`v3 z&mbN-0g6&bB%dkI%_rnB*=zvcqz1R>h0@Zka_#IcYjNG>#6WK#-&v&jUJB@Q5u|O45uJjXXQkt%+Gp3 z$owqov(g>1MCwPhbR+7+s^XP;CRnefmKsa8OD&?0UMPGL)bC$wKTP?C)Y8rv-)6iW z?}!KOEfICI_72PZ5R%vy;6f&;KjQhIYN5AN&f(th95~oN@U4a^90y_%i!{k~;;2pa zI?>FmovJeD^D^h!Kq)ZCdJa`jwUbJA266*$kZI#NRL5hi z3pRA3?Hy?!I%4yfwE0k6 zN`HtlXMh|tu>p*OR{jHu8B(HP3sX`Msurq%Bz3B^o-H)k#i8e#Y-MfO$_G5z#2O~} zxa84R?f|@*}MW898pIQq^fW`4XRT6sRV(8 z9cQ>xAMJ={uG1Q5ACrY>-BSy#sDa*^)VVhN(xnaBlsdJnD)JNLccJo1HQK5~=iOJXc}&C7dt;yvYGMJQsK;^4fshGYYG)3#X_TuCVBYY=z0Z z)|u)|L(ER(Hqa@A!siy+3o_HW3vUpA$jD6YZY$M^yEB-h&0kP6HPM43E4XGZVe1{( zoc#>Dvofm)ceA%|?ICP&6A2;!w~!#O)Gx1nbs6Zzweu~bw1Rb(2!twI1bm&zr5gwl zKqrOi7C>ITs$pNiEm1ldhzVarF)Q_R7cO1eL&PZcFuV`~y?B0YPX|Uz=O5#VpCI`Z z$!9?J5OVs)T|hyvZ!dboKJUBUmsh|wy^qSv>+@NT3DgO&MpvX(A-rOh6NtcO=u zYwoc9IhHlY`sUcHr6E`lXVB*85zK%CTP%}JR@MN7X%vdmj)M*6_qsvC(M~R{H;C1A^IK?? z<@<3ECBEOUFSOBac?H403maejULYQ{#!4;Q$x0nxAO*c1gTEKkqg!fWw7~0%6>DHAvrCVKg6)Qf&a9D~9-cEsE UR^7tqb5&J|`PR}ajMxAE2LdjJdjJ3c literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_yes_cmdr_env.cpython-311.pyc b/tests/__pycache__/test_yes_cmdr_env.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bece98b8308cf521a298a60ceca258c96b365959 GIT binary patch literal 6489 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 Reward: {reward}, Done: {done}") + print(f"Info: {info}") + if "exception" in info: + agent.todo.clear() + + env.step(0) + env.step(0) + + step_count += 1 + + env.close() + +if __name__ == "__main__": + # 配置环境参数 + env = YesCmdrEnv( + data_path="./data/test", + max_team=5, + max_faction=4, + war_fog=False + ) + + test_action_id_transform(env) + test_random_action(env) + test_bot_action(env) + # test_command(env) + diff --git a/yes_cmdr_env.py b/yes_cmdr_env.py new file mode 100644 index 0000000..3c8d1d4 --- /dev/null +++ b/yes_cmdr_env.py @@ -0,0 +1,1267 @@ +import numpy as np +import json +import os +from datetime import datetime +from typing import List, Optional +import gymnasium as gym +from .common.utils import * +from .common.agent import * + +class YesCmdrEnv(gym.Env): + metadata = {"render_modes": ["human", "rgb_array"], "render_fps": 4} + + def __init__(self, + use_real_engine: bool = False, + replay_path: str = None, + campaign_id: str = "NOID", + team_id: int = 0, + max_team: int = 2, + max_faction: int = 2, + data_path: str = "../data", + max_episode_steps: int = 1000, + bot_version: str = "v0", + war_fog: bool = True) -> None: + self._init_flag = False + # set other properties... + self.use_real_engine = use_real_engine + self.replay_path = replay_path + self.campaign_id = campaign_id + self.init_team_id = team_id + self.max_team = max_team + self.max_faction = max_faction + self.data_path = data_path + self.max_episode_steps = max_episode_steps + self.bot_version = bot_version + self.war_fog = war_fog + + def reset(self): + # reset the environment... + # 实际的环境初始化是在第一次调用 reset 方法时进行的 + if not self._init_flag: + self._init_flag = True + self._make_env() + if self.replay_path is not None: + from .common.renderer import Renderer + self._renderer = Renderer(self.map.nodes.values()) + else: + for node in self.map.nodes.values(): + node.reset() + for team in self.teams: + for agent_id, agent in team.agents.items(): + agent.reset() + if agent.pos == (-1, -1): # Illegal position for padding + continue + assert agent.pos in self.map.nodes, f"Invalid agent position: {agent.pos}" + self.map.nodes[agent.pos].agent_id = agent_id + self.map.nodes[agent.pos].team_id = team.team_id + if self.replay_path is not None: + self._frames = [[], []] + self._legal_actions = None + self._legal_action_ids = None + self._spotted_enemy_ids = None + self._team_id = self.init_team_id + self._spotted_agents = None + self.episode_steps = 0 + self._cumulative_rewards = np.zeros(2) + + obs = self.observe() + info = { + "next_team": self.team_id + 1 + } + + return obs, info + + def step(self, action: Union[int, Action]): + if isinstance(action, int): + if action not in self.legal_action_ids: + print(f"Invalid action will be ignored: {action}") + info = {} + info["accum_reward"] = (self._cumulative_rewards[0], self._cumulative_rewards[1]) + info["next_team"] = self.team_id + 1 + info["exception"] = f"Invalid action {action}" + return self.observe(), 0, False, False, info + action = self.id_to_action(action) + elif isinstance(action, Action): + valid, reason = self.check_validity(action) + if not valid: + if action.action_type == ActionType.MOVE and reason.find("occupied") > 0: + print("Trying to move to an adjacent position that is empty.") + dis = float('inf') + for _action in self.legal_actions: + if _action.agent_id == action.agent_id and _action.action_type == ActionType.MOVE and get_axial_dis(_action.des, action.des) < dis: + dis = get_axial_dis(_action.des, action.des) + action = _action + else: + print(f"Invalid action will be ignored: {action}") + print(f"Reason: {reason}") + info = {} + info["accum_reward"] = (self._cumulative_rewards[0], self._cumulative_rewards[1]) + info["next_team"] = self.team_id + 1 + info["exception"] = f"Invalid action: {action}. Reason: {reason}" + return self.observe(), 0, False, False, info + + assert(isinstance(action, Action)) + + reward, terminated = self._player_step(action) + + """ + NOTE: Clear the states before observation + """ + self._legal_actions = None + self._legal_action_ids = None + self._spotted_enemy_ids = None + + obs = self.observe() + truncated = self.episode_steps >= self.max_episode_steps + self._cumulative_rewards[self.team_id] += reward + + info = {} + info["accum_reward"] = (self._cumulative_rewards[0], self._cumulative_rewards[1]) + info["next_team"] = self.team_id + 1 if not terminated else -1 + + if self.replay_path is not None: + self._frames[self.team_id].append(self.render(mode="rgb_array")) + + if terminated or truncated: + # The eval_episode_return is calculated from Player 1's perspective + info["eval_episode_return"] = reward if self.team_id == 0 else -reward + info["done_reason"] = "Terminated" if terminated else "Truncated" + + if self.replay_path is not None: + self.save_replay() + + return obs, reward, terminated, truncated, info + + def render(self, mode="human", attack_agent=None, defend_agent=None): + if mode == "rgb_array": + player_agents = self.teams[self.team_id].agents.values() + return self._renderer.render(self.current_agents, self.spotted_enemy_agents, attack_agent, defend_agent) + elif mode == "human": + return self.observe() + else: + raise ValueError("Invalid render mode: {}".format(mode)) + + def action_to_id(self, action: Action) -> int: + if action.action_type == ActionType.END_OF_TURN: + return 0 + action_space = self.action_space + if action.agent_id not in action_space.keys(): + raise ValueError(f"Invalid agent_id: {action.agent_id[:8]}") + agent_action_space = action_space[action.agent_id] + if action.action_type not in agent_action_space.keys(): + agent = self.get_agent(action.agent_id) + print(agent.available_types, agent.parked_agents) + print(action) + raise ValueError(f"Invalid action_type: {action.action_type.name} for agent {action.agent_id[:8]}. Available action_types: {agent_action_space.keys()}") + action_id = 1 + for agent_id, agent_action_space in action_space.items(): + for action_type, action_space in agent_action_space.items(): + if agent_id != action.agent_id or action_type != action.action_type: + action_id += action_space.n + else: + if action.action_type == ActionType.SWITCH_WEAPON: # 以下标编码,不以位置编码 + assert action.weapon_idx < action_space.n, f"Invalid weapon_idx: {action.weapon_idx}, action_space.n: {action_space.n}" + action_id += action.weapon_idx + elif action.action_type == ActionType.RELEASE: + assert action.target_id < action_space.n, f"Invalid target_idx: {action.target_id}, action_space.n: {action_space.n}" + action_id += action.target_id # 此处为下标 + else: + pos, des = self.get_agent(action.agent_id).pos, action.des + encode_id = encode_axial((des[0] - pos[0], des[1] - pos[1])) + action_id += encode_id - 1 + if action.action_type == ActionType.MOVE: + cur_range = self.get_agent(action.agent_id).mobility + elif action.action_type == ActionType.ATTACK: + cur_range = self.get_agent(action.agent_id).attack_range + elif action.action_type == ActionType.INTERACT: + cur_range = 1 + assert encode_id - 1 < action_space.n, f"Action {action.action_type.name} Distance: {get_axial_dis(pos, des)}, cur_range: {cur_range} encode_id: {encode_id} action_space.n: {action_space.n}\n{action}" + return int(action_id) + assert False, "Should not reach here" + + def id_to_action(self, action_id: int) -> Action: + if action_id == 0: + return Action(ActionType.END_OF_TURN) + action_id -= 1 + action_space = self.action_space + for agent_id, agent_action_space in action_space.items(): + for action_type, action_space in agent_action_space.items(): + if action_id < action_space.n: + if action_type == ActionType.SWITCH_WEAPON: + return Action(agent_id=agent_id, action_type=action_type, weapon_idx=action_id, weapon_name=self.get_agent(agent_id).weapon.name) + elif action_type == ActionType.RELEASE: + return Action(agent_id=agent_id, target_id=action_id, action_type=action_type) + else: + pos = self.get_agent(agent_id).pos + delta = decode_axial(action_id + 1) # 编码 0 是原位置 + des = (pos[0] + delta[0], pos[1] + delta[1]) + return Action(agent_id=agent_id, des=des, action_type=action_type) + else: + action_id -= action_space.n + raise ValueError("Invalid action_id: {}".format(action_id)) + + def get_agent(self, agent_id: str) -> Agent: + for team in self.teams: + if agent_id in team.agents.keys(): + agent = team.agents[agent_id] + # if not agent.alive: + # raise ValueError(f"Agent {agent_id} is not alive") + return agent + raise ValueError(f"Invalid agent id: {agent_id}") + + def get_node(self, pos: Tuple[int, int]) -> TileNode: + if pos not in self.map.nodes: + raise ValueError(f"Invalid position: {pos}") + return self.map.nodes[pos] + + @property + def legal_actions(self) -> List[Action]: + if self._legal_actions is not None: + return self._legal_actions + + map_data = self.map.nodes + player_data = self.current_agents_dict + spotted_enemy_ids = self.spotted_enemy_ids + + _legal_actions = [Action(ActionType.END_OF_TURN)] # Default action + + for agent_id, agent in player_data.items(): + if agent.commenced_action or not agent.alive or agent.pos == (-1, -1) or agent.is_carried: + continue + pos = agent.pos + # 移动 + mobility = min(agent.fuel, agent.mobility) + if mobility > 0: + adj_pos = get_adj_pos(pos, int(mobility)) + aval_data = { + pos: node + for pos in adj_pos if pos in map_data and + ( + (node := map_data[pos]).team_id != self.enemy_id + or + node.agent_id not in spotted_enemy_ids + ) + } + aval_nodes = astar_search(aval_data, agent.move_type, start=pos, limit=mobility) + for des in aval_nodes: + if des == pos: + continue + node = map_data[des] + if node.team_id != self.team_id and node.agent_id not in spotted_enemy_ids: + # 一个格子只能有一个单位 + _legal_actions.append(Action( + agent_id=agent_id, + des=des, + action_type=ActionType.MOVE + )) + elif node.team_id == self.team_id and agent.agent_type in (target := self.get_agent(node.agent_id)).available_types \ + and len(target.parked_agents) < target.capacity: + # 除非终点有可以停靠的单位 + _legal_actions.append(Action( + agent_id=agent_id, + target_id=node.agent_id, + des=des, + action_type=ActionType.MOVE + )) + # 进攻 + if agent.attack_range > 0 and agent.ammo > 0: + for des in get_adj_pos(pos, agent.attack_range): + if des not in map_data: + continue + node = map_data[des] + if node.agent_id in spotted_enemy_ids and self.get_agent(node.agent_id).move_type in agent.strike_types: + # 已发现敌军 + _legal_actions.append(Action( + agent_id=agent_id, + target_id=node.agent_id, + action_type=ActionType.ATTACK, + des=des + )) + # 交互 + for des in get_adj_pos(pos, 1): + if des not in map_data: + continue + node = map_data[des] + if node.team_id == self.team_id and agent.agent_type in (target :=self.get_agent(node.agent_id)).available_types \ + and len(target.parked_agents) < target.capacity: + _legal_actions.append(Action( + agent_id=agent_id, + target_id=node.agent_id, + des=des, + action_type=ActionType.INTERACT + )) + # 释放 + if agent.parked_agents: + for des in get_adj_pos(pos, 1): + if des not in map_data: + continue + node = map_data[des] + if node.team_id != -1: + continue + for idx, agent_to_release in enumerate(agent.parked_agents): + if get_cost(agent_to_release.move_type, node.terrain_type) != 0: + _legal_actions.append(Action( + agent_id=agent_id, + target_id=idx, + des=des, + action_type=ActionType.RELEASE + )) + break + # 切换武器 + if agent.switchable_weapons: + for des in get_adj_pos(pos, 1): + if des not in map_data: + continue + node = map_data[des] + if node.team_id == self.team_id and self.get_agent(node.agent_id).has_supply: + for weapon_idx, weapon in enumerate(agent.switchable_weapons): + if not agent.weapon or weapon.name != agent.weapon.name: + _legal_actions.append(Action( + agent_id=agent_id, + target_id=weapon.name, + weapon_idx=weapon_idx, + action_type=ActionType.SWITCH_WEAPON + )) + + + self._legal_actions = _legal_actions + return self._legal_actions + + @property + def legal_action_ids(self) -> List[int]: + if self._legal_action_ids is not None: + return self._legal_action_ids + self._legal_action_ids = [self.action_to_id(action) for action in self.legal_actions] + return self._legal_action_ids + + @property + def spotted_enemy_ids(self) -> List[str]: + if not self.war_fog: + return self.teams[self.enemy_id].alive_ids + + if self._spotted_enemy_ids is not None: + return self._spotted_enemy_ids + + player_agents = self.current_agents + enemy_data = self.enemy_agents_dict + map_data = self.map.nodes + _spotted_enemy_ids = [] + + for agent in player_agents: + info_level = agent.info_level + for des in get_adj_pos(agent.pos, agent.info_level): + if des not in map_data: + continue + node = map_data[des] + dis = get_axial_dis(agent.pos, des) + if dis == 1: # Adjacent agents are always spotted + dis = -1e9 + if node.team_id == self.enemy_id \ + and enemy_data[node.agent_id].stealth_level < info_level - dis + 1: + _spotted_enemy_ids.append(node.agent_id) + + self._spotted_enemy_ids = _spotted_enemy_ids + return self._spotted_enemy_ids + + def save_replay(self): + if not os.path.exists(self.replay_path): + os.makedirs(self.replay_path) + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + for team_id in range(self.max_team): + path = os.path.join( + self.replay_path, + f"tianqiong_{timestamp}_Team{team_id + 1}.mp4" + ) + self.display_frames_as_mp4(self._frames[team_id], path) + print(f'replay {path} saved!') + + @staticmethod + def display_frames_as_mp4(frames: list, path: str, fps=4) -> None: + assert path.endswith('.mp4'), f'path must end with .mp4, but got {path}' + import imageio + imageio.mimwrite(path, frames, fps=fps) + + def _make_env(self): + # Configuration for file paths + map_path = f"{self.data_path}/MapInfo.json" + agents_path = f"{self.data_path}/AgentsInfo.json" + + # Load map and agents information + with open(map_path, 'r') as f: + origin_map = json.load(f) + with open(agents_path, 'r') as f: + origin_agents = json.load(f) + + _nodes = { + (node := dict_to_node(node_dict)).pos: node for node_dict in origin_map + } + agents_dict_lists = [ + [agent_dict for agent_dict in origin_agents if agent_dict["team_id"] == team_id] + for team_id in range(self.max_team) + ] + _agents = [ + { + (agent := dict_to_agent(agent_dict)).agent_id: agent + for agent_dict in agents_dict_lists[team_id] + } + for team_id in range(self.max_team) + ] + _teams = [ + Team( + team_id=_team_id, + faction_id=_agents[_team_id][0].faction_id, + agents=_agents[_team_id] + ) + for _team_id in range(self.max_team) + ] + + for team in _teams: + for agent_id, agent in team.agents.items(): + if agent.pos == (-1, -1): # Illegal position for padding + continue + assert agent.pos in _nodes, f"Invalid agent position: {agent.pos}" + _nodes[agent.pos].agent_id = agent_id + _nodes[agent.pos].team_id = team.team_id + + _map = Map(_nodes) + self.map = _map + self.teams = _teams + self.obs_shape = _map.width, _map.height + + # Get action space + self._action_spaces = [ + gym.spaces.Dict( + { + f"team_{team.team_id + 1}": gym.spaces.Dict( + { + ActionType.END_OF_TURN: gym.spaces.Discrete(1) # Default action + } + ), + **{ + agent_id: agent.action_space for agent_id, agent in team.agents.items() + } + } + ) + for team in self.teams + ] + + self._action_space_sizes = [ + sum(action_space.n for agent_action_space in self._action_spaces[team_id].values() for action_space in agent_action_space.values()) + for team_id in range(self.max_team) + ] + + self._team_id = 0 + for action_space_size in self._action_space_sizes: + for action_id in range(action_space_size): + assert self.action_to_id(self.id_to_action(action_id)) == action_id + self._team_id += 1 + + # Get observation space + self._observation_spaces = [ + gym.spaces.Dict( + { + "action_mask": gym.spaces.Box(0, 1, (self._action_space_sizes[team_id], ), dtype=np.int8), # Different size for each team + "union_endurance": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "union_info_level": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16), + "union_stealth_level": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16), + "union_mobility": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "union_defense": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16), + "union_damage": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "union_fuel": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "union_ammo": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16), + "enemy_endurance": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "enemy_info_level": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16), + "enemy_stealth_level": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16), + "enemy_mobility": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "enemy_defense": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16), + "enemy_damage": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "enemy_fuel": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.float32), + "enemy_ammo": gym.spaces.Box(0, 1000, self.obs_shape, dtype=np.int16) + } + ) + for team_id in range(self.max_team) + ] + + self._reward_space = gym.spaces.Box(low=-1000, high=1000, shape=(1, ), dtype=np.float32) + + # 预处理己方联盟和所有敌方的单位信息 + self._union_agents = [[] for _ in range(self.max_faction)] + self._union_agents_dict = [{} for _ in range(self.max_faction)] + self._enemy_agents = [[] for _ in range(self.max_faction)] + self._enemy_agents_dict = [{} for _ in range(self.max_faction)] + for team_id in range(self.max_team): + faction_id = self.teams[team_id].faction_id + agents_list = list(self.teams[team_id].agents.values()) + agents_dict = self.teams[team_id].agents + self._union_agents[faction_id].extend(agents_list) + for other_faction_id in range(self.max_faction): + if other_faction_id != faction_id: + self._enemy_agents[other_faction_id].extend(agents_list) + self._enemy_agents_dict[other_faction_id] |= agents_dict + + + @property + def team_id(self): + return self._team_id + + @property + def current_agents(self): + return self.teams[self._team_id].agents.values() + + @property + def current_agents_dict(self): + return self.teams[self._team_id].agents + + @property + def enemy_agents(self): + return self._enemy_agents[self.faction_id] + # if self._enemy_agents is not None: + # return self._enemy_agents + # _enemy_agents = [] + # for team_id in range(self.max_team): + # if self.teams[team_id].faction_id != self.faction_id: + # _enemy_agents.extend(list(self.teams[team_id].agents.values())) + # self._enemy_agents = _enemy_agents + # return self._enemy_agents + + @property + def enemy_agents_dict(self): + return self._enemy_agents_dict[self.faction_id] + # if self._enemy_agents_dict is not None: + # return self._enemy_agents_dict + # _enemy_agents_dict = {} + # for team_id in range(self.max_team): + # if self.teams[team_id].faction_id != self.faction_id: + # for agent_id, agent in self.teams[team_id].agents.items(): + # _enemy_agents_dict[agent_id] = agent + # self._enemy_agents_dict = _enemy_agents_dicct + # return self._enemy_agents_dict + + @property + def spotted_enemy_agents(self): + if self._spotted_enemy_agents is not None: + return self._spotted_enemy_agents + _spotted_enemy_agents = [self.enemy_agents_dict[enemy_id] for enemy_id in self.spotted_enemy_ids] + self._spotted_enemy_agents = _spotted_enemy_agents + + @property + def faction_id(self): + return self.teams[self._team_id].faction_id + + @property + def observation_space(self): + return self._observation_spaces[self.team_id] + + @property + def action_space(self): + return self._action_spaces[self.team_id] + + @property + def action_space_size(self): + return self._action_space_sizes[self.team_id] + + @property + def reward_space(self): + return self._reward_space + + def random_action(self) -> Action: + return np.random.choice(self.legal_actions) + + def next_turn(self): + self._team_id = (self._team_id + 1) % self.max_team + + def bot_action(self) -> Action: + if self.bot_version == "v0": + attack_actions = [action for action in self.legal_actions if action.action_type == ActionType.ATTACK] + if attack_actions: + return np.random.choice(attack_actions) + else: + return self.random_action() + else: + raise NotImplementedError(f"Invalid bot version: {self.bot_version}") + + def _player_step(self, action: Action): + if action.action_type == ActionType.END_OF_TURN: + """ + NOTE: here exchange the player + """ + # 清除动作标记 + for agent in self.current_agents: + agent.commenced_action = False + # 切换玩家 + self.episode_steps += 1 + if not self.use_real_engine: + self.next_turn() + reward, terminated = 0, False # 这里可以加上惩罚项 + elif action.action_type == ActionType.MOVE: + reward = self._move(action.agent_id, action.des, action.target_id) + terminated = False + elif action.action_type == ActionType.ATTACK: + reward, terminated = self._attack(action.agent_id, action.target_id) + elif action.action_type == ActionType.INTERACT: + reward = self._interact(action.agent_id, action.target_id) + terminated = False + elif action.action_type == ActionType.RELEASE: + reward = self._release(action.agent_id, action.target_id, action.des) + terminated = False + elif action.action_type == ActionType.SWITCH_WEAPON: + reward = self._switch_weapon(action.agent_id, action.weapon_idx) + terminated = False + else: + raise ValueError(f"Invalid action type: {action.action_type}") + + for agent in self.current_agents: + for pos in get_adj_pos(agent.pos, 1): + if pos in self.map.nodes and self.map.nodes[pos].team_id == self.team_id: + adj_agent = self.get_agent(self.map.nodes[pos].agent_id) + if not adj_agent.has_supply: + continue + module = adj_agent.supply + if module.add_endurance > 0: + agent.endurance += min(module.add_endurance, agent.max_endurance - agent.endurance) + else: + agent.endurance = agent.max_endurance + if agent.weapon is not None: + if module.add_ammo > 0: + agent.weapon.ammo += min(module.add_ammo, agent.weapon.max_ammo - agent.weapon.ammo) + else: + agent.weapon.ammo = agent.weapon.max_ammo + if module.add_fuel > 0: + agent.fuel += min(module.add_fuel, agent.max_fuel - agent.fuel) + else: + agent.fuel = agent.max_fuel + + return reward, terminated + + def _move(self, agent_id, des, target_id): + map_data = self.map.nodes + agent = self.current_agents_dict[agent_id] + spotted_enemy_ids = self.spotted_enemy_ids + origin_pos = agent.pos + # 清除原位置关联 + node = map_data[agent.pos] + node.agent_id = None + node.team_id = -1 + + mobility = min(agent.mobility, agent.fuel) + adj_pos = get_adj_pos(agent.pos, int(mobility)) + aval_data = { + pos: node + for pos in adj_pos if pos in map_data and + ( + (node := map_data[pos]).team_id != self.enemy_id + or + node.agent_id not in spotted_enemy_ids + ) + } + path = astar_search(aval_data, move_type=agent.move_type, start=agent.pos, goal=des, limit=mobility) + path = [agent.pos] + path + + exception = False + + for cur, nxt in zip(path[:-1], path[1:]): + if self.war_fog and map_data[nxt].team_id == self.enemy_id: # 前进方向遇到敌军,停下 + exception = True + break + if self.replay_path is not None: + self._frames[self.team_id].append(self.render(mode="rgb_array")) + agent.fuel -= get_cost(agent.move_type, map_data[nxt].terrain_type) + agent.pos = nxt + + # 更新位置以及与地图的关联 + cur = agent.pos + agent.commenced_action = True + + if map_data[cur].team_id == -1: + map_data[cur].agent_id = agent_id + map_data[cur].team_id = self.team_id + elif map_data[cur].agent_id == target_id: + carry_agent = self.get_agent(target_id) + assert agent.team_id == carry_agent.team_id + assert agent.agent_type in carry_agent.available_types, f"{agent} not in {carry_agent.available_types} (id: {carry_agent.agent_id[:8]}). " + assert len(carry_agent.parked_agents) < carry_agent.capacity, f"Agent {carry_agent[:8]} is available but full" + carry_agent.parked_agents.append(agent) + agent.is_carried = True + else: + assert exception + + return 0 + + def _attack(self, attack_id, target_id): + agent = self.current_agents[attack_id] + target_agent = self.teams[self.enemy_id].agents[target_id] + + if self.replay_path is not None: + self._frames[self.team_id].extend([self.render(mode="rgb_array", attack_agent=agent, defend_agent=target_agent)] * 4) + + damage = max(agent.damage - target_agent.defense, 0) + damage = min(damage, target_agent.endurance) + target_agent.endurance -= damage + agent.weapon.ammo -= 1 + agent.commenced_action = True + + if target_agent.endurance <= 0: + # 删除与地图关联 + node = self.get_node(target_agent.pos) + node.agent_id = None + node.team_id = -1 + + if self.teams[self.enemy_id].alive_count == 0: + return damage, True + + return damage, False + + def _interact(self, agent_id, target_id): + agent = self.get_agent(agent_id) + target = self.get_agent(target_id) + target.parked_agents.append(agent) + agent.is_carried = True + return 0 # 后续可以考虑修改奖励为补给的线性组合 + + def _release(self, agent_id: str, target_idx: int, des: Tuple[int, int]): + agent = self.get_agent(agent_id) + agent_to_release = agent.parked_agents[target_idx] + agent_to_release.pos = des + node = self.get_node(des) + node.team_id = agent_to_release.team_id + node.agent_id = agent_to_release.agent_id + + for module in agent.modules: + if module.add_endurance > 0: + agent_to_release.endurance += min(module.add_endurance, agent_to_release.max_endurance - agent_to_release.endurance) + else: + agent_to_release.endurance = agent_to_release.max_endurance + if agent_to_release.weapon is not None: + if module.add_ammo > 0: + agent_to_release.weapon.ammo += min(module.add_ammo, agent_to_release.weapon.max_ammo - agent_to_release.weapon.ammo) + else: + agent_to_release.weapon.ammo = agent_to_release.weapon.max_ammo + if module.add_fuel > 0: + agent_to_release.fuel += min(module.add_fuel, agent_to_release.max_fuel - agent_to_release.fuel) + else: + agent_to_release.fuel = agent_to_release.max_fuel + agent_to_release.is_carried = False + + return 0 # 后续可以考虑修改奖励为补给的线性组合 + + def _switch_weapon(self, agent_id: str, weapon_idx: int): + agent = self.get_agent(agent_id) + agent.weapon.reset() + agent.weapon = agent.switchable_weapons[weapon_idx] + return 0 + + def observe(self): + player_agents = self.current_agents + spotted_enemy_agents = self.spotted_enemy_agents + + obs = {} + + for desc, space in self.observation_space.items(): + obs[desc] = np.zeros(space.shape, dtype=space.dtype) + + legal_actions_ids = self.legal_action_ids + + for action_id in legal_actions_ids: + obs["action_mask"][action_id] = 1 + + for agent in player_agents: + pos = agent.pos + obs["player_info_level"][pos[0], pos[1]] = agent.info_level + obs["player_stealth_level"][pos[0], pos[1]] = agent.stealth_level + obs["player_mobility"][pos[0], pos[1]] = agent.mobility + obs["player_defense"][pos[0], pos[1]] = agent.defense + obs["player_damage"][pos[0], pos[1]] = agent.damage + obs["player_fuel"][pos[0], pos[1]] = agent.fuel + obs["player_ammo"][pos[0], pos[1]] = agent.ammo + obs["player_endurance"][pos[0], pos[1]] = agent.endurance + + for agent in spotted_enemy_agents: + pos = agent.pos + obs["enemy_info_level"][pos[0], pos[1]] = agent.info_level + obs["enemy_stealth_level"][pos[0], pos[1]] = agent.stealth_level + obs["enemy_mobility"][pos[0], pos[1]] = agent.mobility + obs["enemy_defense"][pos[0], pos[1]] = agent.defense + obs["enemy_damage"][pos[0], pos[1]] = agent.damage + obs["enemy_fuel"][pos[0], pos[1]] = agent.fuel + obs["enemy_ammo"][pos[0], pos[1]] = agent.ammo + obs["enemy_endurance"][pos[0], pos[1]] = agent.endurance + + return obs + + def command_move(self, agent_id, des: Union[Tuple[int, int], List[Tuple[int, int]]], start_time=0, subtask=False) -> Command: + agent = self.get_agent(agent_id) + state = agent.todo[-1].state if agent.todo else agent.state + if state.pos == des or state.pos in des: + return + + map_data = self.map.nodes + origin_todo_length = len(agent.todo) # 记录原 todo 的长度 + spotted_enemy_ids = self.spotted_enemy_ids + adj_pos = get_adj_pos(state.pos, int(state.fuel)) + adj_pos.append(state.pos) + aval_data = { + pos: node + for pos in adj_pos if pos in map_data and + ( + (node := map_data[pos]).team_id != self.enemy_id + or + node.agent_id not in spotted_enemy_ids + ) + } + path = get_path(agent=agent, map_data=aval_data, start=state.pos, des=des, limit=state.fuel) # 可能没有可行路径 + + for pos in path: + state.fuel -= get_cost(agent.move_type, map_data[pos].terrain_type) + state.pos = pos + agent.todo.append(Action(agent_id=agent_id, action_type=ActionType.MOVE, des=pos, start_time=start_time, state=state)) + + command = Command(agent_id=agent_id, + action_type=ActionType.MOVE, + des=des, + start_time=start_time, + end_time=start_time+len(agent.todo)-origin_todo_length-1, + state=state) + + if not subtask: + end_time = command.end_time + for i in range(origin_todo_length, len(agent.todo)): + agent.todo[i].start_time = start_time + i - origin_todo_length + agent.todo[i].end_time = end_time + agent.todo[-1].end_type = ActionType.MOVE + agent.cmd_todo.append(command) + + return command + + def command_attack(self, attack_id, target_id, attack_count=1, start_time=0, subtask=False) -> Command: + agent = self.get_agent(attack_id) + target_agent = self.get_agent(target_id) + + origin_todo_length = len(agent.todo) # 记录原 todo 的长度 + + state = agent.todo[-1].state if agent.todo else agent.state + + if target_agent.move_type not in state.weapon.strike_types: + flag = False + no_enough_ammo = False + for weapon_idx, weapon in enumerate(agent.switchable_weapons): + if weapon.strike_types and target_agent.move_type in weapon.strike_types: + if weapon.max_ammo < attack_count: + no_enough_ammo = True + try: + self.command_switch(attack_id, weapon_idx, start_time=start_time, subtask=True) + flag = True + break + except Exception as e: + agent.todo = agent.todo[:origin_todo_length] + raise ValueError("Failed switching to the proper weapon") + if not flag: + if no_enough_ammo: + agent.todo = agent.todo[:origin_todo_length] + raise ValueError("Agent has the striking weapon but no one with enough ammo") + else: + agent.todo = agent.todo[:origin_todo_length] + raise ValueError("No proper weapon found") + + state = agent.todo[-1].state if agent.todo else agent.state + + if state.weapon.ammo < attack_count: + if state.weapon.max_ammo >= attack_count: # 不需要换武器,尝试补给 + try: + self.command_supply(attack_id, start_time=start_time, subtask=True) + except Exception as e: + agent.todo = agent.todo[:origin_todo_length] + raise ValueError("No enough ammo and failed to get supplies") + else: # 需要换武器 + flag = False + for weapon_idx, weapon in enumerate(agent.switchable_weapons): + if weapon.strike_types and target_agent.move_type in weapon.strike_types and weapon.max_ammo >= attack_count: + try: + self.command_switch(attack_id, weapon_idx, start_time=start_time, subtask=True) + flag = True + break + except Exception as e: + agent.todo = agent.todo[:origin_todo_length] + raise ValueError("Failed switching to the proper weapon") + if not flag: + agent.todo = agent.todo[:origin_todo_length] + raise ValueError("No proper weapon has enough ammo") + + map_data = self.map.nodes + spotted_enemy_ids = self.spotted_enemy_ids + possible_des = [ + pos for pos in get_adj_pos(target_agent.pos, int(agent.attack_range)) + if pos in map_data and + ( + (node := map_data[pos]).team_id != self.enemy_id + or + node.agent_id not in spotted_enemy_ids + ) + ] + + try: + self.command_move(attack_id, possible_des, start_time=start_time, subtask=True) + except Exception as e: + agent.todo = agent.todo[:origin_todo_length] + raise ValueError("Failed to move to the target") + + state = agent.todo[-1].state if agent.todo else agent.state + + for _ in range(attack_count): + state.weapon.ammo -= 1 + agent.todo.append(Action(agent_id=attack_id, + action_type=ActionType.ATTACK, + target_id=target_id, + des=target_agent.pos, + start_time=start_time, + state=state)) + + command = Command(agent_id=attack_id, + action_type=ActionType.ATTACK, + target_id=target_id, + des=target_agent.pos, + attack_count=attack_count, + start_time=start_time, + end_time=start_time+len(agent.todo)-origin_todo_length-1, + state=state) + + if not subtask: + end_time = command.end_time + for i in range(origin_todo_length, len(agent.todo)): + agent.todo[i].start_time = start_time + i - origin_todo_length + agent.todo[i].end_time = end_time + agent.todo[-1].end_type = ActionType.ATTACK + agent.cmd_todo.append(command) + + return command + + def command_supply(self, agent_id, start_time=0, subtask=False) -> Command: + """ + 需要补给的单位移动至有补给模块的单位附近 + """ + agent = self.get_agent(agent_id) + player_agents = self.teams[agent.team_id].agents.values() + map_data = self.map.nodes + origin_todo_length = len(agent.todo) # 记录原 todo 的长度 + + possible_des = [] + + for _agent in player_agents: + if _agent.has_supply: + for pos in get_adj_pos(_agent.pos, 1): + if pos in map_data: + possible_des.append(pos) + + # print(possible_des) + + self.command_move(agent_id, possible_des, start_time, subtask=True) + + des = agent.todo[-1].des if agent.todo else agent.pos + for pos in get_adj_pos(des, 1): + if pos in map_data and map_data[pos].team_id == agent.team_id and self.get_agent(map_data[pos].agent_id).has_supply: + supply_id = map_data[pos].agent_id + supply_module = self.get_agent(supply_id).supply + + state = agent.todo[-1].state if agent.todo else agent.state + + if supply_module.add_endurance > 0: + state.endurance = min(agent.max_endurance, state.endurance + supply_module.add_endurance) + else: + state.endurance = agent.max_endurance + if supply_module.add_ammo > 0: + state.weapon.ammo = min(state.weapon.max_ammo, state.weapon.ammo + supply_module.add_ammo) + else: + state.weapon.ammo = state.weapon.max_ammo + if supply_module.add_fuel > 0: + state.fuel = min(agent.max_fuel, state.fuel + supply_module.add_fuel) + else: + state.fuel = agent.max_fuel + + command = Command(agent_id=agent_id, + action_type=ActionType.SUPPLY, + des=des, + start_time=start_time, + end_time=start_time+len(agent.todo)-origin_todo_length-1, + state=state) + + if not subtask: + end_time = command.end_time + for i in range(origin_todo_length, len(agent.todo)): + agent.todo[i].start_time = start_time + i - origin_todo_length + agent.todo[i].end_time = end_time + agent.todo[-1].end_type = ActionType.SUPPLY + agent.cmd_todo.append(command) + + return command + + def command_switch(self, agent_id, weapon_idx, start_time=0, subtask=False) -> Command: + agent = self.get_agent(agent_id) + origin_todo_length = len(agent.todo) # 记录原 todo 的长度 + self.command_supply(agent_id, subtask=True) + state = agent.todo[-1].state if agent.todo else agent.state + agent.todo.append(Action(agent_id=agent_id, + action_type=ActionType.SWITCH_WEAPON, + weapon_idx=weapon_idx, + weapon_name=agent.switchable_weapons[weapon_idx].name, + des=state.pos, + start_time=start_time, + state=state)) + + command = Command(agent_id=agent_id, + action_type=ActionType.SWITCH_WEAPON, + weapon_idx=weapon_idx, + weapon_name=agent.switchable_weapons[weapon_idx].name, + des=state.pos, + start_time=start_time, + end_time=start_time+len(agent.todo)-origin_todo_length-1, + state=state) + + if not subtask: + end_time = command.end_time + for i in range(origin_todo_length, len(agent.todo)): + agent.todo[i].start_time = start_time + i - origin_todo_length + agent.todo[i].end_time = end_time + agent.todo[-1].end_type = ActionType.SWITCH_WEAPON + agent.cmd_todo.append(command) + + return command + + def make_plan(self, command: Command, subtask=False): + if command.action_type == ActionType.MOVE: + return self.command_move(command.agent_id, command.des, start_time=command.start_time, subtask=subtask) + elif command.action_type == ActionType.ATTACK: + return self.command_attack(command.agent_id, command.target_id, attack_count=command.attack_count, start_time=command.start_time, subtask=subtask) + elif command.action_type == ActionType.SUPPLY: + return self.command_supply(command.agent_id, start_time=command.start_time, subtask=subtask) + elif command.action_type == ActionType.SWITCH_WEAPON: + return self.command_switch(command.agent_id, command.weapon_idx, start_time=command.start_time, subtask=subtask) + else: + raise ValueError(f"Invalid command type: {command.action_type}") + + def todo_action(self, agent_id: str, retry=0) -> Optional[Action]: + agent = self.get_agent(agent_id) + + if not agent.todo: + return None + + action = agent.todo.pop(0) + valid, msg = self.check_validity(action) + + if valid or retry > 2: + if agent.cmd_todo[0].action_type == action.end_type: + agent.cmd_todo.pop(0) + return action + + print(f"Retrying to plan actions for {agent_id}: {msg}") + + if action.action_type == ActionType.RELEASE: + for pos in get_adj_pos(action.des, 1): + if pos in self.map.nodes and not self.map.nodes[pos].agent_id: + action.des = pos + return action + return None # Wait + + command = agent.cmd_todo.pop(0) + + # 处理异常 + while agent.todo and agent.todo[0].end_type != command.action_type: + agent.todo.pop(0) + if agent.todo: + agent.todo.pop(0) + + current_actions = agent.todo.copy() + current_commands = agent.cmd_todo.copy() + + self.make_plan(command) + agent.todo.extend(current_actions) + agent.cmd_todo.extend(current_commands) + return self.todo_action(agent_id, retry=retry+1) + + def check_validity(self, action: Action) -> Tuple[bool, str]: + """ + 检查动作是否合法 + """ + if action.action_type == ActionType.END_OF_TURN: + return True, "OK" + + map_data = self.map.nodes + spotted_enemy_ids = self.spotted_enemy_ids + if action.agent_id not in self.teams[self.player_id].agents: + return False, f"Invalid agent id: {action.agent_id}" + + agent = self.get_agent(action.agent_id) + + if action.action_type not in agent.action_space.keys(): + return False, f"Invalid action {action.action_type.name} for agent {agent.agent_id[:8]}" + + if action.action_type == ActionType.MOVE: + des = action.des + + node = map_data[des] + + if node.team_id == agent.team_id and (not agent.agent_type in (target := self.get_agent(node.agent_id)).available_types or len(target.parked_agents) >= target.capacity): + return False, f"{node.agent_id[:8]} at {des} is an ally but not available" + + if node.agent_id in spotted_enemy_ids: + return False, f"{des} has been occupied by enemy {node.agent_id[:8]}" + + if get_axial_dis(agent.pos, des) > agent.mobility: + return False, f"Destination is out of range. Mobility: {agent.mobility} Euclidean distance: {get_axial_dis(agent.pos, des)}" + + try: + mobility = min(agent.fuel, agent.mobility) + adj_pos = get_adj_pos(agent.pos, int(mobility)) + aval_data = { + pos: node + for pos in adj_pos if pos in map_data and + ( + (node := map_data[pos]).team_id != self.enemy_id + or + node.agent_id not in spotted_enemy_ids + ) + } + aval_nodes = astar_search(aval_data, agent.move_type, start=agent.pos, limit=mobility) + if action.des in aval_nodes: + return True, "OK" + else: + return False, f"{des} could not be reached in one step." + except Exception as e: + return False, f"Failed to move to {des}: {e}" + + elif action.action_type == ActionType.ATTACK: + try: + target_agent = self.get_agent(action.target_id) + except Exception as e: + return False, f"Invalid target agent id: {action.target_id}, ignored" + + des = action.des + + if agent.team_id == target_agent.team_id: + return False, f"Cannot attack teammate. Attack: {action.agent_id}. Defend: {action.target_id}" + if agent.weapon is None: + return False, "No weapon equipped" + if agent.ammo <= 0: + return False, "No ammo left" + if target_agent.move_type not in agent.strike_types: + return False, "Target agent cannot be attacked with this weapon" + if get_axial_dis(agent.pos, des) > agent.attack_range: + return False, f"Target agent is out of range. Attack range: {agent.attack_range} Distance: {get_axial_dis(agent.pos, des)}" + + elif action.action_type == ActionType.SWITCH_WEAPON: + weapon_idx = action.weapon_idx + around_supply = False + for pos in get_adj_pos(agent.pos, 1): + if pos in map_data and map_data[pos].agent_id and (target_agent := self.get_agent(map_data[pos].agent_id)).team_id == agent.team_id and target_agent.has_supply: + around_supply = True + if not around_supply: + return False, "Agent is not around a supply module" + if weapon_idx >= len(agent.switchable_weapons): + return False, f"Invalid weapon id {weapon_idx}. Max id: {len(agent.switchable_weapons) - 1}" + + elif action.action_type == ActionType.RELEASE: + if action.target_id >= len(agent.parked_agents): + return False, f"Trying to release an agent that is not in the queue. Target id: {action.target_id}, Current queue: {agent.parked_agents}" + target_agent = agent.parked_agents[action.target_id] + des = action.des + if get_axial_dis(agent.pos, des) > 1: + return False, "Only adjacent positions can be released" + if map_data[des].team_id != -1: + return False, "Cannot release agent to occupied position" + + elif action.action_type == ActionType.INTERACT: + agent = self.get_agent(action.agent_id) + target_agent = self.get_agent(action.target_id) + if get_axial_dis(agent.pos, target_agent.pos) > 1: + return False, "Target agent is out of range" + if agent.team_id != target_agent.team_id: + return False, "Cannot interact with enemy agent" + if agent.agent_type not in target_agent.available_types: + return False, f"Target agent {target_agent.agent_id[:8]} is not interactable with agent {agent}" + if len(target_agent.parked_agents) >= target_agent.capacity: + return False, f"Target agent {target_agent.agent_id[:8]} full" + + return True, "OK" + + def update(self, sync_info): + """ + 使用真实引擎 + """ + sync_agents = sync_info['units'] + spotted_enemies = sync_info['spottedHostiles'] + map_data = self.map.nodes + + reward = 0 + + for sync_agent in [*sync_agents, *spotted_enemies]: + agent_id = sync_agent['agent_id'] + agent = self.get_agent(agent_id) + # 清除与地图关联 + if agent.pos != (-1, -1): + node = map_data[agent.pos] + if node.agent_id == agent_id: + node.agent_id = None + node.team_id = -1 + agent.pos = (sync_agent['pos']['q'], sync_agent['pos']['r']) + agent.fuel = sync_agent['fuel'] + if agent.team_id == self.enemy_id: + reward += agent.endurance - sync_agent['endurance'] + agent.endurance = sync_agent['endurance'] + agent.commenced_action = sync_agent['commenced_action'] + current_weapon = sync_agent['current_weapon'] + if current_weapon: + for weapon in agent.switchable_weapons: + if weapon.name == current_weapon: + agent.weapon = weapon + break + agent.weapon.ammo = sync_agent['ammo'] + else: + agent.weapon = None + # 更新与地图关联 + if agent.alive: + node = map_data[agent.pos] + node.agent_id = agent_id + node.team_id = agent.team_id + + for agent_id in self._spotted_enemy_ids: + agent = self.get_agent(agent_id) + # 清除与地图关联 + if agent.pos != (-1, -1): + node = map_data[agent.pos] + if node.agent_id == agent_id: + node.agent_id = None + node.team_id = -1 + + # 更新敌人感知 + self._spotted_enemy_ids = [enemy['agent_id'] for enemy in spotted_enemies] + + """ + NOTE: Clear the states before observation + """ + self._legal_actions = None + self._legal_action_ids = None + self._spotted_agents = None + + obs = self.observe() + truncated = self.episode_steps >= self.max_episode_steps + terminated = self.teams[self.enemy_id].alive_count == 0 + self._cumulative_rewards[self.player_id] += reward + + info = {} + info["accum_reward"] = (self._cumulative_rewards[0], self._cumulative_rewards[1]) + info["next_player"] = self.player_id + 1 if not terminated else -1 + + if self.replay_path is not None: + self._frames[self.player_id].append(self.render(mode="rgb_array")) + + if terminated or truncated: + # The eval_episode_return is calculated from Player 1's perspective + info["eval_episode_return"] = reward if self.player_id == 0 else -reward + info["done_reason"] = "Terminated" if terminated else "Truncated" + + if self.replay_path is not None: + self.save_replay() + + return obs, reward, terminated, truncated, info + + + + \ No newline at end of file