-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengine.py
More file actions
271 lines (227 loc) · 8.82 KB
/
Copy pathengine.py
File metadata and controls
271 lines (227 loc) · 8.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
import random
from dataclasses import dataclass
from typing import List, Tuple, Dict, Union
MAX_TURNS = 150
STARTING_HP = 100
STARTING_AMMO = 10
BULLET_DAMAGE = 25
HEALTH_RESTORE = 30
AMMO_RESTORE = 5
HP_CAP = 100
class PassAction: pass
@dataclass
class MoveAction:
direction: str
@dataclass
class ShootAction:
direction: str
Action = Union[PassAction, MoveAction, ShootAction]
DIRECTION_DELTAS = {
'up': (-1, 0),
'down': ( 1, 0),
'left': ( 0, -1),
'right': ( 0, 1),
}
@dataclass
class TankState:
player_id: int
pos: Tuple[int, int]
hp: int
ammo: int
facing: str
def copy(self) -> 'TankState':
return TankState(self.player_id, self.pos, self.hp, self.ammo, self.facing)
@dataclass
class GameState:
grid: List[List[str]]
powerups: Dict[Tuple[int, int], str]
tanks: List[TankState]
turn: int
max_turns: int = MAX_TURNS
def copy(self) -> 'GameState':
return GameState(
grid = [row[:] for row in self.grid],
powerups = dict(self.powerups),
tanks = [t.copy() for t in self.tanks],
turn = self.turn,
max_turns = self.max_turns,
)
def _get_initial_facing(r: int, c: int) -> str:
if r < 7: return 'down'
if r > 7: return 'up'
if c < 7: return 'right'
return 'left'
def parse_map(map_str: str) -> GameState:
lines = [line.strip() for line in map_str.strip().split('\n') if line.strip()]
if len(lines) != 15:
raise ValueError(f"Invalid map: Expected exactly 15 rows, found {len(lines)}.")
grid = []
powerups = {}
tanks = [None, None]
spawn_count = {'1': 0, '2': 0}
for r, row in enumerate(lines):
if len(row) != 15:
raise ValueError(f"Invalid map: Row {r} has {len(row)} columns; expected exactly 15.")
grid_row = []
for c, char in enumerate(row):
if char not in ('.', '#', 'H', 'A', '1', '2'):
raise ValueError(f"Invalid map: Unsupported character '{char}' at ({r}, {c}).")
if char == '1':
tanks[0] = TankState(0, (r, c), STARTING_HP, STARTING_AMMO, _get_initial_facing(r, c))
grid_row.append('.')
spawn_count['1'] += 1
elif char == '2':
tanks[1] = TankState(1, (r, c), STARTING_HP, STARTING_AMMO, _get_initial_facing(r, c))
grid_row.append('.')
spawn_count['2'] += 1
elif char in ('H', 'A'):
powerups[(r, c)] = char
grid_row.append('.')
else:
grid_row.append(char)
grid.append(grid_row)
if spawn_count['1'] != 1 or spawn_count['2'] != 1:
raise ValueError(f"Invalid map: Must have exactly one '1' and one '2'. Found {spawn_count}.")
return GameState(grid, powerups, tanks, 1)
def is_valid_action(state: GameState, player_id: int, action: Action) -> bool:
if isinstance(action, PassAction):
return True
tank = state.tanks[player_id]
opponent = state.tanks[1-player_id]
if isinstance(action, ShootAction):
return tank.ammo > 0 and action.direction in DIRECTION_DELTAS
if isinstance(action, MoveAction):
if action.direction not in DIRECTION_DELTAS: return False
dr, dc = DIRECTION_DELTAS[action.direction]
nr, nc = tank.pos[0] + dr, tank.pos[1] + dc
if not (0 <= nr < len(state.grid) and 0 <= nc < len(state.grid[0])):
return False
if state.grid[nr][nc] == '#':
return False
if opponent.hp > 0 and (nr, nc) == opponent.pos:
return False
return True
return False
def trace_bullet(grid: List[List[str]], start: Tuple[int, int], direction: str, target_pos: Tuple[int, int]) -> bool:
dr, dc = DIRECTION_DELTAS[direction]
r, c = start[0] + dr, start[1] + dc
while 0 <= r < len(grid) and 0 <= c < len(grid[0]):
if grid[r][c] == '#':
return False
if (r, c) == target_pos:
return True
r += dr
c += dc
return False
def check_winner(state: GameState) -> str:
hp0 = state.tanks[0].hp
hp1 = state.tanks[1].hp
if hp0 <= 0 and hp1 <= 0:
return 'DRAW'
if hp0 <= 0:
return 'PLAYER 1 WINS!'
if hp1 <= 0:
return 'PLAYER 0 WINS!'
if state.turn > state.max_turns:
if hp0 > hp1:
return 'PLAYER 0 WINS!'
if hp1 > hp0:
return 'PLAYER 1 WINS!'
return 'DRAW'
return 'ONGOING'
def apply_actions(state: GameState, a0: Action, a1: Action, resolution_order: Tuple[int, int] = (0, 1)) -> Tuple[GameState, str]:
if resolution_order not in ((0, 1), (1, 0)):
raise ValueError(f"Invalid resolution_order: {resolution_order}. Must be (0, 1) or (1, 0).")
working = state.copy()
submitted_actions = {0: a0, 1: a1}
for i in resolution_order:
tank = working.tanks[i]
if tank.hp <= 0:
continue
action = submitted_actions[i]
if not is_valid_action(working, i, action):
action = PassAction()
if isinstance(action, MoveAction):
dr, dc = DIRECTION_DELTAS[action.direction]
tank.pos = (tank.pos[0] + dr, tank.pos[1] + dc)
tank.facing = action.direction
if tank.pos in working.powerups:
ptype = working.powerups.pop(tank.pos)
if ptype == 'H':
tank.hp = min(tank.hp + HEALTH_RESTORE, HP_CAP)
elif ptype == 'A':
tank.ammo += AMMO_RESTORE
elif isinstance(action, ShootAction):
tank.ammo -= 1
tank.facing = action.direction
opponent = working.tanks[1 - i]
if opponent.hp > 0:
hit = trace_bullet(working.grid, tank.pos, action.direction, opponent.pos)
if hit:
opponent.hp -= BULLET_DAMAGE
working.turn += 1
result = check_winner(working)
return working, result
def make_snapshot(state: GameState, player_id: int) -> dict:
return {
'grid': [row[:] for row in state.grid],
'powerups': dict(state.powerups),
'my_pos': state.tanks[player_id].pos,
'my_hp': state.tanks[player_id].hp,
'my_ammo': state.tanks[player_id].ammo,
'my_facing': state.tanks[player_id].facing,
'opponent_pos': state.tanks[1-player_id].pos,
'opponent_hp': state.tanks[1-player_id].hp,
'opponent_ammo': state.tanks[1-player_id].ammo,
'opponent_facing': state.tanks[1-player_id].facing,
'turn': state.turn,
'player_id': player_id,
'max_turns': state.max_turns,
}
def render_board(state: GameState) -> str:
t0, t1 = state.tanks[0], state.tanks[1]
header = (f"Turn: {state.turn}/{state.max_turns} | " f"P1 HP:{t0.hp} Ammo:{t0.ammo} | " f"P2 HP:{t1.hp} Ammo:{t1.ammo}")
lines = [header]
for r, row in enumerate(state.grid):
line = []
for c, tile in enumerate(row):
pos = (r, c)
if t0.hp > 0 and t0.pos == pos: line.append('1')
elif t1.hp > 0 and t1.pos == pos: line.append('2')
elif pos in state.powerups: line.append(state.powerups[pos])
else: line.append(tile)
lines.append(''.join(line))
return '\n'.join(lines)
def safe_call(bot_func, snapshot: dict) -> Action:
try:
action = bot_func(snapshot)
if isinstance(action, (MoveAction, ShootAction, PassAction)):
return action
return PassAction()
except Exception as e:
print(f'Bot Crashed: {e}')
return PassAction()
@dataclass
class MatchResult:
winner: str
turns_played: int
p0_hp: int
p1_hp: int
def run_match(bot_func_0, bot_func_1, map_str: str, verbose: bool = False, resolution_seed: int = 0) -> MatchResult:
state = parse_map(map_str)
order_rng = random.Random(resolution_seed)
while True:
if verbose:
print(render_board(state))
print('-' * 30)
snap0 = make_snapshot(state, 0)
snap1 = make_snapshot(state, 1)
a0 = safe_call(bot_func_0, snap0)
a1 = safe_call(bot_func_1, snap1)
res_order = (0, 1) if order_rng.randrange(2) == 0 else (1, 0)
state, result = apply_actions(state, a0, a1, resolution_order=res_order)
if result != 'ONGOING':
if verbose:
print(render_board(state))
print(f'Match Concluded! Result: {result}')
return MatchResult(winner = result, turns_played = state.turn - 1, p0_hp = state.tanks[0].hp, p1_hp = state.tanks[1].hp,)