Skip to content

Commit 4ff7450

Browse files
Fixed an issue where capture replays with random boards could not be loaded.
1 parent ae21f3c commit 4ff7450

File tree

6 files changed

+119
-50
lines changed

6 files changed

+119
-50
lines changed

.pylintrc

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,6 @@ recursive=no
104104
# source root.
105105
source-roots=
106106

107-
# When enabled, pylint would attempt to guess common misconfiguration and emit
108-
# user-friendly hints instead of false-positive error messages.
109-
suggestion-mode=yes
110-
111107
# Allow loading of arbitrary C extensions. Extensions are imported into the
112108
# active Python interpreter and may run arbitrary code.
113109
unsafe-load-any-extension=no
@@ -431,22 +427,27 @@ confidence=HIGH,
431427
# --disable=W".
432428
disable=bad-inline-option,
433429
broad-exception-caught,
430+
consider-iterating-dictionary,
434431
deprecated-pragma,
435432
duplicate-code,
436433
file-ignored,
437434
locally-disabled,
438435
missing-module-docstring,
439436
protected-access,
440437
raw-checker-failed,
438+
redefined-builtin,
441439
superfluous-parens,
442440
suppressed-message,
443441
too-few-public-methods,
444442
too-many-arguments,
445443
too-many-branches,
446444
too-many-instance-attributes,
445+
too-many-lines,
447446
too-many-locals,
448447
too-many-positional-arguments,
449448
too-many-public-methods,
449+
too-many-return-statements,
450+
too-many-statements,
450451
unused-argument,
451452
use-symbolic-message-instead,
452453
use-implicit-booleaness-not-comparison-to-string,
@@ -585,7 +586,7 @@ contextmanager-decorators=contextlib.contextmanager
585586
# List of members which are set dynamically and missed by pylint inference
586587
# system, and so shouldn't trigger E1101 when accessed. Python regular
587588
# expressions are accepted.
588-
generated-members=
589+
generated-members=logging.trace
589590

590591
# Tells whether to warn about missing members when the owner of the attribute
591592
# is inferred to be None.

pacai/capture/bin.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
DEFAULT_BOARD: str = 'capture-medium'
1616
DEFAULT_SPRITE_SHEET: str = 'capture'
1717

18-
RANDOM_BOARD_PREFIX: str = 'random'
19-
2018
def set_cli_args(parser: argparse.ArgumentParser, **kwargs: typing.Any) -> argparse.ArgumentParser:
2119
"""
2220
Set Capture-specific CLI arguments.
@@ -69,13 +67,7 @@ def init_from_args(args: argparse.Namespace) -> tuple[dict[int, pacai.core.agent
6967
base_agent_infos[i] = agent_info
7068

7169
# Check for random boards.
72-
if (args.board.startswith(RANDOM_BOARD_PREFIX)):
73-
board_seed = None
74-
if (args.board != RANDOM_BOARD_PREFIX):
75-
# Strip 'random-' and 'random'.
76-
board_seed = int(args.board.removeprefix(RANDOM_BOARD_PREFIX + '-').removeprefix(RANDOM_BOARD_PREFIX))
77-
78-
args.board = pacai.capture.board.generate(seed = board_seed)
70+
args.board = pacai.capture.game.Game.check_for_random_board(args.board)
7971

8072
return base_agent_infos, [], {}
8173

pacai/capture/game.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import argparse
12
import random
23

34
import pacai.capture.gamestate
@@ -6,6 +7,8 @@
67
import pacai.core.game
78
import pacai.core.gamestate
89

10+
RANDOM_BOARD_PREFIX: str = 'random'
11+
912
class Game(pacai.core.game.Game):
1013
"""
1114
A game following the standard rules of Capture.
@@ -16,3 +19,29 @@ def get_initial_state(self,
1619
board: pacai.core.board.Board,
1720
agent_infos: dict[int, pacai.core.agentinfo.AgentInfo]) -> pacai.core.gamestate.GameState:
1821
return pacai.capture.gamestate.GameState(board = board, agent_infos = agent_infos)
22+
23+
@classmethod
24+
def override_args_with_replay(cls,
25+
args: argparse.Namespace, base_agent_infos: dict[int, pacai.core.agentinfo.AgentInfo]) -> None:
26+
super().override_args_with_replay(args, base_agent_infos)
27+
28+
# Check for random boards.
29+
args.board = Game.check_for_random_board(args.board)
30+
31+
@staticmethod
32+
def check_for_random_board(board: str) -> str | pacai.core.board.Board:
33+
"""
34+
Check if the board string is a random board.
35+
If it is a board string represents a random board, generate and return it.
36+
If the board string is not random, return the original string.
37+
"""
38+
39+
if (not board.startswith(RANDOM_BOARD_PREFIX)):
40+
return board
41+
42+
board_seed = None
43+
if (board != RANDOM_BOARD_PREFIX):
44+
# Strip 'random-' and 'random'.
45+
board_seed = int(board.removeprefix(RANDOM_BOARD_PREFIX + '-').removeprefix(RANDOM_BOARD_PREFIX))
46+
47+
return pacai.capture.board.generate(seed = board_seed)

