belote-cli 2.9.2__tar.gz → 3.0.2__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.
Files changed (118) hide show
  1. {belote_cli-2.9.2 → belote_cli-3.0.2}/.claude/settings.local.json +2 -1
  2. {belote_cli-2.9.2 → belote_cli-3.0.2}/CHANGELOG.md +138 -0
  3. {belote_cli-2.9.2 → belote_cli-3.0.2}/DEVELOPMENT.md +45 -8
  4. {belote_cli-2.9.2 → belote_cli-3.0.2}/PKG-INFO +32 -9
  5. {belote_cli-2.9.2 → belote_cli-3.0.2}/README.md +31 -8
  6. {belote_cli-2.9.2 → belote_cli-3.0.2}/pyproject.toml +1 -1
  7. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/__init__.py +1 -1
  8. belote_cli-3.0.2/src/belote/a11y.py +93 -0
  9. belote_cli-3.0.2/src/belote/achievements.py +104 -0
  10. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ai.py +48 -14
  11. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ansi.py +42 -18
  12. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/scoring.py +38 -0
  13. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/modifier_patch.py +2 -1
  14. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/round_driver.py +26 -0
  15. belote_cli-3.0.2/src/belote/belatro/ghost_run.py +83 -0
  16. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/base.py +19 -0
  17. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/registry.py +27 -0
  18. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/main.py +34 -0
  19. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/boss.py +50 -0
  20. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/shop.py +37 -2
  21. belote_cli-3.0.2/src/belote/belatro/run_summary.py +71 -0
  22. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/announce.py +28 -0
  23. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/hud.py +49 -0
  24. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/config.py +8 -0
  25. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/game.py +21 -4
  26. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/gameflow.py +52 -1
  27. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/input.py +12 -8
  28. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/main.py +2 -2
  29. belote_cli-3.0.2/src/belote/replay.py +69 -0
  30. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/scoring.py +171 -20
  31. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/stats.py +21 -0
  32. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/themes.py +35 -2
  33. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/menu.py +8 -3
  34. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/prompts.py +118 -33
  35. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/render.py +207 -62
  36. belote_cli-3.0.2/tests/belatro/test_dead_flag_fixes.py +780 -0
  37. belote_cli-3.0.2/tests/belatro/test_ghost_run.py +49 -0
  38. belote_cli-3.0.2/tests/belatro/test_hud_synergy.py +61 -0
  39. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase0_coverage.py +27 -0
  40. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_progression.py +63 -0
  41. belote_cli-3.0.2/tests/test_a11y.py +135 -0
  42. belote_cli-3.0.2/tests/test_achievements.py +58 -0
  43. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_ai.py +66 -0
  44. belote_cli-3.0.2/tests/test_ansi_helpers.py +162 -0
  45. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_belote.py +91 -0
  46. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_extended.py +44 -0
  47. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_layout.py +2 -2
  48. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_official_rules.py +3 -2
  49. belote_cli-3.0.2/tests/test_replay.py +48 -0
  50. belote_cli-2.9.2/tests/belatro/test_dead_flag_fixes.py +0 -290
  51. {belote_cli-2.9.2 → belote_cli-3.0.2}/.gitignore +0 -0
  52. {belote_cli-2.9.2 → belote_cli-3.0.2}/.python-version +0 -0
  53. {belote_cli-2.9.2 → belote_cli-3.0.2}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
  54. {belote_cli-2.9.2 → belote_cli-3.0.2}/LICENSE +0 -0
  55. {belote_cli-2.9.2 → belote_cli-3.0.2}/scripts/benchmark.py +0 -0
  56. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/__init__.py +0 -0
  57. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/__init__.py +0 -0
  58. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/__init__.py +0 -0
  59. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/economy.py +0 -0
  60. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/run_state.py +0 -0
  61. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/__init__.py +0 -0
  62. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/event_bus.py +0 -0
  63. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/__init__.py +0 -0
  64. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/__init__.py +0 -0
  65. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/annonces.py +0 -0
  66. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/coinche.py +0 -0
  67. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/contract.py +0 -0
  68. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/corrupted.py +0 -0
  69. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/economy.py +0 -0
  70. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
  71. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
  72. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
  73. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
  74. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
  75. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
  76. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/planets.py +0 -0
  77. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/tarots.py +0 -0
  78. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/vouchers.py +0 -0
  79. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/__init__.py +0 -0
  80. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/partner_state.py +0 -0
  81. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/personality.py +0 -0
  82. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/trust.py +0 -0
  83. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/progression/__init__.py +0 -0
  84. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/progression/save.py +0 -0
  85. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/progression/unlocks.py +0 -0
  86. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/__init__.py +0 -0
  87. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/ante.py +0 -0
  88. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/ante_themes.py +0 -0
  89. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/decks.py +0 -0
  90. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/__init__.py +0 -0
  91. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/collection.py +0 -0
  92. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/menu.py +0 -0
  93. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/rules.py +0 -0
  94. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/shop.py +0 -0
  95. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/trust_bar.py +0 -0
  96. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/context.py +0 -0
  97. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/deck.py +0 -0
  98. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/rules.py +0 -0
  99. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/__init__.py +0 -0
  100. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/announce.py +0 -0
  101. {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/layout.py +0 -0
  102. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/__init__.py +0 -0
  103. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/__init__.py +0 -0
  104. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_belatro.py +0 -0
  105. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_boss_modifiers_integration.py +0 -0
  106. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_collection_logic.py +0 -0
  107. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_contract_unlocks.py +0 -0
  108. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_deck_variants.py +0 -0
  109. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_partner_trust.py +0 -0
  110. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase1_plumbing.py +0 -0
  111. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase2_content.py +0 -0
  112. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase3_meta.py +0 -0
  113. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_round_driver.py +0 -0
  114. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_game_logic.py +0 -0
  115. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_gameflow.py +0 -0
  116. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_new_coverage.py +0 -0
  117. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_properties.py +0 -0
  118. {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_undo.py +0 -0
@@ -10,7 +10,8 @@
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)"
14
15
  ]
