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.
- {belote_cli-4.7.1 → belote_cli-4.7.2}/CHANGELOG.md +108 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/DEVELOPMENT.md +1 -1
- {belote_cli-4.7.1 → belote_cli-4.7.2}/PKG-INFO +4 -4
- {belote_cli-4.7.1 → belote_cli-4.7.2}/README.md +3 -3
- {belote_cli-4.7.1 → belote_cli-4.7.2}/pyproject.toml +1 -1
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/__init__.py +1 -1
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/main.py +235 -199
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/game.py +7 -1
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/gameflow.py +16 -19
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/scoring.py +28 -13
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_boss_modifiers_integration.py +33 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_belote.py +104 -8
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_bidding_all_pass.py +50 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/.antigravitycli/69dd05ce-2c1c-4419-8755-e4dd0d4495e8.json +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/.claude/settings.local.json +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/.gitignore +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/.python-version +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/LICENSE +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/scripts/benchmark.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/a11y.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/achievements.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ai.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ansi.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/round_ledger.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/run_state.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/core/scoring.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/modifier_patch.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/engine/round_driver.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ghost_run.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/registry.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/run_summary.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/consumables.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/history.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/hud.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/inventory.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/config.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/context.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/deck.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/input.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/main.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/replay.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/rules.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/stats.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/themes.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/announce.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/fit_guard.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/layout.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/menu.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/prompts.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/src/belote/ui/render.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/__init__.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_belatro.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_boss_contracts.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_consumables_ui.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_dead_flag_fixes.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_decks_4_5.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_endless.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_event_bus.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_ghost_run.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_heist.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_history_overlay.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_hud_synergy.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_hud_toggle.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_inventory_overlay.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_joker_contracts.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_jokers_4_5.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_partner_jokers.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase0_coverage.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_progression.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_round_driver.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_run_summary.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_shop_empty_pools.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_slot_machine_tally.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/belatro/test_voucher_idempotency.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/perf_baselines.json +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_a11y.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_achievements.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_ai.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_alt_screen_scroll.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_announce_stats.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_ansi_helpers.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_benchmark_smoke.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_declaration_tiebreak.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_extended.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_game_logic.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_gameflow.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_hand_auto_sort.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_history_overlay_cache.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_input_eof.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_input_wasd.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_invariants.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_layout.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_new_coverage.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_no_color.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_official_rules.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_perf_regression.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_properties.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_render_diff.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_render_felt_polish.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_replay.py +0 -0
- {belote_cli-4.7.1 → belote_cli-4.7.2}/tests/test_undo.py +0 -0
- {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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 4.7.
|
|
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 (
|
|
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 **
|
|
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**:
|
|
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 (
|
|
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 **
|
|
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**:
|
|
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
|
|
|
@@ -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(
|
|
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,
|