Add Kiai-decided metrics; keep Kiai at +2 (6.6, D47)
Report how often a Tecnica is played and how often Kiai decides a Kumite or an Exam fight. The tie rule moves to RulesConfig.attacker_wins, shared by the engine and the metric; games are unchanged. RULES.md drops the Kiai +1 and catch-up variants from the rules under test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -139,7 +139,7 @@ All other cards are shown and resolved immediately.
|
|||||||
|
|
||||||
| Type | Card | Effect | Qty |
|
| Type | Card | Effect | Qty |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Tecnica | Kiai | +2 to your Kumite total. *(+1 to be tested)* | 3 |
|
| Tecnica | Kiai | +2 to your Kumite total. | 3 |
|
||||||
| Tecnica | Uke (Parata) | If you lose, you are not sent to Start (the track change still applies). A losing defender stays on its square; a losing attacker returns to the square it came from, and its turn ends with no square effect. | 2 |
|
| Tecnica | Uke (Parata) | If you lose, you are not sent to Start (the track change still applies). A losing defender stays on its square; a losing attacker returns to the square it came from, and its turn ends with no square effect. | 2 |
|
||||||
| Tecnica | Kaeshi (Contrattacco) | Reroll your die. | 2 |
|
| Tecnica | Kaeshi (Contrattacco) | Reroll your die. | 2 |
|
||||||
| Movement | Tsugi-ashi | Roll 2d6 and make another full move. That move has its own starting square (for Passo Falso). | 2 |
|
| Movement | Tsugi-ashi | Roll 2d6 and make another full move. That move has its own starting square (for Passo Falso). | 2 |
|
||||||
@@ -174,11 +174,11 @@ All other cards are shown and resolved immediately.
|
|||||||
|
|
||||||
## Rule variants under test
|
## Rule variants under test
|
||||||
|
|
||||||
Parameters to validate by simulation: Kiai +2 vs +1; starting on 3 with 6+ players;
|
Parameters to validate by simulation: starting on 3 with 6+ players; square mix; game
|
||||||
square mix; game length at 3/4/6/8 players; a catch-up rule (Allenamento gives +2 instead
|
length at 3/4/6/8 players.
|
||||||
of +1 to a player two or more belts behind the leader).
|
|
||||||
|
|
||||||
Tested and set aside (simulator switches only): Exam ties to the defender; best-of-3 Exam.
|
Tested and set aside (simulator switches only): Exam ties to the defender; best-of-3 Exam;
|
||||||
|
Kiai +1.
|
||||||
Earlier rules kept as simulator switches: Fatica costing only when you stop on it; Kumite
|
Earlier rules kept as simulator switches: Fatica costing only when you stop on it; Kumite
|
||||||
allowed in the first round.
|
allowed in the first round.
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ Deliverables:
|
|||||||
| D41 | Phase 6 order: board and Fatica (6.4 with 6.5) → seat order (6.2) → catch-up (6.9) → style dynamics (6.3) → Kiai (6.6), player counts (6.7), length → consolidation (6.8). Structural fixes before tuning | 6.1 review. |
|
| D41 | Phase 6 order: board and Fatica (6.4 with 6.5) → seat order (6.2) → catch-up (6.9) → style dynamics (6.3) → Kiai (6.6), player counts (6.7), length → consolidation (6.8). Structural fixes before tuning | 6.1 review. |
|
||||||
| D42 | **Board draft 2 adopted** as `board/quadrant.txt` (`board/board.txt` regenerated; draft 1 is in git history, commit 4998931) | 6.4.0 results. |
|
| D42 | **Board draft 2 adopted** as `board/quadrant.txt` (`board/board.txt` regenerated; draft 1 is in git history, commit 4998931) | 6.4.0 results. |
|
||||||
| D43 | **6.2 seat order suspended**: measuring it needs 4,000+ games per scenario for an effect of a few points; D35 (no Kumite in round 1) stays in the rules. Seat rates are still reported by every run and watched in 6.8 | Developer, after the 4,000-game runs were started. |
|
| D43 | **6.2 seat order suspended**: measuring it needs 4,000+ games per scenario for an effect of a few points; D35 (no Kumite in round 1) stays in the rules. Seat rates are still reported by every run and watched in 6.8 | Developer, after the 4,000-game runs were started. |
|
||||||
| D44 | **6.9 catch-up skipped**: the v0.8 baseline already has 3–4% of player-turns out of contention (worst player 10–13%), so candidate A has little to fix | Developer, after the v0.8 baseline metrics. |
|
| D44 | **6.9 catch-up skipped**: the v0.8 baseline already has 3–4% of player-turns out of contention (worst player 10–13%), so candidate A has little to fix; dropped from RULES.md "variants under test" (never implemented) | Developer, after the v0.8 baseline metrics. |
|
||||||
| D45 | The **`random` lineup is no longer run** in experiments: it never finishes a game and cost ~80% of a run's time. The bot stays in the code and the tests | Developer, 6.10. |
|
| D45 | The **`random` lineup is no longer run** in experiments: it never finishes a game and cost ~80% of a run's time. The bot stays in the code and the tests | Developer, 6.10. |
|
||||||
| D46 | Performance work must leave **every game identical** (same events for the same seed), so every result recorded in this file stays valid | Developer, 6.10. |
|
| D46 | Performance work must leave **every game identical** (same events for the same seed), so every result recorded in this file stays valid | Developer, 6.10. |
|
||||||
|
| D47 | **Kiai stays +2**: +2 and +1 are indistinguishable on every target (6.6); +2 decides more fights (5% vs 3% of Kumite in casual play). `kiai_bonus = 1` stays a simulator switch; RULES.md drops the "+1 to be tested" note | Developer, 6.6 results. |
|
||||||
|
|
||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
@@ -173,7 +174,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:** developer review of the 6.3 results; then 6.6 Kiai, 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 6.10 and D46 is committed (0ca905e); the 6.3 results are not; commit only when the developer asks.
|
**Next action:** 6.7 player counts. 6.2 is suspended (D43), 6.9 skipped (D44); 6.4.1 (chance square) stays optional. Order of the steps: D41. Everything up to 6.6 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):
|
||||||
|
|
||||||
@@ -281,7 +282,20 @@ Deliverables:
|
|||||||
- [x] 6.4.2 Option 1, Fatica on crossing: `RulesConfig.fatica_on_crossing` — each Fatica square entered during a move costs 1 stripe, **1 in total per square whether or not the move stops there** (developer, D33); stopping on it adds nothing more. Engine: apply in `_walk` per square entered (not on teleports / relocations, which never walk). Bot: count Fatica crossed in `_path_score`. Likely needs fewer Fatica squares; test on the current board and on a draft with fewer F. Done as the baseline rule (D36): engine `_step` / `_path_choices`, bot `_path_score`, tests; measured on the current board in the 6.1 re-baseline.
|
- [x] 6.4.2 Option 1, Fatica on crossing: `RulesConfig.fatica_on_crossing` — each Fatica square entered during a move costs 1 stripe, **1 in total per square whether or not the move stops there** (developer, D33); stopping on it adds nothing more. Engine: apply in `_walk` per square entered (not on teleports / relocations, which never walk). Bot: count Fatica crossed in `_path_score`. Likely needs fewer Fatica squares; test on the current board and on a draft with fewer F. Done as the baseline rule (D36): engine `_step` / `_path_choices`, bot `_path_score`, tests; measured on the current board in the 6.1 re-baseline.
|
||||||
- [ ] 6.4.3 If 6.4.1 is run: compare it with the v0.8 baseline on draft 2 (Fatica on crossing, D36) and bring the table to the developer for a decision (then `RULES.md` and D-number).
|
- [ ] 6.4.3 If 6.4.1 is run: compare it with the v0.8 baseline on draft 2 (Fatica on crossing, D36) and bring the table to the developer for a decision (then `RULES.md` and D-number).
|
||||||
- [x] 6.5 Home farming: covered by board draft 2 (6.4.0): Allenamento moved from the south training room onto the Dojo ring; own-quadrant share of square income 74% → 62–67%, short of the 60% aim (players cross their own quadrant for most of the game); draft 2 adopted as is (D42). Original step: after 6.4, if the own-quadrant share of square income is still above 60% (baseline: 71%), move Allenamento squares away from the Safe Rooms in a board draft (`dojo sim --quadrant board/drafts/...`) and compare. The board must still pass `dojo board validate`.
|
- [x] 6.5 Home farming: covered by board draft 2 (6.4.0): Allenamento moved from the south training room onto the Dojo ring; own-quadrant share of square income 74% → 62–67%, short of the 60% aim (players cross their own quadrant for most of the game); draft 2 adopted as is (D42). Original step: after 6.4, if the own-quadrant share of square income is still above 60% (baseline: 71%), move Allenamento squares away from the Safe Rooms in a board draft (`dojo sim --quadrant board/drafts/...`) and compare. The board must still pass `dojo board validate`.
|
||||||
- [ ] 6.6 Kiai +2 vs +1: 1,000 games each on two lineups. Watch the Exam pass rate, upset rate and how often a Tecnica turns a fight.
|
- [x] 6.6 Kiai +2 vs +1: 1,000 games each on two lineups. Watch the Exam pass rate, upset rate and how often a Tecnica turns a fight. **Kiai stays +2 (D47).**
|
||||||
|
- [x] 6.6.1 Metric: `GameMetrics.tecnica_kumite`, `kiai_decided`, `exam_fights`, `exam_kiai_decided`; report line "Tecnica". A fight is decided by Kiai when the result flips with the Kiai bonus removed, dice and other cards unchanged. The tie rule moved to `RulesConfig.attacker_wins`, shared by the engine and the metric (fingerprinted games identical).
|
||||||
|
- [x] 6.6.2 Run: `uv run dojo sim --games 1000 --seed 0 --kiai 2 1 --lineup balanced,farmer,aggressive balanced casual`.
|
||||||
|
|
||||||
|
| Lineup | Kiai | Minutes mean / p90 | Exam passed | Upsets | Tecnica played in; Kiai decided (Kumite / Exam) | Halfway leader wins | Rivals within 1 belt |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| mixed | +2 | 72 / 95 | 40% | 31% | 2%; 1% / 1% | 40% | 0.84 |
|
||||||
|
| mixed | +1 | 72 / 95 | 41% | 31% | 2%; 1% / 1% | 41% | 0.85 |
|
||||||
|
| balanced | +2 | 68 / 92 | 42% | 30% | 2%; 1% / 1% | 39% | 0.81 |
|
||||||
|
| balanced | +1 | 69 / 93 | 41% | 30% | 2%; 1% / 1% | 39% | 0.82 |
|
||||||
|
| casual | +2 | 91 / 122 | 43% | 27% | 12%; 5% / 5% | 39% | 1.42 |
|
||||||
|
| casual | +1 | 91 / 121 | 44% | 27% | 10%; 3% / 4% | 39% | 1.38 |
|
||||||
|
|
||||||
|
- Reading: Kiai +2 and +1 are indistinguishable on every target measure (≤ 1 pp, inside the noise). Tecnica are rare: heuristic bots draw a Nebbia card on 2.3% of turns (0.5 Tecnica per game; Nebbia is worth 0.2 to them, below an Allenamento), casual bots on 9.7% (3.3 Tecnica per game). Even for casual play Kiai decides 5% (+2) vs 3% (+1) of Kumite. Scratch script `tecnica.py` (200 games per lineup).
|
||||||
- [ ] 6.7 Player counts: 3 / 4 / 6 / 8 players, start 0; plus start 3 at 6 and 8 players. Minutes per count; decide whether 6+ players start on Yellow.
|
- [ ] 6.7 Player counts: 3 / 4 / 6 / 8 players, start 0; plus start 3 at 6 and 8 players. Minutes per count; decide whether 6+ players start on Yellow.
|
||||||
- [x] 6.10 Simulation speed (before 6.3; D46: games unchanged). Profile: ~95% of the time was listing moves (`movement._extend`, `Board.neighbours`, the outcome keys in `Engine._path_choices`).
|
- [x] 6.10 Simulation speed (before 6.3; D46: games unchanged). Profile: ~95% of the time was listing moves (`movement._extend`, `Board.neighbours`, the outcome keys in `Engine._path_choices`).
|
||||||
- [x] 6.10.1 Guard: `agents/scripts/fingerprint_games.py` hashes every event of 210 games (mixed / casual at 4 players, 6 players, Fatica on stop with Kumite in round 1, random bots, 60-round cap); `write` before an engine change, `check` after. A tool for refactoring and speed work, not a test (rule changes are expected to differ). Timings came from a scratch benchmark.
|
- [x] 6.10.1 Guard: `agents/scripts/fingerprint_games.py` hashes every event of 210 games (mixed / casual at 4 players, 6 players, Fatica on stop with Kumite in round 1, random bots, 60-round cap); `write` before an engine change, `check` after. A tool for refactoring and speed work, not a test (rule changes are expected to differ). Timings came from a scratch benchmark.
|
||||||
|
|||||||
+1
-4
@@ -368,10 +368,7 @@ class Game:
|
|||||||
uke[side] = True
|
uke[side] = True
|
||||||
|
|
||||||
totals = (dice[0] + extra[0], dice[1] + extra[1])
|
totals = (dice[0] + extra[0], dice[1] + extra[1])
|
||||||
if exam and config.exam_ties_to_defender:
|
attacker_won = config.attacker_wins(*totals, exam=exam)
|
||||||
attacker_won = totals[0] > totals[1]
|
|
||||||
else:
|
|
||||||
attacker_won = totals[0] >= totals[1] # Senshu: ties go to the attacker.
|
|
||||||
winner_belt, loser_belt = belts if attacker_won else belts[::-1]
|
winner_belt, loser_belt = belts if attacker_won else belts[::-1]
|
||||||
if winner_belt == loser_belt:
|
if winner_belt == loser_belt:
|
||||||
kind = FightKind.EQUAL
|
kind = FightKind.EQUAL
|
||||||
|
|||||||
+43
-3
@@ -26,7 +26,7 @@ from dojo.events import (
|
|||||||
TurnSkipped,
|
TurnSkipped,
|
||||||
TurnStarted,
|
TurnStarted,
|
||||||
)
|
)
|
||||||
from dojo.rules import RulesConfig
|
from dojo.rules import Card, RulesConfig
|
||||||
|
|
||||||
# Table-time estimate (plan, resolved questions).
|
# Table-time estimate (plan, resolved questions).
|
||||||
TURN_SECONDS = 45
|
TURN_SECONDS = 45
|
||||||
@@ -59,6 +59,12 @@ class GameMetrics:
|
|||||||
exam_attempts: int
|
exam_attempts: int
|
||||||
exam_passes: int
|
exam_passes: int
|
||||||
exam_challengers: int
|
exam_challengers: int
|
||||||
|
exam_fights: int
|
||||||
|
# Fights with a Tecnica played, and fights whose result flips without the Kiai played
|
||||||
|
# (dice and other cards unchanged).
|
||||||
|
tecnica_kumite: int
|
||||||
|
kiai_decided: int
|
||||||
|
exam_kiai_decided: int
|
||||||
square_gain: int
|
square_gain: int
|
||||||
square_loss: int
|
square_loss: int
|
||||||
card_gain: int
|
card_gain: int
|
||||||
@@ -110,6 +116,7 @@ def measure(
|
|||||||
losses = Counter[Source]()
|
losses = Counter[Source]()
|
||||||
home = away = 0
|
home = away = 0
|
||||||
kumite = upsets = expected = exam_fights = 0
|
kumite = upsets = expected = exam_fights = 0
|
||||||
|
tecnica_kumite = kiai_decided = exam_kiai_decided = 0
|
||||||
chain = max_chain = 0
|
chain = max_chain = 0
|
||||||
exam_attempts = exam_passes = 0
|
exam_attempts = exam_passes = 0
|
||||||
attempts_by_seat = [0] * players
|
attempts_by_seat = [0] * players
|
||||||
@@ -144,10 +151,17 @@ def measure(
|
|||||||
position[player] = destination
|
position[player] = destination
|
||||||
case SentToStart(player=player, square=square):
|
case SentToStart(player=player, square=square):
|
||||||
position[player] = square
|
position[player] = square
|
||||||
case KumiteFought(exam=True):
|
case KumiteFought(exam=True) as fight:
|
||||||
exam_fights += 1
|
exam_fights += 1
|
||||||
case KumiteFought(attacker=attacker, defender=defender, attacker_won=won, kind=kind):
|
exam_kiai_decided += _decided_by_kiai(fight, config)
|
||||||
|
case (
|
||||||
|
KumiteFought(
|
||||||
|
attacker=attacker, defender=defender, attacker_won=won, kind=kind
|
||||||
|
) as fight
|
||||||
|
):
|
||||||
kumite += 1
|
kumite += 1
|
||||||
|
tecnica_kumite += bool(fight.cards)
|
||||||
|
kiai_decided += _decided_by_kiai(fight, config)
|
||||||
upsets += kind is FightKind.UPSET
|
upsets += kind is FightKind.UPSET
|
||||||
expected += kind is FightKind.EXPECTED
|
expected += kind is FightKind.EXPECTED
|
||||||
if attacker == active:
|
if attacker == active:
|
||||||
@@ -201,6 +215,10 @@ def measure(
|
|||||||
exam_attempts=exam_attempts,
|
exam_attempts=exam_attempts,
|
||||||
exam_passes=exam_passes,
|
exam_passes=exam_passes,
|
||||||
exam_challengers=len(challengers),
|
exam_challengers=len(challengers),
|
||||||
|
exam_fights=exam_fights,
|
||||||
|
tecnica_kumite=tecnica_kumite,
|
||||||
|
kiai_decided=kiai_decided,
|
||||||
|
exam_kiai_decided=exam_kiai_decided,
|
||||||
square_gain=gains[Source.SQUARE],
|
square_gain=gains[Source.SQUARE],
|
||||||
square_loss=losses[Source.SQUARE],
|
square_loss=losses[Source.SQUARE],
|
||||||
card_gain=gains[Source.CARD],
|
card_gain=gains[Source.CARD],
|
||||||
@@ -233,6 +251,19 @@ def measure(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _decided_by_kiai(fight: KumiteFought, config: RulesConfig) -> bool:
|
||||||
|
kiai = [0, 0]
|
||||||
|
for player, card in fight.cards:
|
||||||
|
if card is Card.KIAI:
|
||||||
|
kiai[0 if player == fight.attacker else 1] += config.kiai_bonus
|
||||||
|
if not any(kiai):
|
||||||
|
return False
|
||||||
|
without = config.attacker_wins(
|
||||||
|
fight.attacker_total - kiai[0], fight.defender_total - kiai[1], exam=fight.exam
|
||||||
|
)
|
||||||
|
return without != fight.attacker_won
|
||||||
|
|
||||||
|
|
||||||
def _leader(standings: Standings) -> int | None:
|
def _leader(standings: Standings) -> int | None:
|
||||||
top = max(standings)
|
top = max(standings)
|
||||||
leaders = [p for p, t in enumerate(standings) if t == top]
|
leaders = [p for p, t in enumerate(standings) if t == top]
|
||||||
@@ -354,6 +385,11 @@ class Summary:
|
|||||||
exam_attempts_per_game: float
|
exam_attempts_per_game: float
|
||||||
exam_success: float
|
exam_success: float
|
||||||
exam_challengers: float
|
exam_challengers: float
|
||||||
|
# Shares of Kumite (Exam excluded) with a Tecnica played / decided by Kiai; share of
|
||||||
|
# Exam fights decided by Kiai.
|
||||||
|
tecnica_rate: float
|
||||||
|
kiai_decided_rate: float
|
||||||
|
exam_kiai_decided_rate: float
|
||||||
income_per_turn: dict[Source, tuple[float, float]]
|
income_per_turn: dict[Source, tuple[float, float]]
|
||||||
square_income_home_per_turn: float
|
square_income_home_per_turn: float
|
||||||
square_income_away_per_turn: float
|
square_income_away_per_turn: float
|
||||||
@@ -390,6 +426,7 @@ def summarize(scenario: str, games: Iterable[GameMetrics]) -> Summary:
|
|||||||
turns = sum(r.turns for r in rows) or 1
|
turns = sum(r.turns for r in rows) or 1
|
||||||
rounds = sum(r.rounds for r in rows) or 1
|
rounds = sum(r.rounds for r in rows) or 1
|
||||||
kumite = sum(r.kumite for r in rows)
|
kumite = sum(r.kumite for r in rows)
|
||||||
|
exam_fights = sum(r.exam_fights for r in rows)
|
||||||
attempts = sum(r.exam_attempts for r in rows)
|
attempts = sum(r.exam_attempts for r in rows)
|
||||||
choices = sum(r.move_choices for r in rows) or 1
|
choices = sum(r.move_choices for r in rows) or 1
|
||||||
seats = len(rows[0].lineup.split(","))
|
seats = len(rows[0].lineup.split(","))
|
||||||
@@ -420,6 +457,9 @@ def summarize(scenario: str, games: Iterable[GameMetrics]) -> Summary:
|
|||||||
exam_attempts_per_game=attempts / count,
|
exam_attempts_per_game=attempts / count,
|
||||||
exam_success=sum(r.exam_passes for r in rows) / (attempts or 1),
|
exam_success=sum(r.exam_passes for r in rows) / (attempts or 1),
|
||||||
exam_challengers=sum(r.exam_challengers for r in rows) / count,
|
exam_challengers=sum(r.exam_challengers for r in rows) / count,
|
||||||
|
tecnica_rate=sum(r.tecnica_kumite for r in rows) / (kumite or 1),
|
||||||
|
kiai_decided_rate=sum(r.kiai_decided for r in rows) / (kumite or 1),
|
||||||
|
exam_kiai_decided_rate=sum(r.exam_kiai_decided for r in rows) / (exam_fights or 1),
|
||||||
income_per_turn={
|
income_per_turn={
|
||||||
Source.SQUARE: (
|
Source.SQUARE: (
|
||||||
per_turn(r.square_gain for r in rows),
|
per_turn(r.square_gain for r in rows),
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ def format_summary(summary: Summary) -> str:
|
|||||||
f"Longest chain {_spread(summary.max_chain)} (Kumite in one turn, per game)",
|
f"Longest chain {_spread(summary.max_chain)} (Kumite in one turn, per game)",
|
||||||
f"Exam {summary.exam_attempts_per_game:.2f} attempts/game, "
|
f"Exam {summary.exam_attempts_per_game:.2f} attempts/game, "
|
||||||
f"{summary.exam_success:.0%} passed, {summary.exam_challengers:.2f} challengers/game",
|
f"{summary.exam_success:.0%} passed, {summary.exam_challengers:.2f} challengers/game",
|
||||||
|
f"Tecnica played in {summary.tecnica_rate:.0%} of Kumite; Kiai decided "
|
||||||
|
f"{summary.kiai_decided_rate:.0%} of Kumite, {summary.exam_kiai_decided_rate:.0%} of "
|
||||||
|
"Exam fights",
|
||||||
f"Income/turn {income}",
|
f"Income/turn {income}",
|
||||||
f"Square income home {summary.square_income_home_per_turn:+.2f}/turn, "
|
f"Square income home {summary.square_income_home_per_turn:+.2f}/turn, "
|
||||||
f"away {summary.square_income_away_per_turn:+.2f}/turn (net)",
|
f"away {summary.square_income_away_per_turn:+.2f}/turn (net)",
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ class RulesConfig:
|
|||||||
def shifted(self, position: int, delta: int) -> int:
|
def shifted(self, position: int, delta: int) -> int:
|
||||||
return max(0, min(position + delta, self.max_position))
|
return max(0, min(position + delta, self.max_position))
|
||||||
|
|
||||||
|
def attacker_wins(self, attacker_total: int, defender_total: int, *, exam: bool) -> bool:
|
||||||
|
"""Senshu: ties go to the attacker, unless the Exam option gives them to the defender."""
|
||||||
|
if exam and self.exam_ties_to_defender:
|
||||||
|
return attacker_total > defender_total
|
||||||
|
return attacker_total >= defender_total
|
||||||
|
|
||||||
@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."""
|
||||||
|
|||||||
+31
-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
|
from dojo.rules import DEFAULT_RULES, Card
|
||||||
|
|
||||||
HOME_0 = Pos(0, 1) # quadrant 0
|
HOME_0 = Pos(0, 1) # quadrant 0
|
||||||
|
|
||||||
@@ -76,6 +76,36 @@ def test_measure_counts_fights_income_and_exams() -> None:
|
|||||||
assert metrics.minutes == pytest.approx(round((3 * TURN_SECONDS + 5 * KUMITE_SECONDS) / 60, 1))
|
assert metrics.minutes == pytest.approx(round((3 * TURN_SECONDS + 5 * KUMITE_SECONDS) / 60, 1))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("totals", "attacker_won", "cards", "decided"),
|
||||||
|
[
|
||||||
|
((5, 4), True, ((0, Card.KIAI),), True),
|
||||||
|
((5, 3), True, ((0, Card.KIAI),), False), # 3 vs 3 without it: Senshu, still won.
|
||||||
|
((8, 4), True, ((0, Card.KIAI),), False),
|
||||||
|
((4, 5), False, ((1, Card.KIAI),), True),
|
||||||
|
((6, 5), True, ((0, Card.KIAI), (1, Card.KIAI)), False),
|
||||||
|
((5, 4), True, ((0, Card.KAESHI),), False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_kiai_decides_a_fight_only_when_the_result_flips_without_it(
|
||||||
|
totals: tuple[int, int],
|
||||||
|
attacker_won: bool,
|
||||||
|
cards: tuple[tuple[int, Card], ...],
|
||||||
|
decided: bool,
|
||||||
|
) -> None:
|
||||||
|
fight = KumiteFought(0, 1, 0, 0, *totals, attacker_won, FightKind.EQUAL, False, cards)
|
||||||
|
exam = replace(fight, exam=True)
|
||||||
|
|
||||||
|
metrics = run([TurnStarted(2, 0, 7), fight, fought(1, 0, FightKind.EQUAL), exam])
|
||||||
|
summary = summarize("test", [metrics])
|
||||||
|
|
||||||
|
assert (metrics.tecnica_kumite, metrics.kiai_decided) == (1, decided)
|
||||||
|
assert (metrics.exam_fights, metrics.exam_kiai_decided) == (1, decided)
|
||||||
|
assert summary.tecnica_rate == 0.5
|
||||||
|
assert summary.kiai_decided_rate == decided / 2
|
||||||
|
assert summary.exam_kiai_decided_rate == decided
|
||||||
|
|
||||||
|
|
||||||
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