15
16
  }
16
17
  }
@@ -5,6 +5,144 @@ 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.0.2] - 2026-05-08
9
+
10
+ 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.
11
+
12
+ ### Fixed
13
+
14
+ - **`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.
15
+ - **`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.
16
+
17
+ ### Improved
18
+
19
+ - **`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.
20
+ - **`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.
21
+ - **`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.
22
+
23
+ ### Added
24
+
25
+ - **`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.
26
+ - **`DEVELOPMENT.md`** — new "Optional Runtime Flags" section documenting `BELOTE_REPLAY` and `BELOTE_GHOST` next to the existing `BELOTE_A11Y` entry.
27
+
28
+ ### Internal
29
+
30
+ - **Tests**: 509 → 510 (+1).
31
+ - **Strict gates**: pytest 510/510, mypy 0 errors, ruff 0 violations.
32
+
33
+ ### Known issue (not fixed in this cut)
34
+
35
+ - `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.
36
+
37
+ ## [3.0.1] - 2026-05-07
38
+
39
+ 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.
40
+
41
+ ### Fixed
42
+
43
+ - **`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.
44
+ - **`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.
45
+ - **`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.
46
+ - **`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.
47
+
48
+ ### Added
49
+
50
+ - **`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.
51
+ - **`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.
52
+ - **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).
53
+
54
+ ### Improved
55
+
56
+ - **`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.
57
+ - **`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.
58
+ - **`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.
59
+ - **`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.
60
+
61
+ ### Internal
62
+
63
+ - **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).
64
+ - **Strict gates**: pytest 509/509, mypy 0 errors, ruff 0 violations.
65
+ - **Perf baseline**: unchanged from 3.0.0 (sub-millisecond throughout).
66
+
67
+ ## [3.0.0] - 2026-05-07
68
+
69
+ 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.
70
+
71
+ ### Fixed
72
+
73
+ - **`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.
74
+ - **`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`.
75
+ - **`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.
76
+ - **`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.
77
+ - **`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`).
78
+ - **`src/belote/ui/prompts.py`** — three untyped helpers (`_hist_taker_label`, `_hist_contract_label`, `_hist_status`) now annotated with `RoundScore`.
79
+ - **`src/belote/ui/prompts.py::show_history`** — N806 lint on `W_RD/W_TKR/...` constants resolved by lowercasing the locals; behaviour unchanged.
80
+ - **`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.
81
+
82
+ ### Added
83
+
84
+ - **`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.
85
+ - **`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`.
86
+ - **`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.
87
+ - **`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.
88
+ - **`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.
89
+ - **`src/belote/themes.py::THEMES["colorblind"]`** — deuteranopia/protanopia-friendly palette using blue/cyan/orange instead of red/green.
90
+ - **`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.
91
+ - **`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.
92
+ - **`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).
93
+ - **`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.
94
+ - **`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.
95
+
96
+ ### Improved
97
+
98
+ - **`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.
99
+ - **`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`.
100
+ - **`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.
101
+ - **`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).
102
+
103
+ ### Internal
104
+
105
+ - **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.
106
+ - **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.
107
+ - **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.
108
+
109
+ ## [2.9.5] - 2026-05-07
110
+
111
+ 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.
112
+
113
+ ### Fixed
114
+
115
+ - **`src/belote/input.py`** — `Key.THEME` was defined but never mapped to a keystroke; the help text falsely advertised `Shift+T` (which terminals can't generally distinguish in raw mode). Theme cycling during gameplay was unreachable. New mapping: `t/T → Key.THEME`, `h/H → Key.HIST`, `?` → `Key.HELP`, `o/O → Key.SORT`. Both the Unix and Windows readers updated.
116
+ - **`src/belote/input.py`** — `s` was previously stolen by `Key.SORT`, so pressing `S` in round-2 bidding triggered a sort instead of the **Sans Atout** quick-bid the help text promised. Sort now lives on `O` (matching what the help screen always claimed) and `s` falls through to `Key.CHAR` so SA bidding works.
117
+ - **`src/belote/gameflow.py::run_play`** — when the user had already pressed any key earlier in the round (which sets `skip_anims`), the post-trick pause was skipped entirely and the 4th card vanished before it could be read. New `MIN_TRICK_DWELL = 0.5s` non-skippable hold runs after every completed trick (even on the `instant` speed preset) so the player always sees all four cards before the mat clears.
118
+
119
+ ### Added
120
+
121
+ - **`src/belote/ui/render.py`** — visible **slot frames** drawn around each compass position on the trick mat. Implemented via three new helpers (`_slot_anchors`, `_slot_frame_row`, `_felt_pad_ns`, `_we_row`) that paint thin `─`/`│` borders on the felt cells immediately surrounding each card slot, in the felt-placeholder dim colour. Total mat dimensions (`6 + 3*card_h`) are unchanged, so `_calculate_base_row` and `patch_trick_card` continue to work without any coordinate adjustments — patched cards land exactly inside the existing frame.
122
+ - **`src/belote/game.py::RoundScore`** — eight new optional fields (`contract`, `trump`, `taker_seat`, `tricks_ns`, `tricks_ew`, `last_trick_winner`, `decl_summary_ns`, `decl_summary_ew`) populated from `state` and `state.completed_tricks` at scoring time. All fields default so existing test fixtures and any historical `RoundScore` constructions remain valid.
123
+ - **`src/belote/scoring.py::apply_round_score`** — now computes per-team trick counts via `trick_winner_seat`/`team_of`, builds short declaration labels (`"100♥"`, `"Belote"`, `"Carré-J"`, …) gated on the team's `*_decl_pts > 0` so only the *scored* declarations appear, and threads everything into `RoundScore`. New helper `_decl_short_label` covers belote/rebelote/sequence/carre.
124
+ - **`src/belote/ui/prompts.py::show_history`** — rewritten as an 8-column table (`RD | TAKER | CONTRACT | TRICKS | DECLARATIONS | NS | EW | STATUS`) for terminals ≥78 cols, with a 2-line-per-round fallback for narrower terminals. Status colouring: gold `CAPOT`, red `CHUTE`, dim `LITIGE`. Existing scrolling, view-height clamp, and exit-on-any-key behaviour preserved.
125
+ - **`src/belote/ui/render.py::_card_face_internal`** — full GRIMAUD-1898-inspired redraw:
126
+ - Both corners now carry a 3-cell `rank+suit` index (`A♠` top-left, `♠A` bottom-right). The index padding scales with `inner_w`.
127
+ - Pip cards (7-10) at `card_h ≥ 7` get a recognisable pip arrangement instead of a single centred suit symbol.
128
+ - Court cards J/Q/K each get a distinct multi-row motif (sword, jewelled headdress, crown).
129
+ - Aces get a decorative `╭─◆─╮` / `╰─◆─╯` wreath around the central suit.
130
+ - Compact 6×5 layout keeps the single inner row but still benefits from both-corner indices.
131
+ - All variants honour the active theme (`face_card_bg`, `card_face_bg`, `highlight_bg`, `red_fg`/`black_fg`) and the `DIM` prefix for illegal cards. ASCII fallback paths preserved.
132
+ - **`tests/test_extended.py::test_round_score_history_extra_fields`** — pins the new `RoundScore` fields end-to-end through `apply_round_score`.
133
+
134
+ ### Changed
135
+
136
+ - **`src/belote/ui/menu.py`** — main-menu loop now handles `Key.THEME` (cycles forward through `THEMES`), so the new in-game `T` shortcut works at the menu too.
137
+ - **`src/belote/main.py`**, **`src/belote/ui/render.py`** — game-over hint and HUD compact hint updated to advertise the new `[H] History` / `[T] Theme` shortcuts.
138
+ - **`src/belote/ui/prompts.py::show_help`** — help-screen text rewritten to match the new bindings.
139
+ - **`README.md`** — Controls section rewritten; theme section now lists all six themes by name.
140
+ - **`tests/test_layout.py::test_hud_compact_omits_help_hints_and_theme`** — assertion updated for the new compact-HUD hint substring.
141
+
142
+ ### Notes
143
+
144
+ 436/436 tests pass. No gameplay, scoring, or AI-decision changes — all updates are UX (keys, slot framing, dwell, history depth, card glyphs).
145
+
8
146
  ## [2.9.2] - 2026-05-07
9
147
 
10
148
  Render-pipeline fix for Konsole (KDE/Kubuntu) and other strict ANSI terminals where UI elements visibly stacked on top of each other — the top HUD repeating ~6 times, "Theme: Sepia Vintage" duplicating in the right column, "Partner" doubling, the bid prompt repainting below itself, and bid history accumulating between frames. The bug existed in the code on every terminal but VTE-based emulators (LXTerminal, GNOME Terminal, xterm) auto-blanked the leaking cells, masking it. Konsole's Vt102Emulation does not, so the leakage was visible.
@@ -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 (510 tests expected)
88
+ PYTHONPATH=src pytest
89
+ ```
90
90
 
