belote-cli 4.7.1__tar.gz → 4.7.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 (154) hide show
  1. {belote_cli-4.7.1 → belote_cli-4.7.2}/CHANGELOG.md +108 -0
  2. {belote_cli-4.7.1 → belote_cli-4.7.2}/DEVELOPMENT.md +1 -1
  3. {belote_cli-4.7.1 → belote_cli-4.7.2}/PKG-INFO +4 -4
  4. {belote_cli-4.7.1 → belote_cli-4.7.2}/README.md +3 -3
  5. {belote_cli-4.7.1 → belote_cli-4.7.2}/pyproject.toml +1 -1
  6. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/__init__.py +1 -1
  7. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/main.py +235 -199
  8. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/game.py +7 -1
  9. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/gameflow.py +16 -19
  10. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/scoring.py +28 -13
  11. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_boss_modifiers_integration.py +33 -0
  12. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_belote.py +104 -8
  13. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_bidding_all_pass.py +50 -0
  14. {belote_cli-4.7.1 → belote_cli-4.7.2}/.antigravitycli/69dd05ce-2c1c-4419-8755-e4dd0d4495e8.json +0 -0
  15. {belote_cli-4.7.1 → belote_cli-4.7.2}/.claude/settings.local.json +0 -0
  16. {belote_cli-4.7.1 → belote_cli-4.7.2}/.gitignore +0 -0
  17. {belote_cli-4.7.1 → belote_cli-4.7.2}/.python-version +0 -0
  18. {belote_cli-4.7.1 → belote_cli-4.7.2}/LICENSE +0 -0
  19. {belote_cli-4.7.1 → belote_cli-4.7.2}/scripts/benchmark.py +0 -0
  20. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/__init__.py +0 -0
  21. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/a11y.py +0 -0
  22. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/achievements.py +0 -0
  23. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ai.py +0 -0
  24. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ansi.py +0 -0
  25. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/__init__.py +0 -0
  26. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/__init__.py +0 -0
  27. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/economy.py +0 -0
  28. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/round_ledger.py +0 -0
  29. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/run_state.py +0 -0
  30. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/scoring.py +0 -0
  31. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/__init__.py +0 -0
  32. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/event_bus.py +0 -0
  33. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/modifier_patch.py +0 -0
  34. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/round_driver.py +0 -0
  35. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ghost_run.py +0 -0
  36. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/__init__.py +0 -0
  37. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/base.py +0 -0
  38. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/__init__.py +0 -0
  39. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/annonces.py +0 -0
  40. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/coinche.py +0 -0
  41. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/contract.py +0 -0
  42. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/corrupted.py +0 -0
  43. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/economy.py +0 -0
  44. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
  45. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
  46. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
  47. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
  48. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
  49. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
  50. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/planets.py +0 -0
  51. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/registry.py +0 -0
  52. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/tarots.py +0 -0
  53. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/vouchers.py +0 -0
  54. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/__init__.py +0 -0
  55. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/partner_state.py +0 -0
  56. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/personality.py +0 -0
  57. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/trust.py +0 -0
  58. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/progression/__init__.py +0 -0
  59. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/progression/save.py +0 -0
  60. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/progression/unlocks.py +0 -0
  61. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/__init__.py +0 -0
  62. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/ante.py +0 -0
  63. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/ante_themes.py +0 -0
  64. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/boss.py +0 -0
  65. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/decks.py +0 -0
  66. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/shop.py +0 -0
  67. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run_summary.py +0 -0
  68. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/__init__.py +0 -0
  69. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/announce.py +0 -0
  70. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/collection.py +0 -0
  71. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/consumables.py +0 -0
  72. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/history.py +0 -0
  73. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/hud.py +0 -0
  74. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/inventory.py +0 -0
  75. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/menu.py +0 -0
  76. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/rules.py +0 -0
  77. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/shop.py +0 -0
  78. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/trust_bar.py +0 -0
  79. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/config.py +0 -0
  80. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/context.py +0 -0
  81. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/deck.py +0 -0
  82. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/input.py +0 -0
  83. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/main.py +0 -0
  84. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/replay.py +0 -0
  85. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/rules.py +0 -0
  86. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/stats.py +0 -0
  87. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/themes.py +0 -0
  88. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/__init__.py +0 -0
  89. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/announce.py +0 -0
  90. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/fit_guard.py +0 -0
  91. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/layout.py +0 -0
  92. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/menu.py +0 -0
  93. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/prompts.py +0 -0
  94. {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/render.py +0 -0
  95. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/__init__.py +0 -0
  96. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/__init__.py +0 -0
  97. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_belatro.py +0 -0
  98. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_boss_contracts.py +0 -0
  99. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_collection_logic.py +0 -0
  100. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_consumables_ui.py +0 -0
  101. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_contract_unlocks.py +0 -0
  102. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_dead_flag_fixes.py +0 -0
  103. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_deck_variants.py +0 -0
  104. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_decks_4_5.py +0 -0
  105. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_endless.py +0 -0
  106. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_event_bus.py +0 -0
  107. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_ghost_run.py +0 -0
  108. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_heist.py +0 -0
  109. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_history_overlay.py +0 -0
  110. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_hud_synergy.py +0 -0
  111. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_hud_toggle.py +0 -0
  112. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_inventory_overlay.py +0 -0
  113. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_joker_contracts.py +0 -0
  114. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_jokers_4_5.py +0 -0
  115. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_partner_jokers.py +0 -0
  116. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_partner_trust.py +0 -0
  117. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase0_coverage.py +0 -0
  118. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase1_plumbing.py +0 -0
  119. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase2_content.py +0 -0
  120. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase3_meta.py +0 -0
  121. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_progression.py +0 -0
  122. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_round_driver.py +0 -0
  123. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_run_summary.py +0 -0
  124. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_shop_empty_pools.py +0 -0
  125. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_slot_machine_tally.py +0 -0
  126. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_voucher_idempotency.py +0 -0
  127. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/perf_baselines.json +0 -0
  128. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_a11y.py +0 -0
  129. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_achievements.py +0 -0
  130. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_ai.py +0 -0
  131. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_alt_screen_scroll.py +0 -0
  132. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_announce_stats.py +0 -0
  133. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_ansi_helpers.py +0 -0
  134. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_benchmark_smoke.py +0 -0
  135. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_declaration_tiebreak.py +0 -0
  136. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_extended.py +0 -0
  137. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_game_logic.py +0 -0
  138. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_gameflow.py +0 -0
  139. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_hand_auto_sort.py +0 -0
  140. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_history_overlay_cache.py +0 -0
  141. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_input_eof.py +0 -0
  142. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_input_wasd.py +0 -0
  143. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_invariants.py +0 -0
  144. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_layout.py +0 -0
  145. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_new_coverage.py +0 -0
  146. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_no_color.py +0 -0
  147. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_official_rules.py +0 -0
  148. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_perf_regression.py +0 -0
  149. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_properties.py +0 -0
  150. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_render_diff.py +0 -0
  151. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_render_felt_polish.py +0 -0
  152. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_replay.py +0 -0
  153. {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_undo.py +0 -0
  154. {belote_cli-4.7.1 → belote_cli-4.7.2}/uv.lock +0 -0
@@ -5,6 +5,114 @@ 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.7.2] - 2026-05-20
9
+
10
+ Patch release: external-model audit verification pass. A prior audit (pasted
11
+ from another LLM) flagged ~30 issues across bugs / perf / quality. Verifying
12
+ each claim against the live code rejected the false positives (including one
13
+ HIGH-severity claim — "160 event dispatches per round" — that the model
14
+ invented out of whole cloth) and shipped only the genuinely confirmed
15
+ findings. Two real correctness gaps the prior audit missed were caught via
16
+ fresh-additions review. All baselines green: `ruff` 0 violations,
17
+ `mypy --strict` 0 errors, `pytest` 1003/1003.
18
+
19
+ ### Fixed
20
+
21
+ - **(Bug) Malédiction (`invert_scoring`) 4-4 trick tie.** Pre-fix the boss
22
+ flag zeroed the team that won MORE tricks, but a 4-4 split fell through
23
+ both branches and neither side was zeroed — both teams kept their full
24
+ scores under the curse. Rule chosen: the taker is zeroed (the cursed
25
+ side failed to break the tie). `scoring.py::_apply_invert_scoring` adds
26
+ an `else` branch; pinned by
27
+ `test_boss_invert_scoring_4_4_tie_zeros_taker` in
28
+ `tests/belatro/test_boss_modifiers_integration.py`.
29
+
30
+ ### Changed
31
+
32
+ - **`bm: object` → `bm: BossModifiers`** on `_trick_zeroed_by_ban_clubs`,
33
+ `_card_points_with_zero_ranks`, `card_points_with_modifiers`, and
34
+ `_trick_points_with_modifiers` (`scoring.py`). All four helpers were
35
+ typed as `object` with `getattr(bm, "ban_clubs", False)` defensive
36
+ lookups; the actual callsites always pass `BossModifiers`. Now type-
37
+ safe attribute access — `mypy --strict` enforces every new field is
38
+ spelled correctly.
39
+ - **`UICallbacks` extracted from `BelAtroGame._play_blind`** to a module-
40
+ top class in `belatro/main.py`. The nested class spanned 189 lines and
41
+ captured 8 closure variables (`run`, `profile`, `save_manager`, `hud`,
42
+ `acc`, `trust_bar`, `show_north`, plus a dead-write `last_display_state`
43
+ cell). Now an explicit constructor; `last_display_state` removed
44
+ (write-only, never read).
45
+ - **Litige pool carry across all-pass redeals is now documented.** Added
46
+ a `reset_round_fields` docstring entry calling out that `litige_points`
47
+ is deliberately NOT in the reset list — the pool survives across
48
+ rounds (including all-pass redeals) until a non-litige scoring round
49
+ consumes it. Pinned by
50
+ `test_litige_pool_survives_all_pass_redeal` +
51
+ `test_litige_pool_survives_two_consecutive_all_pass_redeals` in
52
+ `tests/test_bidding_all_pass.py`.
53
+
54
+ ### Performance
55
+
56
+ - **`detect_sequences` is now `@lru_cache(128)`.** Microbench placed it
57
+ at ~25% of `score_round` cost when `initial_hands` is populated, and
58
+ `get_declarations` runs the same 4 hand tuples through the helper
59
+ twice per round (once at bid-acceptance time in `game.py`, once during
60
+ `score_round` itself). Cache hits eliminate the second pass.
61
+ `score_round` macrobench: 300 µs → 227 µs (~24% on the full path).
62
+ Cards are frozen dataclasses (hashable); all callers treat the
63
+ returned list as read-only.
64
+
65
+ ### Tests strengthened
66
+
67
+ - **`test_last_trick_bonus_applied`** (`tests/test_belote.py`) → split
68
+ into `test_capot_subsumes_last_trick_bonus` (asserts the actual
69
+ `CAPOT_BASE` total) and new `test_last_trick_bonus_applied_in_normal_round`
70
+ (deterministic 4-NS/4-EW non-capot fixture proving the +10 dix-de-der
71
+ actually lands on the taker side). Previously only asserted `is_capot
72
+ is True` — the +10 mechanic the test name promised was untested.
73
+ - **`test_non_capot_points_sum_162`** (`tests/test_belote.py`) →
74
+ conditional `if not breakdown.is_capot:` replaced with explicit
75
+ `assert not breakdown.is_capot` plus the unconditional conservation
76
+ check. The old form passed vacuously if `make_deck()` ordering ever
77
+ produced a capot.
78
+
79
+ ### Internal
80
+
81
+ - **Contract→word a11y mapping deduplicated** to a `_contract_word(state)`
82
+ helper in `gameflow.py`. Two near-identical 8-line blocks collapsed
83
+ into one helper call each (bid→play transition; round-result speak).
84
+
85
+ ### Documentation
86
+
87
+ - README test count claims and CHANGELOG / DEVELOPMENT references
88
+ updated 999 → 1003.
89
+
90
+ ### Verified-false audit claims (re-investigate at your peril)
91
+
92
+ For the record so the next audit cycle doesn't relitigate items the
93
+ external model flagged but the code disagreed with:
94
+
95
+ - `partner()` if-chain vs modular arithmetic — style only, not a bug.
96
+ - `start_round` "doesn't reset belote_holders" — self-refuted (line 308
97
+ passes `belote_holders={}`).
98
+ - Score-animation labels "confusing" — `gameflow.py:458` already labels
99
+ by `breakdown.taker_team`.
100
+ - `_empty_breakdown` taker_team default — only returned on contract-
101
+ inactive states; harmless.
102
+ - `BelAtroRun.consume()` "removes before use" — wrapped in
103
+ `contextlib.suppress(ValueError)`, semantically correct.
104
+ - `_bonus_money` "race" — Le Fantôme writes to `acc._ledger.money`
105
+ directly since 4.6.2; main.py only consumes seal_round-routed amounts.
106
+ - "160 event dispatches per round" — false. Jokers subscribe to
107
+ `TrickWonEvent` / `BeloteAnnouncedEvent` / `DeclarationScoredEvent`
108
+ only; per-card emit doesn't iterate jokers. Real count is ~85-100
109
+ dispatches per round and they're already O(jokers × events).
110
+ - LeRebelle / RebeloteEcho / TierceCharger ungated under boss flags —
111
+ verified gated. `game.py:907` short-circuits `BeloteAnnouncedEvent`
112
+ under `no_belote`; `round_driver.py:467-473` documents
113
+ TierceCharger's intentional un-gating; LeMathématicien / QuinteRoyale
114
+ read `event.points` which Le Mime forces to 0.
115
+
8
116
  ## [4.7.1] - 2026-05-19
9
117
 
10
118
  Patch release: post-4.7.0 audit pass that found two latent correctness
@@ -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 (999 tests expected)
87
+ # Full test suite (1003 tests expected)
88
88
  PYTHONPATH=src pytest
89
89
  ```
90
90
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: belote-cli
3
- Version: 4.7.1
3
+ Version: 4.7.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
@@ -263,7 +263,7 @@ belote/
263
263
  │ ├── input.py # Platform-dispatched key reader and interruptible sleep
264
264
  │ ├── stats.py # Global and session statistics tracking
265
265
  │ └── rules.py # Game rules content
266
- ├── tests/ # Comprehensive test suite (999 tests)
266
+ ├── tests/ # Comprehensive test suite (1003 tests)
267
267
  ├── scripts/ # Performance benchmarks
268
268
  ├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
269
269
  ├── LICENSE # MIT License
@@ -278,14 +278,14 @@ belote/
278
278
  PYTHONPATH=src pytest
279
279
  ```
280
280
 
281
- Currently **999 tests** passing with 100% coverage on game-logic modules (4.7.1).
281
+ Currently **1003 tests** passing with 100% coverage on game-logic modules (4.7.2).
282
282
 
283
283
  ## Technical Integrity
284
284
 
285
285
  The codebase is strictly validated with the following tools:
286
286
  - **mypy**: 0 errors (strict type safety)
287
287
  - **ruff**: 0 violations (linting & formatting)
288
- - **pytest**: 999/999 passed
288
+ - **pytest**: 1003/1003 passed
289
289
  - **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
290
290
  - **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
291
291
 
@@ -220,7 +220,7 @@ belote/
220
220
  │ ├── input.py # Platform-dispatched key reader and interruptible sleep
221
221
  │ ├── stats.py # Global and session statistics tracking
222
222
  │ └── rules.py # Game rules content
223
- ├── tests/ # Comprehensive test suite (999 tests)
223
+ ├── tests/ # Comprehensive test suite (1003 tests)
224
224
  ├── scripts/ # Performance benchmarks
225
225
  ├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
226
226
  ├── LICENSE # MIT License
@@ -235,14 +235,14 @@ belote/
235
235
  PYTHONPATH=src pytest
236
236
  ```
237
237
 
238
- Currently **999 tests** passing with 100% coverage on game-logic modules (4.7.1).
238
+ Currently **1003 tests** passing with 100% coverage on game-logic modules (4.7.2).
239
239
 
240
240
  ## Technical Integrity
241
241
 
242
242
  The codebase is strictly validated with the following tools:
243
243
  - **mypy**: 0 errors (strict type safety)
244
244
  - **ruff**: 0 violations (linting & formatting)
245
- - **pytest**: 999/999 passed
245
+ - **pytest**: 1003/1003 passed
246
246
  - **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
247
247
  - **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
248
248
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "belote-cli"
7
- version = "4.7.1"
7
+ version = "4.7.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__ = "4.7.1"
1
+ __version__ = "4.7.2"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -7,8 +7,14 @@ from __future__ import annotations
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from belote.deck import Card
11
+ from belote.game import GameState, Seat
12
+
10
13
  from ..input import KeyReader
11
14
  from .ghost_run import GhostRecorder
15
+ from .progression.save import Profile
16
+ from .ui.hud import BelAtroHUD
17
+ from .ui.trust_bar import TrustBar
12
18
 
13
19
  from .core.run_state import BelAtroRun
14
20
  from .core.scoring import ScoreAccumulator
@@ -22,6 +28,225 @@ from .ui.menu import BelAtroMainMenu
22
28
  from .ui.shop import ShopScreen
23
29
 
24
30
 
31
+ class UICallbacks(RoundUICallbacks):
32
+ """BelAtro round-UI callbacks. Wraps the classic ``prompt_bid`` /
33
+ ``prompt_card`` flow with overlay handling, BelAtro HUD + trust-bar
34
+ repaint, L'Architecte buy-contract, Dix de Der Heist prompt, and
35
+ surcoinche prompt.
36
+
37
+ Extracted from ``BelAtroGame._play_blind`` for readability — the
38
+ eight dependencies were previously captured via closure cells. The
39
+ interfaces (RoundUICallbacks methods) are unchanged.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ reader: KeyReader,
45
+ run: BelAtroRun,
46
+ profile: Profile,
47
+ save_manager: SaveManager,
48
+ acc: ScoreAccumulator,
49
+ hud: BelAtroHUD,
50
+ trust_bar: TrustBar,
51
+ show_north: bool,
52
+ ) -> None:
53
+ self.reader = reader
54
+ self.run = run
55
+ self.profile = profile
56
+ self.save_manager = save_manager
57
+ self.acc = acc
58
+ self.hud = hud
59
+ self.trust_bar = trust_bar
60
+ self.show_north = show_north
61
+
62
+ def _show_overlay(self, state: GameState) -> None:
63
+ # 4.6.3: I/V now toggles BelAtro top HUD visibility (joker pip
64
+ # strip, ante line, chips×mult, trust bar, synergy tooltip).
65
+ # When hidden, the classic HUD's `Trump:` / `Taker:` fields on
66
+ # row 1 are no longer painted over. `invalidate_diff()` is
67
+ # required so `display()` re-paints row 1 from scratch instead
68
+ # of diffing against the cached frame that still believed the
69
+ # joker strip occupied cols 2–25.
70
+ from ..ui.render import display, invalidate_diff
71
+ from .ui.announce import toggle_top_hud
72
+
73
+ toggle_top_hud()
74
+ invalidate_diff()
75
+ display(state, show_north_hand=self.show_north)
76
+ self.hud.render(self.acc, state)
77
+ self.trust_bar.render()
78
+
79
+ def prompt_bid(self, state: GameState) -> object:
80
+ from ..ui.prompts import prompt_bid
81
+ from .ui.announce import BelAtroAnnounce
82
+
83
+ # L'Architecte (4.5.0): offer to buy the contract for $10
84
+ # before showing the normal bid UI. Re-checked each call (the
85
+ # loop may come around to SOUTH again after a pass) — money has
86
+ # to be high enough at the moment of purchase.
87
+ if (
88
+ self.run.card_enhancements.get("buy_contract")
89
+ and self.run.economy.money >= 10
90
+ and BelAtroAnnounce.yes_no(
91
+ "L'Architecte: buy the contract for $10?", self.reader
92
+ )
93
+ ):
94
+ chosen = BelAtroAnnounce.buy_contract_picker(self.reader)
95
+ if chosen is not None:
96
+ self.run.economy.money -= 10
97
+ BelAtroAnnounce.banner(
98
+ "Contract bought — $10 spent",
99
+ self.reader,
100
+ hold=0.8,
101
+ )
102
+ return chosen
103
+
104
+ while True:
105
+ res = prompt_bid(state, self.reader)
106
+ if res == "OVERLAY":
107
+ self._show_overlay(state)
108
+ continue
109
+ if res == "QUIT":
110
+ raise KeyboardInterrupt
111
+ if isinstance(res, str):
112
+ return None
113
+ return res
114
+
115
+ def prompt_card(self, state: GameState) -> tuple[Card, GameState]:
116
+ from ..ui.prompts import prompt_card
117
+
118
+ # 4.7.0 follow-up: hook the BelAtro HUD + trust bar into the
119
+ # classic prompt_card loop so the persistent slot-machine tally
120
+ # readout (gated on `_top_hud_visible`) repaints after every
121
+ # `display()` call. Without this hook, between-tricks the player
122
+ # sees the felt mat but no readout — the HUD is only refreshed
123
+ # by `on_card_played` (post-play) and `_show_overlay` (I-toggle).
124
+ def _hud_after_display(s: GameState) -> None:
125
+ self.hud.render(self.acc, s)
126
+ self.trust_bar.render()
127
+
128
+ while True:
129
+ card, new_state = prompt_card(
130
+ state,
131
+ self.reader,
132
+ show_north_hand=self.show_north,
133
+ after_display=_hud_after_display,
134
+ )
135
+ if card == "OVERLAY":
136
+ self._show_overlay(state)
137
+ continue
138
+ if card == "INVENTORY":
139
+ # 4.7.0: V key — open the InventoryOverlay (read-only
140
+ # view of jokers/vouchers/consumables/permanent
141
+ # bonuses/contract levels). Wraps with invalidate_diff()
142
+ # then re-renders so the player returns to a clean
143
+ # card-selection prompt.
144
+ self._show_inventory(state)
145
+ continue
146
+ if card is None:
147
+ raise KeyboardInterrupt
148
+ if card == "UNDO":
149
+ continue
150
+ if isinstance(card, str):
151
+ continue
152
+ return card, new_state
153
+
154
+ def _show_inventory(self, state: GameState) -> None:
155
+ """Open the V-key inventory overlay and repaint on exit."""
156
+ from belote.ui.render import display, invalidate_diff
157
+
158
+ from .ui.inventory import InventoryOverlay
159
+
160
+ InventoryOverlay(self.run, self.reader).open()
161
+ invalidate_diff()
162
+ display(state, show_north_hand=self.show_north)
163
+ self.hud.render(self.acc, state)
164
+ self.trust_bar.render()
165
+
166
+ def on_card_played(self, state: GameState, seat: Seat, card: Card) -> None:
167
+ from dataclasses import replace as dc_replace
168
+
169
+ from ..ui.render import display
170
+
171
+ if not state.current_trick and state.completed_tricks:
172
+ display_state = dc_replace(state, current_trick=state.completed_tricks[-1])
173
+ else:
174
+ display_state = state
175
+ display(display_state, show_north_hand=self.show_north)
176
+ self.hud.render(self.acc, display_state)
177
+ self.trust_bar.render()
178
+
179
+ def on_trick_end(self, state: GameState, winner: Seat, points: int) -> None:
180
+ # 4.7.0: animated odometer-style tally replaces the static
181
+ # multi-line popup. The popup helper is kept defined for one
182
+ # release in case a future BelAtro overlay needs the per-trick
183
+ # log breakdown.
184
+ from .ui.announce import BelAtroAnnounce
185
+
186
+ BelAtroAnnounce.slot_machine_tally(
187
+ self.acc, state, self.reader, points=points
188
+ )
189
+
190
+ def on_round_end(self, breakdown: object) -> None:
191
+ pass
192
+
193
+ def prompt_surcoinche(self, state: GameState, coincheur: Seat) -> bool:
194
+ """3.7.1 D3: ask the NS taker whether to surcoinche after EW coinches."""
195
+ from .ui.announce import BelAtroAnnounce
196
+
197
+ prompt = f"{coincheur.name} coinched! Surcoinche back?"
198
+ return BelAtroAnnounce.yes_no(prompt, self.reader)
199
+
200
+ def prompt_heist(self, state: GameState) -> bool:
201
+ """4.7.0: Dix de Der Heist declaration prompt.
202
+
203
+ Two gates collapse the prompt when the heist has no value:
204
+ - ``state.taker == Seat.SOUTH``: only the player declares; AI
205
+ seats never get the prompt. The engine already gates on this
206
+ too — belt-and-suspenders here so a future direct caller
207
+ can't slip past.
208
+ - ``self.acc.interest_rate > 0``: with rate=0 the multiplier
209
+ is 1× (no reward), so declaring is pure downside. Default
210
+ Economy.interest_rate is 0 — La Voûte voucher or one of the
211
+ rate-bumping tarots must be purchased to enable.
212
+
213
+ Discoverability hint (4.7.0): when the player takes a contract
214
+ without La Voûte, show a one-time explainer banner.
215
+ ``self.profile.seen_heist_hint`` is flipped True and persisted,
216
+ so the hint never shows again for this profile.
217
+ """
218
+ from belote.game import Seat as _Seat
219
+
220
+ from .ui.announce import BelAtroAnnounce
221
+
222
+ if state.taker != _Seat.SOUTH:
223
+ return False
224
+ if self.acc.interest_rate <= 0:
225
+ if not self.profile.seen_heist_hint:
226
+ BelAtroAnnounce.banner(
227
+ "Tip: buy the La Voûte voucher in the shop to unlock "
228
+ "the Dix de Der Heist (×2+ Mult on trick 8).",
229
+ self.reader,
230
+ hold=2.5,
231
+ )
232
+ self.profile.seen_heist_hint = True
233
+ self.save_manager.save_profile(self.profile)
234
+ return False
235
+ multiplier = 1 + self.acc.interest_rate
236
+ declared = BelAtroAnnounce.yes_no(
237
+ f"DIX DE DER HEIST — Win trick 8 for ×{multiplier} Mult, "
238
+ "or lose trick 8 and forfeit tricks 1-7 chips. Declare?",
239
+ self.reader,
240
+ )
241
+ if declared:
242
+ BelAtroAnnounce.banner(
243
+ "DIX DE DER HEIST DECLARED — all in on trick 8",
244
+ self.reader,
245
+ hold=1.5,
246
+ )
247
+ return declared
248
+
249
+
25
250
  class BelAtroGame:
26
251
  def __init__(self) -> None:
27
252
  import os
@@ -202,204 +427,6 @@ class BelAtroGame:
202
427
  from .ui.announce import reset_tally_state
203
428
  reset_tally_state()
204
429
  show_north = self.run.show_north_hand or self.run.partner.trust.shares_void_info
205
- run = self.run # captured for UICallbacks closure (see prompt_bid)
206
- # 4.7.0: captured for the one-time La Voûte hint in prompt_heist.
207
- profile = self.profile
208
- save_manager = self.save_manager
209
-
210
- last_display_state: list[GameState | None] = [None] # mutable cell for closure
211
-
212
- from belote.deck import Card
213
- from belote.game import GameState, Seat
214
-
215
- class UICallbacks(RoundUICallbacks):
216
- def __init__(self, reader: KeyReader):
217
- self.reader = reader
218
-
219
- def _show_overlay(self, state: GameState) -> None:
220
- # 4.6.3: I/V now toggles BelAtro top HUD visibility (joker pip
221
- # strip, ante line, chips×mult, trust bar, synergy tooltip).
222
- # When hidden, the classic HUD's `Trump:` / `Taker:` fields on
223
- # row 1 are no longer painted over. `invalidate_diff()` is
224
- # required so `display()` re-paints row 1 from scratch instead
225
- # of diffing against the cached frame that still believed the
226
- # joker strip occupied cols 2–25.
227
- from ..ui.render import display, invalidate_diff
228
- from .ui.announce import toggle_top_hud
229
-
230
- toggle_top_hud()
231
- invalidate_diff()
232
- display(state, show_north_hand=show_north)
233
- hud.render(acc, state)
234
- trust_bar.render()
235
-
236
- def prompt_bid(self, state: GameState) -> object:
237
- from ..ui.prompts import prompt_bid
238
-
239
- # L'Architecte (4.5.0): offer to buy the contract for $10
240
- # before showing the normal bid UI. We re-check eligibility
241
- # each time prompt_bid is called (the loop may come around to
242
- # SOUTH again after a pass) — money has to be high enough at
243
- # the moment of purchase. `run` is the BelAtroRun captured in
244
- # the enclosing closure.
245
- if (
246
- run.card_enhancements.get("buy_contract")
247
- and run.economy.money >= 10
248
- and BelAtroAnnounce.yes_no(
249
- "L'Architecte: buy the contract for $10?", self.reader
250
- )
251
- ):
252
- chosen = BelAtroAnnounce.buy_contract_picker(self.reader)
253
- if chosen is not None:
254
- run.economy.money -= 10
255
- BelAtroAnnounce.banner(
256
- "Contract bought — $10 spent",
257
- self.reader,
258
- hold=0.8,
259
- )
260
- return chosen
261
-
262
- while True:
263
- res = prompt_bid(state, self.reader)
264
- if res == "OVERLAY":
265
- self._show_overlay(state)
266
- continue
267
- if res == "QUIT":
268
- raise KeyboardInterrupt
269
- if isinstance(res, str):
270
- return None
271
- return res
272
-
273
- def prompt_card(self, state: GameState) -> tuple[Card, GameState]:
274
- from ..ui.prompts import prompt_card
275
-
276
- # 4.7.0 follow-up: hook the BelAtro HUD + trust bar into the
277
- # classic prompt_card loop so the persistent slot-machine
278
- # tally readout (gated on `_top_hud_visible`) repaints after
279
- # every `display()` call. Without this hook, between-tricks
280
- # the player sees the felt mat but no readout — the HUD is
281
- # only refreshed by `on_card_played` (post-play) and
282
- # `_show_overlay` (I-toggle).
283
- def _hud_after_display(s: GameState) -> None:
284
- hud.render(acc, s)
285
- trust_bar.render()
286
-
287
- while True:
288
- card, new_state = prompt_card(
289
- state,
290
- self.reader,
291
- show_north_hand=show_north,
292
- after_display=_hud_after_display,
293
- )
294
- if card == "OVERLAY":
295
- self._show_overlay(state)
296
- continue
297
- if card == "INVENTORY":
298
- # 4.7.0: V key — open the InventoryOverlay (read-only
299
- # view of jokers/vouchers/consumables/permanent
300
- # bonuses/contract levels). Wraps with
301
- # invalidate_diff() then re-renders so the player
302
- # returns to a clean card-selection prompt.
303
- self._show_inventory(state)
304
- continue
305
- if card is None:
306
- raise KeyboardInterrupt
307
- if card == "UNDO":
308
- continue
309
- if isinstance(card, str):
310
- continue
311
- return card, new_state
312
-
313
- def _show_inventory(self, state: GameState) -> None:
314
- """Open the V-key inventory overlay and repaint on exit."""
315
- from belote.ui.render import display, invalidate_diff
316
-
317
- from .ui.inventory import InventoryOverlay
318
-
319
- InventoryOverlay(run, self.reader).open()
320
- invalidate_diff()
321
- display(state, show_north_hand=show_north)
322
- hud.render(acc, state)
323
- trust_bar.render()
324
-
325
- def on_card_played(self, state: GameState, seat: Seat, card: Card) -> None:
326
- from dataclasses import replace as dc_replace
327
-
328
- from ..ui.render import display
329
-
330
- if not state.current_trick and state.completed_tricks:
331
- display_state = dc_replace(state, current_trick=state.completed_tricks[-1])
332
- else:
333
- display_state = state
334
- last_display_state[0] = display_state
335
- display(display_state, show_north_hand=show_north)
336
- hud.render(acc, display_state)
337
- trust_bar.render()
338
-
339
- def on_trick_end(self, state: GameState, winner: Seat, points: int) -> None:
340
- # 4.7.0: animated odometer-style tally replaces the static
341
- # multi-line popup. The popup helper is kept defined for one
342
- # release in case a future BelAtro overlay needs the per-trick
343
- # log breakdown.
344
- BelAtroAnnounce.slot_machine_tally(
345
- acc, state, self.reader, points=points
346
- )
347
-
348
- def on_round_end(self, breakdown: object) -> None:
349
- pass
350
-
351
- def prompt_surcoinche(self, state: GameState, coincheur: Seat) -> bool:
352
- """3.7.1 D3: ask the NS taker whether to surcoinche after EW coinches."""
353
- prompt = f"{coincheur.name} coinched! Surcoinche back?"
354
- return BelAtroAnnounce.yes_no(prompt, self.reader)
355
-
356
- def prompt_heist(self, state: GameState) -> bool:
357
- """4.7.0: Dix de Der Heist declaration prompt.
358
-
359
- Two gates collapse the prompt when the heist has no value:
360
- - `state.taker == Seat.SOUTH`: only the player declares; AI
361
- seats never get the prompt. The engine already gates on
362
- this too — belt-and-suspenders here so a future direct
363
- caller can't slip past.
364
- - `acc.interest_rate > 0`: with rate=0 the multiplier is
365
- 1× (no reward), so declaring is pure downside. Default
366
- Economy.interest_rate is 0 — La Voûte voucher or one of
367
- the rate-bumping tarots must be purchased to enable.
368
-
369
- Discoverability hint (4.7.0): when the player takes a
370
- contract without La Voûte, show a one-time explainer
371
- banner. `profile.seen_heist_hint` is flipped True and
372
- persisted, so the hint never shows again for this profile.
373
- Without this, fresh players would never see the heist —
374
- the prompt is silently gated off and there's no signal
375
- pointing at the unlock path.
376
- """
377
- if state.taker != Seat.SOUTH:
378
- return False
379
- if acc.interest_rate <= 0:
380
- if not profile.seen_heist_hint:
381
- BelAtroAnnounce.banner(
382
- "Tip: buy the La Voûte voucher in the shop to unlock "
383
- "the Dix de Der Heist (×2+ Mult on trick 8).",
384
- self.reader,
385
- hold=2.5,
386
- )
387
- profile.seen_heist_hint = True
388
- save_manager.save_profile(profile)
389
- return False
390
- multiplier = 1 + acc.interest_rate
391
- declared = BelAtroAnnounce.yes_no(
392
- f"DIX DE DER HEIST — Win trick 8 for ×{multiplier} Mult, "
393
- "or lose trick 8 and forfeit tricks 1-7 chips. Declare?",
394
- self.reader,
395
- )
396
- if declared:
397
- BelAtroAnnounce.banner(
398
- "DIX DE DER HEIST DECLARED — all in on trick 8",
399
- self.reader,
400
- hold=1.5,
401
- )
402
- return declared
403
430
 
404
431
  # Check if boss
405
432
  boss = None
@@ -494,7 +521,16 @@ class BelAtroGame:
494
521
  partner=self.run.partner,
495
522
  boss=boss,
496
523
  target_score=self.run.target_score,
497
- ui_callbacks=UICallbacks(self.reader),
524
+ ui_callbacks=UICallbacks(
525
+ self.reader,
526
+ self.run,
527
+ self.profile,
528
+ self.save_manager,
529
+ acc,
530
+ hud,
531
+ trust_bar,
532
+ show_north,
533
+ ),
498
534
  acc=acc,
499
535
  card_enhancements=round_flags,
500
536
  recorder=self._ghost_recorder,
@@ -259,7 +259,13 @@ class GameState:
259
259
 
260
260
 
261
261
  def reset_round_fields(state: GameState, **kwargs: object) -> GameState:
262
- """Return a new state with round-specific fields reset to defaults."""
262
+ """Return a new state with round-specific fields reset to defaults.
263
+
264
+ `litige_points` is deliberately NOT reset here. The litige pool survives
265
+ across rounds — including all-pass redeals — until a non-litige scoring
266
+ round consumes it (see `scoring.py::apply_round_score`). Tests in
267
+ `tests/test_bidding_all_pass.py` pin this contract.
268
+ """
263
269
  reset_values: dict[str, object] = {
264
270
  "trump": None,
265
271
  "taker": None,