main
zuchaoli 2024-12-18 21:30:53 +08:00
commit 33ced41bc0
20 changed files with 5757 additions and 0 deletions

0
__init__.py 100644
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

540
common/agent.py 100644
View File

@ -0,0 +1,540 @@
from abc import ABC
import copy
from enum import Enum
from typing import List, Tuple
import gymnasium as gym
class MyEnum(Enum):
@classmethod
def from_value(cls, value):
"""根据数值获取相应的枚举成员"""
for member in cls:
if member.value == value:
return member
raise ValueError(f"{value} is not a valid value for {cls.__name__}")
@classmethod
def from_string(cls, name):
"""根据字符串获取相应的枚举成员"""
try:
return cls[name]
except KeyError:
raise ValueError(f"{name} is not a valid {cls.__name__}")
@classmethod
def from_input(cls, input):
"""根据输入(字符串或数值)获取相应的枚举成员"""
if isinstance(input, str):
return cls.from_string(input)
elif isinstance(input, int):
return cls.from_value(input)
else:
raise ValueError("Input must be either a string or an integer.")
class MoveType(MyEnum):
AIR = 0
GROUND = 1
SEA = 2
SUBWATER = 3
class TerrainType(MyEnum):
PLAIN = 0
ROAD = 1
SEA = 2
SUBWATER = 3
class ActionType(MyEnum):
ILLEGAL = -1
END_OF_TURN = 0
MOVE = 1
ATTACK = 2
INTERACT = 3
RELEASE = 4
SWITCH_WEAPON = 5
SUPPLY = 6
class ActionStatus(MyEnum):
VALID = 0
OCCUPIED_DES = 1
OUT_OF_RANGE = 2
class AgentType(MyEnum):
Infantry = 0 # 步兵
Tank = 1 # 装甲单位
AntiAir = 2 # 自行防空炮
MobilizedInfantry = 3 # 机械化步兵
Helicopter = 4 # 直升机
Fighter = 5 # 战斗机
UAV = 6 # 无人机
CombatShip = 7 # 战舰
Carrier = 8 # 航母
Artillery = 9 # 火炮
Construction = 10 # 建筑
MissileLauncher = 11 # 导弹发射车
TransportHelicopter = 12 # 运输直升机
SupplyTruck = 13 # 移动补给车
Submarine = 14 # 潜艇
AdvancedTank = 15 # 先进坦克
Airport = 16 # 机场
SupplyStation = 17 # 补给站
RadarStation = 18 # 雷达站
Bomber = 19 # 轰炸机
AWACS = 20 # 预警机
TransportShip = 21 # 运输船
CommandPost = 22 # 指挥部
class ModuleType(MyEnum):
HANGER = 0
SUPPLY = 1
TRANSPORT = 2
class Weapon:
def __init__(self, name: str, attack_range: int, damage: float, max_ammo: int, ammo: int = None, strike_types: List[MoveType] = []):
self.name = name
self.attack_range = attack_range
self.damage = damage
self.max_ammo = max_ammo
self.ammo = ammo if ammo is not None else max_ammo
self.strike_types = strike_types
def reset(self):
self.ammo = self.max_ammo
def to_dict(self):
return {
"name": self.name,
"attack_range": self.attack_range,
"damage": self.damage,
"max_ammo": self.max_ammo,
"ammo": self.ammo,
"strike_types": [strike_type.name for strike_type in self.strike_types]
}
def __deepcopy__(self, memo):
copied_strike_types = copy.deepcopy(self.strike_types, memo)
return Weapon(self.name, self.attack_range, self.damage, self.max_ammo, self.ammo, copied_strike_types)
class Action:
def __init__(self,
action_type: ActionType=ActionType.ILLEGAL,
agent_id: str = "",
des: Tuple[int, int] = (-1, -1),
target_id: str = "",
weapon_id: int = 0,
weapon_name: str = "",
start_time: int = 0,
end_time: int = -1,
state: 'AgentState' = None,
**kwargs):
self.agent_id = agent_id
self.target_id = target_id
self.des = des
self.action_type = action_type
self.weapon_id = weapon_id
self.weapon_name = weapon_name
self.start_time = start_time
self.end_time = start_time if end_time == -1 else end_time
self.end_type = None
self.state = state
for key, value in kwargs.items():
setattr(self, key, value)
def json(self) -> 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 = []

155
common/renderer.py 100644
View File

@ -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)

301
common/utils.py 100644
View File

@ -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 []
)

View File

@ -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": []
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,89 @@
import sys
sys.path.append("../")
from yes_cmdr.yes_cmdr_env import YesCmdrEnv
from yes_cmdr.common.agent import Action, Command, ActionType
def test_action_id_transform(env):
print("Testing action id transform")
env.reset()
for idx in range(env.action_space_size):
assert env.action_to_id(env.id_to_action(idx)) == idx, f"Expected {env.id_to_action(idx)} as {idx} but got {env.action_to_id(env.id_to_action(idx))}"
env.step(0)
for idx in range(env.action_space_size):
assert env.action_to_id(env.id_to_action(idx)) == idx, f"Expected {env.id_to_action(idx)} as {idx} but got {env.action_to_id(env.id_to_action(idx))}"
def test_random_action(env):
print("Testing random action")
env.reset()
for i in range(100):
action = env.random_action()
assert env.check_validity(action)[0], f"Action {action} is not valid"
env.step(action)
def test_bot_action(env):
print("Testing bot action")
env.reset()
for i in range(100):
action = env.bot_action()
assert env.check_validity(action)[0], f"Action {action} is not valid"
env.step(action)
def test_command(env):
env.reset()
done = False
step_count = 1
for idx in range(env.action_space_size):
assert env.action_to_id(env.id_to_action(idx)) == idx, f"Expected {env.id_to_action(idx)} as {idx} but got {env.action_to_id(env.id_to_action(idx))}"
agents_data = env.teams[0].agents
agents = agents_data.values()
for agent in agents:
print(agent.to_dict())
env.make_plan(Command(agent_id="9f19", target_id="95f3", attack_count=1, action_type=ActionType.ATTACK, start_time=1))
env.make_plan(Command(agent_id="9f19", target_id="2477", attack_count=1, action_type=ActionType.ATTACK, start_time=5))
for idx, action in enumerate(agents_data["9f19"].todo):
print(action)
env.make_plan(Command(agent_id="cc4c", target_id="c53f", attack_count=4, action_type=ActionType.ATTACK))
for idx, action in enumerate(agents_data["cc4c"].todo):
print(action)
while not done and step_count <= 20:
for agent in agents:
if agent.todo and agent.todo[0].start_time <= step_count:
action = env.todo_action(agent.agent_id)
print(action)
obs, reward, terminated, truncated, info = env.step(action)
print(f"Step {step_count}: Action {action.action_type.name} -> 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)

1267
yes_cmdr_env.py 100644

File diff suppressed because it is too large Load Diff