91
- Current baseline:
91
+ Current baseline (3.0.2):
92
92
  - **mypy**: 0 errors (strict mode)
93
93
  - **ruff**: 0 violations
94
- - **pytest**: 435 tests, 0 failures
94
+ - **pytest**: 510 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.9.2
3
+ Version: 3.0.2
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,30 @@ 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
- ## Now Featuring: BelAtro Expansion
48
+ ## What's new in 3.0.2
49
+
50
+ - **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.
51
+ - **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.
52
+ - **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.
53
+ - **Test coverage** — 510 tests (up from 509).
54
+
55
+ ## What's new in 3.0.1
56
+
57
+ - **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.
58
+ - **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.
59
+
60
+ ## What's new in 3.0.0
61
+
62
+ - **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.
63
+ - **Joker editions** — Foil (+50 chips), Holo (+10 mult), Polychrome (×1.5 mult), Negative (extra slot) randomly stamp shop jokers.
64
+ - **Three new boss blinds** — Le Sauvage (Aces = 0), L'Iconoclaste (Jacks = 0, even trump-J), Le Mime (Declarations = 0).
65
+ - **Achievements** — six classic-mode milestones tracked across sessions.
66
+ - **Colorblind palette** + **screen-reader hints** (`BELOTE_A11Y=1`) for accessibility.
67
+ - **Replay analyzer** — module added (post-round Hard-AI comparison). User-facing wiring landed in 3.0.2 behind `BELOTE_REPLAY=1`.
68
+ - **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`.
69
+ - **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.
70
+
71
+ ## BelAtro Expansion
49
72
 
50
73
  **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
74
 
@@ -170,12 +193,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
170
193
  ## Controls
171
194
 
172
195
  **General:**
173
- - `?` or `H`: Show keyboard shortcut help
196
+ - `?`: Show keyboard shortcut help
174
197
  - `M`: Toggle sound effects on/off
175
- - `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
198
+ - `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
176
199
  - `Q`: Quit to main menu or exit
177
- - `t`: View Game History (Round-by-round)
178
- - `T`: Switch UI Theme
200
+ - `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
201
+ - `T`: Cycle UI Theme
179
202
 
