belote-cli 4.7.2__tar.gz → 4.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. {belote_cli-4.7.2 → belote_cli-4.8.0}/CHANGELOG.md +176 -0
  2. {belote_cli-4.7.2 → belote_cli-4.8.0}/DEVELOPMENT.md +14 -1
  3. {belote_cli-4.7.2 → belote_cli-4.8.0}/PKG-INFO +7 -6
  4. {belote_cli-4.7.2 → belote_cli-4.8.0}/README.md +6 -5
  5. {belote_cli-4.7.2 → belote_cli-4.8.0}/pyproject.toml +1 -1
  6. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/__init__.py +1 -1
  7. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ai.py +5 -3
  8. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/core/run_state.py +10 -0
  9. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/core/scoring.py +11 -1
  10. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/corrupted.py +7 -1
  11. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/main.py +15 -0
  12. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/announce.py +149 -2
  13. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/hud.py +84 -26
  14. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/shop.py +98 -0
  15. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/trust_bar.py +49 -0
  16. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/gameflow.py +32 -5
  17. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/themes.py +23 -0
  18. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ui/__init__.py +18 -2
  19. belote_cli-4.8.0/src/belote/ui/anim.py +211 -0
  20. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ui/announce.py +60 -5
  21. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ui/render.py +108 -2
  22. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_decks_4_5.py +34 -0
  23. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_hud_toggle.py +49 -0
  24. belote_cli-4.8.0/tests/belatro/test_joker_callouts.py +31 -0
  25. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_joker_contracts.py +23 -0
  26. belote_cli-4.8.0/tests/belatro/test_shop_animations.py +64 -0
  27. belote_cli-4.8.0/tests/test_anim_helpers.py +99 -0
  28. belote_cli-4.8.0/tests/test_belote_stinger.py +37 -0
  29. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_render_diff.py +31 -0
  30. belote_cli-4.8.0/tests/test_winner_glow.py +53 -0
  31. belote_cli-4.7.2/.antigravitycli/69dd05ce-2c1c-4419-8755-e4dd0d4495e8.json +0 -1
  32. {belote_cli-4.7.2 → belote_cli-4.8.0}/.claude/settings.local.json +0 -0
  33. {belote_cli-4.7.2 → belote_cli-4.8.0}/.gitignore +0 -0
  34. {belote_cli-4.7.2 → belote_cli-4.8.0}/.python-version +0 -0
  35. {belote_cli-4.7.2 → belote_cli-4.8.0}/LICENSE +0 -0
  36. {belote_cli-4.7.2 → belote_cli-4.8.0}/scripts/benchmark.py +0 -0
  37. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/__init__.py +0 -0
  38. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/a11y.py +0 -0
  39. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/achievements.py +0 -0
  40. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ansi.py +0 -0
  41. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/__init__.py +0 -0
  42. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/core/__init__.py +0 -0
  43. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/core/economy.py +0 -0
  44. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/core/round_ledger.py +0 -0
  45. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/engine/__init__.py +0 -0
  46. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/engine/event_bus.py +0 -0
  47. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/engine/modifier_patch.py +0 -0
  48. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/engine/round_driver.py +0 -0
  49. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ghost_run.py +0 -0
  50. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/__init__.py +0 -0
  51. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/base.py +0 -0
  52. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/__init__.py +0 -0
  53. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/annonces.py +0 -0
  54. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/coinche.py +0 -0
  55. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/contract.py +0 -0
  56. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/economy.py +0 -0
  57. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
  58. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
  59. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
  60. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
  61. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
  62. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
  63. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/planets.py +0 -0
  64. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/registry.py +0 -0
  65. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/tarots.py +0 -0
  66. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/items/vouchers.py +0 -0
  67. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/partner/__init__.py +0 -0
  68. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/partner/partner_state.py +0 -0
  69. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/partner/personality.py +0 -0
  70. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/partner/trust.py +0 -0
  71. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/progression/__init__.py +0 -0
  72. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/progression/save.py +0 -0
  73. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/progression/unlocks.py +0 -0
  74. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/run/__init__.py +0 -0
  75. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/run/ante.py +0 -0
  76. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/run/ante_themes.py +0 -0
  77. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/run/boss.py +0 -0
  78. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/run/decks.py +0 -0
  79. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/run/shop.py +0 -0
  80. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/run_summary.py +0 -0
  81. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/__init__.py +0 -0
  82. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/collection.py +0 -0
  83. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/consumables.py +0 -0
  84. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/history.py +0 -0
  85. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/inventory.py +0 -0
  86. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/menu.py +0 -0
  87. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/belatro/ui/rules.py +0 -0
  88. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/config.py +0 -0
  89. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/context.py +0 -0
  90. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/deck.py +0 -0
  91. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/game.py +0 -0
  92. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/input.py +0 -0
  93. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/main.py +0 -0
  94. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/replay.py +0 -0
  95. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/rules.py +0 -0
  96. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/scoring.py +0 -0
  97. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/stats.py +0 -0
  98. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ui/fit_guard.py +0 -0
  99. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ui/layout.py +0 -0
  100. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ui/menu.py +0 -0
  101. {belote_cli-4.7.2 → belote_cli-4.8.0}/src/belote/ui/prompts.py +0 -0
  102. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/__init__.py +0 -0
  103. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/__init__.py +0 -0
  104. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_belatro.py +0 -0
  105. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_boss_contracts.py +0 -0
  106. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_boss_modifiers_integration.py +0 -0
  107. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_collection_logic.py +0 -0
  108. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_consumables_ui.py +0 -0
  109. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_contract_unlocks.py +0 -0
  110. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_dead_flag_fixes.py +0 -0
  111. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_deck_variants.py +0 -0
  112. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_endless.py +0 -0
  113. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_event_bus.py +0 -0
  114. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_ghost_run.py +0 -0
  115. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_heist.py +0 -0
  116. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_history_overlay.py +0 -0
  117. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_hud_synergy.py +0 -0
  118. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_inventory_overlay.py +0 -0
  119. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_jokers_4_5.py +0 -0
  120. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_partner_jokers.py +0 -0
  121. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_partner_trust.py +0 -0
  122. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_phase0_coverage.py +0 -0
  123. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_phase1_plumbing.py +0 -0
  124. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_phase2_content.py +0 -0
  125. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_phase3_meta.py +0 -0
  126. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_progression.py +0 -0
  127. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_round_driver.py +0 -0
  128. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_run_summary.py +0 -0
  129. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_shop_empty_pools.py +0 -0
  130. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_slot_machine_tally.py +0 -0
  131. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/belatro/test_voucher_idempotency.py +0 -0
  132. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/perf_baselines.json +0 -0
  133. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_a11y.py +0 -0
  134. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_achievements.py +0 -0
  135. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_ai.py +0 -0
  136. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_alt_screen_scroll.py +0 -0
  137. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_announce_stats.py +0 -0
  138. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_ansi_helpers.py +0 -0
  139. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_belote.py +0 -0
  140. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_benchmark_smoke.py +0 -0
  141. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_bidding_all_pass.py +0 -0
  142. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_declaration_tiebreak.py +0 -0
  143. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_extended.py +0 -0
  144. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_game_logic.py +0 -0
  145. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_gameflow.py +0 -0
  146. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_hand_auto_sort.py +0 -0
  147. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_history_overlay_cache.py +0 -0
  148. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_input_eof.py +0 -0
  149. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_input_wasd.py +0 -0
  150. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_invariants.py +0 -0
  151. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_layout.py +0 -0
  152. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_new_coverage.py +0 -0
  153. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_no_color.py +0 -0
  154. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_official_rules.py +0 -0
  155. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_perf_regression.py +0 -0
  156. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_properties.py +0 -0
  157. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_render_felt_polish.py +0 -0
  158. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_replay.py +0 -0
  159. {belote_cli-4.7.2 → belote_cli-4.8.0}/tests/test_undo.py +0 -0
  160. {belote_cli-4.7.2 → belote_cli-4.8.0}/uv.lock +0 -0
