belote-cli 2.9.5__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {belote_cli-2.9.5 → belote_cli-3.2.0}/.claude/settings.local.json +3 -1
- belote_cli-3.2.0/AGENT.md +12 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/CHANGELOG.md +221 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/DEVELOPMENT.md +45 -8
- {belote_cli-2.9.5 → belote_cli-3.2.0}/PKG-INFO +53 -6
- {belote_cli-2.9.5 → belote_cli-3.2.0}/README.md +52 -5
- {belote_cli-2.9.5 → belote_cli-3.2.0}/pyproject.toml +1 -1
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/__init__.py +1 -1
- belote_cli-3.2.0/src/belote/a11y.py +93 -0
- belote_cli-3.2.0/src/belote/achievements.py +104 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ai.py +78 -24
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ansi.py +42 -18
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/core/run_state.py +12 -1
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/core/scoring.py +50 -12
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/engine/modifier_patch.py +20 -21
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/engine/round_driver.py +39 -0
- belote_cli-3.2.0/src/belote/belatro/ghost_run.py +83 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/base.py +19 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/hand_comp.py +7 -9
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/trick_timing.py +7 -2
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/registry.py +47 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/tarots.py +21 -9
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/main.py +45 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/run/boss.py +70 -20
- belote_cli-3.2.0/src/belote/belatro/run/shop.py +177 -0
- belote_cli-3.2.0/src/belote/belatro/run_summary.py +71 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/announce.py +28 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/hud.py +49 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/shop.py +90 -3
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/config.py +8 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/game.py +24 -24
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/gameflow.py +43 -2
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/input.py +2 -2
- belote_cli-3.2.0/src/belote/replay.py +72 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/scoring.py +170 -58
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/stats.py +27 -5
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/themes.py +35 -2
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ui/layout.py +3 -1
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ui/menu.py +4 -4
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ui/prompts.py +27 -26
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ui/render.py +26 -12
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_belatro.py +100 -4
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_boss_modifiers_integration.py +35 -0
- belote_cli-3.2.0/tests/belatro/test_dead_flag_fixes.py +816 -0
- belote_cli-3.2.0/tests/belatro/test_ghost_run.py +49 -0
- belote_cli-3.2.0/tests/belatro/test_hud_synergy.py +61 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_phase0_coverage.py +27 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_phase1_plumbing.py +106 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_phase2_content.py +104 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_phase3_meta.py +28 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_progression.py +63 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_round_driver.py +6 -4
- belote_cli-3.2.0/tests/test_a11y.py +135 -0
- belote_cli-3.2.0/tests/test_achievements.py +58 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_ai.py +66 -0
- belote_cli-3.2.0/tests/test_ansi_helpers.py +162 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_belote.py +115 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_official_rules.py +37 -2
- belote_cli-3.2.0/tests/test_replay.py +48 -0
- belote_cli-2.9.5/src/belote/belatro/run/shop.py +0 -107
- belote_cli-2.9.5/tests/belatro/test_dead_flag_fixes.py +0 -290
- {belote_cli-2.9.5 → belote_cli-3.2.0}/.gitignore +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/.python-version +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/LICENSE +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/scripts/benchmark.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/context.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/deck.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/main.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/rules.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ui/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/src/belote/ui/announce.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/__init__.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_extended.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_game_logic.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_gameflow.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_layout.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_new_coverage.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_properties.py +0 -0
- {belote_cli-2.9.5 → belote_cli-3.2.0}/tests/test_undo.py +0 -0
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"Bash(python3 -m pytest tests/ -q --tb=no)",
|
|
11
11
|
"Bash(python3 -m pytest tests/ -x -q)",
|
|
12
12
|
"Bash(PYTHONPATH=src python3 *)",
|
|
13
|
-
"Bash(.venv/bin/python -m mypy src/)"
|
|
13
|
+
"Bash(.venv/bin/python -m mypy src/)",
|
|
14
|
+
"Bash(PYTHONPATH=src python -m pytest --tb=short -q)",
|
|
15
|
+
"Bash(python3 *)"
|
|
14
16
|
]
|
|
15
17
|
}
|
|
16
18
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
You are auditing a Python codebase for bugs.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
1. Every finding has fields: file:line, claim, evidence, confidence (CONFIRMED|LIKELY|SPECULATIVE), severity.
|
|
5
|
+
2. CONFIRMED requires citing both the buggy code AND the code that proves the bug (e.g., the missing reader for a "dead
|
|
6
|
+
flag", the caller that passes the bad input).
|
|
7
|
+
3. Severity P0/P1 requires CONFIRMED. LIKELY findings max out at P2.
|
|
8
|
+
4. For any "X is unused/dead/never called" claim: paste `grep -rn "X" .` output. Zero non-test hits required.
|
|
9
|
+
5. For any "Y crashes" claim: name the input that triggers the crash and the call path that delivers it.
|
|
10
|
+
6. End the report with a "Findings I considered but rejected" section — at least 3 items. This forces you to
|
|
11
|
+
demonstrate you tried to falsify.
|
|
12
|
+
7. Use the tools provided (Read, Grep, Bash). Do not reason from memory about file contents.
|
|
@@ -5,6 +5,227 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.2.0] - 2026-05-10
|
|
9
|
+
|
|
10
|
+
Two-audit reconciliation release — the prioritized fix list distilled from Qwen 3.6 27B + Ring 1T audits (~30 raw claims, ~half held up under verification). Twelve real bugs fixed across joker logic, registry hygiene, RNG determinism, and UI offsets; one new finding (Tarot RNG was also unseeded) caught by the fresh-hunt pass. Eleven audit claims rejected as false positives are catalogued in the plan file so they aren't re-investigated. 528 tests passing (up from 525), ruff and mypy strict still clean.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`src/belote/belatro/items/jokers/hand_comp.py::LaSentinelle`** — Detection of the trump Jack now keys on the NS *team* via `team_of(seat) == 0` instead of `seat == Seat.SOUTH`. Pre-3.2 the joker was silently no-op when North (the partner) was dealt the trump Jack, even though Belote's "you" is team-level. Trick-win detection follows the same team rule. Regressions: `tests/belatro/test_dead_flag_fixes.py::test_la_sentinelle_arms_when_partner_plays_trump_jack`, `test_la_sentinelle_does_not_arm_for_opponent_jack`.
|
|
15
|
+
- **`src/belote/belatro/items/jokers/trick_timing.py::LeDernierMot`** — Dix de Der replacement now fires whenever the NS team wins the last trick (`team_of(event.winner) == 0`), not only when South personally takes it. Pre-3.2 the joker silently did nothing when partner won the closing trick. Regressions: `tests/belatro/test_belatro.py::TestLeDernierMot::test_north_last_trick_returns_result`, `test_east_last_trick_returns_none`.
|
|
16
|
+
- **`src/belote/belatro/items/jokers/corrupted.py::LEgoiste` → `src/belote/belatro/core/scoring.py::ScoreAccumulator.get_total`** — Final chip total is now `max(0, state._chips)`. L'Égoïste subtracts `event.card_points` for every partner-won trick; with enough partner wins the running total could cross zero, producing a negative final round score. Clamping at the scoring boundary preserves the intermediate accounting log while guaranteeing the visible score is never negative.
|
|
17
|
+
- **`src/belote/belatro/engine/round_driver.py:236-249`** — NS-taker `auto_coinche` path now re-emits `BidMadeEvent` with the new `coinche_level` so jokers/HUD subscribed to `on_bid` see the bump. The EW-taker branch above always emitted; this NS-side branch silently set `coinche_level = 1` without notifying subscribers.
|
|
18
|
+
- **`src/belote/belatro/core/run_state.py::BelAtroRun.advance_blind`** — Victory now sets both `run_won = True` and `run_over = True`, so downstream callers can rely on `run_over` alone as the terminal-state signal. `enter_endless()` resets both, re-opening the run for endless mode. Pre-3.2 the main loop only terminated via a `break` after a `run_won` check — semantically correct but fragile under refactors.
|
|
19
|
+
- **`src/belote/belatro/items/registry.py::ItemRegistry.register_*`** — All four register methods (`joker` / `planet` / `tarot` / `voucher`) now assert that an existing entry under the same `id` is the *same class*. Pre-3.2 a typo'd duplicate ID would silently overwrite the prior class, and the override would never surface until the original behaviour visibly broke. Idempotent re-registration of the same class still works for the test-suite swap pattern.
|
|
20
|
+
- **`src/belote/belatro/engine/modifier_patch.py`** — `boss_fields` is now derived from `BossModifiers`' dataclass fields via `dataclasses.fields(BossModifiers)` instead of a hardcoded set. Pre-3.2 a new boss flag added to `BossModifiers` would be silently no-op'd until someone remembered to add it to the hardcoded allowlist in lock-step.
|
|
21
|
+
|
|
22
|
+
### Determinism
|
|
23
|
+
|
|
24
|
+
- **`src/belote/belatro/run/shop.py::Shop.generate_inventory`** — All RNG calls (`random.random` / `random.choice` / `random.sample` across edition rolls, joker pick, tarot/planet pick, voucher pick) now use `self.run._get_rng()` instead of the module-level `random`. Pre-3.2 shop contents were non-deterministic even with a seeded run, which broke ghost-run reproducibility. `Shop._roll_edition` signature changed to accept an explicit `rng` argument; the `test_shop_edition_weights_match_distribution` test was updated to pass the seeded RNG directly instead of monkey-patching `shop_mod.random.random`.
|
|
25
|
+
- **`src/belote/belatro/items/tarots.py`** — `LeJugement`, `LaPretresse`, and `LeFou` all now draw from `run._get_rng()` instead of the module-level `random`. Module-level `import random` removed.
|
|
26
|
+
|
|
27
|
+
### Improved
|
|
28
|
+
|
|
29
|
+
- **`LaPretresse` planet picks now deduplicate** — switched from two independent `random.choice(planets)` calls to `rng.sample(planets, k=2)`, so the tarot can no longer pick the same planet twice. Falls back to a single pick when the planet pool has fewer than 2 entries.
|
|
30
|
+
- **`LeJugement` slot-full notification** — new `BelAtroRun.last_tarot_message: str | None` field carries a non-fatal failure reason ("joker slots are full — no joker granted") when the tarot can't complete. Pre-3.2 the joker was silently dropped with no UI signal. Cleared whenever a tarot is used.
|
|
31
|
+
- **`src/belote/ui/render.py::patch_trick_card`** — Now reads `_last_rendered_unpadded_h` (set by `render()`) and threads it into `_calculate_base_row`, so single-card patches re-apply the same vertical-centering offset `render()` used. Pre-3.2 it passed the "I don't know" sentinel (0) and skipped the offset entirely, drawing cards too high on tall terminals (>40 rows).
|
|
32
|
+
- **`src/belote/ui/layout.py`** — `hud_style` docstring corrected. Pre-3.2 it claimed `"verbose" / "standard" / "compact"`, but no preset used `"standard"` and no consumer recognized it — only `"verbose"` and `"compact"` are real.
|
|
33
|
+
|
|
34
|
+
### Rejected (catalogued so they aren't re-investigated)
|
|
35
|
+
|
|
36
|
+
Eleven claims from the input audits were rejected after verification against the actual code:
|
|
37
|
+
|
|
38
|
+
- LaBalance voucher (`tie_breaks_for_taker`) and LaCompetition (`separate_scoring`) flags — **both consumed** in `src/belote/scoring.py` and `src/belote/belatro/main.py`. Qwen flagged both as P0 dead-flag bugs; verification falsified both.
|
|
39
|
+
- LeFou tarot "chain broken" — `run_state.py::consume` sets `last_consumable_id` *before* `item.use()` runs, so chaining works as intended.
|
|
40
|
+
- `no_belote_rebelote` deck-mod flag — consumed at `src/belote/scoring.py:630`.
|
|
41
|
+
- `_pending_tierce_charge` cross-round leak — each blind constructs a fresh `ScoreAccumulator` (main.py:126) and `drive_round` builds a fresh `GameState` via `new_game()` (round_driver.py:84), so `_joker_state` is empty at every round start. No cross-round persistence path exists.
|
|
42
|
+
- `fuse_jokers` "loses `on_purchase` effects" — `on_purchase` mutates `run` state (which survives fusion); re-applying on the fused instance would *double-apply* cumulative effects (LeDemon's trust drop). Pre-3.2 behaviour is correct.
|
|
43
|
+
- IllegalMoveError in `round_driver.py:291` — reachable only via test MockCallbacks; production `prompt_card` has a guard.
|
|
44
|
+
- `_card_beats` defensive `assert trump is not None` — unreachable under current contract invariants.
|
|
45
|
+
- `display_hud` no clear-to-EOL — HUD is rebuilt fresh per call; the claim was wrong.
|
|
46
|
+
- Libra planet description — "×4 instead of ×3" matches the payout; mechanism is additive per coinche level but the description references the result.
|
|
47
|
+
- `get_total()` float precision — explicit `int()` guard at scoring.py:248-249.
|
|
48
|
+
- KeyboardInterrupt save — profile is saved *before* the loop starts; only intra-run delta is lost.
|
|
49
|
+
|
|
50
|
+
### Internal
|
|
51
|
+
|
|
52
|
+
- **Tests**: 525 → 528 (+3 net: −1 test renamed/repurposed for LeDernierMot team check, +2 new for La Sentinelle partner-detection and EW opponent rejection).
|
|
53
|
+
- **Strict gates**: pytest 528/528, mypy 0 errors, ruff 0 violations across `src/` and `tests/`.
|
|
54
|
+
- **Audit plan**: `~/.claude/plans/between-these-two-plans-graceful-puppy.md` — captures the two source audits, the verification pass that filtered them, the implementation order, and the catalogue of rejected claims.
|
|
55
|
+
|
|
56
|
+
## [3.1.0] - 2026-05-08
|
|
57
|
+
|
|
58
|
+
Audit-action release — implements the prioritized fix list from 3.0.3. One real correctness bug fixed, one unreachable feature wired up, one money-leak path closed, three measurable perf wins, and the long-standing `modifier_patch` underscore shim retired. 525 tests passing (up from 510), ruff and mypy strict still clean across 75 source files.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`src/belote/game.py:843-855` (HUD multi-boss running total)** — Under `Les Clubs Bannis + Le Roi Mort` (or any combo of `ban_clubs` with a rank-zero boss), the live HUD running total in `play_card` over-credited a clubs-led trick: the `ban_clubs → trick_pts = 0` branch was immediately overwritten by the rank-zero recompute. The eventual round score was already correct (different code path through `scoring.py`). Now `play_card` delegates to `scoring.trick_card_points`, the canonical helper that composes every boss zero-rank flag, `ban_clubs`, and the SE-trump scale in a single pass — the HUD cannot drift from the round score under any boss combo. Regression: `tests/test_official_rules.py::test_hud_running_total_under_multi_boss_ban_clubs_plus_kings_zero`.
|
|
63
|
+
- **`src/belote/belatro/run/shop.py::buy_item` (consumable money-leak)** — Slot-capacity check is now hoisted *above* `Economy.spend_money`. Pre-3.1.0 the player's money was charged for a Tarot/Planet purchase even when consumable slots were full, and the item was silently dropped. New `Shop.last_buy_failure: str | None` carries the reason ("slots_full" / "no_money") so the shop UI surfaces a `BelAtroAnnounce.banner("Slots full — sell first")` banner. Regressions: `tests/belatro/test_belatro.py::TestShop::test_buy_consumable_with_full_slots_does_not_charge_money`, `test_buy_joker_with_full_slots_does_not_charge_money`, `test_buy_item_no_money_records_no_money_failure`.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **TierceForge UI integration** (`src/belote/belatro/ui/shop.py`) — The `TierceForge` voucher shipped in 3.0.0 with a working `forge_tierce(run, planet_id)` backend (`src/belote/belatro/items/vouchers.py:129`) but no UI caller; the feature was unreachable. The shop now shows a "Forge ×N/3" tile when the voucher is owned, opens a numbered planet picker on Enter, and surfaces a confirmation banner on success. Regressions: `tests/belatro/test_phase2_content.py::test_forge_tierce_voucher_spends_charges_and_levels_planet`, `test_forge_tierce_blocked_when_charges_below_three`.
|
|
68
|
+
- **Block-policy regressions for Tarot overflow** — `LeJugement` and `LaPretresse` are now pinned to no-op when joker/consumable slots are at capacity (rather than partial-grant). Tests: `test_le_jugement_no_op_when_joker_slots_full`, `test_la_pretresse_no_op_when_consumable_slots_full`.
|
|
69
|
+
- **`tests/belatro/test_phase1_plumbing.py::test_joker_state_only_contains_scalar_values`** — Walks every registered joker through `on_round_start` + four event hooks and asserts no mutable container leaks into `_joker_state`. Locks the contract that lets the per-event copy stay shallow (3.1.0 dropped the deepcopy).
|
|
70
|
+
- **`tests/belatro/test_phase1_plumbing.py::test_shop_edition_weights_match_distribution`** — 10 000-roll empirical check on `Shop._roll_edition()`, ±1% per bucket. Catches accidental edits to the `_EDITION_WEIGHTS` table.
|
|
71
|
+
- **`tests/belatro/test_phase3_meta.py::test_endless_ante_target_scaling`** + `test_endless_ante_offset_zero_matches_base_table` — pin the `100 × 1.5^(ante-1) × blind × 2.2^offset` formula and the static-table parity invariant.
|
|
72
|
+
- **`tests/belatro/test_phase2_content.py::test_le_fou_no_prior_consumable_falls_back_to_random_tarot`** — covers the `last_id == self.id` defensive branch in `tarots.py::LeFou.use`.
|
|
73
|
+
- **`tests/belatro/test_boss_modifiers_integration.py::test_invariant_no_underscore_boss_attrs`** — anti-pattern lock for the architecture-pinned rule that boss flags must be reached via `state.boss_modifiers.X`, never `getattr(state, "_X", False)`.
|
|
74
|
+
|
|
75
|
+
### Improved
|
|
76
|
+
|
|
77
|
+
- **`src/belote/scoring.py` (winners-threading)** — `score_round` already pre-computed the per-trick winner list (3.0.2); the residual `trick_winner_seat` recomputations in the Malédiction branch (lines 776-793) and `apply_round_score` (lines 843-855) are now eliminated. Per-team trick counts ride on the new `ScoringBreakdown.tricks_ns` / `tricks_ew` fields (default 0; `apply_round_score` falls back to walking when a hand-constructed breakdown leaves them at default). Net: ~16 fewer `trick_winner_seat` calls per round.
|
|
78
|
+
- **`src/belote/belatro/core/scoring.py::ScoreAccumulator.update_state` (deepcopy → shallow)** — Replaced the per-event `copy.deepcopy(state._joker_state)` with `dict(state._joker_state)`. All current `_joker_state` writers store scalars (bool/int/str), so the deep-copy was over-defensive — and ran ~20×/round. Module-level `import copy` and `from dataclasses import replace` removed (they were also reimported inside two methods). Contract is locked by the new scalar-invariant test.
|
|
79
|
+
- **`src/belote/ai.py` (Hard AI hot-loop allocations)** — `_hard_play` precomputes `hand_suit_counts: Counter[Suit]`, `my_trumps`, `opp_trumps` once per turn and threads them into `_score_card_play` / `_score_leading_strategy` / `_score_discarding_strategy`. Pre-3.1.0 these counters were rebuilt for every candidate card — a four-card legal set walked the hand and `memory.played` four times each.
|
|
80
|
+
- **`@dataclass(slots=True)` on `Statistics`, `SessionStats`, `ScoreAccumulator`** (`src/belote/stats.py`, `src/belote/belatro/core/scoring.py`). Frequently-instantiated containers; ~40 bytes saved per instance. `BelAtroRun` deliberately stays non-slotted (its `__post_init__` lazy-init pattern fights `slots=True`).
|
|
81
|
+
- **`src/belote/stats.py:97-98`** — `print(..., file=sys.stderr)` on save failure swapped for `logging.getLogger(__name__).warning`. Removed unused `import sys`.
|
|
82
|
+
- **`src/belote/input.py:138, 160`** — bare `except Exception:` in key-press parsing narrowed to `(UnicodeDecodeError,)` and `(ValueError, UnicodeDecodeError)`. Genuine bugs surface; key-press robustness preserved.
|
|
83
|
+
- **`src/belote/replay.py:46`** — explanatory comment added above the `# noqa: BLE001` so the broad-except rationale is visible at the call site.
|
|
84
|
+
- **`src/belote/game.py:213-217, 220-224`** — docstring on `belote_holders` and `_joker_state` documenting the "always replace, never mutate-in-place" contract for mutable dicts inside the frozen `GameState`.
|
|
85
|
+
|
|
86
|
+
### Removed
|
|
87
|
+
|
|
88
|
+
- **`modifier_patch.py` underscore shim** — The `state.patch("_X", True)` → `state.patch("X", True)` migration is complete. All 23 boss `apply()` methods in `src/belote/belatro/run/boss.py` were rewritten in lock-step. The leading-underscore strip in `PatchedGameState.patch()` and the `__getattr__` fallback to `boss_modifiers.X` are gone; `patch()` now asserts loud on a leading-underscore key. The `getattr(state, "_X", False)` reading anti-pattern is locked against in `test_invariant_no_underscore_boss_attrs`.
|
|
89
|
+
|
|
90
|
+
### Internal
|
|
91
|
+
|
|
92
|
+
- **Tests**: 510 → 525 (+15).
|
|
93
|
+
- **Strict gates**: pytest 525/525, mypy 0 errors, ruff 0 violations across `src/` and `tests/`.
|
|
94
|
+
- **Audit plan**: `~/.claude/plans/bug-hunt-code-performance-sleepy-ritchie.md`.
|
|
95
|
+
|
|
96
|
+
## [3.0.3] - 2026-05-08
|
|
97
|
+
|
|
98
|
+
Full-codebase audit pass + documentation accuracy. No behaviour changes; the audit produced a prioritized findings list and corrected three stale README counts. Planned fixes (one P0 functional, two P0 perf/quality, five P1, seven P2) are tracked for follow-up cuts and not yet implemented.
|
|
99
|
+
|
|
100
|
+
### Fixed (documentation)
|
|
101
|
+
|
|
102
|
+
- **`README.md`** — "Full Boss Blind Suite: All 18 unique bosses" → "All 21 unique bosses". 3.0.0 added Le Sauvage / L'Iconoclaste / Le Mime to bring `ALL_BOSS_MODIFIERS` (in `src/belote/belatro/run/boss.py`) to 21; the showcase line was never bumped.
|
|
103
|
+
- **`README.md`** — two stale "(435 tests)" / "pytest: 435/435 passed" references corrected to 510, matching `pytest --collect-only` and the figure already present at `README.md:250` ("Currently 510 tests passing").
|
|
104
|
+
|
|
105
|
+
### Audit findings (planning only — implementation deferred)
|
|
106
|
+
|
|
107
|
+
A three-agent audit covered the classic engine vs. canonical Belote rules, BelAtro content wiring (jokers / bosses / planets / vouchers / tarots / editions / unlocks), and performance / code-quality hotspots across ~7,100 LOC. Headline: engine is rule-correct; BelAtro content matrix is 93/93 wired (21 bosses, 8 planets, 36 jokers, 4 editions, 12 vouchers, 12 tarots).
|
|
108
|
+
|
|
109
|
+
Findings tracked at `~/.claude/plans/bug-hunt-code-performance-atomic-sutton.md`:
|
|
110
|
+
- **P0-1** — `EventBus.emit` still never called (carried over from 3.0.2). `L'Exécuteur` / `L'Idéologue` / `Le Fanatique` unlocks silently never fire.
|
|
111
|
+
- **P0-2** — `legal_cards()` LRU wrapper rebuilds `Card` objects on every cache hit (`src/belote/game.py:475-653`); est. 5–8% AI-turn regression vs. caching the resolved tuple.
|
|
112
|
+
- **P0-3** — `play_card()` is 174 LOC / cyclomatic ~20 (`src/belote/game.py:777-950`); split into `_update_belote_tracker` / `_apply_play_modifiers` / `_resolve_trick_complete`.
|
|
113
|
+
- **P0-4** — `_calculate_base_points()` accepts an optional pre-computed `winners` arg; cache-miss callers walk all 8 tricks twice (`src/belote/scoring.py:580-588`). Make required.
|
|
114
|
+
- **P1-1** — `card_points(trump: Suit)` lies about None; 8 `# type: ignore` markers across `game.py` / `scoring.py` should drop once signature becomes `Suit | None`.
|
|
115
|
+
- **P1-2** — Boss zero-rank logic duplicated across three sites (`game.py:856-872`, `scoring.py:390-400`, `scoring.py:429-440`); extract a single `apply_zero_rank_bosses(card, trump, bm)` helper. Highest-leverage maintenance fix.
|
|
116
|
+
- **P1-3..P1-5** — `_hard_bid` recomputes void counts inside the suit loop; `trick_rank()` called twice per overtrump check; missing docstrings on hot APIs.
|
|
117
|
+
- **P2** — carré KeyError harden, `REBELOTE_POINTS = 40` variant doc, AI memory reset hardening, `render()` 129-LOC split, `register_all_items` `__all__`, voucher / tarot integration test (24 effects to cover).
|
|
118
|
+
|
|
119
|
+
### Internal
|
|
120
|
+
|
|
121
|
+
- **Tests**: 510 (unchanged).
|
|
122
|
+
- **Strict gates**: pytest 510/510, mypy 0 errors, ruff 0 violations (all unchanged from 3.0.2).
|
|
123
|
+
|
|
124
|
+
### Carried forward
|
|
125
|
+
|
|
126
|
+
- `EventBus.emit` wiring fix (P0-1 above) remains deferred. Now planned for 3.0.4 alongside the perf wins.
|
|
127
|
+
|
|
128
|
+
## [3.0.2] - 2026-05-08
|
|
129
|
+
|
|
130
|
+
Audit pass — wired two previously-dead 3.0.0 modules behind opt-in env vars, removed redundant work from `score_round()`, and pinned every boss modifier's patch keys against typo regressions.
|
|
131
|
+
|
|
132
|
+
### Fixed
|
|
133
|
+
|
|
134
|
+
- **`src/belote/belatro/main.py` + `src/belote/belatro/engine/round_driver.py`** — `GhostRecorder` (`src/belote/belatro/ghost_run.py`) was imported nowhere outside its own tests since it shipped in 3.0.0; ghost recording silently never happened. Wired through `drive_round()` via a new optional `recorder` param so bids, plays, and round-end breakdowns are now captured. Gated on `BELOTE_GHOST=1` so default play is unchanged. Saves to `~/.local/share/belote/ghosts/<label>-<seed>.json` on run end.
|
|
135
|
+
- **`src/belote/gameflow.py`** — `replay.analyze_round()` / `summarize()` (`src/belote/replay.py`) was never called from any runtime path since it shipped in 3.0.0; the post-round Hard-AI comparison the README advertised silently never fired. `run_play()` now optionally accumulates `(state, played_card)` pairs for South; `run_round()` reads `BELOTE_REPLAY=1` once per round, runs the analyzer post-scoring, and prints a one-line `Replay: Optimal plays: N/M (P%)` summary. UNDO clears the buffer so the report matches the play that actually finished the round.
|
|
136
|
+
|
|
137
|
+
### Improved
|
|
138
|
+
|
|
139
|
+
- **`src/belote/scoring.py::score_round`** — pre-computes the per-trick winner list once at the top and threads it into `_calculate_base_points` and `_apply_scoring_modifiers`. Both helpers used to re-call `trick_winner_seat()` for every completed trick; under `separate_scoring` + `queen_spades_penalty` the 8-trick walk could run 3× per round.
|
|
140
|
+
- **`src/belote/belatro/items/registry.py::register_all_items`** — now idempotent. A module-level `_registered` flag short-circuits subsequent calls; second clause `and registry.jokers` re-runs when a caller has swapped the global to a fresh empty `ItemRegistry` (the test-suite pattern at `tests/belatro/test_belatro.py::TestItemRegistry.setup_method`). Saves the 4× `dir(mod)` walk on every `BelAtroRun` after the first.
|
|
141
|
+
- **`src/belote/ai.py::_special_bid`** — `_suit_lengths(hand)` is now computed once and threaded into `_easy_special` / `_medium_special` / `_hard_special`. The three branches each used to rebuild it from scratch.
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
|
|
145
|
+
- **`tests/belatro/test_phase0_coverage.py::test_every_boss_modifier_actually_patches_a_flag`** — pin against a typo'd `state.patch("_misspelled", True)` key silently producing a no-op boss. Iterates `ALL_BOSS_MODIFIERS`, asserts each `.flags()` differs from default `BossModifiers()`. The `boss_fields` allow-list in `engine/modifier_patch.py` is now load-bearing for correctness — this test surfaces drift.
|
|
146
|
+
- **`DEVELOPMENT.md`** — new "Optional Runtime Flags" section documenting `BELOTE_REPLAY` and `BELOTE_GHOST` next to the existing `BELOTE_A11Y` entry.
|
|
147
|
+
|
|
148
|
+
### Internal
|
|
149
|
+
|
|
150
|
+
- **Tests**: 509 → 510 (+1).
|
|
151
|
+
- **Strict gates**: pytest 510/510, mypy 0 errors, ruff 0 violations.
|
|
152
|
+
|
|
153
|
+
### Known issue (not fixed in this cut)
|
|
154
|
+
|
|
155
|
+
- `src/belote/belatro/engine/event_bus.py::EventBus.emit` is never called anywhere in the source. `unlock_tracker.subscribe_to(bus)` registers `on_event` but receives no events, so `_handle_round_end`'s unlocks for **L'Exécuteur** (first Capot), **L'Idéologue** (Sans Atout NS win), and **Le Fanatique** (Tout Atout NS win) silently never fire under normal play. Out of scope for 3.0.2 (fix would change ordering with `acc.update_state` in `round_driver._emit`); flagged for a follow-up.
|
|
156
|
+
|
|
157
|
+
## [3.0.1] - 2026-05-07
|
|
158
|
+
|
|
159
|
+
Post-3.0.0 audit pass — four player-visible / correctness bugs introduced or missed by the 3.0.0 cut, plus a batch of test-coverage and small-correctness improvements. No behaviour changes for code paths that were already correct.
|
|
160
|
+
|
|
161
|
+
### Fixed
|
|
162
|
+
|
|
163
|
+
- **`src/belote/game.py:860-873`** — `play_card()`'s per-trick running total honoured `kings_zero` and `tens_zero` but ignored the new 3.0.0 `aces_zero` and `jacks_zero` flags. Final scoring (`scoring.py::_calculate_base_points`) was correct, but the live HUD running total under Le Sauvage / L'Iconoclaste was wrong until the round ended. Mirrored the canonical `scoring.py` zero-rank pattern; `bm` aliased once for readability.
|
|
164
|
+
- **`src/belote/belatro/ui/hud.py::_SYNERGY_PAIRS`** — referenced four joker IDs (`le_finisseur`, `le_dix_de_der`, `le_marseillais`, `carre_aces_x2`) that don't exist in the registry. Two of the four pairs were dead code that could never fire. Removed; `validate_synergy_ids()` now exposes a self-check, and `register_all_items()` asserts every synergy ID resolves so future typos surface at import time.
|
|
165
|
+
- **`src/belote/gameflow.py:248-258`** — the a11y trick-winner announcement used raw `card_points()` and ignored every boss zero-rank flag, so screen-reader users heard inflated trick scores under Le Sauvage / L'Iconoclaste / Le Roi Mort / Les Dix Maudits / Les Clubs Bannis. New canonical helper `scoring.trick_card_points(state, trick)` is now the single source of truth for per-trick boss-aware totals.
|
|
166
|
+
- **`src/belote/ai.py::AIMemory.last_voids_key`** — the new-round reset in `update_memory()` cleared `played`, `known_voids`, and `processed_tricks_count` but not the cache key. After the prior round's final key (e.g. `(7, 4)`), a fresh-round `(0, 0)` or `(0, 1)` could coincidentally match a key seen during round 1 and cause `_update_voids` to skip processing. Added `last_voids_key = None` to the reset.
|
|
167
|
+
|
|
168
|
+
### Added
|
|
169
|
+
|
|
170
|
+
- **`src/belote/scoring.py::trick_card_points`** — public helper for "card-point sum of one trick under all active boss zero-rank flags." Used by gameflow's a11y hook.
|
|
171
|
+
- **`src/belote/belatro/ui/hud.py::validate_synergy_ids`** — return the synergy IDs that aren't registered as jokers. Used by `register_all_items()` for the new startup self-check.
|
|
172
|
+
- **Tests**: `tests/test_a11y.py` (8 cases for `trick_card_points` + a11y stderr emit), `tests/belatro/test_hud_synergy.py` (5 cases for the synergy registry), HOLO/POLYCHROME edition tests + four `separate_scoring × zero-flag` composition tests in `tests/belatro/test_dead_flag_fixes.py`, cross-round void-cache regression test in `tests/test_ai.py`. Tests grew 489 → 509 (+20).
|
|
173
|
+
|
|
174
|
+
### Improved
|
|
175
|
+
|
|
176
|
+
- **`src/belote/belatro/run_summary.py`** — resolved path is cached at module level after the first call, so `mkdir` is no longer re-attempted on every BelAtro exit.
|
|
177
|
+
- **`src/belote/a11y.py`** — `BELOTE_A11Y` env var resolved once at module import (`_ENABLED` module variable). Tests use `_refresh_enabled_from_env()` to re-read after `monkeypatch.setenv`. Saves ~30 environ lookups per round in the disabled path.
|
|
178
|
+
- **`src/belote/scoring.py:649-668`** — Sans Atout Capot branch now asserts `taker_belote == 0 and defender_belote == 0`. Belote/Rebelote requires a unique trump suit, so this invariant should always hold under SA — the assertion documents it and surfaces any future regression that leaks belote points into the SA path.
|
|
179
|
+
- **`src/belote/belatro/run/boss.py::LeMime`** — docstring notes the redundancy between `declarations_zero` and `separate_scoring` and points to the regression test that pins their composition.
|
|
180
|
+
|
|
181
|
+
### Internal
|
|
182
|
+
|
|
183
|
+
- **Tests**: 489 → 509 (+20). All new modules covered: a11y boss-aware pts (8), HUD synergy registry (5), HOLO/POLYCHROME editions (2), separate_scoring × zero-flag matrix (4), cross-round void cache (1).
|
|
184
|
+
- **Strict gates**: pytest 509/509, mypy 0 errors, ruff 0 violations.
|
|
185
|
+
- **Perf baseline**: unchanged from 3.0.0 (sub-millisecond throughout).
|
|
186
|
+
|
|
187
|
+
## [3.0.0] - 2026-05-07
|
|
188
|
+
|
|
189
|
+
Bug-hunt + audit pass — three player-visible features that were registered but silently no-ops are now wired, the Capot scoring under Sans Atout / Tout Atout has been corrected, mypy is once again strict-clean, and a batch of P3 features lands behind the new BelAtro Endless mode flow.
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
|
|
193
|
+
- **`src/belote/scoring.py::score_round`** — Capot reward used a flat `CAPOT_BASE = 252` for every contract, over-paying SA Capots (252 vs the contract-correct 220) and under-paying TA Capots (252 vs 348). New `CAPOT_BASE_SANS_ATOUT = 220` and `CAPOT_BASE_TOUT_ATOUT = 348` constants in `config.py`; scoring now branches on `state.contract`. `tests/test_belote.py::TestCapotPerContract` covers all three contracts; `tests/test_official_rules.py::test_sans_atout_score_round_baseline` updated from 252 → 220.
|
|
194
|
+
- **`src/belote/belatro/items/planets.py::TheSun`** — `level_up_reward()` returned `{"bonus_mult_per_trick": 1.0}` but no consumer ever read the key. Wired into `belatro/core/scoring.py::ScoreAccumulator` on `TrickWonEvent` when `event.trump == Suit.TOUT_ATOUT and event.trick_number > 4`.
|
|
195
|
+
- **`src/belote/belatro/items/planets.py::Libra`** — `coinche_multiplier` was set on the contract level but never read. Now consumed at `RoundEndEvent` time, scaled by `event.coinche_level`, and gated on the round being a non-failed taker win for NS.
|
|
196
|
+
- **`src/belote/scoring.py`** — `RoundScore` was constructed via `**common_kwargs` splat; mypy lost the per-field types and reported 8 errors. Inlined into both branches.
|
|
197
|
+
- **`src/belote/ui/render.py::_slot_frame_row`** — variable shadowing (`for c in range(...)` then `for c in cells`) made mypy infer `cells[i]` as `int`. Renamed the inner loop variables (`c → i`, `c → cell`).
|
|
198
|
+
- **`src/belote/ui/prompts.py`** — three untyped helpers (`_hist_taker_label`, `_hist_contract_label`, `_hist_status`) now annotated with `RoundScore`.
|
|
199
|
+
- **`src/belote/ui/prompts.py::show_history`** — N806 lint on `W_RD/W_TKR/...` constants resolved by lowercasing the locals; behaviour unchanged.
|
|
200
|
+
- **`src/belote/ai.py::_process_trick_voids`** — under the `republicain_wild` flag (Le Républicain deck / boss reuse), playing a 7 or 8 off-suit no longer falsely flags the player as void in the lead suit. Hard AI's void inference now skips wild ranks when the flag is active.
|
|
201
|
+
|
|
202
|
+
### Added
|
|
203
|
+
|
|
204
|
+
- **`src/belote/belatro/main.py`** — post-Ante-8 endless prompt. After winning Ante 8, the run offers `Continue into Endless Mode? (Ante 9+ scales ×2.2)`. Built on the existing `BelAtroRun.endless` / `endless_ante_offset` infrastructure plus a new `BelAtroAnnounce.yes_no` helper.
|
|
205
|
+
- **`src/belote/belatro/items/base.py::Edition`** — new enum (NONE/FOIL/HOLO/POLYCHROME/NEGATIVE). Shop generation rolls per-joker editions; Foil adds +50 chips per trigger, Holo +10 mult, Polychrome ×1.5 mult, Negative grants an extra joker slot at purchase. Wiring lives in `belatro/run/shop.py` and `belatro/core/scoring.py::_apply_edition`.
|
|
206
|
+
- **`src/belote/belatro/run/boss.py`** — three new boss blinds (Le Sauvage / L'Iconoclaste / Le Mime) and three new `BossModifiers` flags (`aces_zero`, `jacks_zero`, `declarations_zero`) read by the existing scoring path.
|
|
207
|
+
- **`src/belote/belatro/ui/hud.py::detect_synergies`** — small registry of known joker pair combos; renders a `★ SYN×N` badge on the HUD when any pair (or 3+ jokers) is held.
|
|
208
|
+
- **`src/belote/achievements.py`** + **`src/belote/stats.py::Statistics.achievements`** — six classic-mode achievements (first Capot, 3 Capots in a session, 2 Capot streak, 300+ point round, hard win, ten games played) auto-evaluated post-round / post-game.
|
|
209
|
+
- **`src/belote/themes.py::THEMES["colorblind"]`** — deuteranopia/protanopia-friendly palette using blue/cyan/orange instead of red/green.
|
|
210
|
+
- **`src/belote/a11y.py`** — screen-reader hints. Cards played, trick winners, and round results emit one-line plain-text descriptions to stderr when the env var `BELOTE_A11Y=1` is set.
|
|
211
|
+
- **`src/belote/replay.py`** — `analyze_round()` runs the just-played decisions through the Hard AI and reports per-decision agreement; `summarize()` produces a one-line `Optimal: N/M (X%)` string.
|
|
212
|
+
- **`src/belote/belatro/ghost_run.py`** — `GhostRecorder` accumulates seed + bid + play events and serializes them to JSONL under the user data dir. Foundation for a future ghost-replay viewer; the JSON format is versioned (v1).
|
|
213
|
+
- **`src/belote/belatro/run_summary.py`** — appends a one-line per-run summary (deck, ante, jokers, won) to `~/.local/share/belote/run_history.jsonl` on BelAtro exit. Best-effort, OSError-swallowed.
|
|
214
|
+
- **`src/belote/gameflow.py::show_hand_preview`** — short `Dealing…` flourish before the hand preview, gated on the existing speed setting and skippable via Space/Esc.
|
|
215
|
+
|
|
216
|
+
### Improved
|
|
217
|
+
|
|
218
|
+
- **`src/belote/ansi.py`** — 16 palette accessors (`felt_bg()`, `red_fg()`, …) previously hit `theme_manager.get_current()` per call. Cached the active `Theme` at module level and registered an invalidation callback with `theme_manager`. Lower per-render dict-lookup overhead.
|
|
219
|
+
- **`src/belote/themes.py::ThemeManager`** — redundant class-level `_current_theme_name` removed; new public `current_name` property; `_initialized` guard now uses `getattr` for clarity. Eight call sites in `ui/menu.py`, `ui/prompts.py`, and `ui/render.py` migrated off `_current_theme_name`.
|
|
220
|
+
- **`src/belote/ui/render.py`** — `_LAST_RENDER_KEY` list-of-one singleton replaced with a module-level variable + `global` declaration; same behaviour, less Python idiom drag.
|
|
221
|
+
- **`src/belote/ai.py::AIMemory.last_voids_key`** — caches `(completed_count, current_trick_len)` so `_update_voids` short-circuits on repeat calls within the same trick decision (lookahead exploration triggered redundant scans).
|
|
222
|
+
|
|
223
|
+
### Internal
|
|
224
|
+
|
|
225
|
+
- **Performance baseline (post-fix)**: `scripts/benchmark.py` reports render 0.271ms (±0.044), AI Easy/Med/Hard 0.015 / 0.030 / 0.026 ms, BelAtro update 0.032 ms, scoring 0.169 ms, deal 0.071 ms, legal_cards 0.012 ms.
|
|
226
|
+
- **Tests**: 446 → 489 (+43). New files: `tests/test_ansi_helpers.py`, `tests/test_achievements.py`, `tests/test_replay.py`, `tests/belatro/test_ghost_run.py`. Existing files extended with Capot-per-contract, Sun/Libra wiring, TA→le_fanatique unlock, republicain-void edge case, and three new boss tests.
|
|
227
|
+
- **Strict mode**: README's "0 mypy errors / 0 ruff violations" claim was inaccurate at 2.9.5 (18 mypy + 10 ruff at audit time). Both gates are now actually clean.
|
|
228
|
+
|
|
8
229
|
## [2.9.5] - 2026-05-07
|
|
9
230
|
|
|
10
231
|
In-game keyboard shortcuts cleaned up, the trick mat now anchors every played card inside a visible per-seat slot, the round history overlay carries the full per-round picture, and the cards have been redrawn in a GRIMAUD-1898 style with both-corner indices and patterned pip layouts.
|
|
@@ -78,21 +78,28 @@ PYTHONPATH=src pytest --cov=belote --cov-report=term-missing
|
|
|
78
78
|
The project maintains zero lint and type-check violations. Run all checks with:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
# Type checking (0 errors expected)
|
|
82
|
-
PYTHONPATH=src mypy
|
|
81
|
+
# Type checking (0 errors expected, strict mode)
|
|
82
|
+
PYTHONPATH=src mypy --strict src/
|
|
83
83
|
|
|
84
84
|
# Linting (0 violations expected)
|
|
85
|
-
ruff check
|
|
86
|
-
# Full test suite (435 tests expected)
|
|
87
|
-
PYTHONPATH=src pytest
|
|
85
|
+
ruff check src/ tests/
|
|
88
86
|
|
|
89
|
-
#
|
|
87
|
+
# Full test suite (528 tests expected)
|
|
88
|
+
PYTHONPATH=src pytest
|
|
89
|
+
```
|
|
90
90
|
|
|
91
|
-
Current baseline:
|
|
91
|
+
Current baseline (3.2.0):
|
|
92
92
|
- **mypy**: 0 errors (strict mode)
|
|
93
93
|
- **ruff**: 0 violations
|
|
94
|
-
- **pytest**:
|
|
94
|
+
- **pytest**: 528 tests, 0 failures
|
|
95
95
|
|
|
96
|
+
Run all gates before committing:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
PYTHONPATH=src python -m pytest --tb=short -q && \
|
|
100
|
+
python -m mypy --strict src/ && \
|
|
101
|
+
python -m ruff check src/ tests/
|
|
102
|
+
```
|
|
96
103
|
|
|
97
104
|
## Benchmarking
|
|
98
105
|
|
|
@@ -101,6 +108,36 @@ A benchmarking script is provided to measure rendering and AI performance:
|
|
|
101
108
|
PYTHONPATH=src python scripts/benchmark.py
|
|
102
109
|
```
|
|
103
110
|
|
|
111
|
+
3.0.0 baseline numbers (Linux, Python 3.10+, 1000 iterations):
|
|
112
|
+
- Render: 0.27 ms (±0.04)
|
|
113
|
+
- AI Hard decide_card: 0.026 ms (±0.003)
|
|
114
|
+
- BelAtro state update: 0.032 ms (±0.004)
|
|
115
|
+
- score_round: 0.169 ms
|
|
116
|
+
- legal_cards: 0.012 ms
|
|
117
|
+
|
|
118
|
+
Use these as a regression-detection floor for future changes.
|
|
119
|
+
|
|
120
|
+
## Accessibility
|
|
121
|
+
|
|
122
|
+
Set `BELOTE_A11Y=1` to emit one-line plain-text descriptions of card plays,
|
|
123
|
+
trick winners, and round results to stderr — readable by terminal screen
|
|
124
|
+
readers (Orca, NVDA over WSL, VoiceOver via iTerm2).
|
|
125
|
+
|
|
126
|
+
## Optional Runtime Flags
|
|
127
|
+
|
|
128
|
+
The following environment variables enable opt-in features. Each is read
|
|
129
|
+
once at startup; toggling mid-run has no effect.
|
|
130
|
+
|
|
131
|
+
- `BELOTE_REPLAY=1` — after every Classic round, print a one-line summary
|
|
132
|
+
of how often South's plays matched the Hard-AI's preferred line
|
|
133
|
+
(e.g. `Replay: Optimal plays: 6/8 (75%)`). Educational only — never
|
|
134
|
+
affects scoring. Backed by `src/belote/replay.py`.
|
|
135
|
+
- `BELOTE_GHOST=1` — silently record every BelAtro run (seed, deck,
|
|
136
|
+
bids, plays, round outcomes) to
|
|
137
|
+
`~/.local/share/belote/ghosts/<label>-<seed>.json`. The file is written
|
|
138
|
+
once when the run ends. Useful for sharing or replaying interesting
|
|
139
|
+
runs. Backed by `src/belote/belatro/ghost_run.py`.
|
|
140
|
+
|
|
104
141
|
## Releasing a New Version
|
|
105
142
|
|
|
106
143
|
### Code-only update (push to GitHub without releasing a new PyPI version)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: A 4-player terminal card game
|
|
5
5
|
Project-URL: Homepage, https://github.com/ElysiumDisc/belote
|
|
6
6
|
Project-URL: Repository, https://github.com/ElysiumDisc/belote
|
|
@@ -45,7 +45,54 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
|
|
46
46
|
Complete implementation of the French card game Belote for the terminal, with a full-screen green felt table and full card graphics at compass positions (N/W/E/S).
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## What's new in 3.2.0
|
|
49
|
+
|
|
50
|
+
- **Joker correctness** — La Sentinelle and Le Dernier Mot both used to key on `Seat.SOUTH` instead of the NS team, so the joker silently no-op'd when North (the AI partner) held the trump Jack or won the last trick. Both now correctly fire on team membership.
|
|
51
|
+
- **Score floor** — L'Égoïste's `add_chips = -event.card_points` per partner-won trick could drive the running total negative and produce a negative final round score; `ScoreAccumulator.get_total` now clamps at 0 so the intermediate log can still show the deduction without the visible score going below zero.
|
|
52
|
+
- **Auto-coinche event parity** — The NS-taker `auto_coinche` boss path now re-emits `BidMadeEvent` with the new `coinche_level`, matching the EW-taker branch. Pre-3.2 jokers and HUD subscribed to `on_bid` silently missed this code path.
|
|
53
|
+
- **Determinism** — The shop and the three RNG-using tarots (`LeJugement`, `LaPretresse`, `LeFou`) now all draw from the run's seeded `_get_rng()` instead of the module-level `random`. Ghost-run replays are now reproducible across shop generations. `LaPretresse` additionally `sample`s instead of `choice`-ing twice, so the two planets it grants are always distinct.
|
|
54
|
+
- **Registry / boss-field hygiene** — `register_joker/planet/tarot/voucher` now assert against duplicate IDs (typo'd registrations used to silently overwrite the original). `boss_fields` in `modifier_patch.py` is now derived from `BossModifiers`' dataclass fields, so adding a new flag no longer requires updating an out-of-band allowlist.
|
|
55
|
+
- **UI fix** — `patch_trick_card` now re-applies `render`'s vertical-centering offset; on tall terminals (>40 rows) it used to draw single-card patches too high.
|
|
56
|
+
- **Audit reconciliation** — This release consolidates the verified findings from two independent LLM code audits (Qwen 3.6 27B + Ring 1T). Both audits had load-bearing false positives — Qwen's two P0 "dead voucher / dead boss flag" claims were both wrong (the flags are consumed); Ring's "critical IllegalMoveError" only fires under test mocks. Eleven rejected claims are catalogued in the changelog so they aren't re-investigated.
|
|
57
|
+
- **Test coverage** — 528 tests (up from 525). Strict gates still clean: pytest 528/528, mypy 0 errors, ruff 0 violations.
|
|
58
|
+
|
|
59
|
+
## What's new in 3.1.0
|
|
60
|
+
|
|
61
|
+
- **Bug fixes** — HUD running-total no longer drifts under multi-boss combos (`Les Clubs Bannis + Le Roi Mort` style: pre-3.1.0 the rank-zero recompute silently overwrote the `ban_clubs` zeroing). `Shop.buy_item` no longer charges money when consumable slots are full — the "Slots full — sell first" banner now fires before any spend.
|
|
62
|
+
- **TierceForge wired up** — the voucher shipped in 3.0.0 with a working backend but no UI caller; the feature was unreachable. The shop now shows a "Forge ×N/3" tile when the voucher is owned, opens a numbered planet picker on Enter, and confirms the level-up via a banner.
|
|
63
|
+
- **Performance** — `score_round` and `apply_round_score` no longer re-walk the trick list (~16 fewer `trick_winner_seat` calls per round). The per-event `copy.deepcopy` in `ScoreAccumulator.update_state` is gone (~20 deepcopies/round saved); replaced with a shallow `dict(...)` plus a scalar-only invariant test that locks the contract. Hard-AI's `_score_card_play` precomputes hand suit counts and trump tallies once per turn instead of per candidate.
|
|
64
|
+
- **Cleanup** — the `modifier_patch` underscore-prefix shim is gone (23 boss `apply()` methods rewritten to use unprefixed field names; the `getattr(state, "_X", False)` anti-pattern is now locked against by a regression test). `slots=True` added to `Statistics`, `SessionStats`, `ScoreAccumulator`. Bare `except Exception:` in key-press parsing narrowed; `print → logging` in stats.
|
|
65
|
+
- **Test coverage** — 525 tests (up from 510). Strict gates still clean: pytest 525/525, mypy 0 errors, ruff 0 violations.
|
|
66
|
+
|
|
67
|
+
## What's new in 3.0.3
|
|
68
|
+
|
|
69
|
+
- **Full-codebase audit** — three-agent pass over the classic engine, BelAtro content wiring, and perf / code-quality hotspots (~7,100 LOC). Headline: engine is rule-correct against canonical French Belote; BelAtro content matrix is **93/93 wired** (21 bosses, 8 planets, 36 jokers, 4 editions, 12 vouchers, 12 tarots). Prioritized findings list (1 P0 functional, 2 P0 perf, 5 P1, 7 P2) tracked for follow-up cuts; implementation landed in 3.1.0.
|
|
70
|
+
- **Doc accuracy** — README boss-count corrected (18 → 21; 3.0.0 added Le Sauvage / L'Iconoclaste / Le Mime), and two stale `(435 tests)` references bumped to 510 to match the figure already present elsewhere in the file.
|
|
71
|
+
|
|
72
|
+
## What's new in 3.0.2
|
|
73
|
+
|
|
74
|
+
- **Replay analyzer + Ghost run wired up** — both shipped in 3.0.0 as code modules but were never called from the running game. Now opt-in behind `BELOTE_REPLAY=1` (post-round Hard-AI comparison) and `BELOTE_GHOST=1` (per-run JSON dump to `~/.local/share/belote/ghosts/`). See DEVELOPMENT.md › Optional Runtime Flags.
|
|
75
|
+
- **Performance** — `score_round()` now caches per-trick winners once instead of recomputing them in each boss-modifier helper (2-3× walks → 1× walk per round). `register_all_items()` is now idempotent so test setup no longer re-walks every items module per `BelAtroRun`. Bidding's special-bid path (TA / SA) hoists `_suit_lengths` out of the per-difficulty branches.
|
|
76
|
+
- **Defensive pin** — every entry in `ALL_BOSS_MODIFIERS` is now asserted to actually toggle a `BossModifiers` field via `.flags()`. Catches typo'd `state.patch("_misspelled", True)` keys at test time rather than letting the boss silently no-op.
|
|
77
|
+
- **Test coverage** — 525 tests (up from 509).
|
|
78
|
+
|
|
79
|
+
## What's new in 3.0.1
|
|
80
|
+
|
|
81
|
+
- **Bug fixes** — `play_card()` running total now honours `aces_zero` / `jacks_zero` (Le Sauvage / L'Iconoclaste); the screen-reader trick-winner pts are now boss-aware; the HUD synergy registry no longer references nonexistent joker IDs; the AI void-cache key is now reset across rounds.
|
|
82
|
+
- **Test coverage** — 509 tests (up from 489): HOLO/POLYCHROME editions, multi-boss composition (`separate_scoring × zero-flag`), a11y boss-aware pts, synergy registry self-check.
|
|
83
|
+
|
|
84
|
+
## What's new in 3.0.0
|
|
85
|
+
|
|
86
|
+
- **Endless mode** — beat Ante 8 in BelAtro and the run offers a continuation: targets scale ×2.2 per ante and a furthest-ante leaderboard tracks how deep you go.
|
|
87
|
+
- **Joker editions** — Foil (+50 chips), Holo (+10 mult), Polychrome (×1.5 mult), Negative (extra slot) randomly stamp shop jokers.
|
|
88
|
+
- **Three new boss blinds** — Le Sauvage (Aces = 0), L'Iconoclaste (Jacks = 0, even trump-J), Le Mime (Declarations = 0).
|
|
89
|
+
- **Achievements** — six classic-mode milestones tracked across sessions.
|
|
90
|
+
- **Colorblind palette** + **screen-reader hints** (`BELOTE_A11Y=1`) for accessibility.
|
|
91
|
+
- **Replay analyzer** — module added (post-round Hard-AI comparison). User-facing wiring landed in 3.0.2 behind `BELOTE_REPLAY=1`.
|
|
92
|
+
- **Ghost run recording** + **run summary log** — modules added (serialize a run / append per-run JSON). Run-summary fires automatically; ghost-run user-facing wiring landed in 3.0.2 behind `BELOTE_GHOST=1`.
|
|
93
|
+
- **Bug fixes** — Capot under Sans Atout / Tout Atout now uses the correct base (220 / 348, not 252); The Sun and Libra planets actually do something now; AI void inference no longer mis-flags voids under Le Républicain wild 7/8.
|
|
94
|
+
|
|
95
|
+
## BelAtro Expansion
|
|
49
96
|
|
|
50
97
|
**BelAtro** is a major roguelite expansion inspired by *Balatro*. Play through 8 Antes of escalating difficulty, build a deck of powerful Jokers, and use Tarot cards and Planets to break the game!
|
|
51
98
|
|
|
@@ -199,7 +246,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
199
246
|
|
|
200
247
|
- **BelAtro Roguelite Mode:** A massive expansion featuring 36 Jokers, 12 Tarot cards, 8 Planets, 12 Vouchers, and permanent upgrades.
|
|
201
248
|
- **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
|
|
202
|
-
- **Full Boss Blind Suite:** All
|
|
249
|
+
- **Full Boss Blind Suite:** All 21 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
|
|
203
250
|
- **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
|
|
204
251
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
205
252
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
@@ -251,7 +298,7 @@ belote/
|
|
|
251
298
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
252
299
|
│ ├── stats.py # Global and session statistics tracking
|
|
253
300
|
│ └── rules.py # Game rules content
|
|
254
|
-
├── tests/ # Comprehensive test suite (
|
|
301
|
+
├── tests/ # Comprehensive test suite (528 tests)
|
|
255
302
|
├── scripts/ # Performance benchmarks
|
|
256
303
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
257
304
|
├── LICENSE # MIT License
|
|
@@ -267,14 +314,14 @@ belote/
|
|
|
267
314
|
PYTHONPATH=src pytest
|
|
268
315
|
```
|
|
269
316
|
|
|
270
|
-
Currently **
|
|
317
|
+
Currently **528 tests** passing with 100% coverage on game-logic modules.
|
|
271
318
|
|
|
272
319
|
## Technical Integrity
|
|
273
320
|
|
|
274
321
|
The codebase is strictly validated with the following tools:
|
|
275
322
|
- **mypy**: 0 errors (strict type safety)
|
|
276
323
|
- **ruff**: 0 violations (linting & formatting)
|
|
277
|
-
- **pytest**:
|
|
324
|
+
- **pytest**: 528/528 passed
|
|
278
325
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
279
326
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
280
327
|
|
|
@@ -2,7 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
Complete implementation of the French card game Belote for the terminal, with a full-screen green felt table and full card graphics at compass positions (N/W/E/S).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What's new in 3.2.0
|
|
6
|
+
|
|
7
|
+
- **Joker correctness** — La Sentinelle and Le Dernier Mot both used to key on `Seat.SOUTH` instead of the NS team, so the joker silently no-op'd when North (the AI partner) held the trump Jack or won the last trick. Both now correctly fire on team membership.
|
|
8
|
+
- **Score floor** — L'Égoïste's `add_chips = -event.card_points` per partner-won trick could drive the running total negative and produce a negative final round score; `ScoreAccumulator.get_total` now clamps at 0 so the intermediate log can still show the deduction without the visible score going below zero.
|
|
9
|
+
- **Auto-coinche event parity** — The NS-taker `auto_coinche` boss path now re-emits `BidMadeEvent` with the new `coinche_level`, matching the EW-taker branch. Pre-3.2 jokers and HUD subscribed to `on_bid` silently missed this code path.
|
|
10
|
+
- **Determinism** — The shop and the three RNG-using tarots (`LeJugement`, `LaPretresse`, `LeFou`) now all draw from the run's seeded `_get_rng()` instead of the module-level `random`. Ghost-run replays are now reproducible across shop generations. `LaPretresse` additionally `sample`s instead of `choice`-ing twice, so the two planets it grants are always distinct.
|
|
11
|
+
- **Registry / boss-field hygiene** — `register_joker/planet/tarot/voucher` now assert against duplicate IDs (typo'd registrations used to silently overwrite the original). `boss_fields` in `modifier_patch.py` is now derived from `BossModifiers`' dataclass fields, so adding a new flag no longer requires updating an out-of-band allowlist.
|
|
12
|
+
- **UI fix** — `patch_trick_card` now re-applies `render`'s vertical-centering offset; on tall terminals (>40 rows) it used to draw single-card patches too high.
|
|
13
|
+
- **Audit reconciliation** — This release consolidates the verified findings from two independent LLM code audits (Qwen 3.6 27B + Ring 1T). Both audits had load-bearing false positives — Qwen's two P0 "dead voucher / dead boss flag" claims were both wrong (the flags are consumed); Ring's "critical IllegalMoveError" only fires under test mocks. Eleven rejected claims are catalogued in the changelog so they aren't re-investigated.
|
|
14
|
+
- **Test coverage** — 528 tests (up from 525). Strict gates still clean: pytest 528/528, mypy 0 errors, ruff 0 violations.
|
|
15
|
+
|
|
16
|
+
## What's new in 3.1.0
|
|
17
|
+
|
|
18
|
+
- **Bug fixes** — HUD running-total no longer drifts under multi-boss combos (`Les Clubs Bannis + Le Roi Mort` style: pre-3.1.0 the rank-zero recompute silently overwrote the `ban_clubs` zeroing). `Shop.buy_item` no longer charges money when consumable slots are full — the "Slots full — sell first" banner now fires before any spend.
|
|
19
|
+
- **TierceForge wired up** — the voucher shipped in 3.0.0 with a working backend but no UI caller; the feature was unreachable. The shop now shows a "Forge ×N/3" tile when the voucher is owned, opens a numbered planet picker on Enter, and confirms the level-up via a banner.
|
|
20
|
+
- **Performance** — `score_round` and `apply_round_score` no longer re-walk the trick list (~16 fewer `trick_winner_seat` calls per round). The per-event `copy.deepcopy` in `ScoreAccumulator.update_state` is gone (~20 deepcopies/round saved); replaced with a shallow `dict(...)` plus a scalar-only invariant test that locks the contract. Hard-AI's `_score_card_play` precomputes hand suit counts and trump tallies once per turn instead of per candidate.
|
|
21
|
+
- **Cleanup** — the `modifier_patch` underscore-prefix shim is gone (23 boss `apply()` methods rewritten to use unprefixed field names; the `getattr(state, "_X", False)` anti-pattern is now locked against by a regression test). `slots=True` added to `Statistics`, `SessionStats`, `ScoreAccumulator`. Bare `except Exception:` in key-press parsing narrowed; `print → logging` in stats.
|
|
22
|
+
- **Test coverage** — 525 tests (up from 510). Strict gates still clean: pytest 525/525, mypy 0 errors, ruff 0 violations.
|
|
23
|
+
|
|
24
|
+
## What's new in 3.0.3
|
|
25
|
+
|
|
26
|
+
- **Full-codebase audit** — three-agent pass over the classic engine, BelAtro content wiring, and perf / code-quality hotspots (~7,100 LOC). Headline: engine is rule-correct against canonical French Belote; BelAtro content matrix is **93/93 wired** (21 bosses, 8 planets, 36 jokers, 4 editions, 12 vouchers, 12 tarots). Prioritized findings list (1 P0 functional, 2 P0 perf, 5 P1, 7 P2) tracked for follow-up cuts; implementation landed in 3.1.0.
|
|
27
|
+
- **Doc accuracy** — README boss-count corrected (18 → 21; 3.0.0 added Le Sauvage / L'Iconoclaste / Le Mime), and two stale `(435 tests)` references bumped to 510 to match the figure already present elsewhere in the file.
|
|
28
|
+
|
|
29
|
+
## What's new in 3.0.2
|
|
30
|
+
|
|
31
|
+
- **Replay analyzer + Ghost run wired up** — both shipped in 3.0.0 as code modules but were never called from the running game. Now opt-in behind `BELOTE_REPLAY=1` (post-round Hard-AI comparison) and `BELOTE_GHOST=1` (per-run JSON dump to `~/.local/share/belote/ghosts/`). See DEVELOPMENT.md › Optional Runtime Flags.
|
|
32
|
+
- **Performance** — `score_round()` now caches per-trick winners once instead of recomputing them in each boss-modifier helper (2-3× walks → 1× walk per round). `register_all_items()` is now idempotent so test setup no longer re-walks every items module per `BelAtroRun`. Bidding's special-bid path (TA / SA) hoists `_suit_lengths` out of the per-difficulty branches.
|
|
33
|
+
- **Defensive pin** — every entry in `ALL_BOSS_MODIFIERS` is now asserted to actually toggle a `BossModifiers` field via `.flags()`. Catches typo'd `state.patch("_misspelled", True)` keys at test time rather than letting the boss silently no-op.
|
|
34
|
+
- **Test coverage** — 525 tests (up from 509).
|
|
35
|
+
|
|
36
|
+
## What's new in 3.0.1
|
|
37
|
+
|
|
38
|
+
- **Bug fixes** — `play_card()` running total now honours `aces_zero` / `jacks_zero` (Le Sauvage / L'Iconoclaste); the screen-reader trick-winner pts are now boss-aware; the HUD synergy registry no longer references nonexistent joker IDs; the AI void-cache key is now reset across rounds.
|
|
39
|
+
- **Test coverage** — 509 tests (up from 489): HOLO/POLYCHROME editions, multi-boss composition (`separate_scoring × zero-flag`), a11y boss-aware pts, synergy registry self-check.
|
|
40
|
+
|
|
41
|
+
## What's new in 3.0.0
|
|
42
|
+
|
|
43
|
+
- **Endless mode** — beat Ante 8 in BelAtro and the run offers a continuation: targets scale ×2.2 per ante and a furthest-ante leaderboard tracks how deep you go.
|
|
44
|
+
- **Joker editions** — Foil (+50 chips), Holo (+10 mult), Polychrome (×1.5 mult), Negative (extra slot) randomly stamp shop jokers.
|
|
45
|
+
- **Three new boss blinds** — Le Sauvage (Aces = 0), L'Iconoclaste (Jacks = 0, even trump-J), Le Mime (Declarations = 0).
|
|
46
|
+
- **Achievements** — six classic-mode milestones tracked across sessions.
|
|
47
|
+
- **Colorblind palette** + **screen-reader hints** (`BELOTE_A11Y=1`) for accessibility.
|
|
48
|
+
- **Replay analyzer** — module added (post-round Hard-AI comparison). User-facing wiring landed in 3.0.2 behind `BELOTE_REPLAY=1`.
|
|
49
|
+
- **Ghost run recording** + **run summary log** — modules added (serialize a run / append per-run JSON). Run-summary fires automatically; ghost-run user-facing wiring landed in 3.0.2 behind `BELOTE_GHOST=1`.
|
|
50
|
+
- **Bug fixes** — Capot under Sans Atout / Tout Atout now uses the correct base (220 / 348, not 252); The Sun and Libra planets actually do something now; AI void inference no longer mis-flags voids under Le Républicain wild 7/8.
|
|
51
|
+
|
|
52
|
+
## BelAtro Expansion
|
|
6
53
|
|
|
7
54
|
**BelAtro** is a major roguelite expansion inspired by *Balatro*. Play through 8 Antes of escalating difficulty, build a deck of powerful Jokers, and use Tarot cards and Planets to break the game!
|
|
8
55
|
|
|
@@ -156,7 +203,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
156
203
|
|
|
157
204
|
- **BelAtro Roguelite Mode:** A massive expansion featuring 36 Jokers, 12 Tarot cards, 8 Planets, 12 Vouchers, and permanent upgrades.
|
|
158
205
|
- **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
|
|
159
|
-
- **Full Boss Blind Suite:** All
|
|
206
|
+
- **Full Boss Blind Suite:** All 21 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
|
|
160
207
|
- **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
|
|
161
208
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
162
209
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
@@ -208,7 +255,7 @@ belote/
|
|
|
208
255
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
209
256
|
│ ├── stats.py # Global and session statistics tracking
|
|
210
257
|
│ └── rules.py # Game rules content
|
|
211
|
-
├── tests/ # Comprehensive test suite (
|
|
258
|
+
├── tests/ # Comprehensive test suite (528 tests)
|
|
212
259
|
├── scripts/ # Performance benchmarks
|
|
213
260
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
214
261
|
├── LICENSE # MIT License
|
|
@@ -224,14 +271,14 @@ belote/
|
|
|
224
271
|
PYTHONPATH=src pytest
|
|
225
272
|
```
|
|
226
273
|
|
|
227
|
-
Currently **
|
|
274
|
+
Currently **528 tests** passing with 100% coverage on game-logic modules.
|
|
228
275
|
|
|
229
276
|
## Technical Integrity
|
|
230
277
|
|
|
231
278
|
The codebase is strictly validated with the following tools:
|
|
232
279
|
- **mypy**: 0 errors (strict type safety)
|
|
233
280
|
- **ruff**: 0 violations (linting & formatting)
|
|
234
|
-
- **pytest**:
|
|
281
|
+
- **pytest**: 528/528 passed
|
|
235
282
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
236
283
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
237
284
|
|