Fix the bots' corner bias; settle seat order and RULES v0.9 (6.2, 6.8)

Bots now pick their start square and break path ties in their own
corner's frame; the absolute tie-break made corners 1 and 2 win 25-28%.
With that fixed, seat order is within target at 4 players, and a +1
stripe first-player bonus (new switch) gains only ~1 point, so no
compensation. RULES.md v0.9 lists the tested alternatives; the plan has
the 6.8 consolidation run and the seat-order study.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 18:27:06 +02:00
parent 52d8af620b
commit 79cbbccbb2
9 changed files with 178 additions and 24 deletions
+11 -9
View File
@@ -1,9 +1,9 @@
# Il Labirinto del Dojo — Rules (draft v0.8)
# Il Labirinto del Dojo — Rules (draft v0.9)
Working language: English. The final gift edition will be produced separately in Italian.
Items marked **[VARIANT]** are unresolved and will be decided by simulation (see
[Rule variants under test](#rule-variants-under-test)).
Every rule has been checked by simulation; alternatives that were tried and set aside are
listed under [Tested alternatives](#tested-alternatives).
## Goal
@@ -174,14 +174,16 @@ All other cards are shown and resolved immediately.
- **Challenger loses**: −3 and sent to Start. The defender gains nothing.
- If the challenger plays Uke and loses, they return to the square they entered from instead of Start.
## Rule variants under test
## Tested alternatives
Parameters to validate by simulation: square mix; game length.
Tried in simulation and set aside; the simulator keeps them as switches:
Tested and set aside (simulator switches only): Exam ties to the defender; best-of-3 Exam;
Kiai +1; a failed Exam costing 0 or 1 stripe instead of 3.
Earlier rules kept as simulator switches: Fatica costing only when you stop on it; Kumite
allowed in the first round.
- Final Exam: ties to the defender; best of 3; the defender fighting with their own belt.
- Kiai +1 instead of +2.
- A failed Exam costing 0 or 1 stripe instead of 3.
- Fatica costing a stripe only when you stop on it.
- Kumite allowed in the first round.
- One extra starting stripe for the first player.
## FAQ
+73 -2
View File
@@ -67,6 +67,8 @@ Deliverables:
| D48 | **6+ players start on 3 (Yellow)**; any player count may start on 3 as a "shorter game" option (saves 15–20% of the time, 6.7). 8 players stays long (107 / 142 min mixed / casual from Yellow) and is accepted: it will rarely be played. Balancing simulations stay at start 0 at every count (developer); `--start 3` reproduces the rules' 6+ setup or the shorter game | Developer, 6.7 results. |
| D49 | **Chance square parked** (6.4.1 and 6.4.3): Fatica on crossing (D36) on board draft 2 (D42) already makes squares matter; 6.4 closed | Developer. |
| D50 | **Length accepted as is; a failed Exam still costs 3 stripes.** L1 (Brown floor against Fatica) rejected; L2 (failed Exam −1 / 0) saves only 3–5 min on the mean and weakens comebacks (6.11.4). `exam_failure_loss` stays a simulator switch. At 4 players mixed and balanced are inside the target; casual (91 / 122) and all-aggressive (90 / 127) stay slightly outside, within the "1–2 h" promise; the shorter-game start on Yellow (D48) is the players' lever | Developer, 6.11 results. |
| D51 | **Bots decide alike in every corner** (start square and tied paths broken in the player's own frame), overriding D40 for this bias: the old tie-break by absolute board order made corners 1 and 2 win 25–28% and corners 0 and 3 23% | Developer, 6.2.1 diagnosis. |
| D52 | **No first-player compensation**: the seat order stays as it is. At 4 players every seat is inside 25% ± 3 pp; +1 stripe for the first player gains about 1 point (6.2.4), not worth an extra rule. `first_player_bonus` stays a simulator switch (no CLI axis) | Developer, 6.2.4 results. |
## Phases
@@ -177,7 +179,7 @@ Deliverables:
- One change at a time. When a change is adopted, re-run the 6.1 baseline so later steps start from the current rules, and log the decision (D-number) here and in `RULES.md`.
- Results go in this file as a table per step (command line, scenario, key measures).
**Next action:** 6.8 consolidation, when the developer says to continue. 6.2 is suspended (D43), 6.9 skipped (D44); 6.4.1 (chance square) stays optional. Order of the steps: D41. Everything up to D50 is committed; commit only when the developer asks.
**Next action:** redo the 6.8 run with the fixed bots (D51), then 6.8.5 and Phase 7. 6.2 is suspended (D43), 6.9 skipped (D44), 6.4.1 parked (D49). Everything up to D52 is committed; commit only when the developer asks.
**Targets** (confirmed by the developer as a starting point, D32):
@@ -249,7 +251,43 @@ Deliverables:
Fix found while planning Dojo tolls: a Brown player (15) walking a Fatica drops to 14 and must not enter the Dojo on that move (the belt changes at once, FAQ). `_path_choices` no longer offers such Dojo paths (`_still_brown_at_dojo`; a Kumite won on the way re-offers paths). The first run without the fix differed by ≤ 3 minutes and ≤ 2 pp per seat.
Findings: (1) at 3 belts the contention targets are met with a wide margin (5% of player-turns, worst player 10–15%). (2) Fatica on crossing on the current board (5 F per quadrant, 20 in all, 10% of the squares) barely touches the heuristic bots (−0.03 to −0.05 per turn) but costs the casual bot −0.12 per turn and adds ~44 minutes (127 mean, p90 174): the board has too many Fatica on ordinary routes for players who do not plan around them. (3) No Kumite in round 1 alone moves seat 3 by about 1 pp (within the ±1.4 pp noise of 1,000 games); 6.2 measures it on 4,000 games. (4) Mixed v0.8 shows seat 0 at 30% and seat 2 at 20%: not seen in the same-bot lineups; to confirm in 6.2 before reading anything into it.
- [ ] 6.2 Seat order — **suspended (D43)**; runs stopped before any result (monitor; developer concerned): 4,000 games, 4 players, lineups mixed and `random`; 2,000 games at 3 and 6 players. If a seat is outside 25% ± 3 pp, diagnose with the 6.0.3 breakdown (hypotheses: moving last means seeing everyone else move first; Exam defender selection; Sensei / Peso cards) and bring candidate fixes to the developer. No fix without agreement.
- [x] 6.2 Seat order (closed: D51, D52) — **reopened by the developer after 6.8.4** (was suspended, D43); runs stopped before any result (monitor; developer concerned): 4,000 games, 4 players, lineups mixed and `random`; 2,000 games at 3 and 6 players. If a seat is outside 25% ± 3 pp, diagnose with the 6.0.3 breakdown (hypotheses: moving last means seeing everyone else move first; Exam defender selection; Sensei / Peso cards) and bring candidate fixes to the developer. No fix without agreement.
- [x] 6.2.1 Diagnosis, 4 players, mixed lineup, 4,000 games per run (scratch `seats.py`). Seat and corner are the same at 4 players, so the run was repeated with the corners rotated by 1, 2, 3 seats and averaged, separating turn order from board position:
| Averaged over | Win rate 0 / 1 / 2 / 3 | First to Brown | Track after round 1 | Net Kumite stripes per game | Attacked per game in rounds 2–3 |
|---|---|---|---|---|---|
| seats (turn order) | 23.9 / 24.2 / 25.5 / 26.4 | 23.9 / 24.0 / 25.7 / 26.4 | 0.74 / 0.76 / 0.76 / 0.76 | +0.08 / +0.44 / +0.79 / +1.12 | 0.95 / 0.78 / 0.54 / 0.41 |
| corners (board position) | 23.3 / 27.6 / 25.5 / 23.6 | 22.6 / 30.4 / 25.4 / 21.6 | 0.70 / 0.81 / 0.81 / 0.69 | +0.56 / +0.54 / +0.49 / +0.84 | 0.64 / 0.76 / 0.70 / 0.57 |
- Corner effect (the larger one, 23–28%) is a **bot artifact**: `bots.start_square` breaks the tie between equally distant Safe Room squares by absolute (row, col), so in the quadrant's own frame corners 0 and 3 start on (1, 2) and corners 1 and 2 on (2, 1), which is the better square. The board itself is symmetric. It also shows that the Safe Room squares are not equivalent (a player picks where to start, RULES.md "Safe areas").
- Turn-order effect (smaller, 24 → 26%, within the 25% ± 3 pp target): later seats are attacked less early in the game (rounds 2–3: 0.95 vs 0.41 times) and end up +1 stripe from Kumite over a game. The earlier seats walk into the open first and the later ones answer.
- Other bot tie-breaks (`max` over paths offered in N, E, S, W order) are also orientation-dependent; not measured yet.
- [x] 6.2.2 Bot fix (developer, D51): `bots.start_square` breaks ties in the Safe Room's own frame (every corner starts from (2, 1) in the quadrant frame, the better square); `HeuristicAgent.place_pawn` uses it; `HeuristicAgent.choose_path` breaks score ties by the path in the player's own frame (`bots.best_of`, `bots.own_frame`). Tests: same start square from every corner, tied paths chosen alike from every corner. Every game changes (fingerprints must be re-recorded; earlier tables stay as recorded, measured with the biased bots).
- Re-run of 6.2.1 with the fix (4 × 4,000 games): corners 24.0 / 25.3 / 25.4 / 25.4 (was 23.3–27.6), track after round 1 equal (0.80–0.81); seats 24.3 / 24.6 / 25.4 / 25.7. Attacked in rounds 2–3 still 0.81 / 0.59 / 0.39 / 0.27 by seat.
- [x] 6.2.3 Seat order with the fixed bots (`dojo sim`, start 0; 4,000 games at 4 players, 2,000 at 3 / 6 / 8):
| Players | Fair share | mixed, seats in turn order | casual, seats in turn order |
|---|---|---|---|
| 3 | 33.3 | 30.6 / 33.7 / 35.7 | 34.0 / 32.0 / 34.0 |
| 4 | 25.0 | 23.2 / 25.5 / 25.5 / 25.8 | 24.0 / 26.0 / 25.7 / 24.3 |
| 6 | 16.7 | 13.4 / 17.3 / 16.4 / 15.4 / 17.9 / 19.6 | 15.2 / 16.1 / 16.2 / 16.2 / 19.0 / 17.2 |
| 8 | 12.5 | 10.1 / 11.5 / 12.0 / 13.5 / 12.0 / 12.5 / 13.8 / 14.6 | 10.8 / 12.6 / 11.9 / 13.1 / 12.2 / 13.9 / 11.7 / 14.0 |
Mixed 4 players now takes 68 min (72 with the biased bots). Reading: the first seat is 1–3 points under its fair share at every count (more with heuristic than casual bots); at 4 players every seat is inside 25% ± 3 pp. At 6 players the two seats alone in a corner (seats 4 and 5, `seat_corners(6)` = 0, 1, 0, 1, 2, 3) win 17–20%, the four sharing a Safe Room 13–17%.
- [x] 6.2.4 First-player compensation, +1 stripe (developer's choice to test): `RulesConfig.first_player_bonus` (default 0; `RulesConfig.start_track(seat)` used by the engine and the metrics). No sim axis yet (scratch `bonus.py`, same seeds as 6.2.3; the bonus-0 rows reproduce 6.2.3 exactly).
| Players | Lineup | First seat, bonus 0 → 1 (fair share) | All seats with bonus 1 | Minutes 0 → 1 |
|---|---|---|---|---|
| 3 | mixed | 30.6 → 33.2 (33.3) | 33.2 / 32.5 / 34.2 | 53 → 52 |
| 3 | casual | 34.0 → 33.8 | 33.8 / 30.9 / 35.4 | 70 → 70 |
| 4 | mixed | 23.2 → 22.6 (25.0) | 22.6 / 25.2 / 26.0 / 26.2 | 68 → 68 |
| 4 | casual | 24.0 → 25.4 | 25.4 / 25.9 / 24.4 / 24.3 | 90 → 90 |
| 6 | mixed | 13.3 → 14.8 (16.7) | 14.8 / 17.3 / 14.8 / 14.9 / 18.1 / 20.1 | 101 → 100 |
| 6 | casual | 15.2 → 16.1 | 16.1 / 16.8 / 15.4 / 16.2 / 19.3 / 16.1 | 130 → 131 |
| 8 | mixed | 10.1 → 10.7 (12.5) | 10.7 / 12.3 / 10.8 / 12.6 / 12.3 / 14.4 / 13.8 / 13.1 | 130 → 128 |
| 8 | casual | 10.8 → 12.0 | 12.0 / 12.1 / 12.6 / 13.1 / 12.2 / 12.0 / 13.6 / 12.4 | 168 → 168 |
Reading: +1 stripe lifts the first seat by about 1 point on average (−0.6 to +2.6; the noise is ±0.7–1 point), not enough to close the gap at 6–8 players with heuristic bots, and nothing at 4 players mixed. No other seat is hurt systematically; length unchanged.
- [x] 6.3 Style dynamics: same-style lineups (`farmer`, `aggressive`, `balanced`, `casual`) and one-against-three (`aggressive,farmer,farmer,farmer`, `farmer,aggressive,aggressive,aggressive`, `balanced,farmer,farmer,farmer`). Watch: all-farmer stalling (length, games finished, Kumite per round), a dominant style, time out of contention.
- Run: `uv run dojo sim --games 1000 --seed 0 --lineup farmer aggressive balanced casual aggressive,farmer,farmer,farmer farmer,aggressive,aggressive,aggressive balanced,farmer,farmer,farmer` (rules v0.8, board draft 2, 4 players; 17 s on 16 workers).
@@ -364,6 +402,39 @@ Deliverables:
Exam pass rate unchanged (40–44%). Reading: −1 saves 3–5 minutes on the mean and 6–12 on the p90; 0 adds only 1–2 more. The price is the comeback: the first Brown keeps retrying instead of dropping back (fewer different challengers), lead changes fall by ~1 per game, the halfway leader wins 3–8 points more, the worst player is out a little longer. No setting brings casual under p90 110.
- [ ] 6.9 Catch-up candidate A — **skipped (D44)**. Was: `RulesConfig` switch, Allenamento +2 for a player 2+ belts behind the leader (bot aware of it); compare on the contention, lead and length measures and on the style win rates.
- [ ] 6.8 Consolidation: the final configuration at 3 / 4 / 6 / 8 players (start 0 as for balancing, plus start 3 at 6 and 8 as in the rules, D48) with all lineups except `random` (D45), 2,000 games each; every target checked; `RULES.md` updated to the next version with the decisions; open items moved to Phase 7.
- [x] 6.8.1 Run (rules v0.9 = v0.8 + D47–D50, board draft 2; 2,000 games per scenario, 3 min 27 s): `uv run dojo sim --games 2000 --seed 0 --players 3 4 6 8 --start 0 --lineup balanced,farmer,aggressive balanced casual aggressive`, and the same with `--players 6 8 --start 3`. All games finished; Exam pass rate 41–44% everywhere; win rate by bot style (mixed) within 4 pp at every count.
| Players | Start | Lineup | Minutes mean / p90 | Exam passed | Halfway leader wins | Out of contention; worst player | Rivals within 1 belt | Win rate by seat |
|---|---|---|---|---|---|---|---|---|
| 3 | 0 | mixed | 54 / 75 | 42% | 50% | 3%; 9% | 0.60 | 29 / 37 / 34 |
| 3 | 0 | balanced | 53 / 72 | 42% | 46% | 3%; 7% | 0.59 | 33 / 34 / 34 |
| 3 | 0 | casual | 71 / 98 | 44% | 45% | 2%; 6% | 1.04 | 33 / 33 / 34 |
| 3 | 0 | aggressive | 64 / 90 | 42% | 52% | 6%; 14% | 0.36 | 29 / 34 / 37 |
| 4 | 0 | mixed | 72 / 98 | 41% | 40% | 4%; 13% | 0.86 | 24 / 27 / 26 / 24 |
| 4 | 0 | balanced | 68 / 92 | 42% | 40% | 3%; 11% | 0.82 | 22 / 26 / 26 / 26 |
| 4 | 0 | casual | 91 / 123 | 43% | 38% | 4%; 12% | 1.41 | 22 / 27 / 24 / 26 |
| 4 | 0 | aggressive | 90 / 128 | 42% | 43% | 8%; 21% | 0.51 | 21 / 26 / 29 / 24 |
| 6 | 0 | mixed | 106 / 144 | 42% | 31% | 9%; 29% | 1.17 | 12 / 16 / 15 / 18 / 20 / 20 |
| 6 | 0 | balanced | 100 / 135 | 42% | 30% | 8%; 28% | 1.07 | 13 / 17 / 13 / 18 / 19 / 20 |
| 6 | 0 | casual | 135 / 182 | 42% | 27% | 8%; 27% | 1.90 | 16 / 15 / 15 / 16 / 20 / 18 |
| 6 | 0 | aggressive | 131 / 183 | 42% | 29% | 12%; 35% | 0.78 | 13 / 19 / 14 / 17 / 20 / 17 |
| 8 | 0 | mixed | 133 / 183 | 42% | 27% | 11%; 37% | 1.44 | 9 / 12 / 11 / 12 / 13 / 13 / 14 / 15 |
| 8 | 0 | balanced | 127 / 172 | 43% | 25% | 11%; 38% | 1.30 | 9 / 11 / 11 / 13 / 13 / 13 / 14 / 15 |
| 8 | 0 | casual | 170 / 231 | 44% | 23% | 11%; 38% | 2.15 | 12 / 12 / 12 / 13 / 14 / 12 / 14 / 12 |
| 8 | 0 | aggressive | 167 / 234 | 43% | 25% | 16%; 43% | 1.01 | 9 / 13 / 11 / 14 / 14 / 12 / 15 / 13 |
| 6 | 3 | mixed | 85 / 123 | 42% | 31% | 8%; 28% | 1.16 | 13 / 17 / 13 / 17 / 20 / 20 |
| 6 | 3 | balanced | 81 / 115 | 42% | 29% | 7%; 28% | 1.17 | 15 / 15 / 15 / 17 / 21 / 18 |
| 6 | 3 | casual | 113 / 160 | 42% | 30% | 8%; 28% | 1.92 | 14 / 16 / 15 / 16 / 19 / 20 |
| 6 | 3 | aggressive | 106 / 158 | 42% | 32% | 12%; 38% | 0.81 | 11 / 18 / 14 / 18 / 22 / 18 |
| 8 | 3 | mixed | 107 / 150 | 42% | 27% | 10%; 38% | 1.51 | 10 / 12 / 10 / 13 / 14 / 13 / 14 / 13 |
| 8 | 3 | balanced | 101 / 145 | 42% | 28% | 11%; 40% | 1.35 | 10 / 12 / 11 / 12 / 15 / 14 / 13 / 13 |
| 8 | 3 | casual | 144 / 203 | 42% | 24% | 11%; 40% | 2.33 | 11 / 12 / 11 / 13 / 13 / 13 / 14 / 13 |
| 8 | 3 | aggressive | 135 / 202 | 41% | 27% | 17%; 49% | 1.05 | 10 / 12 / 10 / 14 / 14 / 14 / 14 / 13 |
- [x] 6.8.2 Targets at 4 players (start 0): inside for mixed and balanced on every measure. Accepted misses (D50): casual 91 / 123 and aggressive 90 / 128 minutes; aggressive seats 21–29% and rivals 0.51 (edge). 3 players: short (53–71 min), halfway leader wins 45–52% (edge of 35–55%), rivals 0.36–0.60 with heuristic bots. 6–8 players: long (D48 accepted), worst player out 27–49% of turns, halfway leader wins 23–32%.
- [x] 6.8.3 `RULES.md` v0.9: D47–D50 in the text; "Rule variants under test" replaced by "Tested alternatives" (the simulator switches).
- [x] 6.8.4 Seat order (finding; studied in 6.2, closed by D51 and D52): the first seat is behind at every count, and more so with more players — 4 players 21–24% (fair 25%), 6 players 11–16% (fair 17%), 8 players 9–12% (fair 12.5%); later seats rise to 20% (6) and 15% (8). At 6 players seats 4 and 5 are the two players alone in a corner (`seat_corners` gives 0, 1, 0, 1, 2, 3) and win most (17–22%), so sharing a Safe Room costs too. 6.2 (suspended, D43) would be the step to study it.
- [ ] 6.8.5 Open items moved to Phase 7.
### Phase 7 — Finalisation (later)
+36 -6
View File
@@ -8,11 +8,11 @@ position, plus closeness to the Dojo once Brown); a ``Profile`` sets its attitud
import math
import random
from collections import deque
from collections.abc import Mapping, Sequence
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from functools import cache
from dojo.board import Board, Pos, Square, is_tunnel
from dojo.board import Board, Pos, Square, is_tunnel, rotate
from dojo.engine import FightView, GameView, PlayerView
from dojo.movement import Path
from dojo.rules import BLACK, Card
@@ -128,9 +128,33 @@ def farthest_from_dojo(board: Board) -> int:
@cache
def start_square(board: Board, corner: int) -> Pos:
"""The square of a Safe Room a bot starts from: the one nearest to the Dojo."""
"""The square of a Safe Room a bot starts from: the one nearest to the Dojo.
Ties go to the square furthest down, then right, in the room's own frame, so every corner
starts alike; on board draft 2 that is the better of the two (plan 6.2.1).
"""
distance = dojo_distance(board)
return min(sorted(board.safe_room(corner)), key=lambda s: distance[s])
return min(board.safe_room(corner), key=lambda s: (distance[s], _negated(own_frame(s, corner))))
def own_frame(square: Pos, corner: int) -> Pos:
"""``square`` as seen from quadrant 0, i.e. rotated back from ``corner``'s quadrant."""
return rotate(square, -corner)
def _negated(p: Pos) -> tuple[int, int]:
return (-p.row, -p.col)
def best_of[T](
options: Sequence[T], score: Callable[[T], float], frame: Callable[[T], tuple[Pos, ...]]
) -> T:
"""The highest-scoring option; ties go to the lowest ``frame`` key, so that a bot decides
alike in every corner instead of by absolute board order."""
scored = [(score(option), option) for option in options]
top = max(value for value, _ in scored)
tied = [option for value, option in scored if value == top]
return tied[0] if len(tied) == 1 else min(tied, key=frame)
@dataclass(frozen=True)
@@ -158,12 +182,18 @@ class HeuristicAgent:
# Agent protocol ---------------------------------------------------------------------
def place_pawn(self, view: GameView, squares: Sequence[Pos]) -> Pos:
return self._nearest_to_dojo(view.board, squares)
square = start_square(view.board, self._me(view).corner)
return square if square in squares else self._nearest_to_dojo(view.board, squares)
def choose_path(self, view: GameView, paths: Sequence[Path]) -> Path:
v0 = self._value(view, self._me(view))
opponents = self._fightable(view)
return max(paths, key=lambda path: self._path_score(view, path, opponents, v0))
corner = self._me(view).corner
return best_of(
paths,
lambda path: self._path_score(view, path, opponents, v0),
lambda path: tuple(own_frame(cell, corner) for cell in path),
)
def use_tunnel(self, view: GameView, exit: Pos) -> bool:
me = self._me(view)
+1 -1
View File
@@ -177,7 +177,7 @@ class Game:
self._discard: list[Card] = []
self.players = [
Player(seat, corner, min(board.safe_room(corner)), config.start_position)
Player(seat, corner, min(board.safe_room(corner)), config.start_track(seat))
for seat, corner in enumerate(seat_corners(len(self.agents)))
]
for player in self.players:
+1 -1
View File
@@ -109,7 +109,7 @@ def measure(
) -> GameMetrics:
"""Replay ``events`` of one game. ``corners[seat]`` and ``bots[seat]`` describe each seat."""
players = len(corners)
track = [config.start_position] * players
track = [config.start_track(seat) for seat in range(players)]
position: list[Pos | None] = [None] * players
history: list[Standings] = []
gains = Counter[Source]()
+8
View File
@@ -64,6 +64,8 @@ class RulesConfig:
stripes_per_belt: int = 3
max_belt_bonus: int = 2
start_position: int = 0
# Extra stripes for the first player in turn order (6.2: compensation under test).
first_player_bonus: int = 0
deck: tuple[tuple[Card, int], ...] = DEFAULT_DECK
# Safety stop for simulations; not a rule of the game.
max_rounds: int = 200
@@ -73,6 +75,8 @@ class RulesConfig:
raise ValueError(f"exam_rounds must be odd and positive, got {self.exam_rounds}")
if self.exam_failure_loss < 0:
raise ValueError(f"exam_failure_loss must be >= 0, got {self.exam_failure_loss}")
if self.first_player_bonus < 0:
raise ValueError(f"first_player_bonus must be >= 0, got {self.first_player_bonus}")
if self.first_kumite_round < 1:
raise ValueError(f"first_kumite_round must be positive, got {self.first_kumite_round}")
@@ -99,6 +103,10 @@ class RulesConfig:
return attacker_total > defender_total
return attacker_total >= defender_total
def start_track(self, seat: int) -> int:
bonus = self.first_player_bonus if seat == 0 else 0
return self.shifted(self.start_position, bonus)
@property
def exam_wins_needed(self) -> int:
"""Rounds the challenger must win: the Exam is a best-of-``exam_rounds`` series."""
+26 -4
View File
@@ -3,9 +3,9 @@
import random
import pytest
from scripted import make_game, put
from scripted import fixture_board, make_game, put
from dojo.board import Board, Pos
from dojo.board import Board, Pos, rotate
from dojo.bots import (
AGGRESSIVE,
BALANCED,
@@ -15,7 +15,9 @@ from dojo.bots import (
HeuristicAgent,
Profile,
RandomAgent,
own_frame,
series_probability,
start_square,
win_probability,
)
from dojo.engine import Agent, FightView, Game, RandomSource
@@ -73,9 +75,29 @@ def test_heuristic_sees_no_opponent_to_fight_in_round_one() -> None:
put(game, 1, TATAMI, track=3)
# In round 2 the aggressive bot picks the fight (test_profiles_differ_on_an_even_fight).
choice = HeuristicAgent(AGGRESSIVE).choose_path(game.view(0), [(Pos(5, 3),), (TATAMI,)])
# Two plain Tatami tie; the tie goes to the lower square in the bot's frame, (4, 2).
choice = HeuristicAgent(AGGRESSIVE).choose_path(game.view(0), [(TATAMI,), (Pos(4, 2),)])
assert choice == (Pos(5, 3),)
assert choice == (Pos(4, 2),)
def test_bots_start_from_the_same_square_of_every_safe_room() -> None:
board = fixture_board()
assert len({own_frame(start_square(board, corner), corner) for corner in range(4)}) == 1
def test_tied_paths_are_chosen_alike_from_every_corner() -> None:
game, _, _ = make_game(players=4)
agent = HeuristicAgent(BALANCED)
chosen = set()
for seat, player in enumerate(game.players):
player.position = rotate(Pos(4, 3), player.corner)
tatami = [(rotate(square, player.corner),) for square in (Pos(5, 3), Pos(3, 3), Pos(4, 2))]
choice = agent.choose_path(game.view(seat), tatami)
chosen.add(own_frame(choice[0], player.corner))
assert chosen == {Pos(3, 3)}
def test_brown_heuristic_takes_the_exam_when_it_can() -> None:
+11
View File
@@ -246,6 +246,17 @@ def test_no_kumite_in_round_one_an_occupied_tunnel_exit_cannot_be_used() -> None
assert game.players[0].position == Pos(1, 6)
def test_first_player_bonus_raises_only_the_first_seat() -> None:
game, _, _ = make_game(players=3, config=RulesConfig(start_position=3, first_player_bonus=1))
assert [p.track for p in game.players] == [4, 3, 3]
def test_first_player_bonus_cannot_be_negative() -> None:
with pytest.raises(ValueError, match="first_player_bonus"):
RulesConfig(first_player_bonus=-1)
def test_first_kumite_round_must_be_positive() -> None:
with pytest.raises(ValueError, match="first_kumite_round"):
RulesConfig(first_kumite_round=0)
+11 -1
View File
@@ -17,7 +17,7 @@ from dojo.events import (
TurnStarted,
)
from dojo.metrics import KUMITE_SECONDS, TURN_SECONDS, GameMetrics, measure, summarize
from dojo.rules import DEFAULT_RULES, Card
from dojo.rules import DEFAULT_RULES, Card, RulesConfig
HOME_0 = Pos(0, 1) # quadrant 0
@@ -106,6 +106,16 @@ def test_kiai_decides_a_fight_only_when_the_result_flips_without_it(
assert summary.exam_kiai_decided_rate == decided
def test_first_player_bonus_is_counted_from_the_start() -> None:
config = RulesConfig(first_player_bonus=1)
metrics = measure(
[GameOver(None, 1)], config, (0, 1), ("balanced", "farmer"), scenario="t", seed=0
)
assert metrics.final_track == (1, 0)
def test_standings_are_sampled_at_turn_ends() -> None:
metrics = run(
[