belote-cli 4.9.5__tar.gz → 4.9.7__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 (179) hide show
  1. {belote_cli-4.9.5 → belote_cli-4.9.7}/.gitignore +4 -0
  2. {belote_cli-4.9.5 → belote_cli-4.9.7}/CHANGELOG.md +127 -0
  3. {belote_cli-4.9.5 → belote_cli-4.9.7}/DEVELOPMENT.md +112 -41
  4. {belote_cli-4.9.5 → belote_cli-4.9.7}/PKG-INFO +9 -8
  5. {belote_cli-4.9.5 → belote_cli-4.9.7}/README.md +7 -6
  6. {belote_cli-4.9.5 → belote_cli-4.9.7}/pyproject.toml +1 -1
  7. {belote_cli-4.9.5 → belote_cli-4.9.7}/scripts/benchmark.py +8 -8
  8. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/__init__.py +1 -1
  9. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/a11y.py +60 -7
  10. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/achievements.py +11 -3
  11. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ai.py +21 -21
  12. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ansi.py +71 -11
  13. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/core/run_state.py +9 -0
  14. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/core/scoring.py +16 -3
  15. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/engine/event_bus.py +20 -0
  16. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/engine/round_driver.py +99 -42
  17. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ghost_run.py +10 -2
  18. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/base.py +12 -0
  19. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/contract.py +5 -3
  20. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/economy.py +8 -1
  21. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/trick_timing.py +11 -2
  22. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/partner_jokers/shaper.py +2 -2
  23. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/tarots.py +4 -1
  24. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/main.py +88 -23
  25. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/progression/save.py +82 -23
  26. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/progression/unlocks.py +3 -2
  27. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/run/ante_themes.py +19 -3
  28. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/run/shop.py +43 -1
  29. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/announce.py +12 -1
  30. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/history.py +4 -2
  31. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/hud.py +10 -5
  32. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/rules.py +2 -2
  33. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/shop.py +71 -5
  34. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/trust_bar.py +31 -7
  35. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/config.py +12 -1
  36. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/game.py +35 -6
  37. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/gameflow.py +16 -1
  38. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/input.py +77 -14
  39. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/main.py +11 -6
  40. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/scoring.py +14 -7
  41. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/stats.py +89 -13
  42. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/themes.py +25 -5
  43. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/menu.py +17 -3
  44. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/render.py +28 -11
  45. belote_cli-4.9.7/tests/belatro/test_audit_round_fixes.py +558 -0
  46. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_belatro.py +17 -3
  47. belote_cli-4.9.7/tests/belatro/test_design_calls.py +115 -0
  48. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_phase3_meta.py +13 -2
  49. belote_cli-4.9.7/tests/belatro/test_registry_integrity.py +61 -0
  50. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_round_driver.py +19 -6
  51. belote_cli-4.9.7/tests/conftest.py +24 -0
  52. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_a11y.py +35 -0
  53. belote_cli-4.9.7/tests/test_achievements.py +151 -0
  54. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_alt_screen_scroll.py +2 -2
  55. belote_cli-4.9.7/tests/test_hud_fit.py +72 -0
  56. belote_cli-4.9.7/tests/test_input_escape_seqs.py +136 -0
  57. belote_cli-4.9.7/tests/test_menu_target_and_exit.py +134 -0
  58. belote_cli-4.9.7/tests/test_render_golden.py +82 -0
  59. belote_cli-4.9.7/tests/test_scoring_totals.py +62 -0
  60. belote_cli-4.9.7/tests/test_stats_persistence.py +193 -0
  61. belote_cli-4.9.7/tests/test_wcwidth.py +88 -0
  62. {belote_cli-4.9.5 → belote_cli-4.9.7}/uv.lock +1 -1
  63. belote_cli-4.9.5/.claude/settings.local.json +0 -19
  64. belote_cli-4.9.5/tests/test_achievements.py +0 -58
  65. {belote_cli-4.9.5 → belote_cli-4.9.7}/.python-version +0 -0
  66. {belote_cli-4.9.5 → belote_cli-4.9.7}/LICENSE +0 -0
  67. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/__init__.py +0 -0
  68. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/__init__.py +0 -0
  69. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/core/__init__.py +0 -0
  70. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/core/economy.py +0 -0
  71. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/core/round_ledger.py +0 -0
  72. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/engine/__init__.py +0 -0
  73. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/engine/modifier_patch.py +0 -0
  74. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/__init__.py +0 -0
  75. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/__init__.py +0 -0
  76. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/annonces.py +0 -0
  77. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/coinche.py +0 -0
  78. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/corrupted.py +0 -0
  79. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
  80. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
  81. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
  82. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
  83. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/planets.py +0 -0
  84. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/registry.py +0 -0
  85. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/items/vouchers.py +0 -0
  86. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/partner/__init__.py +0 -0
  87. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/partner/partner_state.py +0 -0
  88. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/partner/personality.py +0 -0
  89. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/partner/trust.py +0 -0
  90. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/progression/__init__.py +0 -0
  91. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/run/__init__.py +0 -0
  92. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/run/ante.py +0 -0
  93. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/run/boss.py +0 -0
  94. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/run/decks.py +0 -0
  95. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/run_summary.py +0 -0
  96. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/__init__.py +0 -0
  97. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/collection.py +0 -0
  98. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/consumables.py +0 -0
  99. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/inventory.py +0 -0
  100. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/belatro/ui/menu.py +0 -0
  101. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/context.py +0 -0
  102. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/deck.py +0 -0
  103. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/replay.py +0 -0
  104. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/rules.py +0 -0
  105. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/__init__.py +0 -0
  106. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/anim.py +0 -0
  107. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/announce.py +0 -0
  108. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/fit_guard.py +0 -0
  109. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/layout.py +0 -0
  110. {belote_cli-4.9.5 → belote_cli-4.9.7}/src/belote/ui/prompts.py +0 -0
  111. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/__init__.py +0 -0
  112. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/__init__.py +0 -0
  113. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_boss_contracts.py +0 -0
  114. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_boss_modifiers_integration.py +0 -0
  115. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_collection_logic.py +0 -0
  116. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_competition_malediction.py +0 -0
  117. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_consumables_ui.py +0 -0
  118. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_contract_unlocks.py +0 -0
  119. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_dead_flag_fixes.py +0 -0
  120. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_deck_variants.py +0 -0
  121. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_decks_4_5.py +0 -0
  122. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_endless.py +0 -0
  123. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_event_bus.py +0 -0
  124. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_ghost_run.py +0 -0
  125. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_handler_registry.py +0 -0
  126. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_heist.py +0 -0
  127. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_history_overlay.py +0 -0
  128. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_hud_synergy.py +0 -0
  129. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_hud_toggle.py +0 -0
  130. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_inventory_overlay.py +0 -0
  131. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_joker_callouts.py +0 -0
  132. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_joker_contracts.py +0 -0
  133. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_jokers_4_5.py +0 -0
  134. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_partner_jokers.py +0 -0
  135. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_partner_trust.py +0 -0
  136. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_phase0_coverage.py +0 -0
  137. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_phase1_plumbing.py +0 -0
  138. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_phase2_content.py +0 -0
  139. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_progression.py +0 -0
  140. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_run_summary.py +0 -0
  141. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_shop_animations.py +0 -0
  142. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_shop_empty_pools.py +0 -0
  143. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_slot_machine_tally.py +0 -0
  144. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/belatro/test_voucher_idempotency.py +0 -0
  145. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/perf_baselines.json +0 -0
  146. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_ai.py +0 -0
  147. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_ai_deluge_voids.py +0 -0
  148. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_ai_sans_atout.py +0 -0
  149. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_anim_helpers.py +0 -0
  150. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_announce_stats.py +0 -0
  151. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_ansi_helpers.py +0 -0
  152. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_belote.py +0 -0
  153. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_belote_stinger.py +0 -0
  154. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_benchmark_smoke.py +0 -0
  155. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_bidding_all_pass.py +0 -0
  156. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_declaration_tiebreak.py +0 -0
  157. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_deluge_legal_cards.py +0 -0
  158. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_extended.py +0 -0
  159. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_g1_coinche_classic.py +0 -0
  160. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_g2_ai_signals.py +0 -0
  161. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_game_logic.py +0 -0
  162. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_gameflow.py +0 -0
  163. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_hand_auto_sort.py +0 -0
  164. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_history_overlay_cache.py +0 -0
  165. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_input_eof.py +0 -0
  166. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_input_wasd.py +0 -0
  167. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_invariants.py +0 -0
  168. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_layout.py +0 -0
  169. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_new_coverage.py +0 -0
  170. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_no_color.py +0 -0
  171. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_official_rules.py +0 -0
  172. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_perf_regression.py +0 -0
  173. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_properties.py +0 -0
  174. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_render_diff.py +0 -0
  175. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_render_felt_polish.py +0 -0
  176. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_render_hand_lift.py +0 -0
  177. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_replay.py +0 -0
  178. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_undo.py +0 -0
  179. {belote_cli-4.9.5 → belote_cli-4.9.7}/tests/test_winner_glow.py +0 -0