@@ -5,6 +5,182 @@ 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.8.0] - 2026-05-21
9
+
10
+ Feature release: a twelfth theme, a coordinated animation polish pass for
11
+ BelAtro, and a tactile feel pass for classic-mode Belote. All additions are
12
+ UI-only — zero rules / scoring / AI changes. Every new helper follows the
13
+ established `try / finally invalidate_diff()` architectural rule (4.6.4 /
14
+ 4.0.0). New `BELOTE_NO_ANIM=1` env-var kill switch short-circuits every new
15
+ animation to its end-state for slow terminals or scripted runs.
16
+
17
+ ### Added
18
+
19
+ - **Twelfth theme: Sunset Magma** (`src/belote/themes.py::sunset_magma`).
20
+ Deep-magenta felt, coral suits, smoky-purple "blacks", amber-sunset
21
+ highlights, magenta-on-cream banners. Fills the warm orange/magenta niche
22
+ none of the existing 11 themes occupied. Cycles via the `T` key and
23
+ shows up in the main-menu theme selector automatically — no other code
24
+ changes needed because `ThemeManager.set_current` validates dynamically.
25
+ - **Shared animation toolkit** (`src/belote/ui/anim.py`). New module with
26
+ three easing helpers (`ease_out_quad`, `ease_in_out_quad`,
27
+ `ease_out_cubic`) and three painted-frame helpers (`pulse_text`,
28
+ `float_text`, `tick_bar`). Every painted helper ends with
29
+ `invalidate_diff()` in `finally`; every interruptible delay routes
30
+ through `reader.read_timeout(...)` so test stubs (and the existing
31
+ `slot_machine_tally` idiom) work uniformly. Env switch
32
+ `BELOTE_NO_ANIM=1` short-circuits to the end-state, paired with
33
+ `_refresh_animations_enabled_from_env()` for test fixtures.
34
+ - **(BelAtro / B1) Joker trigger callouts in the slot-machine tally**
35
+ (`belatro/ui/announce.py`). Per-trick log entries added by
36
+ `ScoreAccumulator._log` (joker firings, planet/Carnet/Architecte
37
+ attributions) now float above the bucket row as labelled callouts
38
+ (`⚡ Foo: +25 chips` / `✦ Bar: ×2.5 Mult` / `$ Architecte: +$2 …`).
39
+ New module-level `_last_log_count` cursor + `_classify_callout` /
40
+ `_emit_callouts` helpers; reset by `reset_tally_state()`. Capped at 4
41
+ callouts per trick to keep the moment under ~600 ms.
42
+ - **(BelAtro / B2) Shop purchase and reroll feedback**
43
+ (`belatro/ui/shop.py`). New `_animate_purchase(idx, num_items, money_before,
44
+ money_after)` (gold pulse on the bought slot + `tick_bar` money tick-down)
45
+ and `_animate_reroll(num_items)` (pulsed "↻ rerolling..." cue before the
46
+ new inventory paints). Hooked from the existing `run()` loop without
47
+ changing shop state semantics.
48
+ - **(BelAtro / B3) Target-crossing celebration**
49
+ (`belatro/ui/announce.py::_emit_target_celebration`). Fires once per
50
+ round on the first tally where the running total crosses
51
+ `acc.target_score`: gold pulse on the odometer line plus a `★ TARGET ★`
52
+ floater above it. The existing 1.2× flame row is unchanged.
53
+ - **(BelAtro / B4) Trust-bar tick-up animation**
54
+ (`belatro/ui/trust_bar.py::TrustBar.animate_change`). After the
55
+ round-end trust mutations in `belatro/main.py` (`blind_beaten` /
56
+ `blind_failed` / `big_margin_win` / `chute` / `capot_together`), the bar
57
+ animates from the pre-round value to the post-round value via `tick_bar`.
58
+ Snapshot taken right after `trust = self.run.partner.trust`; animation
59
+ fires only when `trust.value != pre_trust_value`.
60
+ - **(Classic / C3) Tactile play-trail on SOUTH plays**
61
+ (`belote/ui/render.py::slide_card_to_table_hint`). A brief vertical
62
+ sparkle trail (`✦/✧/·`) from the south hand toward the south trick
63
+ slot, painted just before the played card lands via `patch_trick_card`.
64
+ AI plays are unchanged. Self-cleaning trail; ~120 ms, skippable.
65
+ - **(Classic / C4) Trick-winner glow + hold**
66
+ (`belote/ui/render.py::pulse_winner_glow`). After all four cards land
67
+ and the MIN_TRICK_DWELL pause completes, a 3-frame gold→white→gold pulse
68
+ on the bottom hint row announces `★ <Direction> wins the trick ★`
69
+ before the trick sweeps. Computed via `trick_winner_seat` (Rupture
70
+ swing takes effect at scoring, so the on-table label stays accurate).
71
+ - **(Classic / C5) Dramatic centered Belote / Rebelote stinger**
72
+ (`belote/ui/announce.py::belote_stinger`). Replaces the modest
73
+ one-line `announce("Belote!")` / `announce("Rebelote!")` call with a
74
+ 4-row centered banner framed in `╔═╗║╚═╝` chars, painted in the active
75
+ theme's `banner_bg()` + `gold_fg()`. Falls back to the slim
76
+ `announce()` path on terminals narrower than 32 columns. Routed through
77
+ `gameflow.py`'s existing `current.announced` dispatch.
78
+
79
+ ### Internal
80
+
81
+ - `belote/ui/__init__.py` now re-exports `belote_stinger`,
82
+ `pulse_winner_glow`, and `slide_card_to_table_hint` alongside the
83
+ existing UI surface.
84
+ - Test count baseline: **1028** (4.7.3 had 1007; +21 in 4.8.0). New files:
85
+ `tests/test_anim_helpers.py` (8), `tests/test_belote_stinger.py` (2),
86
+ `tests/test_winner_glow.py` (5), `tests/belatro/test_joker_callouts.py`
87
+ (5), `tests/belatro/test_shop_animations.py` (2). All anim tests use
88
+ `sys.modules["belote.ui.render"]` to bypass the `belote.ui` re-export
89
+ shadow (per the 4.0.0 architectural note).
90
+ - **Deferred from this release:** the original plan included a
91
+ "card lift on selection" (C1) and "smooth highlight slide between cards"
92
+ (C2) for the south-hand cursor. Both require deeper changes to the
93
+ multi-row horizontal hand renderer than fit cleanly in the same release
94
+ as the new toolkit; tracked for a follow-up. The remaining tactile
95
+ effects (C3 trail, C4 winner glow, C5 stinger) ship as planned.
96
+
97
+ ## [4.7.3] - 2026-05-21
98
+
99
+ Patch release: targeted bug-hunt + performance + code-logic audit. Three
100
+ parallel exploration passes (classic engine, BelAtro layer, UI/render)
101
+ surfaced ~30 candidate findings; verifying each against the live code
102
+ rejected the false positives (deck.py "deluge scores 0", LePasseur "missing
103
+ re_emit guard", show_rules "missing invalidate on scroll", show_history
104
+ "missing term_h in cache key", legal_cards "missing trick_rank hoist") and
105
+ shipped only the verified-true delta. All baselines green: `ruff` 0
106
+ violations, `mypy --strict` 0 errors, `pytest` 1007/1007.
107
+
108
+ ### Fixed
109
+
110
+ - **(Bug) `announce()` did not invalidate the render-diff baseline.** The
111
+ function paints a transient banner with absolute cursor positioning,
112
+ bypassing `display()`. Without a post-paint `invalidate_diff()` the next
113
+ `display()` diffed against `_last_emitted_lines` (which has no record of
114
+ the banner) and could leave the banner visible as a ghost on the bottom
115
+ row. Same architectural rule as `show_help` / `show_history` /
116
+ `show_rules` / `show_card_detail` / `show_round_summary` /
117
+ `animate_score_update`. `announce()` was the last unfixed site of the
118
+ 4.0.0 / 4.6.4 finally-pattern sweep. Pinned by
119
+ `test_announce_invalidates_diff_baseline` in `tests/test_render_diff.py`.
120
+ - **(Bug) L'Infiltré × La Déluge interaction.** The `ghost_lead` deck rule
121
+ paid `+2 Mult / +$1` when NS won a trick by playing trump on a "non-trump
122
+ lead". The is-trump-lead check at `belatro/core/scoring.py:414-417`
123
+ considered only `lead_suit == event.trump` and the TOUT_ATOUT case —
124
+ it did not honour `seven_eight_trump` (La Déluge), so a 7-led or 8-led
125
+ trick was incorrectly treated as a non-trump lead and the bonus could
126
+ fire even though the lead was effectively trump. Pinned by
127
+ `test_ghost_lead_silent_when_lead_is_seven_under_deluge` in
128
+ `tests/belatro/test_decks_4_5.py`.
129
+ - **(Defensive) `LeDemon.on_purchase` is now idempotent.** Re-running the
130
+ hook on an already-owned joker (a future save/load round-trip or replay-
131
+ resume tool) would have compounded the trust subtraction. New
132
+ `_applied_purchase_ids: set[str]` field on `BelAtroRun` (mirrors
133
+ `_applied_voucher_ids` from 3.9.3) short-circuits the second call. Pinned
134
+ by `test_le_demon_on_purchase_is_idempotent` in
135
+ `tests/belatro/test_joker_contracts.py`.
136
+
137
+ ### Changed
138
+
139
+ - **AI comment about La Déluge corrected** (`src/belote/ai.py:293-296`).
140
+ The pre-4.7.3 comment claimed "promotes 7s/8s of trump above the Jack" —
141
+ but `deck.py::trick_rank` puts the 7 at rank 8 and the 8 at rank 9
142
+ (the two LOWEST trumps, scoring 0). The boss description in
143
+ `boss.py:95` ("become trump") matches the code, not the comment. The
144
+ comment now states the actual behaviour so future maintainers don't
145
+ chase a phantom bug.
146
+ - **`render_joker_pip_strip` / `render_synergy_tooltip` split into
147
+ builders + writers** (`belatro/ui/hud.py`). Pre-4.7.3 each helper did
148
+ its own `sys.stdout.write + flush` outside `BelAtroHUD._render`'s
149
+ batched parts list, costing 2–3 syscalls per HUD refresh instead of
150
+ one. New `build_joker_pip_strip` / `build_synergy_tooltip` return
151
+ strings; the legacy `render_*` wrappers (used by direct callers and
152
+ tests) still exist for backward compatibility. `BelAtroHUD.render`
153
+ and `_render_compact` now embed both builders into a single batched
154
+ write. Pinned by `test_belatro_hud_render_writes_once` in
155
+ `tests/belatro/test_hud_toggle.py`.
156
+
157
+ ### Performance
158
+
159
+ - **`_get_card_face` reads `_cached_theme_name` instead of
160
+ `theme_manager.current_name`** (`belote/ui/render.py:314`). The module
161
+ already caches the theme name (line 84) and refreshes it via the theme
162
+ callback (line 95); `_get_card_face` is called ~52 times per game
163
+ frame, so eliminating the per-call property lookup shaves a few
164
+ microseconds off the render budget. The cache is kept in sync via the
165
+ existing theme-change callback. `clear_card_cache` ensures the card
166
+ face cache is reset on every theme change, so reading the cached name
167
+ is safe.
168
+
169
+ ### Internal
170
+
171
+ - New `BelAtroRun._applied_purchase_ids: set[str]` field for joker
172
+ on_purchase idempotency (analogous to `_applied_voucher_ids`). Empty
173
+ by default; populated lazily by corrupted jokers with non-idempotent
174
+ on_purchase actions.
175
+ - `build_joker_pip_strip(...) -> str` / `build_synergy_tooltip(...) -> str`
176
+ public builder API in `belatro/ui/hud.py`; `render_joker_pip_strip` /
177
+ `render_synergy_tooltip` retained as thin wrappers around the builders.
178
+
179
+ ### Test count baseline
180
+
181
+ - 1007 (4.7.2 had 1003; +4 in 4.7.3 across `test_render_diff.py`,
182
+ `test_decks_4_5.py`, `test_joker_contracts.py`, `test_hud_toggle.py`).
183
+
8
184
  ## [4.7.2] - 2026-05-20