180
203
  **Classic Belote:**
181
204
  - `↑` `↓`: Navigate options
@@ -183,7 +206,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
183
206
  - `Enter`: Select option / Enter submenu
184
207
 
185
208
  **BelAtro (Roguelite):**
186
- - `S`: View current Run State and Jokers
187
209
  - `1`-`5`: Inspect specific Jokers in the Shop
188
210
  - `U`: Use a consumable (Tarot/Planet) during gameplay
189
211
 
@@ -194,6 +216,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
194
216
  - `O`: Sort hand by suit and rank
195
217
  - `Z`: Undo last move
196
218
  - `Space` or `Esc`: Skip animations
219
+ - During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
197
220
 
198
221
  ## Features
199
222
 
@@ -204,7 +227,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
204
227
  - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
205
228
  - **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
206
229
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
207
- - **Customizable Themes:** Switch between different color palettes (e.g., Classic Green, Dark Blue, Royal Purple) using the `T` key during gameplay.
230
+ - **Customizable Themes:** Switch between six color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast) using the `T` key during gameplay.
208
231
  - **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
209
232
  - **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
210
233
  - **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
@@ -267,7 +290,7 @@ belote/
267
290
  PYTHONPATH=src pytest
268
291
  ```
269
292
 
270
- Currently **435 tests** passing with 100% coverage on core logic.
293
+ Currently **510 tests** passing with 100% coverage on game-logic modules.
271
294
 
272
295
  ## Technical Integrity
273
296
 
@@ -2,7 +2,30 @@
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
- ## Now Featuring: BelAtro Expansion
5
+ ## What's new in 3.0.2
6
+
7
+ - **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.
8
+ - **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.
9
+ - **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.
10
+ - **Test coverage** — 510 tests (up from 509).
11
+
12
+ ## What's new in 3.0.1
13
+
14
+ - **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.
15
+ - **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.
16
+
17
+ ## What's new in 3.0.0
18
+
19
+ - **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.
20
+ - **Joker editions** — Foil (+50 chips), Holo (+10 mult), Polychrome (×1.5 mult), Negative (extra slot) randomly stamp shop jokers.
21
+ - **Three new boss blinds** — Le Sauvage (Aces = 0), L'Iconoclaste (Jacks = 0, even trump-J), Le Mime (Declarations = 0).
22
+ - **Achievements** — six classic-mode milestones tracked across sessions.
23
+ - **Colorblind palette** + **screen-reader hints** (`BELOTE_A11Y=1`) for accessibility.
24
+ - **Replay analyzer** — module added (post-round Hard-AI comparison). User-facing wiring landed in 3.0.2 behind `BELOTE_REPLAY=1`.
25
+ - **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`.
26
+ - **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.
27
+
28
+ ## BelAtro Expansion
6
29
 