@@ -147,3 +147,7 @@ cython_debug/
147
147
 
148
148
  # PyCharm
149
149
  .idea/
150
+
151
+ # Editor swap files
152
+ *.kate-swp
153
+ *.swp
@@ -5,6 +5,133 @@ 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
+ ## [4.9.7] - 2026-11-01
9
+
10
+ Repo-wide audit: four parallel reviewers swept the classic engine, the
11
+ classic UI/terminal layer, the BelAtro engine/run/progression code and the
12
+ BelAtro items/UI; every finding was verified (most with a repro script) before
13
+ fixing. Test count 1111 → 1228.
14
+
15
+ ### Fixed — test suite
16
+
17
+ - **Tests no longer depend on (or write to) your real profile.** A new
18
+ `tests/conftest.py` points `HOME` / `XDG_CONFIG_HOME` / `XDG_DATA_HOME` at a
19
+ temp dir. The suite used to render with whatever theme the developer last
20
+ picked (the golden-frame hashes only passed with `moonlit_tavern` saved) and
21
+ `theme_manager.set_current()` rewrote `~/.config/belote/theme.json`.
22
+ Golden hashes re-pinned to the default `classic_green` theme.
23
+ - Hardcoded `/home/mrrobot/belote/` paths in `tests/test_alt_screen_scroll.py`
24
+ replaced with a path relative to the test file.
25
+ - `scripts/benchmark.py` is now `mypy --strict` clean.
26
+
27
+ ### Fixed — classic game
28
+
29
+ - **L'Anarchie scored every trick with the end-of-round trump.** Tricks now
30
+ record the trump they were played under (`GameState.trick_trumps`) and final
31
+ scoring uses it (was wrong in ~99% of L'Anarchie rounds).
32
+ - A **worthless carré of 7s/8s outranked the other team's sequences** and
33
+ wiped out their points. 0-point carrés no longer take part in declarations.
34
+ - **Hard AI threw high off-suit cards to the opponents**, believing an
35
+ off-suit Ace beat a led King (normal and Tout Atout contracts).
36
+ - **Tout Atout under L'Anarchie** silently became a one-suit game while still
37
+ being scored as Tout Atout; trump no longer rotates under TA.
38
+ - Capots by the opponents counted as **your** capots in stats/achievements;
39
+ `first_capot` used `== 1`; "Triple Coup" read lifetime instead of session capots.
40
+ - 6+ card sequences were labelled "0" in the round summary.
41
+
42
+ ### Fixed — terminal, UI and files
43
+
44
+ - `--target 750` (non-preset) crashed the main menu.
45
+ - Stats file: atomic writes; corrupt / wrong-shaped files no longer crash the
46
+ game (moved aside to `stats.json.bak`).
47
+ - Unknown escape sequences leaked as keystrokes (F2 quit the game, Ctrl+→
48
+ played card 5, Delete typed `~`); Ctrl-C now quits.
49
+ - In-game `FitAbortedError` (terminal too small → Q) exits cleanly.
50
+ - Spacious HUD overflowed at 120–143 columns and scrolled the screen.
51
+ - Game-over screen "staircased" (bare `\n` in raw mode).
52
+ - Pressing T crashed on a read-only config dir; theme saves are atomic.
53
+ - The game can now start on Windows (`termios` imported only on Unix); classic
54
+ stats honour `%APPDATA%`.
55
+ - `BELOTE_A11Y=1` no longer scribbles over the game screen — see
56
+ DEVELOPMENT.md → Accessibility (`BELOTE_A11Y_OUT`).
57
+
58
+ ### Fixed — BelAtro
59
+
60
+ - **All-pass deals ended the run**; they now redeal (and round-end jokers no
61
+ longer fire on the abandoned deal).
62
+ - Partner bidding was backwards in round 1 (could take any suit except the
63
+ legal one; L'Économe could never bid).
64
+ - L'Architecte "buy contract" crashed in round 1 after taking $10.
65
+ - Le Traître / L'Agent Double replaced the boss's larger sabotage set.
66
+ - L'Avocat's doubled target was ignored for payout, Le Puriste, trust and history.
67
+ - Ghost runs: real seeds, seeded rounds, unique file names.
68
+ - Corrupt / newer-schema `profile.json` no longer crashes launch.
69
+ - Capots by EW counted as yours (unlocks, Pluto); Mercury/Pluto paid on EW contracts.
70
+ - The shop offered jokers you already own (duplicates corrupted each other's
71
+ state); Le Jugement too.
72
+ - Le Patriote, L'Illusionniste and La Sentinelle (P) mishandled Tout Atout;
73
+ Le Passeur paid $2 when North bid Sans Atout.
74
+ - Shop cards: frame misaligned, long names overflowed, editions invisible.
75
+ - Compact HUD: trust bar overwrote the synergy tooltip.
76
+ - History overlay overflowed 90–99 column terminals.
77
+ - An ante-8 victory continued into endless mode was recorded as a loss.
78
+
79
+ ### Changed — design calls
80
+
81
+ - **Le Diable** now works: for one round North over-cuts with its lowest
82
+ winning trump whenever it can — even over your winning card.
83
+ - **Sell jokers** in the shop with `S` for half their cost (same value as
84
+ La Tempérance). Corrupted jokers can't be sold; selling a Negative joker
85
+ returns its slot.
86
+ - **Le Sergent** grows with the streak: +0.5, +1.0, +1.5, then +2.0 per trick
87
+ (was a flat +0.5 that ignored "consecutive").
88
+ - **Le Tournoi** really offers you a coinche; theme flags no longer outlive
89
+ their ante. **Le Café**'s +25 chips is a petit-blind bonus, not permanent.
90
+ - **Auto-Capot** (trust 9+) is no longer advertised — it was never implemented.
91
+ - **Trust 3+ shows North's void suits** (`✦ Void: ♠♦`) instead of the whole
92
+ hand; seeing the full hand needs Le Carnet.
93
+ - **Pluto** pays only when you took the contract and made it as a capot.
94
+
95
+ ## [4.9.6] - 2026-06-01
96
+
97
+ Follow-up bug-hunt, logic, and performance audit. Three parallel Explore agents
98
+ re-swept the classic engine, BelAtro mechanics, and the UI/render layer; every
99
+ "major" finding was then traced to exact code and verified before acting. The
100
+ result: **no critical or major bug survived verification.** The flagged items
101
+ were all either false positives (Le Passeur "double-payout" — the post-coinche
102
+ `BidMadeEvent` re-emit carries the taker's seat + a real trump, so the
103
+ North-pass gate never matches; Le Carnet's +1 Mult *is* wired via
104
+ `acc.carnet_active`; Tout Streak / Quinte Royale re-emit guards are already
105
+ present; `modifier_patch` is a fresh per-instance proxy each round) or
106
+ intentional, test-pinned behaviour (Le Patriote's `// 2`). Benchmarks confirmed
107
+ the render/scoring/AI paths are already well within budget (render ≈0.4 ms, E2E
108
+ round ≈4–6 ms). This release therefore lands the one genuine latent gap the
109
+ audit surfaced plus a layer of behaviour-locking tests. Test count 1079 → 1111.
110
+
111
+ ### Added
112
+
113
+ - **Minimal wide-character (wcwidth) support** (`src/belote/ansi.py`). `visible_len()`
114
+ previously counted codepoints, so `ansi_center()` / `ansi_ljust()` and the
115
+ render diff's per-row width would mis-align by one cell per wide glyph (emoji,
116
+ CJK). A new dependency-free `char_width()` (stdlib `unicodedata`) returns 0
117
+ for combining/zero-width marks, 2 for East-Asian Wide/Fullwidth, 1 otherwise,
118
+ and `visible_len()` now measures display **cells**. A pure-ASCII escape-free
119
+ fast path keeps the hot render path allocation-free and byte-identical, so
120
+ there is no performance regression. A new `ansi_truncate()` (cell-aware,
121
+ escape-skipping) replaces the raw byte-slices that trimmed BelAtro HUD joker
122
+ names and synergy lines (`belatro/ui/hud.py`), which could previously split a
123
+ wide glyph.
124
+ - **Behaviour-locking tests** that freeze what the audit confirmed correct:
125
+ `tests/test_render_golden.py` (full-frame snapshot + per-row cell-width
126
+ fingerprint — the regression guard for the wcwidth change),
127
+ `tests/test_wcwidth.py` (per-character widths and helper cell-correctness),
128
+ `tests/test_scoring_totals.py` (the canonical 152 / 120 / 248 point totals,
129
+ belote 20, capot bases 252 / 220 / 348, dix de der +10), and
130
+ `tests/belatro/test_registry_integrity.py` (item-census counts 42 jokers /
131
+ 8 planets / 12 tarots / 12 vouchers, id↔key consistency, no duplicate display
132
+ names — so a duplicate-id registration that silently overwrites an item now
133
+ fails a test).
134
+
8
135
  ## [4.9.5] - 2026-05-28
9
136
 
10
137
  Bug-hunt, logic, and performance audit pass. Three parallel Explore agents
@@ -2,6 +2,61 @@
2
2
 
3
3
  Welcome to the Belote development guide. This project is structured as a standard Python package.
4
4
 
5
+ ## Cheat Sheet
6
+
7
+ Everything below assumes you're in the repo with the venv active:
8
+
9
+ ```bash
10
+ cd ~/Projects/belote
11
+ source .venv/bin/activate # gives you `python`, `pip`, pytest, mypy, ruff
12
+ ```
13
+
14
+ **1. Check everything is green (do this before every commit):**
15
+ ```bash
16
+ PYTHONPATH=src python -m pytest -q && \
17
+ PYTHONPATH=src python -m mypy --strict src/ && \
18
+ python -m ruff check src/ tests/
19
+ ```
20
+
21
+ **2. Push code to GitHub (no PyPI release):**
22
+ ```bash
23
+ git status # see what changed — don't commit editor swap files
24
+ git add <files> # or: git add -A (after checking git status)
25
+ git commit -m "what changed"
26
+ git push origin master
27
+ ```
28
+
29
+ **3. Release a new version to PyPI** (example: 4.9.7):
30
+ ```bash
31
+ # a) bump the version in BOTH files (they must match):
32
+ # pyproject.toml -> version = "4.9.7"
33
+ # src/belote/__init__.py -> __version__ = "4.9.7"
34
+ # b) add a "## 4.9.7" entry at the top of CHANGELOG.md
35
+ # c) run the gates (step 1), then commit, tag and push:
36
+ git add -A
37
+ git commit -m "Release v4.9.7"
38
+ git tag -a v4.9.7 -m "v4.9.7"
39
+ git push origin master --tags
40
+ # d) build, check and upload:
41
+ rm -rf dist/ build/ *.egg-info/
42
+ pipx run build --sdist --wheel
43
+ pipx run twine check dist/*
44
+ pipx run twine upload dist/*
45
+ # username: __token__
46
+ # password: your PyPI API token (starts with pypi-)
47
+ # make one at https://pypi.org/manage/account/token/
48
+ ```
49
+
50
+ **4. Update the game installed with pipx** (from PyPI — https://pypi.org/project/belote-cli/):
51
+ ```bash
52
+ pipx install belote-cli # first time only
53
+ pipx upgrade belote-cli # after each release
54
+ belote --version # confirm the new version
55
+ ```
56
+ PyPI can take a minute to show a new upload. If `pipx upgrade` says you're
57
+ already on the latest, wait a moment and retry, or force it with
58
+ `pipx install --force belote-cli`.
59
+
5
60
  ## Setup
6
61
 
7
62
  1. **Clone the repository:**
@@ -10,15 +65,17 @@ Welcome to the Belote development guide. This project is structured as a standar
10
65
  cd belote
11
66
  ```
12
67
 
13
- 2. **Create a virtual environment:**
68
+ 2. **Create a virtual environment** (use `python3` — many Linux distros don't
69
+ ship a bare `python` or `pip` outside a venv):
14
70
  ```bash
15
- python -m venv .venv
71
+ python3 -m venv .venv
16
72
  source .venv/bin/activate # On Windows: .venv\Scripts\activate
17
73
  ```
18
74
 
19
- 3. **Install in editable mode:**
75
+ 3. **Install in editable mode with the test + dev tools** (pytest, pytest-cov,
76
+ mypy, ruff):
20
77
  ```bash
21
- pip install -e .
78
+ pip install -e ".[test,dev]"
22
79
  ```
23
80
 
24
81
  ## Running the Game
@@ -42,29 +99,31 @@ PYTHONPATH=src python3 -m belote.main
42
99
 
43
100
  ## Testing
44
101
 
45
- We use `pytest` for testing. Install it if you haven't already:
46
- ```bash
47
- pip install pytest
48
- ```
102
+ We use `pytest` (installed by `pip install -e ".[test,dev]"` above).
103
+
104
+ `tests/conftest.py` points `HOME`, `XDG_CONFIG_HOME` and `XDG_DATA_HOME` at a
105
+ throwaway temp dir for the whole run, so the suite never reads or writes your
106
+ real `~/.config/belote/theme.json`, stats or BelAtro saves, and always renders
107
+ with the default theme (the golden-frame snapshot depends on that).
49
108
 
50
109
  Run tests:
51
110
  ```bash
52
111
  # Run all tests (Classic + BelAtro)
53
112
  PYTHONPATH=src pytest
54
113
 
55
- # Run only Classic Belote tests
56
- PYTHONPATH=src pytest tests/
114
+ # Run only Classic Belote tests (tests/belatro/ lives inside tests/)
115
+ PYTHONPATH=src pytest tests/ --ignore=tests/belatro
57
116
 
58
117
  # Run only BelAtro tests
59
118
  PYTHONPATH=src pytest tests/belatro/
60
119
 
61
120
  # Run a single test file
62
- PYTHONPATH=src pytest tests/test_game.py
63
- PYTHONPATH=src pytest tests/belatro/test_scoring.py
121
+ PYTHONPATH=src pytest tests/test_game_logic.py
122
+ PYTHONPATH=src pytest tests/belatro/test_round_driver.py
64
123
 
65
124
  # Run a single test by name
66
- PYTHONPATH=src pytest tests/test_game.py::test_play_card_legal
67
- PYTHONPATH=src pytest -k "test_scoring"
125
+ PYTHONPATH=src pytest tests/test_render_golden.py::test_golden_frame_sha256
126
+ PYTHONPATH=src pytest -k "capot"
68
127
 
69
128
  # Run with verbose output
70
129
  PYTHONPATH=src pytest -v
@@ -84,7 +143,7 @@ PYTHONPATH=src mypy --strict src/
84
143
  # Linting (0 violations expected)
85
144
  ruff check src/ tests/
86
145
 
87
- # Full test suite (1079 tests expected)
146
+ # Full test suite (1228 tests expected)
88
147
  PYTHONPATH=src pytest
89
148
  ```
90
149
 
@@ -92,7 +151,7 @@ Run all gates before committing:
92
151
 
93
152
  ```bash
94
153
  PYTHONPATH=src python -m pytest --tb=short -q && \
95
- python -m mypy --strict src/ && \
154
+ PYTHONPATH=src python -m mypy --strict src/ && \
96
155
  python -m ruff check src/ tests/
97
156
  ```
98
157
 
@@ -116,8 +175,18 @@ Use these as a regression-detection floor for future changes.
116
175
 
117
176
  Set `BELOTE_A11Y=1` to emit one-line plain-text descriptions of card plays,
118
177
  trick winners, locked contracts (4.6.5), declarations (4.6.5), and round
119
- results to stderr — readable by terminal screen readers (Orca, NVDA over
120
- WSL, VoiceOver via iTerm2).
178
+ results — readable by terminal screen readers (Orca, NVDA over WSL,
179
+ VoiceOver via iTerm2).
180
+
181
+ Where the lines go (4.9.7 — writing them onto the full-screen game used to
182
+ scroll and scramble the frame):
183
+
184
+ - `BELOTE_A11Y_OUT=/path` — appended to that file or terminal device, e.g.
185
+ run `tty` in a second terminal and use its `/dev/pts/N`.
186
+ - otherwise stderr, when it's redirected (`belote 2>a11y.txt`) or is a
187
+ different terminal from the game.
188
+ - otherwise `~/.local/share/belote/a11y.log` (`%APPDATA%\belote\a11y.log` on
189
+ Windows) — follow it with `tail -f` in another terminal.
121
190
 
122
191
  The env var is read once at import; tests that mutate it must call
123
192
  `belote.a11y._refresh_enabled_from_env()` after the patch. Hook points live
@@ -136,8 +205,8 @@ once at startup; toggling mid-run has no effect.
136
205
  affects scoring. Backed by `src/belote/replay.py`.
137
206
  - `BELOTE_GHOST=1` — silently record every BelAtro run (seed, deck,
138
207
  bids, plays, round outcomes) to
139
- `~/.local/share/belote/ghosts/<label>-<seed>.json`. The file is written
140
- once when the run ends. Useful for sharing or replaying interesting
208
+ `~/.local/share/belote/ghosts/<label>-<seed>-<timestamp>.json`. The file
209
+ is written once when the run ends and never overwrites an earlier one. Useful for sharing or replaying interesting
141
210
  runs. Backed by `src/belote/belatro/ghost_run.py`.
142
211
  - `NO_COLOR=<any-non-empty>` — suppress truecolor SGR escapes from
143
212
  `fg()` / `bg()` per the [no-color.org](https://no-color.org/) spec.
@@ -164,9 +233,9 @@ once at startup; toggling mid-run has no effect.
164
233
 
165
234
  ## Releasing a New Version
166
235
 
167
- ### Code-only update (push to GitHub without releasing a new PyPI version)
236
+ The short version is in the [Cheat Sheet](#cheat-sheet) at the top. Details:
168
237
 
169
- If you're just iterating on code, fixing typos, updating docs, etc., and don't want to cut a new PyPI release yet:
238
+ ### Code-only update (push to GitHub without releasing a new PyPI version)
170
239
 
171
240
  ```bash
172
241
  git add <files>
@@ -174,32 +243,32 @@ git commit -m "<what changed>"
174
243
  git push origin master
175
244
  ```
176
245
 
177
- ## Releasing a New Version (Manual)
246
+ ### Full release (GitHub + PyPI)
178
247
 
179
- 1. **Bump the version** in BOTH `pyproject.toml` AND `src/belote/__init__.py`
248
+ 1. **Run all gates** (see Code Quality) — don't release a red build.
249
+ 2. **Bump the version** in BOTH `pyproject.toml` AND `src/belote/__init__.py`
180
250
  (they must stay in sync — `belote --version` / `belatro --version` read
181
251
  `__version__` while PyPI / pipx read `pyproject.toml`).
182
- 2. **Add a CHANGELOG entry** at the top of `CHANGELOG.md`.
183
- 3. **Clean stale build artifacts:**
252
+ 3. **Add a CHANGELOG entry** at the top of `CHANGELOG.md`.
253
+ 4. **Commit, tag and push:**
184
254
  ```bash
185
- rm -rf dist/ build/ *.egg-info/
255
+ git add -A # check `git status` first
256
+ git commit -m "Release vX.Y.Z"
257
+ git tag -a vX.Y.Z -m "vX.Y.Z"
258
+ git push origin master --tags
186
259
  ```
187
- 4. **Build, validate, and upload:**
260
+ 5. **Clean stale build artifacts, then build, validate and upload:**
188
261
  ```bash
262
+ rm -rf dist/ build/ *.egg-info/
189
263
  pipx run build --sdist --wheel
190
264
  pipx run twine check dist/*
191
265
  pipx run twine upload dist/*
192
266
  ```
193
-
194
- *Note: `twine upload` will prompt for your PyPI credentials or use your `~/.pypirc` file.*
195
-
196
- 5. **Commit and tag in git:**
197
- ```bash
198
- git add pyproject.toml CHANGELOG.md
199
- git commit -m "Release vX.Y.Z"
200
- git tag -a vX.Y.Z -m "vX.Y.Z"
201
- git push origin master --tags
202
- ```
267
+ `twine upload` uses `~/.pypirc` if present; otherwise it prompts — enter
268
+ `__token__` as the username and a PyPI API token as the password.
269
+ PyPI never accepts the same version twice, so a botched upload means
270
+ bumping to the next patch version.
271
+ 6. **Update your own install:** `pipx upgrade belote-cli`.
203
272
 
204
273
  ## Performance measurement
205
274
 
@@ -221,8 +290,10 @@ PYTHONPATH=src python scripts/benchmark.py
221
290
  If you suspect a per-frame allocation regression, the canonical hot paths to
222
291
  profile live (e.g. with `py-spy record --rate 100`) are `render()` in
223
292
  `src/belote/ui/render.py` and `_calculate_legal_cards_impl` in
224
- `src/belote/game.py`. The 4.9.4 / 4.9.5 audit passes found only minor wins
293
+ `src/belote/game.py`. The 4.9.4 / 4.9.5 / 4.9.6 audit passes found only minor wins
225
294
  beyond those already landed in the 4.6.x / 4.7.x perf cleanups (4.9.5 bumped
226
295
  the `_pip_at` cache to 8192, trimmed `visible_len`'s cache to ANSI strings,
227
- and memoised the BelAtro history overlay) — start from a profiler trace, not
228
- from guesswork, before adding new caches.
296
+ and memoised the BelAtro history overlay; 4.9.6 made `visible_len` measure
297
+ terminal *cells* via a minimal `char_width`, keeping the allocation-free
298
+ ASCII fast path so the hot path is unchanged) — start from a profiler trace,
299
+ not from guesswork, before adding new caches.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: belote-cli
3
- Version: 4.9.5
3
+ Version: 4.9.7
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
@@ -157,8 +157,8 @@ belatro
157
157
  # Install in editable mode (recommended for development)
158
158
  pip install -e .
159
159
 
160
- # Or install from PyPI (once uploaded)
161
- pip install belote-cli
160
+ # Or install from PyPI (recommended for playing)
161
+ pipx install belote-cli # update later with: pipx upgrade belote-cli
162
162
 
163
163
  # Play using the belote command
164
164
  belote
@@ -189,6 +189,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
189
189
  **BelAtro (Roguelite):**
190
190
  - `1`-`5`: Inspect specific Jokers in the Shop
191
191
  - `C`: Open the Consumables tray in the Shop (use Tarots / Planets you've purchased)
192
+ - `S` *(4.9.7)*: Sell a joker in the Shop for half its cost (corrupted jokers can't be sold)
192
193
 
193
194
  **Gameplay:**
194
195
  - `←` `→` or `↑` `↓` or **`W` `A` `S` `D`** *(4.5.0)*: Move selection
@@ -211,7 +212,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
211
212
  - **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
212
213
  - **Full Boss Blind Suite:** All 21 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
213
214
  - **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
214
- - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
215
+ - **Partner Trust:** Build a relationship with your AI partner to unlock synergies. At trust 3+ the trust bar shows which suits North is void in (`✦ Void: ♠♦`); seeing North's full hand requires the Le Carnet voucher.
215
216
  - **Rich Terminal UI:** Full-screen themed felt table with detailed card graphics and "You" vs "Partner" terminology.
216
217
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
217
218
  - **Customizable Themes:** Switch between thirteen color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast, Colorblind, Forest Night, Moonlit Tavern, Royal Purple, Emerald Isle, Sunset Magma, Provence Lavande) using the `T` key during gameplay.
@@ -264,7 +265,7 @@ belote/
264
265
  │ ├── input.py # Platform-dispatched key reader and interruptible sleep
265
266
  │ ├── stats.py # Global and session statistics tracking
266
267
  │ └── rules.py # Game rules content
267
- ├── tests/ # Comprehensive test suite (1079 tests)
268
+ ├── tests/ # Comprehensive test suite (1228 tests)
268
269
  ├── scripts/ # Performance benchmarks
269
270
  ├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
270
271
  ├── LICENSE # MIT License
@@ -279,14 +280,14 @@ belote/
279
280
  PYTHONPATH=src pytest
280
281
  ```
281
282
 
282
- Currently **1079 tests** passing with 100% coverage on game-logic modules (4.9.5).
283
+ Currently **1228 tests** passing with 100% coverage on game-logic modules (4.9.7).
283
284
 
284
285
  ## Technical Integrity
285
286
 
286
287
  The codebase is strictly validated with the following tools:
287
288
  - **mypy**: 0 errors (strict type safety)
288
289
  - **ruff**: 0 violations (linting & formatting)
289
- - **pytest**: 1044/1044 passed
290
+ - **pytest**: 1228/1228 passed
290
291
  - **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
291
292
  - **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
292
293
 
@@ -114,8 +114,8 @@ belatro
114
114
  # Install in editable mode (recommended for development)
115
115
  pip install -e .
116
116
 
117
- # Or install from PyPI (once uploaded)
118
- pip install belote-cli
117
+ # Or install from PyPI (recommended for playing)
118
+ pipx install belote-cli # update later with: pipx upgrade belote-cli
119
119
 
120
120
  # Play using the belote command
121
121
  belote
@@ -146,6 +146,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
146
146
  **BelAtro (Roguelite):**
147
147
  - `1`-`5`: Inspect specific Jokers in the Shop
148
148
  - `C`: Open the Consumables tray in the Shop (use Tarots / Planets you've purchased)
149
+ - `S` *(4.9.7)*: Sell a joker in the Shop for half its cost (corrupted jokers can't be sold)
149
150
 
150
151
  **Gameplay:**
151
152
  - `←` `→` or `↑` `↓` or **`W` `A` `S` `D`** *(4.5.0)*: Move selection
@@ -168,7 +169,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
168
169
  - **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
169
170
  - **Full Boss Blind Suite:** All 21 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
170
171
  - **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
171
- - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
172
+ - **Partner Trust:** Build a relationship with your AI partner to unlock synergies. At trust 3+ the trust bar shows which suits North is void in (`✦ Void: ♠♦`); seeing North's full hand requires the Le Carnet voucher.
172
173
  - **Rich Terminal UI:** Full-screen themed felt table with detailed card graphics and "You" vs "Partner" terminology.
173
174
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
174
175
  - **Customizable Themes:** Switch between thirteen color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast, Colorblind, Forest Night, Moonlit Tavern, Royal Purple, Emerald Isle, Sunset Magma, Provence Lavande) using the `T` key during gameplay.
@@ -221,7 +222,7 @@ belote/
221
222
  │ ├── input.py # Platform-dispatched key reader and interruptible sleep
222
223
  │ ├── stats.py # Global and session statistics tracking
223
224
  │ └── rules.py # Game rules content
224
- ├── tests/ # Comprehensive test suite (1079 tests)
225
+ ├── tests/ # Comprehensive test suite (1228 tests)
225
226
  ├── scripts/ # Performance benchmarks
226
227
  ├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
227
228
  ├── LICENSE # MIT License
@@ -236,14 +237,14 @@ belote/
236
237
  PYTHONPATH=src pytest
237
238
  ```
238
239
 
239
- Currently **1079 tests** passing with 100% coverage on game-logic modules (4.9.5).
240
+ Currently **1228 tests** passing with 100% coverage on game-logic modules (4.9.7).
240
241
 
241
242
  ## Technical Integrity
242
243
 
243
244
  The codebase is strictly validated with the following tools:
244
245
  - **mypy**: 0 errors (strict type safety)
245
246
  - **ruff**: 0 violations (linting & formatting)
246
- - **pytest**: 1044/1044 passed
247
+ - **pytest**: 1228/1228 passed
247
248
  - **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
248
249
  - **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
249
250
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "belote-cli"
7
- version = "4.9.5"
7
+ version = "4.9.7"
8
8
  description = "A 4-player terminal card game"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,6 +4,7 @@ import random
4
4
  import statistics
5
5
  import sys
6
6
  import time
7
+ from dataclasses import replace
7
8
  from pathlib import Path
8
9
 
9
10
  # Add src to sys.path
@@ -23,7 +24,6 @@ def benchmark_render(iterations: int = 100) -> float:
23
24
  rng = random.Random(42)
24
25
  state = start_round(state, rng)
25
26
  # Mock some state transitions to get into PLAYING phase
26
- from belote.game import replace
27
27
 
28
28
  state = replace(state, phase=Phase.PLAYING, trump=Suit.SPADES, taker=Seat.SOUTH)
29
29
 
@@ -50,7 +50,6 @@ def benchmark_ai(difficulty: Difficulty, iterations: int = 50) -> float:
50
50
  state = new_game()
51
51
  rng = random.Random(42)
52
52
  state = start_round(state, rng)
53
- from belote.game import replace
54
53
 
55
54
  state = replace(state, phase=Phase.PLAYING, trump=Suit.SPADES, taker=Seat.SOUTH)
56
55
 
@@ -69,12 +68,13 @@ def benchmark_ai(difficulty: Difficulty, iterations: int = 50) -> float:
69
68
  def benchmark_belatro_bus(num_jokers: int = 5, iterations: int = 1000) -> float:
70
69
  from belote.belatro.core.scoring import ScoreAccumulator
71
70
  from belote.belatro.engine.event_bus import TrickWonEvent
71
+ from belote.belatro.items.base import Joker
72
72
  from belote.belatro.items.jokers.contract import LeDiplomate
73
73
  from belote.deck import Card, Rank
74
74
 
75
75
  print(f"Benchmarking BelAtro State Update ({num_jokers} Jokers) over {iterations} iterations...")
76
76
 
77
- jokers = [LeDiplomate() for _ in range(num_jokers)]
77
+ jokers: list[Joker] = [LeDiplomate() for _ in range(num_jokers)]
78
78
  acc = ScoreAccumulator()
79
79
  acc.attach_jokers(jokers)
80
80
 
@@ -102,7 +102,7 @@ def benchmark_belatro_bus(num_jokers: int = 5, iterations: int = 1000) -> float:
102
102
 
103
103
  def benchmark_scoring(iterations: int = 1000) -> float:
104
104
  from belote.deck import Card, Rank
105
- from belote.game import Phase, TrickCard, replace
105
+ from belote.game import Phase, TrickCard
106
106
  from belote.scoring import score_round
107
107
 
108
108
  print(f"Benchmarking score_round() over {iterations} iterations...")
@@ -147,7 +147,7 @@ def benchmark_deal(iterations: int = 1000) -> float:
147
147
 
148
148
 
149
149
  def benchmark_legal_cards(iterations: int = 1000) -> float:
150
- from belote.game import clear_legal_cards_cache, legal_cards, replace
150
+ from belote.game import clear_legal_cards_cache, legal_cards
151
151
  print(f"Benchmarking legal_cards() (cache cleared per call) over {iterations} iterations...")
152
152
 
153
153
  state = new_game()
@@ -172,7 +172,7 @@ def benchmark_legal_cards_cached(iterations: int = 1000) -> float:
172
172
  `benchmark_legal_cards` above invalidates every iteration and so reflects
173
173
  worst-case-only time.
174
174
  """
175
- from belote.game import clear_legal_cards_cache, legal_cards, replace
175
+ from belote.game import clear_legal_cards_cache, legal_cards
176
176
  print(f"Benchmarking legal_cards() (warm cache) over {iterations} iterations...")
177
177
 
178
178
  state = new_game()
@@ -198,7 +198,7 @@ def benchmark_trick_scoring(iterations: int = 1000) -> float:
198
198
  (final round score). One of the hottest functions in a played round.
199
199
  """
200
200
  from belote.deck import Card, Rank
201
- from belote.game import TrickCard, replace
201
+ from belote.game import TrickCard
202
202
  from belote.scoring import trick_card_points
203
203
 
204
204
  print(f"Benchmarking trick_card_points() over {iterations} iterations...")
@@ -228,7 +228,7 @@ def benchmark_ai_legality_filter(iterations: int = 500) -> float:
228
228
  AI calls `legal_cards` once per decision; an unrepresentative cold-cache
229
229
  benchmark above would over-attribute AI time to legality checks.
230
230
  """
231
- from belote.game import legal_cards, replace
231
+ from belote.game import legal_cards
232
232
  print(f"Benchmarking AI legality filter over {iterations} iterations...")
233
233
 
234
234
  state = new_game()
@@ -1,3 +1,3 @@
1
- __version__ = "4.9.5"
1
+ __version__ = "4.9.7"
2
2
 
3
3
  __all__ = ["__version__"]