"""Deterministic dice and agents for engine tests.""" from collections import deque from collections.abc import Iterable, Sequence from dataclasses import dataclass, field, replace from pathlib import Path from dojo.board import Board, Pos, build_board from dojo.engine import FightView, Game, GameView from dojo.grid import parse_grid from dojo.movement import Path as Walk from dojo.rules import DEFAULT_RULES, Card, RulesConfig FIXTURE = Path(__file__).parent / "fixtures" / "quadrant_valid.txt" NO_CARDS: tuple[tuple[Card, int], ...] = () class ScriptedDice: """Rolls come from a fixed list; running out means the engine rolled more than expected.""" def __init__(self, rolls: Iterable[int] = ()) -> None: self.rolls = deque(rolls) def d6(self) -> int: if not self.rolls: raise AssertionError("unexpected die roll") return self.rolls.popleft() def shuffle(self, cards: list[Card]) -> None: """Keep the given order, so tests control the deck.""" @dataclass class ScriptedAgent: # Each entry picks the offered path ending on that square, or exactly that path. ends: deque[Pos | Walk] = field(default_factory=deque) start_square: Pos | None = None use_tunnels: bool = False tunnel_destination: Pos | None = None sfida_target: int | None = None irimi_target: int | None = None exam_defender: int | None = None keep_drawn: bool = False plays: frozenset[Card] = frozenset() fight_views: list[FightView] = field(default_factory=list) offered_paths: list[Sequence[Walk]] = field(default_factory=list) path_views: list[GameView] = field(default_factory=list) def place_pawn(self, view: GameView, squares: Sequence[Pos]) -> Pos: if self.start_square is not None and self.start_square in squares: return self.start_square return squares[0] def choose_path(self, view: GameView, paths: Sequence[Walk]) -> Walk: self.offered_paths.append(paths) self.path_views.append(view) if not self.ends: return paths[0] wanted = self.ends.popleft() for path in paths: if path == wanted or (isinstance(wanted, Pos) and path[-1] == wanted): return path raise AssertionError(f"no offered path matches {wanted}") def use_tunnel(self, view: GameView, exit: Pos) -> bool: return self.use_tunnels def choose_tunnel_destination(self, view: GameView, tunnels: Sequence[Pos]) -> Pos: return self.tunnel_destination if self.tunnel_destination is not None else tunnels[0] def choose_sfida_target(self, view: GameView, targets: Sequence[int]) -> int: return self.sfida_target if self.sfida_target is not None else targets[0] def choose_irimi_target(self, view: GameView, targets: Sequence[int]) -> int: return self.irimi_target if self.irimi_target is not None else targets[0] def choose_exam_defender(self, view: GameView, candidates: Sequence[int]) -> int: return self.exam_defender if self.exam_defender is not None else candidates[0] def keep_tecnica(self, view: GameView, held: Card, drawn: Card) -> Card: return drawn if self.keep_drawn else held def play_tecnica(self, view: GameView, fight: FightView) -> bool: self.fight_views.append(fight) return view.tecnica in self.plays def fixture_board() -> Board: return build_board(parse_grid(FIXTURE.read_text(encoding="utf-8"))) def make_game( players: int = 2, config: RulesConfig = DEFAULT_RULES, dice: Iterable[int] = (), deck: tuple[tuple[Card, int], ...] = NO_CARDS, board: Board | None = None, ) -> tuple[Game, list[ScriptedAgent], ScriptedDice]: """A game on the fixture board with an empty deck unless ``deck`` is given. It starts in the first round that allows Kumite; tests of round 1 set ``game.round``. """ agents = [ScriptedAgent() for _ in range(players)] randomness = ScriptedDice(dice) game = Game(board or fixture_board(), replace(config, deck=deck), agents, randomness) game.round = config.first_kumite_round return game, agents, randomness def put(game: Game, player: int, position: Pos, track: int | None = None) -> None: game.players[player].position = position if track is not None: game.players[player].track = track def turn(game: Game, agents: list[ScriptedAgent], player: int, *ends: Pos | Walk) -> None: """Play ``player``'s turn, choosing paths by ``ends`` in order (see ``ScriptedAgent``).""" game.current = player agents[player].ends.extend(ends) game.play_turn()