Add failed-Exam loss switch; keep -3 and accept the length (6.11, D50)

RulesConfig.exam_failure_loss (bots aware) with the sim axis --exam-loss.
Losing 1 or 0 stripes saves 3-5 minutes but weakens comebacks, so the
rules keep -3. Default games are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 17:51:13 +02:00
parent 542ac1b15f
commit 52d8af620b
11 changed files with 116 additions and 17 deletions
+9 -5
View File
@@ -68,7 +68,7 @@ def test_sim_prints_a_summary_per_scenario_and_writes_games(
assert main([*argv, "--csv", str(games)]) == EXIT_OK
output = capsys.readouterr().out
assert "== exam:none p3 kiai+2 start0 balanced,farmer,aggressive (2 games" in output
assert "== exam:none p3 kiai+2 start0 fail-3 balanced,farmer,aggressive (2 games" in output
assert "== exam:c p3 kiai+2 start0" in output
rows = games.read_text(encoding="utf-8").splitlines()
assert rows[0].startswith("scenario,seed,lineup,winner_seat")
@@ -78,21 +78,25 @@ def test_sim_prints_a_summary_per_scenario_and_writes_games(
def test_sim_writes_one_summary_row_per_scenario(tmp_path: Path) -> None:
path = tmp_path / "summary.csv"
argv = ["sim", "--games", "2", "--players", "3", "4", "--lineup", "casual", "--jobs", "1"]
argv += ["--exam-loss", "3", "0"]
assert main([*argv, "--summary-csv", str(path)]) == EXIT_OK
with path.open(encoding="utf-8", newline="") as file:
rows = list(csv.DictReader(file))
assert [row["scenario"] for row in rows] == [
"exam:c p3 kiai+2 start0 casual",
"exam:c p4 kiai+2 start0 casual",
"exam:c p3 kiai+2 start0 fail-3 casual",
"exam:c p3 kiai+2 start0 fail-0 casual",
"exam:c p4 kiai+2 start0 fail-3 casual",
"exam:c p4 kiai+2 start0 fail-0 casual",
]
assert {"rounds_mean", "square_gain_per_turn", "win_rate_casual"} <= rows[0].keys()
assert (rows[0]["seat3_win_rate"], rows[1]["seat3_win_rate"] != "") == ("", True)
assert (rows[0]["seat3_win_rate"], rows[2]["seat3_win_rate"] != "") == ("", True)
@pytest.mark.parametrize(
"bad", [["--lineup", "balanced,cheater"], ["--games", "0"], ["--exam", "b"]]
"bad",
[["--lineup", "balanced,cheater"], ["--games", "0"], ["--exam", "b"], ["--exam-loss", "-1"]],
)
def test_sim_rejects_bad_arguments(bad: list[str], capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as exit_info:
+16
View File
@@ -677,6 +677,22 @@ def test_failing_the_exam_costs_a_belt_and_the_defender_gains_nothing() -> None:
assert game.players[2].track == 12
@pytest.mark.parametrize(("loss", "track"), [(0, 15), (1, 14)])
def test_failed_exam_loss_is_configurable(loss: int, track: int) -> None:
config = RulesConfig(exam_failure_loss=loss)
game, agents = dojo_game([1, 1, 1, 6], config=config, p1=9, p2=12)
turn(game, agents, 0, Pos(6, 7))
assert game.players[0].track == track
assert game.players[0].position == START_0
def test_failed_exam_loss_cannot_be_negative() -> None:
with pytest.raises(ValueError, match="exam_failure_loss"):
RulesConfig(exam_failure_loss=-1)
def test_uke_in_the_exam_returns_to_the_entrance() -> None:
game, agents = dojo_game([1, 1, 1, 6], p1=9, p2=12)
game.players[0].tecnica = Card.UKE
+13 -4
View File
@@ -11,13 +11,17 @@ LINEUP = ("balanced", "random")
def test_matrix_crosses_every_axis() -> None:
scenarios = matrix(["none", "acd"], [3, 4], [2, 1], [0], [LINEUP])
scenarios = matrix(["none", "acd"], [3, 4], [2, 1], [0], [LINEUP], [3, 0])
assert len(scenarios) == 8
assert scenarios[0].name == "exam:none p3 kiai+2 start0 balanced,random"
assert len(scenarios) == 16
assert scenarios[0].name == "exam:none p3 kiai+2 start0 fail-3 balanced,random"
assert scenarios[0].config == RulesConfig(exam_defender_black=False)
assert scenarios[-1].config == RulesConfig(
exam_ties_to_defender=True, exam_defender_black=True, exam_rounds=3, kiai_bonus=1
exam_ties_to_defender=True,
exam_defender_black=True,
exam_rounds=3,
exam_failure_loss=0,
kiai_bonus=1,
)
@@ -64,6 +68,11 @@ def test_invalid_scenarios_are_rejected(
Scenario(exam, players, 2, 0, lineup)
def test_invalid_exam_loss_is_rejected() -> None:
with pytest.raises(ValueError, match="exam_failure_loss"):
Scenario("c", 4, 2, 0, LINEUP, exam_loss=-1)
def test_runs_are_reproducible_and_independent_of_worker_count() -> None:
scenarios = matrix(["none", "acd"], [3], [2], [0], [LINEUP])