9
185
 
10
186
  Patch release: external-model audit verification pass. A prior audit (pasted
@@ -84,7 +84,7 @@ PYTHONPATH=src mypy --strict src/
84
84
  # Linting (0 violations expected)
85
85
  ruff check src/ tests/
86
86
 
87
- # Full test suite (1003 tests expected)
87
+ # Full test suite (1007 tests expected)
88
88
  PYTHONPATH=src pytest
89
89
  ```
90
90
 
@@ -143,6 +143,19 @@ once at startup; toggling mid-run has no effect.
143
143
  `fg()` / `bg()` per the [no-color.org](https://no-color.org/) spec.
144
144
  Bold/dim/underline/reverse/strikethrough and cursor sequences remain
145
145
  (they aren't color). Added in 3.9.0. Backed by `src/belote/ansi.py`.
146
+ - `BELOTE_NO_ANIM=1` — short-circuit every 4.8.0 animation helper
147
+ (`pulse_text`, `float_text`, `tick_bar`, the joker callouts, the shop
148
+ purchase/reroll feedback, the trust-bar tick-up, the classic-mode
149
+ trail / winner glow) to its end-state with no perceptible delay.
150
+ Useful on slow terminals, in CI, or under scripted runs. Read once at
151
+ import; tests that mutate it must call
152
+ `belote.ui.anim._refresh_animations_enabled_from_env()` after the
153
+ patch. Independent of `BELOTE_NO_DIFF` — each lever is its own
154
+ toggle. Backed by `src/belote/ui/anim.py`.
155
+ - `BELOTE_NO_DIFF=1` — disable the render-diff layer in
156
+ `belote/ui/render.py::display`; every call paints a full frame
157
+ instead of only changed rows. Escape hatch for debugging visual
158
+ artifacts on uncommon terminal emulators.
146
159
 
147
160
  ## Releasing a New Version
148
161
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: belote-cli
3
- Version: 4.7.2
3
+ Version: 4.8.0
4
4
  Summary: A 4-player terminal card game
5
5
  Project-URL: Homepage, https://github.com/ElysiumDisc/belote
6
6
  Project-URL: Repository, https://github.com/ElysiumDisc/belote
@@ -43,7 +43,7 @@ Description-Content-Type: text/markdown
43
43
 
44
44
  # Belote – 4-Player Terminal Card Game
45
45
 
46
- Complete implementation of the French card game Belote for the terminal, with a full-screen themed felt table (11 palettes), full card graphics at compass positions (N/W/E/S), vignette and braille pip-texture polish, and an optional decorative outer frame.
46
+ Complete implementation of the French card game Belote for the terminal, with a full-screen themed felt table (12 palettes), full card graphics at compass positions (N/W/E/S), vignette and braille pip-texture polish, and an optional decorative outer frame.
47
47
 
48
48
  ## BelAtro Expansion
49
49
 
@@ -207,13 +207,14 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
207
207
  - **Dix de Der Heist (4.7.0):** Take a contract and declare a heist before trick 1 — win trick 8 for a `×(1 + interest_rate)` Mult multiplier on your round score; lose it and forfeit your card chips from tricks 1–7. Gated on owning **La Voûte** voucher (`interest_rate > 0`). AI never declares.
208
208
  - **Slot-Machine Score Tally (4.7.0):** Per-trick odometer animation replaces the static popup. Chip-bucket fills, mult pulses, total ticks toward target, and a flame row crowns the odometer when you blow past 120% of the blind. Skippable on SPACE / ESC / ENTER. Final readout persists in the HUD between tricks (toggled by `I` alongside the top HUD). Suppressed under Le Brouillard (`hide_hud`) and La Compétition (`separate_scoring`).
209
209
  - **Inventory Overlay on V (4.7.0):** Press `V` mid-game to inspect everything you own — jokers (with edition tags + per-edition bonus blurb), vouchers, consumables, permanent chip / mult bonuses, and per-contract planet levels. List view → ↑/↓ navigate, Enter for detail, Esc/V/Q close. Read-only counterpart to the `C` consumables-action tray.
210
+ - **Animation Polish (4.8.0):** Per-joker callouts float above the slot-machine tally as each joker fires, target-crossing pops a `★ TARGET ★` flag, shop purchases pulse + tick the money down, rerolls fade, and the trust bar ticks up between rounds. Belote / Rebelote now get a dramatic 4-row centered stinger. Classic-mode SOUTH plays paint a tactile sparkle trail; the trick winner glows briefly before the trick clears. All animations skip on any key press and respect `BELOTE_NO_ANIM=1` for slow terminals.
210
211
  - **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
211
212
  - **Full Boss Blind Suite:** All 21 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
212
213
  - **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
213
214
  - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
214
215
  - **Rich Terminal UI:** Full-screen themed felt table with detailed card graphics and "You" vs "Partner" terminology.
215
216
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
216
- - **Customizable Themes:** Switch between eleven color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast, Colorblind, Forest Night, Moonlit Tavern, Royal Purple, Emerald Isle) using the `T` key during gameplay.
217
+ - **Customizable Themes:** Switch between twelve 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) using the `T` key during gameplay.
217
218
  - **Polished Felt Mat (3.9.4):** The trick mat now has a subtle vignette at its edges, a faint deterministic braille pip-dot texture (fabric-weave feel without intrusive glyphs), and — at standard/spacious terminal sizes — a decorative `╔═══◆═══...═══◆═══╗` outer frame with corner ornaments.
218
219
  - **Selection HUD (3.9.4):** Selecting a card in hand now paints a highlighted bar under it AND a centered `► A♠ — Trump ◄` readout below, color-coded by suit / trump / legality.
219
220
  - **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
@@ -263,7 +264,7 @@ belote/
263
264
  │ ├── input.py # Platform-dispatched key reader and interruptible sleep
264
265
  │ ├── stats.py # Global and session statistics tracking
265
266
  │ └── rules.py # Game rules content
266
- ├── tests/ # Comprehensive test suite (1003 tests)
267
+ ├── tests/ # Comprehensive test suite (1007 tests)
267
268
  ├── scripts/ # Performance benchmarks
268
269
  ├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
269
270
  ├── LICENSE # MIT License
@@ -278,14 +279,14 @@ belote/
278
279
  PYTHONPATH=src pytest
279
280
  ```
280
281
 
281
- Currently **1003 tests** passing with 100% coverage on game-logic modules (4.7.2).
282
+ Currently **1028 tests** passing with 100% coverage on game-logic modules (4.8.0).
282
283
 
283
284
  ## Technical Integrity
284
285
 
285
286
  The codebase is strictly validated with the following tools:
286
287
  - **mypy**: 0 errors (strict type safety)
287
288
  - **ruff**: 0 violations (linting & formatting)
288
- - **pytest**: 1003/1003 passed
289
+ - **pytest**: 1007/1007 passed
289
290
  - **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
290
291
  - **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
291
292
 
@@ -1,6 +1,6 @@
1
1
  # Belote – 4-Player Terminal Card Game
2
2
 
3
- Complete implementation of the French card game Belote for the terminal, with a full-screen themed felt table (11 palettes), full card graphics at compass positions (N/W/E/S), vignette and braille pip-texture polish, and an optional decorative outer frame.
3
+ Complete implementation of the French card game Belote for the terminal, with a full-screen themed felt table (12 palettes), full card graphics at compass positions (N/W/E/S), vignette and braille pip-texture polish, and an optional decorative outer frame.
4
4
 
5
5
  ## BelAtro Expansion
6
6
 
@@ -164,13 +164,14 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
164
164
  - **Dix de Der Heist (4.7.0):** Take a contract and declare a heist before trick 1 — win trick 8 for a `×(1 + interest_rate)` Mult multiplier on your round score; lose it and forfeit your card chips from tricks 1–7. Gated on owning **La Voûte** voucher (`interest_rate > 0`). AI never declares.
165
165
  - **Slot-Machine Score Tally (4.7.0):** Per-trick odometer animation replaces the static popup. Chip-bucket fills, mult pulses, total ticks toward target, and a flame row crowns the odometer when you blow past 120% of the blind. Skippable on SPACE / ESC / ENTER. Final readout persists in the HUD between tricks (toggled by `I` alongside the top HUD). Suppressed under Le Brouillard (`hide_hud`) and La Compétition (`separate_scoring`).
166
166
  - **Inventory Overlay on V (4.7.0):** Press `V` mid-game to inspect everything you own — jokers (with edition tags + per-edition bonus blurb), vouchers, consumables, permanent chip / mult bonuses, and per-contract planet levels. List view → ↑/↓ navigate, Enter for detail, Esc/V/Q close. Read-only counterpart to the `C` consumables-action tray.
167
+ - **Animation Polish (4.8.0):** Per-joker callouts float above the slot-machine tally as each joker fires, target-crossing pops a `★ TARGET ★` flag, shop purchases pulse + tick the money down, rerolls fade, and the trust bar ticks up between rounds. Belote / Rebelote now get a dramatic 4-row centered stinger. Classic-mode SOUTH plays paint a tactile sparkle trail; the trick winner glows briefly before the trick clears. All animations skip on any key press and respect `BELOTE_NO_ANIM=1` for slow terminals.
167
168
  - **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
168
169
  - **Full Boss Blind Suite:** All 21 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
169
170
  - **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
170
171
  - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
171
172
  - **Rich Terminal UI:** Full-screen themed felt table with detailed card graphics and "You" vs "Partner" terminology.
172
173
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
173
- - **Customizable Themes:** Switch between eleven color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast, Colorblind, Forest Night, Moonlit Tavern, Royal Purple, Emerald Isle) using the `T` key during gameplay.
174
+ - **Customizable Themes:** Switch between twelve 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) using the `T` key during gameplay.
174
175
  - **Polished Felt Mat (3.9.4):** The trick mat now has a subtle vignette at its edges, a faint deterministic braille pip-dot texture (fabric-weave feel without intrusive glyphs), and — at standard/spacious terminal sizes — a decorative `╔═══◆═══...═══◆═══╗` outer frame with corner ornaments.