pacai/capture/game_test.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
3+
import edq.testing.unittest
4+
import edq.util.dirent
5+
6+
import pacai.capture.bin
7+
8+
class GameTest(edq.testing.unittest.BaseTest):
9+
""" Test specifics for capture games. """
10+
11+
def test_load_randomboard_replay(self):
12+
""" Test loading a replay that has a random board. """
13+
14+
temp_dir = edq.util.dirent.get_temp_dir(prefix = 'pacai-test-')
15+
replay_path = os.path.join(temp_dir, 'test.replay')
16+
17+
expected_score = -10
18+
19+
# Run a short capture game and save the replay.
20+
argv = [
21+
'--seed', '4',
22+
'--quiet',
23+
'--board', 'random-6',
24+
'--red', 'capture-team-baseline',
25+
'--blue', 'capture-team-dummy',
26+
'--ui', 'null',
27+
'--max-turns', '80',
28+
'--save-path', replay_path,
29+
30+
]
31+
_, results = pacai.capture.bin.main(argv = argv)
32+
33+
self.assertEqual(expected_score, results[0].score)
34+
35+
# Replay the game and get the same result.
36+
argv = [
37+
'--quiet',
38+
'--ui', 'null',
39+
'--replay-path', replay_path,
40+
41+
]
42+
_, results = pacai.capture.bin.main(argv = argv)
43+
44+
self.assertEqual(expected_score, results[0].score)

pacai/core/game.py

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,43 @@ def _receive_user_inputs(self,
452452
for user_inputs in agent_user_inputs.values():
453453
user_inputs += new_user_inputs
454454

455+
@classmethod
456+
def override_args_with_replay(cls,
457+
args: argparse.Namespace, base_agent_infos: dict[int, pacai.core.agentinfo.AgentInfo]) -> None:
458+
"""
459+
Override the args with the settings from the replay in the args.
460+
Children may extend this for additional functionality.
461+
"""
462+
463+
logging.info("Loading replay from '%s'.", args.replay_path)
464+
replay_info = typing.cast(GameResult, edq.util.json.load_object_path(args.replay_path, GameResult))
465+
466+
# Overrides from the replay info.
467+
args.board = replay_info.game_info.board_source
468+
args.seed = replay_info.game_info.seed
469+
470+
# Special settings for replays.
471+
args.num_games = 1
472+
args.num_training = 0
473+
args.max_turns = len(replay_info.history)
474+
475+
# Script the moves for each agent based on the replay's history.
476+
scripted_actions: dict[int, list[pacai.core.action.Action]] = {}
477+
for item in replay_info.history:
478+
if (item.agent_index not in scripted_actions):
479+
scripted_actions[item.agent_index] = []
480+
481+
scripted_actions[item.agent_index].append(item.get_action())
482+
483+
base_agent_infos.clear()
484+
485+
for (agent_index, actions) in scripted_actions.items():
486+
base_agent_infos[agent_index] = pacai.core.agentinfo.AgentInfo(
487+
name = pacai.util.alias.AGENT_SCRIPTED.short,
488+
move_delay = replay_info.game_info.agent_infos[agent_index].move_delay,
489+
actions = actions,
490+
)
491+
455492
def set_cli_args(parser: argparse.ArgumentParser, default_board: str | None = None) -> argparse.ArgumentParser:
456493
"""
457494
Set common CLI arguments.
@@ -553,7 +590,7 @@ def init_from_args(
553590
# then all the core arguments are loaded differently (directly from the file).
554591
# Use the replay file to override all the current options.
555592
if (args.replay_path is not None):
556-
_override_args_with_replay(args, base_agent_infos)
593+
game_class.override_args_with_replay(args, base_agent_infos)
557594
remove_agent_indexes = []
558595

559596
if (args.board is None):
@@ -635,40 +672,6 @@ def init_from_args(
635672

636673
return args
637674

638-
def _override_args_with_replay(args: argparse.Namespace, base_agent_infos: dict[int, pacai.core.agentinfo.AgentInfo]) -> None:
639-
"""
640-
Override the args with the settings from the replay in the args.
641-
"""
642-
643-
logging.info("Loading replay from '%s'.", args.replay_path)
644-
replay_info = typing.cast(GameResult, edq.util.json.load_object_path(args.replay_path, GameResult))
645-
646-
# Overrides from the replay info.
647-
args.board = replay_info.game_info.board_source
648-
args.seed = replay_info.game_info.seed
649-
650-
# Special settings for replays.
651-
args.num_games = 1
652-
args.num_training = 0
653-
args.max_turns = len(replay_info.history)
654-
655-
# Script the moves for each agent based on the replay's history.
656-
scripted_actions: dict[int, list[pacai.core.action.Action]] = {}
657-
for item in replay_info.history:
658-
if (item.agent_index not in scripted_actions):
659-
scripted_actions[item.agent_index] = []
660-
661-
scripted_actions[item.agent_index].append(item.get_action())
662-
663-
base_agent_infos.clear()
664-
665-
for (agent_index, actions) in scripted_actions.items():
666-
base_agent_infos[agent_index] = pacai.core.agentinfo.AgentInfo(
667-
name = pacai.util.alias.AGENT_SCRIPTED.short,
668-
move_delay = replay_info.game_info.agent_infos[agent_index].move_delay,
669-
actions = actions,
670-
)
671-
672675
def _parse_agent_infos(
673676
agent_indexes: list[int],
674677
raw_args: list[str],

pacai/ui/web.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def _handle_request(self, data_handler: typing.Callable) -> None:
192192
self.end_headers()
193193

194194
if (payload is not None):
195-
self.wfile.write(payload) # type: ignore[arg-type]
195+
self.wfile.write(payload)
196196

197197
def _route(self, path: str, params: dict[str, typing.Any]) -> RequestHandlerResult:
198198
path = path.strip()

0 commit comments

Comments
 (0)