7
30
  **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
31
 
@@ -127,12 +150,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
127
150
  ## Controls
128
151
 
129
152
  **General:**
130
- - `?` or `H`: Show keyboard shortcut help
153
+ - `?`: Show keyboard shortcut help
131
154
  - `M`: Toggle sound effects on/off
132
- - `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
155
+ - `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
133
156
  - `Q`: Quit to main menu or exit
134
- - `t`: View Game History (Round-by-round)
135
- - `T`: Switch UI Theme
157
+ - `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
158
+ - `T`: Cycle UI Theme
136
159
 
137
160
  **Classic Belote:**
138
161
  - `↑` `↓`: Navigate options
@@ -140,7 +163,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
140
163
  - `Enter`: Select option / Enter submenu
141
164
 
142
165
  **BelAtro (Roguelite):**
143
- - `S`: View current Run State and Jokers
144
166
  - `1`-`5`: Inspect specific Jokers in the Shop
145
167
  - `U`: Use a consumable (Tarot/Planet) during gameplay
146
168
 
@@ -151,6 +173,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
151
173
  - `O`: Sort hand by suit and rank
152
174
  - `Z`: Undo last move
153
175
  - `Space` or `Esc`: Skip animations
176
+ - During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
154
177
 
155
178
  ## Features
156
179
 
@@ -161,7 +184,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
161
184
  - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
162
185
  - **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
163
186
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
164
- - **Customizable Themes:** Switch between different color palettes (e.g., Classic Green, Dark Blue, Royal Purple) using the `T` key during gameplay.
187
+ - **Customizable Themes:** Switch between six color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast) using the `T` key during gameplay.
165
188
  - **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
166
189
  - **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
167
190
  - **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
@@ -224,7 +247,7 @@ belote/
224
247
  PYTHONPATH=src pytest
225
248
  ```
