Files
dojo_cate/tests/scripted.py
T

117 lines
4.4 KiB
Python

"""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 VARIANT_A, 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 = VARIANT_A,
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."""
agents = [ScriptedAgent() for _ in range(players)]
randomness = ScriptedDice(dice)
game = Game(board or fixture_board(), replace(config, deck=deck), agents, randomness)
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()