175
176
  - **Selection HUD (3.9.4):** Selecting a card in hand now paints a highlighted bar under it AND a centered `► A♠ — Trump ◄` readout below, color-coded by suit / trump / legality.
176
177
  - **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
@@ -220,7 +221,7 @@ belote/
220
221
  │ ├── input.py # Platform-dispatched key reader and interruptible sleep
221
222
  │ ├── stats.py # Global and session statistics tracking
222
223
  │ └── rules.py # Game rules content
223
- ├── tests/ # Comprehensive test suite (1003 tests)
224
+ ├── tests/ # Comprehensive test suite (1007 tests)
224
225
  ├── scripts/ # Performance benchmarks
225
226
  ├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
226
227
  ├── LICENSE # MIT License
@@ -235,14 +236,14 @@ belote/
235
236
  PYTHONPATH=src pytest
236
237
  ```
237
238
 
238
- Currently **1003 tests** passing with 100% coverage on game-logic modules (4.7.2).
239
+ Currently **1028 tests** passing with 100% coverage on game-logic modules (4.8.0).
239
240
 
240
241
  ## Technical Integrity
241
242
 
242
243
  The codebase is strictly validated with the following tools:
243
244
  - **mypy**: 0 errors (strict type safety)
244
245
  - **ruff**: 0 violations (linting & formatting)
245
- - **pytest**: 1003/1003 passed
246
+ - **pytest**: 1007/1007 passed
246
247
  - **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
247
248
  - **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
248
249
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "belote-cli"
7
- version = "4.7.2"
7
+ version = "4.8.0"
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__ = "4.7.2"
1
+ __version__ = "4.8.0"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -290,9 +290,11 @@ class AIPlayer:
290
290
  """Decide which card to play."""
291
291
  hand = state.hand_of(self.seat)
292
292
  legal = legal_cards(state, self.seat)
293
- # La Déluge boss promotes 7s/8s of trump above the Jack every ranking
294
- # / point read in this method must respect that flag or the AI will
295
- # pick the wrong cards.
293
+ # La Déluge boss makes 7s and 8s of any suit rank as trump (the two
294
+ # LOWEST trumps 7 at rank 8, 8 at rank 9, both scoring 0 points;
295
+ # see deck.py::trick_rank and ::card_points). Every ranking / point
296
+ # read in this method must thread `_se` through or the AI under-
297
+ # values trump cards that should beat them.
296
298
  self._se = state.boss_modifiers.seven_eight_trump
297
299
 
298
300
  # Boss: L'Agent Double (Partner sabotages on 3 random tricks)
@@ -104,6 +104,16 @@ class BelAtroRun:
104
104
  # future save/load round-trip — gets the same protection automatically.
105
105
  _applied_voucher_ids: set[str] = field(default_factory=set)
106
106
 
107
+ # ── Idempotency guard for Joker.on_purchase() ──────────
108
+ # Corrupted jokers with non-idempotent on_purchase actions (LeDemon's
109
+ # trust subtraction) record their id here on first apply. Re-application
110
+ # paths (a future save/load round-trip, replay tooling) consult this set
111
+ # and short-circuit so a once-paid cost never compounds. Joker on_purchase
112
+ # actions that are already idempotent (boolean flags like
113
+ # LeTraitre.partner_throws_trick, LAgentDouble.agent_double_joker) don't
114
+ # need to consult this set, but adding their id is harmless. 4.7.3.
115
+ _applied_purchase_ids: set[str] = field(default_factory=set)
116
+
107
117
  # ── Recent-boss tracker (3.9.3 Phase 5) ────────────────
108
118
  # Used by the BelAtro main loop to suppress immediate boss repeats in
109
119
  # endless mode. The deque holds at most 2 recent boss ids; the selector
@@ -407,13 +407,23 @@ class ScoreAccumulator:
407
407
  # of lead (legal_cards forbids trumping while holding lead).
408
408
  # +2 Mult, +$1.
409
409
  if joker_state.get("ghost_lead") and event.trump is not None:
410
- lead_suit = event.cards[0].suit if event.cards else None
410
+ lead_card = event.cards[0] if event.cards else None
411
+ lead_suit = lead_card.suit if lead_card else None
411
412
  # Under TOUT_ATOUT every card is trump, so no play can be
412
413
  # "void of the led suit" — is_trump_lead resolves to True
413
414
  # and the bonus is correctly gated off.
415
+ # Under La Déluge (seven_eight_trump), a 7 or 8 of any
416
+ # suit also functions as trump — a 7-led trick is a
417
+ # trump-led trick even when lead_suit != event.trump.
418
+ se_lead = (
419
+ state.boss_modifiers.seven_eight_trump
420
+ and lead_card is not None
421
+ and lead_card.rank in (Rank.SEVEN, Rank.EIGHT)
422
+ )
414
423
  is_trump_lead = (
415
424
  lead_suit == event.trump
416
425
  or event.trump == Suit.TOUT_ATOUT
426
+ or se_lead
417
427
  )
418
428
  if lead_suit is not None and not is_trump_lead:
419
429
  seat = event.leader_seat
@@ -40,7 +40,13 @@ class LeDemon(Joker):
40
40
  is_corrupted = True
41
41
 
42
42
  def on_purchase(self, run: BelAtroRun) -> None:
43
- # Degrade trust by 3, making partner play worse
43
+ # Degrade trust by 3, making partner play worse. 4.7.3: idempotency
44
+ # guard — without it, a save/load round-trip or replay-resume tool
45
+ # that re-runs on_purchase on already-owned jokers would compound
46
+ # the cost. Voucher.apply() solved the same problem in 3.9.3.
47
+ if self.id in run._applied_purchase_ids:
48
+ return
49
+ run._applied_purchase_ids.add(self.id)
44
50
  run.partner.trust.value = max(0, run.partner.trust.value - 3)
45
51
 
46
52
  def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
@@ -576,6 +576,11 @@ class BelAtroGame:
576
576
  from belote.scoring import score_round
577
577
  bd = score_round(final_state)
578
578
  trust = self.run.partner.trust
579
+ # 4.8.0 / B4: snapshot for the trust-bar tick animation. The mutations
580
+ # in the lock_trust-gated branches below shift this between 0 and 10;
581
+ # we animate from the pre-block value to the final value after the
582
+ # block completes so the player sees the change land.
583
+ pre_trust_value = trust.value
579
584
 
580
585
  # Phase 2.2: drain pending Tierce charges into the run state.
581
586
  pending = final_state._joker_state.get("_pending_tierce_charge", 0)
@@ -668,6 +673,16 @@ class BelAtroGame:
668
673
  elif bd.is_capot and bd.taker_team == 0:
669
674
  trust.capot_together()
670
675
 
676
+ # 4.8.0 / B4: animate the trust bar from its pre-round value to its
677
+ # post-round value. No-op when nothing changed (lock_trust path, or
678
+ # symmetric mutations that net to zero). Routed through the bar's
679
+ # tick helper so each intermediate value paints + briefly holds.
680
+ # `trust_bar` is the local TrustBar instance constructed earlier in
681
+ # this method (it's also passed into UICallbacks below); `self`
682
+ # (BelAtroGame) does not have its own trust_bar attribute.
683
+ if trust.value != pre_trust_value:
684
+ trust_bar.animate_change(pre_trust_value, self.reader)
685
+
671
686
  # 3.3.0: append a BelAtro-side history entry (the [H] overlay reads
672
687
  # `self.run.history` via the override hook installed in `start()`).
673
688
  self._record_history_entry(