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:
@@ -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.
|
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
|
Every rule has been checked by simulation; alternatives that were tried and set aside are
|
||||||
[Rule variants under test](#rule-variants-under-test)).
|
listed under [Tested alternatives](#tested-alternatives).
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
@@ -174,14 +174,16 @@ All other cards are shown and resolved immediately.
|
|||||||
- **Challenger loses**: −3 and sent to Start. The defender gains nothing.
|
- **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.
|
- 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;
|
- Final Exam: ties to the defender; best of 3; the defender fighting with their own belt.
|
||||||
Kiai +1; a failed Exam costing 0 or 1 stripe instead of 3.
|
- Kiai +1 instead of +2.
|
||||||
Earlier rules kept as simulator switches: Fatica costing only when you stop on it; Kumite
|
- A failed Exam costing 0 or 1 stripe instead of 3.
|
||||||
allowed in the first round.
|
- 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
|
## FAQ
|
||||||
|
|
||||||
|
|||||||
@@ -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. |
|
| 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. |
|
| 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. |
|
| 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
|
## 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`.
|
- 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).
|
- 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):
|
**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.
|
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.
|
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.
|
- [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).
|
- 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.
|
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.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.
|
- [ ] 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)
|
### Phase 7 — Finalisation (later)
|
||||||
|
|
||||||
|
|||||||
+36
-6
@@ -8,11 +8,11 @@ position, plus closeness to the Dojo once Brown); a ``Profile`` sets its attitud
|
|||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cache
|
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.engine import FightView, GameView, PlayerView
|
||||||
from dojo.movement import Path
|
from dojo.movement import Path
|
||||||
from dojo.rules import BLACK, Card
|
from dojo.rules import BLACK, Card
|
||||||
@@ -128,9 +128,33 @@ def farthest_from_dojo(board: Board) -> int:
|
|||||||
|
|
||||||
@cache
|
@cache
|
||||||
def start_square(board: Board, corner: int) -> Pos:
|
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)
|
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)
|
@dataclass(frozen=True)
|
||||||
@@ -158,12 +182,18 @@ class HeuristicAgent:
|
|||||||
# Agent protocol ---------------------------------------------------------------------
|
# Agent protocol ---------------------------------------------------------------------
|
||||||
|
|
||||||
def place_pawn(self, view: GameView, squares: Sequence[Pos]) -> Pos:
|
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:
|
def choose_path(self, view: GameView, paths: Sequence[Path]) -> Path:
|
||||||
v0 = self._value(view, self._me(view))
|
v0 = self._value(view, self._me(view))
|
||||||
opponents = self._fightable(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:
|
def use_tunnel(self, view: GameView, exit: Pos) -> bool:
|
||||||
me = self._me(view)
|
me = self._me(view)
|
||||||
|
|||||||
+1
-1
@@ -177,7 +177,7 @@ class Game:
|
|||||||
self._discard: list[Card] = []
|
self._discard: list[Card] = []
|
||||||
|
|
||||||
self.players = [
|
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 seat, corner in enumerate(seat_corners(len(self.agents)))
|
||||||
]
|
]
|
||||||
for player in self.players:
|
for player in self.players:
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ def measure(
|
|||||||
) -> GameMetrics:
|
) -> GameMetrics:
|
||||||
"""Replay ``events`` of one game. ``corners[seat]`` and ``bots[seat]`` describe each seat."""
|
"""Replay ``events`` of one game. ``corners[seat]`` and ``bots[seat]`` describe each seat."""
|
||||||
players = len(corners)
|
players = len(corners)
|
||||||
track = [config.start_position] * players
|
track = [config.start_track(seat) for seat in range(players)]
|
||||||
position: list[Pos | None] = [None] * players
|
position: list[Pos | None] = [None] * players
|
||||||
history: list[Standings] = []
|
history: list[Standings] = []
|
||||||
gains = Counter[Source]()
|
gains = Counter[Source]()
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ class RulesConfig:
|
|||||||
stripes_per_belt: int = 3
|
stripes_per_belt: int = 3
|
||||||
max_belt_bonus: int = 2
|
max_belt_bonus: int = 2
|
||||||
start_position: int = 0
|
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
|
deck: tuple[tuple[Card, int], ...] = DEFAULT_DECK
|
||||||
# Safety stop for simulations; not a rule of the game.
|
# Safety stop for simulations; not a rule of the game.
|
||||||
max_rounds: int = 200
|
max_rounds: int = 200
|
||||||
@@ -73,6 +75,8 @@ class RulesConfig:
|
|||||||
raise ValueError(f"exam_rounds must be odd and positive, got {self.exam_rounds}")
|
raise ValueError(f"exam_rounds must be odd and positive, got {self.exam_rounds}")
|
||||||
if self.exam_failure_loss < 0:
|
if self.exam_failure_loss < 0:
|
||||||
raise ValueError(f"exam_failure_loss must be >= 0, got {self.exam_failure_loss}")
|
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:
|
if self.first_kumite_round < 1:
|
||||||
raise ValueError(f"first_kumite_round must be positive, got {self.first_kumite_round}")
|
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
|
||||||
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
|
@property
|
||||||
def exam_wins_needed(self) -> int:
|
def exam_wins_needed(self) -> int:
|
||||||
"""Rounds the challenger must win: the Exam is a best-of-``exam_rounds`` series."""
|
"""Rounds the challenger must win: the Exam is a best-of-``exam_rounds`` series."""
|
||||||
|
|||||||
+26
-4
@@ -3,9 +3,9 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
import pytest
|
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 (
|
from dojo.bots import (
|
||||||
AGGRESSIVE,
|
AGGRESSIVE,
|
||||||
BALANCED,
|
BALANCED,
|
||||||
@@ -15,7 +15,9 @@ from dojo.bots import (
|
|||||||
HeuristicAgent,
|
HeuristicAgent,
|
||||||
Profile,
|
Profile,
|
||||||
RandomAgent,
|
RandomAgent,
|
||||||
|
own_frame,
|
||||||
series_probability,
|
series_probability,
|
||||||
|
start_square,
|
||||||
win_probability,
|
win_probability,
|
||||||
)
|
)
|
||||||
from dojo.engine import Agent, FightView, Game, RandomSource
|
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)
|
put(game, 1, TATAMI, track=3)
|
||||||
|
|
||||||
# In round 2 the aggressive bot picks the fight (test_profiles_differ_on_an_even_fight).
|
# 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:
|
def test_brown_heuristic_takes_the_exam_when_it_can() -> None:
|
||||||
|
|||||||
@@ -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)
|
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:
|
def test_first_kumite_round_must_be_positive() -> None:
|
||||||
with pytest.raises(ValueError, match="first_kumite_round"):
|
with pytest.raises(ValueError, match="first_kumite_round"):
|
||||||
RulesConfig(first_kumite_round=0)
|
RulesConfig(first_kumite_round=0)
|
||||||
|
|||||||
+11
-1
@@ -17,7 +17,7 @@ from dojo.events import (
|
|||||||
TurnStarted,
|
TurnStarted,
|
||||||
)
|
)
|
||||||
from dojo.metrics import KUMITE_SECONDS, TURN_SECONDS, GameMetrics, measure, summarize
|
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
|
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
|
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:
|
def test_standings_are_sampled_at_turn_ends() -> None:
|
||||||
metrics = run(
|
metrics = run(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user