Initial commit, with rules 0.5 and basic bots
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user