226
249
 
227
- Currently **435 tests** passing with 100% coverage on core logic.
250
+ Currently **510 tests** passing with 100% coverage on game-logic modules.
228
251
 
229
252
  ## Technical Integrity
230
253
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "belote-cli"
7
- version = "2.9.2"
7
+ version = "3.0.2"
8
8
  description = "A 4-player terminal card game"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
- __version__ = "2.9.2"
1
+ __version__ = "3.0.2"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -0,0 +1,93 @@
1
+ """3.0.0: optional screen-reader hints.
2
+
3
+ When the env var ``BELOTE_A11Y`` is truthy, key in-game events emit a plain-text
4
+ line to stderr — readable by terminal screen readers such as Orca, NVDA in WSL,
5
+ or VoiceOver via iTerm2. Disabled by default so it doesn't pollute output for
6
+ sighted players.
7
+
8
+ Hooked from gameflow.py (card plays, trick winners, round results) and from
9
+ belatro/main.py (boss reveal, ante advance, run won/lost). Each hook is a
10
+ single line — no rich formatting — so the screen reader can speak it cleanly.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import sys
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from .deck import Card
21
+ from .game import Seat
22
+
23
+
24
+ # 3.0.1: resolve the env var once at import. The flag is read on every card
25
+ # play (~32 times/round); a dict lookup is cheap but bypassing it keeps
26
+ # `speak()` essentially free in the disabled path.
27
+ _TRUTHY = {"1", "true", "yes", "on"}
28
+ _ENABLED: bool = os.environ.get("BELOTE_A11Y", "").lower() in _TRUTHY
29
+
30
+
31
+ def is_enabled() -> bool:
32
+ """Return whether a11y hints are enabled.
33
+
34
+ Reads the cached module-level value (set at import). Tests that use
35
+ `monkeypatch.setenv("BELOTE_A11Y", ...)` should call
36
+ `_refresh_enabled_from_env()` after the patch.
37
+ """
38
+ return _ENABLED
39
+
40
+
41
+ def _refresh_enabled_from_env() -> None:
42
+ """Re-read BELOTE_A11Y from the live environment. Public for tests."""
43
+ global _ENABLED
44
+ _ENABLED = os.environ.get("BELOTE_A11Y", "").lower() in _TRUTHY
45
+
46
+
47
+ def speak(line: str) -> None:
48
+ """Emit one line to stderr — only when BELOTE_A11Y is enabled."""
49
+ if _ENABLED:
50
+ sys.stderr.write(line + "\n")
51
+ sys.stderr.flush()
52
+
53
+
54
+ # ── Convenience formatters ────────────────────────────────────────────────
55
+
56
+
57
+ def _suit_word(suit_symbol: str) -> str:
58
+ return {
59
+ "♠": "spades",
60
+ "♥": "hearts",
61
+ "♦": "diamonds",
62
+ "♣": "clubs",
63
+ }.get(suit_symbol, suit_symbol)
64
+
65
+
66
+ def card_word(card: Card) -> str:
67
+ rank = card.rank.value
68
+ rank_word = {
69
+ "7": "seven",
70
+ "8": "eight",
71
+ "9": "nine",
72
+ "10": "ten",
73
+ "J": "jack",
74
+ "Q": "queen",
75
+ "K": "king",
76
+ "A": "ace",
77
+ }.get(rank, rank)
78
+ return f"{rank_word} of {_suit_word(card.suit.symbol)}"
79
+
80
+
81
+ def announce_play(seat: Seat, card: Card) -> None:
82
+ speak(f"{seat.name.lower()} plays {card_word(card)}.")
83
+
84
+
85
+ def announce_trick_won(winner: Seat, points: int) -> None:
86
+ speak(f"{winner.name.lower()} wins the trick worth {points} points.")
87
+
88
+
89
+ def announce_round_result(taker_total: int, defender_total: int, taker_team_label: str) -> None:
90
+ speak(
91
+ f"round complete. {taker_team_label} taker scores {taker_total}; "
92
+ f"defenders score {defender_total}."
93
+ )
@@ -0,0 +1,104 @@
1
+ """3.0.0: lightweight achievement registry for classic Belote.
2
+
3
+ Achievements are defined statically here, evaluated against the running
4
+ ``Statistics`` object after each round/game, and persisted via the existing
5
+ ``stats.save_stats()`` path. BelAtro has its own unlock system in
6
+ ``progression/save.py``; this module is for the classic mode only.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from .stats import Statistics
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Achievement:
18
+ id: str
19
+ title: str
20
+ description: str
21
+
22
+
23
+ # Catalog. New achievements: add a row here AND a check in
24
+ # `evaluate_round` / `evaluate_game`. IDs are stable strings — never rename.
25
+ ACHIEVEMENTS: tuple[Achievement, ...] = (
26
+ Achievement(
27
+ "first_capot",
28
+ "Le Grand Chelem",
29
+ "Score your first Capot.",
30
+ ),
31
+ Achievement(
32
+ "capot_x3",
33
+ "Triple Coup",
34
+ "Score 3 Capots in a single session.",
35
+ ),
36
+ Achievement(
37
+ "capot_streak_2",
38
+ "Vague de Capots",
39
+ "Capot two rounds in a row.",
40
+ ),
41
+ Achievement(
42
+ "high_round_300",
43
+ "Cartes Pleines",
44
+ "Score 300+ points in a single round (declarations + Capot).",
45
+ ),
46
+ Achievement(
47
+ "win_hard",
48
+ "Le Maître",
49
+ "Win a game on Hard difficulty.",
50
+ ),
51
+ Achievement(
52
+ "ten_games_played",
53
+ "Habitué",
54
+ "Play 10 games.",
55
+ ),
56
+ )
57
+
58
+
59
+ def evaluate_round(stats: Statistics, *, points_scored: int, was_capot: bool) -> list[Achievement]:
60
+ """Check post-round triggers; return list of newly unlocked achievements.
61
+
62
+ Mutates ``stats.achievements`` to record unlocks.
63
+ """
64
+ newly: list[Achievement] = []
65
+
66
+ def _try(aid: str) -> None:
67
+ if stats.unlock_achievement(aid):
68
+ for a in ACHIEVEMENTS:
69
+ if a.id == aid:
70
+ newly.append(a)
71
+ break
72
+
73
+ if was_capot:
74
+ if stats.capots_achieved == 1:
75
+ _try("first_capot")
76
+ if stats.capots_achieved >= 3:
77
+ _try("capot_x3")
78
+ if stats.current_capot_streak >= 2:
79
+ _try("capot_streak_2")
80
+
81
+ if points_scored >= 300:
82
+ _try("high_round_300")
83
+
84
+ return newly
85
+
86
+
87
+ def evaluate_game(
88
+ stats: Statistics, *, won: bool, difficulty: str
89
+ ) -> list[Achievement]:
90
+ newly: list[Achievement] = []
91
+
92
+ def _try(aid: str) -> None:
93
+ if stats.unlock_achievement(aid):
94
+ for a in ACHIEVEMENTS:
95
+ if a.id == aid:
96
+ newly.append(a)
97
+ break
98
+
99
+ if won and difficulty == "hard":
100
+ _try("win_hard")
101
+ if stats.games_played >= 10:
102
+ _try("ten_games_played")
103
+
104
+ return newly