belote-cli 3.3.2__tar.gz → 3.3.4__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-3.3.2 → belote_cli-3.3.4}/CHANGELOG.md +76 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/DEVELOPMENT.md +5 -4
- {belote_cli-3.3.2 → belote_cli-3.3.4}/PKG-INFO +16 -4
- {belote_cli-3.3.2 → belote_cli-3.3.4}/README.md +15 -3
- {belote_cli-3.3.2 → belote_cli-3.3.4}/pyproject.toml +1 -1
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/__init__.py +1 -1
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/tarots.py +9 -1
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/main.py +5 -3
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/context.py +0 -14
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/game.py +10 -4
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/gameflow.py +0 -8
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/input.py +0 -5
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ui/__init__.py +1 -4
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ui/announce.py +0 -32
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ui/menu.py +0 -5
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ui/prompts.py +0 -13
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_belatro.py +98 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_hud_synergy.py +20 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_game_logic.py +51 -1
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_gameflow.py +0 -1
- belote_cli-3.3.4/tests/test_properties.py +166 -0
- belote_cli-3.3.4/tests/test_replay.py +183 -0
- belote_cli-3.3.2/tests/test_properties.py +0 -63
- belote_cli-3.3.2/tests/test_replay.py +0 -88
- {belote_cli-3.3.2 → belote_cli-3.3.4}/.claude/settings.local.json +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/.gitignore +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/.python-version +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/LICENSE +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/scripts/benchmark.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/a11y.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/achievements.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ai.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ansi.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/core/run_state.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/core/scoring.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/engine/modifier_patch.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/engine/round_driver.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ghost_run.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/registry.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/run_summary.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/history.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/hud.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/config.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/deck.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/main.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/replay.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/rules.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/scoring.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/stats.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/themes.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ui/layout.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/src/belote/ui/render.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/__init__.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_boss_modifiers_integration.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_dead_flag_fixes.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_ghost_run.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_history_overlay.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_phase0_coverage.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_progression.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/belatro/test_round_driver.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_a11y.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_achievements.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_ai.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_ansi_helpers.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_belote.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_extended.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_layout.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_new_coverage.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_official_rules.py +0 -0
- {belote_cli-3.3.2 → belote_cli-3.3.4}/tests/test_undo.py +0 -0
|
@@ -5,6 +5,82 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.3.4] - 2026-05-10
|
|
9
|
+
|
|
10
|
+
Portability release — removes all terminal-bell / sound code, which was triggering SIGSYS ("Bad system call") on Alpine 23 (musl libc) the moment the first trick completed in classic Belote mode. BelAtro mode was unaffected on the same Alpine box (it never imported `play_sound`), and Kubuntu / Lubuntu 24.10 / 25.10 (glibc) were unaffected in either mode. Rather than guard the BEL writes behind a libc-detection flag, the entire sound subsystem is removed: classic Belote and BelAtro now share the same "no bells" baseline. 549 tests still passing, ruff and mypy strict still clean.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- **`src/belote/ui/announce.py::play_sound`** — terminal-bell helper (writes `\a` bytes for `trick` / `belote` / `declaration` / `chute` / `capot` events). Was called from five sites in `gameflow.py` (post-trick, capot, first-trick declarations, Belote announcement, chute on failed contract); all five call sites are deleted along with the function. BelAtro's `engine/round_driver.py` never imported it, so no BelAtro behaviour changes.
|
|
15
|
+
- **`src/belote/ui/announce.py::is_muted` / `toggle_mute`** — wrappers around `AUDIO.is_muted()` / `AUDIO.toggle_mute()`. Re-exports dropped from `src/belote/ui/__init__.py::__all__`.
|
|
16
|
+
- **`src/belote/context.py::AudioManager` + the `AUDIO` singleton** — process-wide mute state holder. `TerminalContext` and the `TERMINAL` singleton are kept (they back the terminal-size cache used elsewhere).
|
|
17
|
+
- **`src/belote/input.py::Key.MUTE` + the `m` / `b"m"` key bindings** — `M` no longer triggers a special key event in either `_UnixKeyReader` or `_WindowsKeyReader`; it now falls through to `Key.CHAR` like any other letter (Belote has no other meaning for `M`).
|
|
18
|
+
- **Three `case Key.MUTE: toggle_mute()` branches in `src/belote/ui/prompts.py`** (card prompt, bid prompt, rules viewer) — deleted along with the `from .announce import is_muted, toggle_mute` import.
|
|
19
|
+
- **Two `case Key.MUTE: toggle_mute()` branches in `src/belote/ui/menu.py`** (AI config submenu, main menu) — deleted along with the `from .announce import toggle_mute` import.
|
|
20
|
+
- **`[M] Toggle Sound Effects` line from the in-game help screen** (`src/belote/ui/prompts.py::show_help`) — plus the live `(Currently: ON/OFF)` status line that reflected `is_muted()`.
|
|
21
|
+
- **`tests/test_gameflow.py`** — the obsolete `unittest.mock.patch("belote.gameflow.play_sound")` mock inside `test_run_play_8_tricks`'s `ExitStack` is gone; the test still passes (the underlying `display` / `patch_trick_card` / `announce` / `prompt_card` mocks remain).
|
|
22
|
+
|
|
23
|
+
### Internal
|
|
24
|
+
|
|
25
|
+
- **Tests**: still 549 passing.
|
|
26
|
+
- **Strict gates**: pytest 549/549, mypy 0 errors (75 files — `context.py` lost one class but kept the module), ruff 0 violations.
|
|
27
|
+
- **Unused-import sweep**: `green_fg` dropped from `src/belote/ui/prompts.py` imports (only the deleted `sound_status` line used it).
|
|
28
|
+
|
|
29
|
+
### Why drop the bell instead of guarding it on musl
|
|
30
|
+
|
|
31
|
+
`play_sound` only writes BEL (`\a`) bytes to stdout; writing those bytes is just `write(2)` and doesn't itself trigger SIGSYS on any sane libc. Whatever the precise mechanism on Alpine 23 (terminal-driver quirk, blocked downstream ioctl, or musl-specific signal-frame interaction with the existing `signal.signal(SIGINT/SIGTERM)` registration in `main.py:132-133`), the simplest and most robust answer is to stop writing the bell at all. Modern terminal emulators on every tested distro either ignored or visually-flashed the bell — no user-meaningful audio was being produced. The mute toggle exists only to suppress those flashes; with the bell gone, the toggle is dead weight.
|
|
32
|
+
|
|
33
|
+
## [3.3.3] - 2026-05-10
|
|
34
|
+
|
|
35
|
+
Audit-of-audit release — a fresh three-agent codebase pass (classic engine / BelAtro mode / tests + UI) produced ~50 candidate findings. Verification cut that to **3 real fixes** plus **3 net-new invariant test suites** for properties the prior 3.3.x cycles silently relied on. ~14 rejected claims are catalogued at the bottom of this entry so they aren't re-investigated next cycle. 549 tests passing (up from 537), ruff and mypy strict still clean. Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-tingly-barto.md`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **`src/belote/game.py::sort_hand` (F1)** — Under the Tout Atout contract every card should sort by the trump rank ladder (`J > 9 > A > 10 > K > Q > 8 > 7`). Pre-3.3.3 the sort key gated `_TRUMP_RANK_IDX` on `c.suit == trump`, which is *always* false when `trump is Suit.TOUT_ATOUT` because `Card.suit` is one of `SPADES/HEARTS/DIAMONDS/CLUBS` (TA is a contract-level marker, not a card suit). Result: the South hand displayed in the non-trump order whenever the player bid or held TA. Fix: explicit `all_trump = trump is Suit.TOUT_ATOUT` branch in `sort_key`. Also extends `_SUIT_IDX_CACHE` to pre-build the TA entry so the hot path stays cache-resident. UI-only — no scoring impact. Regression test in `tests/test_game_logic.py::test_sort_hand_uses_trump_ladder_under_tout_atout`.
|
|
40
|
+
- **`src/belote/belatro/main.py::_play_blind` (F2)** — Boss assignment on the boss blind now draws from `self.run._get_rng().choice(ALL_BOSS_MODIFIERS)`. Pre-3.3.3 the function imported `random` inline and called `random.choice()` on the module-level RNG — the same class of bug the 3.2.0 release fixed for shop generation and the three RNG-using tarots (`LeJugement` / `LaPretresse` / `LeFou`). Boss assignment was the last unseeded RNG site in the BelAtro round flow; ghost-run reproducibility now observes the same boss for the same seed regardless of prior process-wide RNG state. Regression tests in `tests/belatro/test_belatro.py::TestBossSelectionDeterminism` (behaviour + source-grep against the anti-pattern).
|
|
41
|
+
- **`src/belote/belatro/items/tarots.py::LeJugement` (F3)** — The tarot's description promises *"a random Common Joker"* but the implementation drew from `registry.get_available_jokers(run.profile)` — the full unlocked pool across all rarities. Late-run players with Rare/Legendary jokers unlocked could roll Legendary off this tarot, which is strictly stronger than advertised and mis-prices the consumable. Fix: filter the pool to `getattr(v, "rarity", Rarity.COMMON) == Rarity.COMMON` before the choice; existing empty-pool guard handles the (rare) case where no Commons are available. Regression tests in `tests/belatro/test_belatro.py::TestLeJugementRarity`.
|
|
42
|
+
|
|
43
|
+
### Added — invariant tests
|
|
44
|
+
|
|
45
|
+
These are the three test suites the 3.3.x bug cycle has been silently asking for. Each one would have caught at least one prior bug from below.
|
|
46
|
+
|
|
47
|
+
- **`tests/test_properties.py` — scoring conservation per contract (T1)** — Three new tests drive seeded full rounds and assert `table_taker_pts + table_defender_pts == 162` (normal) / `258` (Tout Atout) / `130` (Sans Atout). Plus a card-consumption invariant: after a full round every hand is empty and exactly 8 tricks were recorded. Would have caught the L'Anarchie belote-zero (3.3.1) and the La Rupture HUD divergence (3.3.1/3.3.2) years earlier had it existed at the time. Also includes a small `_drive_full_round` helper for future scoring-pin tests (handles the round-2-only TA/SA bidding flow).
|
|
48
|
+
- **`tests/test_replay.py` — replay round-trip + seeded determinism (T2)** — Two new tests: (a) record each played card from a seeded run, replay them into a fresh `GameState` built from the same seed, assert identical final state across `team_scores` / `completed_tricks` / `belote_tracker` / `belote_announcer` / `last_trick_winner`; (b) drive the same seed twice and assert identical 32-card sequences. Pins the determinism promise the 3.3.1 AI-RNG fix and the 3.3.2 replay-RNG fix established.
|
|
49
|
+
- **`tests/belatro/test_hud_synergy.py` — solo-half pair test (T3)** — The existing file already exercises the "both halves present → badge fires" direction. The new test adds the negative direction: for each pair in `_SYNERGY_PAIRS`, feed a single half into `detect_synergies` and assert no badge fires. Trip-wire for any change to the synergy matcher that accidentally promotes lone jokers to a pair badge.
|
|
50
|
+
|
|
51
|
+
### Internal
|
|
52
|
+
|
|
53
|
+
- **Tests**: 537 → 549 (+12 — 3 F-regressions + 4 T1 + 2 T2 + 1 T3 + extra cross-suit / TA sanity assertions).
|
|
54
|
+
- **Strict gates**: pytest 549/549, mypy 0 errors (76 files), ruff 0 violations.
|
|
55
|
+
- **`_SUIT_IDX_CACHE` widened**: now pre-builds for `(None, Suit.TOUT_ATOUT, *_SUITS_ORDER)` instead of just `(None, *_SUITS_ORDER)`. Removes a per-render cache miss under TA but is otherwise a no-op.
|
|
56
|
+
|
|
57
|
+
### Rejected — claims catalogued (so they aren't re-investigated)
|
|
58
|
+
|
|
59
|
+
The three Explore agents that drove this audit surfaced many plausible-sounding findings; the ones below fell apart on direct read of the current code and are documented here to save the next cycle from re-investigating them.
|
|
60
|
+
|
|
61
|
+
**Already fixed in 3.3.1/3.3.2 (agents read against stale priors):**
|
|
62
|
+
- "`_hard_play` returns `legal[0]` under Sans Atout" — fixed in 3.3.1.
|
|
63
|
+
- "`AIPlayer.__init__` constructs unseeded `Random()`" — fixed in 3.3.1; `analyze_round` followed in 3.3.2.
|
|
64
|
+
- "`AIMemory.last_voids_key` not reset on mid-round undo" — fixed in 3.3.1.
|
|
65
|
+
- "Live HUD diverges from final score under La Rupture" — fixed in 3.3.1 (`compute_trick_winners`) + 3.3.2 (`is_capot(tricks=…)`).
|
|
66
|
+
- "Belote/Rebelote silently zeroed under L'Anarchie when trump rotates" — fixed in 3.3.1 via `GameState.belote_announcer`.
|
|
67
|
+
|
|
68
|
+
**Interpretive, not bugs:**
|
|
69
|
+
- "`ScoreAccumulator` applies edition before partner-tier scaling, so Holo isn't tier-scaled" — by design. Editions ride along once per joker trigger; tier extras re-apply the *base* joker result. Otherwise an elite-tier Polychrome partner joker would compound geometrically.
|
|
70
|
+
- "Libra's `coinche_multiplier=1.0 × event.coinche_level` makes coinche pay ×5, not ×4" — description is ambiguous; the math matches the Phase 3 design doc (`+1 Mult per coinche level on success`).
|
|
71
|
+
- "Pluto `capot_bonus = 48` is additive to 252 = 300" — that *is* the advertised behaviour.
|
|
72
|
+
- "`_TIERCE_LIKE` has title-case dead entries (`Tierce`/`Quarte`/`Quinte`)" — `decl.kind` is always lowercase (`sequence`/`carre`/`belote`/`rebelote`), so only `"sequence"` ever matches. The joker fires correctly on every Tierce/Quarte/Quinte; the title-case entries are dead but harmless.
|
|
73
|
+
- "QuinteRoyale arms on `event.points >= 100` instead of declaration length" — Quinte = 100 pts in classic Belote and `event.points` is the unmodified `get_declaration_points([...])` computed inline at emit time; the proxy is sound.
|
|
74
|
+
- "`EventBus.emit` has no try/except around handlers" — broad-except would mask real bugs in joker/accumulator code. Current handlers are internal; an exception should surface in dev/test rather than be swallowed.
|
|
75
|
+
- "Negative-edition jokers still pay the 1.5× shop markup" — design: Negative is the rarest edition and grants a permanent +1 joker slot.
|
|
76
|
+
- "Boss `random.choice` doesn't respect profile unlocks" — there is no boss-unlock system in the data model; all bosses are always available by design.
|
|
77
|
+
- "ToutStreak / LeSergent reset semantics don't match flavour text" — joker authoring judgment call. Behaviour matches the registry definition.
|
|
78
|
+
|
|
79
|
+
**Already addressed by existing code:**
|
|
80
|
+
- "`ScoreAccumulator.update_state` clones `_joker_state` per event" — intentional shallow copy; `test_joker_state_only_contains_scalar_values` pins the scalar invariant.
|
|
81
|
+
- "Registry duplicate-ID overwrites silently" — fixed in 3.2.0; all four `register_*` methods assert same-class re-registration.
|
|
82
|
+
- "`_SUIT_IDX_CACHE` missing TOUT_ATOUT" — addressed as part of F1 (now in the cache).
|
|
83
|
+
|
|
8
84
|
## [3.3.2] - 2026-05-10
|
|
9
85
|
|
|
10
86
|
Residual-audit release — a fresh full-codebase pass after 3.3.1 turned up three real findings (a HIGH live-HUD divergence under La Rupture, a MEDIUM determinism leak in `replay.analyze_round`, and a LOW cosmetic chips display). The same pass flagged ~5 plausible-sounding "performance wins" and other claims that fell apart on verification — catalogued in the plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-cheeky-globe.md` so they aren't re-investigated. 537 tests passing (up from 535), ruff and mypy strict still clean.
|
|
@@ -84,14 +84,15 @@ PYTHONPATH=src mypy --strict src/
|
|
|
84
84
|
# Linting (0 violations expected)
|
|
85
85
|
ruff check src/ tests/
|
|
86
86
|
|
|
87
|
-
# Full test suite (
|
|
87
|
+
# Full test suite (549 tests expected)
|
|
88
88
|
PYTHONPATH=src pytest
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Current baseline (3.3.
|
|
92
|
-
- **mypy**: 0 errors (strict mode)
|
|
91
|
+
Current baseline (3.3.4):
|
|
92
|
+
- **mypy**: 0 errors (strict mode, 75 files)
|
|
93
93
|
- **ruff**: 0 violations
|
|
94
|
-
- **pytest**:
|
|
94
|
+
- **pytest**: 549 tests, 0 failures
|
|
95
|
+
- 3.3.4 covered: removed all terminal-bell / sound code (`play_sound`, `AudioManager`, `[M]` mute key) to fix a SIGSYS crash on Alpine 23 / musl after the first classic-mode trick. BelAtro and glibc distros were unaffected; classic Belote and BelAtro now share the same "no bells" baseline.
|
|
95
96
|
|
|
96
97
|
Run all gates before committing:
|
|
97
98
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.4
|
|
4
4
|
Summary: A 4-player terminal card game
|
|
5
5
|
Project-URL: Homepage, https://github.com/ElysiumDisc/belote
|
|
6
6
|
Project-URL: Repository, https://github.com/ElysiumDisc/belote
|
|
@@ -45,6 +45,20 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
|
|
46
46
|
Complete implementation of the French card game Belote for the terminal, with a full-screen green felt table and full card graphics at compass positions (N/W/E/S).
|
|
47
47
|
|
|
48
|
+
## What's new in 3.3.4
|
|
49
|
+
|
|
50
|
+
- **Portability fix** — Removed all terminal-bell / sound code, which was triggering SIGSYS ("Bad system call") on Alpine 23 (musl libc) the moment the first trick completed in classic Belote mode. BelAtro mode and every glibc-based distro (Kubuntu / Lubuntu 24.10 / 25.10) were unaffected, but rather than guard the BEL writes behind a libc check, the entire sound subsystem is gone — `play_sound`, `AudioManager` / `AUDIO`, `is_muted` / `toggle_mute`, the `[M]` mute key, and the help-screen mute line. Classic Belote and BelAtro now share the same "no bells" baseline.
|
|
51
|
+
- **Test coverage** — Still 549 tests. Strict gates clean: pytest 549/549, mypy 0 errors, ruff 0 violations.
|
|
52
|
+
|
|
53
|
+
## What's new in 3.3.3
|
|
54
|
+
|
|
55
|
+
- **Determinism** — Boss assignment in BelAtro mode now draws from the run's seeded RNG instead of the module-level `random`. This was the last unseeded RNG site in the round flow (shop and tarots were converted in 3.2.0, AI in 3.3.1, replay analysis in 3.3.2). Same seed now reproduces the same boss on the boss blind.
|
|
56
|
+
- **UI** — Hands sort by the trump-rank ladder under Tout Atout (Jack first, then 9 / A / 10 / K / Q / 8 / 7). Pre-3.3.3 the predicate `c.suit == trump` was always false under TA because Card.suit is never `Suit.TOUT_ATOUT`, so the South hand displayed in the non-trump order whenever the player bid TA.
|
|
57
|
+
- **Tarot** — Le Jugement now correctly grants only Common jokers as advertised. Pre-3.3.3 the code drew from the full unlocked pool, so late-run players with Rare/Legendary unlocks could roll out-of-rarity jokers off this tarot.
|
|
58
|
+
- **Test moat** — Three new invariant test suites added: scoring-conservation property (`table_taker + table_defender == 162 / 258 / 130` for normal / Tout Atout / Sans Atout), seeded-round replay determinism (same seed → same card sequence → same final state), and HUD synergy-badge negative test (solo half of a pair must not fire the badge). These would have caught most of the 3.3.x bug cycle from below.
|
|
59
|
+
- **Audit reconciliation** — A fresh three-agent codebase pass surfaced ~50 candidate findings; the three above held up under verification and the rest are catalogued in `CHANGELOG.md` so they aren't re-investigated.
|
|
60
|
+
- **Test coverage** — 549 tests (up from 537). Strict gates still clean: pytest 549/549, mypy 0 errors (76 files), ruff 0 violations.
|
|
61
|
+
|
|
48
62
|
## What's new in 3.3.1
|
|
49
63
|
|
|
50
64
|
- **Scoring correctness under bosses** — La Rupture (`no_consecutive_team_wins`) used to be applied to the live HUD but silently ignored by `score_round`, so the running total and final score diverged and impossible capots could be reported. L'Anarchie (dynamic trump) rotated `state.trump` mid-round, after which scoring's `belote_holders.get(trump)` lookup missed any Belote announced on the original trump and silently zeroed the 20/40 bonus. Both are fixed: a new `compute_trick_winners` helper is the single source of truth for La Rupture-aware winner resolution, and a new `belote_announcer: Seat` field captures the announcing seat at declaration time so the rotated trump no longer matters.
|
|
@@ -234,7 +248,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
234
248
|
|
|
235
249
|
**General:**
|
|
236
250
|
- `?`: Show keyboard shortcut help
|
|
237
|
-
- `M`: Toggle sound effects on/off
|
|
238
251
|
- `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
239
252
|
- `Q`: Quit to main menu or exit
|
|
240
253
|
- `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
|
|
@@ -275,7 +288,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
275
288
|
- **Statistics:** unified global tracking of games played, win rates, best rounds, and BelAtro expansion milestones.
|
|
276
289
|
- **Responsive Layout (3 tiers):** Three preset layouts — **compact** (80×32, fits 1366×768), **standard** (96×38), **spacious** (120×48+). The game picks the largest preset that fits your terminal on every render, so resizing mid-game adapts automatically; cards, side columns, and HUD verbosity all scale with the preset. Vertical centering pads tall terminals so the game never clings to the top.
|
|
277
290
|
- **Alternate Screen Buffer:** Both classic Belote and BelAtro run in a dedicated terminal buffer for a clean, non-overlapping interface — your shell scrollback stays untouched after you quit.
|
|
278
|
-
- **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
|
|
279
291
|
- **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
|
|
280
292
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
281
293
|
- **High Fidelity:** Implementation of French Belote rules according to the [official rules of the Fédération Française de Belote](https://www.ffbelote.org/regles-officielle-belote/), including a two-round bidding system, "Dix de Der", "Capot" (252 pts), and "Litige" (tie-break). All six contracts are bidable in round 2: the four card suits, **Tout Atout** (every suit acts as trump within its own led-suit group; press `a`), and **Sans Atout** (no trump, lead-suit highest wins; press `s`).
|
|
@@ -307,7 +319,7 @@ belote/
|
|
|
307
319
|
│ ├── scoring.py # Declarations, round scoring, capot
|
|
308
320
|
│ ├── ai.py # Three-tier AI (easy/medium/hard)
|
|
309
321
|
│ ├── config.py # Global configuration and timings
|
|
310
|
-
│ ├── context.py # Global managers (
|
|
322
|
+
│ ├── context.py # Global managers (Terminal)
|
|
311
323
|
│ ├── themes.py # Color theme management
|
|
312
324
|
│ ├── ui/ # Modular UI package
|
|
313
325
|
│ ├── ansi.py # ANSI escape helpers (colors, cursor)
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Complete implementation of the French card game Belote for the terminal, with a full-screen green felt table and full card graphics at compass positions (N/W/E/S).
|
|
4
4
|
|
|
5
|
+
## What's new in 3.3.4
|
|
6
|
+
|
|
7
|
+
- **Portability fix** — Removed all terminal-bell / sound code, which was triggering SIGSYS ("Bad system call") on Alpine 23 (musl libc) the moment the first trick completed in classic Belote mode. BelAtro mode and every glibc-based distro (Kubuntu / Lubuntu 24.10 / 25.10) were unaffected, but rather than guard the BEL writes behind a libc check, the entire sound subsystem is gone — `play_sound`, `AudioManager` / `AUDIO`, `is_muted` / `toggle_mute`, the `[M]` mute key, and the help-screen mute line. Classic Belote and BelAtro now share the same "no bells" baseline.
|
|
8
|
+
- **Test coverage** — Still 549 tests. Strict gates clean: pytest 549/549, mypy 0 errors, ruff 0 violations.
|
|
9
|
+
|
|
10
|
+
## What's new in 3.3.3
|
|
11
|
+
|
|
12
|
+
- **Determinism** — Boss assignment in BelAtro mode now draws from the run's seeded RNG instead of the module-level `random`. This was the last unseeded RNG site in the round flow (shop and tarots were converted in 3.2.0, AI in 3.3.1, replay analysis in 3.3.2). Same seed now reproduces the same boss on the boss blind.
|
|
13
|
+
- **UI** — Hands sort by the trump-rank ladder under Tout Atout (Jack first, then 9 / A / 10 / K / Q / 8 / 7). Pre-3.3.3 the predicate `c.suit == trump` was always false under TA because Card.suit is never `Suit.TOUT_ATOUT`, so the South hand displayed in the non-trump order whenever the player bid TA.
|
|
14
|
+
- **Tarot** — Le Jugement now correctly grants only Common jokers as advertised. Pre-3.3.3 the code drew from the full unlocked pool, so late-run players with Rare/Legendary unlocks could roll out-of-rarity jokers off this tarot.
|
|
15
|
+
- **Test moat** — Three new invariant test suites added: scoring-conservation property (`table_taker + table_defender == 162 / 258 / 130` for normal / Tout Atout / Sans Atout), seeded-round replay determinism (same seed → same card sequence → same final state), and HUD synergy-badge negative test (solo half of a pair must not fire the badge). These would have caught most of the 3.3.x bug cycle from below.
|
|
16
|
+
- **Audit reconciliation** — A fresh three-agent codebase pass surfaced ~50 candidate findings; the three above held up under verification and the rest are catalogued in `CHANGELOG.md` so they aren't re-investigated.
|
|
17
|
+
- **Test coverage** — 549 tests (up from 537). Strict gates still clean: pytest 549/549, mypy 0 errors (76 files), ruff 0 violations.
|
|
18
|
+
|
|
5
19
|
## What's new in 3.3.1
|
|
6
20
|
|
|
7
21
|
- **Scoring correctness under bosses** — La Rupture (`no_consecutive_team_wins`) used to be applied to the live HUD but silently ignored by `score_round`, so the running total and final score diverged and impossible capots could be reported. L'Anarchie (dynamic trump) rotated `state.trump` mid-round, after which scoring's `belote_holders.get(trump)` lookup missed any Belote announced on the original trump and silently zeroed the 20/40 bonus. Both are fixed: a new `compute_trick_winners` helper is the single source of truth for La Rupture-aware winner resolution, and a new `belote_announcer: Seat` field captures the announcing seat at declaration time so the rotated trump no longer matters.
|
|
@@ -191,7 +205,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
191
205
|
|
|
192
206
|
**General:**
|
|
193
207
|
- `?`: Show keyboard shortcut help
|
|
194
|
-
- `M`: Toggle sound effects on/off
|
|
195
208
|
- `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
196
209
|
- `Q`: Quit to main menu or exit
|
|
197
210
|
- `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
|
|
@@ -232,7 +245,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
232
245
|
- **Statistics:** unified global tracking of games played, win rates, best rounds, and BelAtro expansion milestones.
|
|
233
246
|
- **Responsive Layout (3 tiers):** Three preset layouts — **compact** (80×32, fits 1366×768), **standard** (96×38), **spacious** (120×48+). The game picks the largest preset that fits your terminal on every render, so resizing mid-game adapts automatically; cards, side columns, and HUD verbosity all scale with the preset. Vertical centering pads tall terminals so the game never clings to the top.
|
|
234
247
|
- **Alternate Screen Buffer:** Both classic Belote and BelAtro run in a dedicated terminal buffer for a clean, non-overlapping interface — your shell scrollback stays untouched after you quit.
|
|
235
|
-
- **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
|
|
236
248
|
- **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
|
|
237
249
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
238
250
|
- **High Fidelity:** Implementation of French Belote rules according to the [official rules of the Fédération Française de Belote](https://www.ffbelote.org/regles-officielle-belote/), including a two-round bidding system, "Dix de Der", "Capot" (252 pts), and "Litige" (tie-break). All six contracts are bidable in round 2: the four card suits, **Tout Atout** (every suit acts as trump within its own led-suit group; press `a`), and **Sans Atout** (no trump, lead-suit highest wins; press `s`).
|
|
@@ -264,7 +276,7 @@ belote/
|
|
|
264
276
|
│ ├── scoring.py # Declarations, round scoring, capot
|
|
265
277
|
│ ├── ai.py # Three-tier AI (easy/medium/hard)
|
|
266
278
|
│ ├── config.py # Global configuration and timings
|
|
267
|
-
│ ├── context.py # Global managers (
|
|
279
|
+
│ ├── context.py # Global managers (Terminal)
|
|
268
280
|
│ ├── themes.py # Color theme management
|
|
269
281
|
│ ├── ui/ # Modular UI package
|
|
270
282
|
│ ├── ansi.py # ANSI escape helpers (colors, cursor)
|
|
@@ -32,9 +32,17 @@ class LeJugement(Tarot):
|
|
|
32
32
|
description = "Instantly gain a random Common Joker."
|
|
33
33
|
|
|
34
34
|
def use(self, run: BelAtroRun, context: object) -> None:
|
|
35
|
+
from .base import Rarity
|
|
35
36
|
from .registry import registry
|
|
36
37
|
run.last_tarot_message = None
|
|
37
|
-
|
|
38
|
+
# Description promises a Common joker — pre-3.3.3 the pool was the
|
|
39
|
+
# full unlocked set, so late-run players could roll Rare/Legendary
|
|
40
|
+
# off this tarot and mis-price it. Filter to Rarity.COMMON only.
|
|
41
|
+
avail = {
|
|
42
|
+
k: v
|
|
43
|
+
for k, v in registry.get_available_jokers(run.profile).items()
|
|
44
|
+
if getattr(v, "rarity", Rarity.COMMON) == Rarity.COMMON
|
|
45
|
+
}
|
|
38
46
|
if not avail:
|
|
39
47
|
run.last_tarot_message = "Le Jugement: no jokers available to grant."
|
|
40
48
|
return
|
|
@@ -270,11 +270,13 @@ class BelAtroGame:
|
|
|
270
270
|
if self.run.card_enhancements.pop("disable_next_boss", False):
|
|
271
271
|
pass # boss stays None; deliberately skip the reveal animation
|
|
272
272
|
else:
|
|
273
|
-
import random
|
|
274
|
-
|
|
275
273
|
from .run.boss import ALL_BOSS_MODIFIERS
|
|
276
274
|
|
|
277
|
-
|
|
275
|
+
# Use the run's seeded RNG, not the module-level random — same
|
|
276
|
+
# determinism fix the 3.2.0 release applied to shop generation
|
|
277
|
+
# and the three RNG-using tarots. Boss assignment was the last
|
|
278
|
+
# unseeded RNG site in the BelAtro round flow.
|
|
279
|
+
boss_cls = self.run._get_rng().choice(ALL_BOSS_MODIFIERS)
|
|
278
280
|
boss = boss_cls()
|
|
279
281
|
BelAtroAnnounce.boss_reveal(boss, self.reader)
|
|
280
282
|
|
|
@@ -4,18 +4,6 @@ import shutil
|
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
class AudioManager:
|
|
8
|
-
def __init__(self) -> None:
|
|
9
|
-
self.muted = False
|
|
10
|
-
|
|
11
|
-
def toggle_mute(self) -> bool:
|
|
12
|
-
self.muted = not self.muted
|
|
13
|
-
return self.muted
|
|
14
|
-
|
|
15
|
-
def is_muted(self) -> bool:
|
|
16
|
-
return self.muted
|
|
17
|
-
|
|
18
|
-
|
|
19
7
|
class TerminalContext:
|
|
20
8
|
def __init__(self) -> None:
|
|
21
9
|
self._size_cache: tuple[int, int] | None = None
|
|
@@ -30,6 +18,4 @@ class TerminalContext:
|
|
|
30
18
|
self._size_cache = None
|
|
31
19
|
|
|
32
20
|
|
|
33
|
-
# Global instances
|
|
34
|
-
AUDIO = AudioManager()
|
|
35
21
|
TERMINAL = TerminalContext()
|
|
@@ -1059,21 +1059,27 @@ def _build_suit_idx(trump: Suit | None) -> dict[Suit, int]:
|
|
|
1059
1059
|
|
|
1060
1060
|
|
|
1061
1061
|
# Pre-compute suit→position maps for every possible trump value (None + the
|
|
1062
|
-
# four card suits). sort_hand is called frequently during
|
|
1063
|
-
# keeps the hot path branch-free.
|
|
1062
|
+
# four card suits + TOUT_ATOUT). sort_hand is called frequently during
|
|
1063
|
+
# rendering and this keeps the hot path branch-free.
|
|
1064
1064
|
_SUIT_IDX_CACHE: Final[dict[Suit | None, dict[Suit, int]]] = {
|
|
1065
|
-
trump: _build_suit_idx(trump) for trump in (None, *_SUITS_ORDER)
|
|
1065
|
+
trump: _build_suit_idx(trump) for trump in (None, Suit.TOUT_ATOUT, *_SUITS_ORDER)
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
1068
1068
|
|
|
1069
1069
|
def sort_hand(hand: tuple[Card, ...], trump: Suit | None) -> tuple[Card, ...]:
|
|
1070
1070
|
"""Sort hand by suit and rank (trump first, then others, honors together)."""
|
|
1071
1071
|
suit_idx = _SUIT_IDX_CACHE.get(trump) or _build_suit_idx(trump)
|
|
1072
|
+
# Under Tout Atout every card is trump, so the trump-rank ladder applies
|
|
1073
|
+
# to *every* suit. `c.suit == trump` would always be False here because
|
|
1074
|
+
# Card.suit is one of SPADES/HEARTS/DIAMONDS/CLUBS — TOUT_ATOUT is a
|
|
1075
|
+
# contract-level marker, never a card suit.
|
|
1076
|
+
all_trump = trump is Suit.TOUT_ATOUT
|
|
1072
1077
|
|
|
1073
1078
|
def sort_key(c: Card) -> tuple[int, int]:
|
|
1079
|
+
is_trump = all_trump or c.suit == trump
|
|
1074
1080
|
return (
|
|
1075
1081
|
suit_idx[c.suit],
|
|
1076
|
-
_TRUMP_RANK_IDX[c.rank] if
|
|
1082
|
+
_TRUMP_RANK_IDX[c.rank] if is_trump else _NORMAL_RANK_IDX[c.rank],
|
|
1077
1083
|
)
|
|
1078
1084
|
|
|
1079
1085
|
return tuple(sorted(hand, key=sort_key))
|
|
@@ -46,7 +46,6 @@ from .ui import (
|
|
|
46
46
|
announce,
|
|
47
47
|
display,
|
|
48
48
|
patch_trick_card,
|
|
49
|
-
play_sound,
|
|
50
49
|
prompt_bid,
|
|
51
50
|
prompt_card,
|
|
52
51
|
)
|
|
@@ -187,7 +186,6 @@ def run_play(
|
|
|
187
186
|
|
|
188
187
|
# 3. If this completes a trick, pause longer and show announcements
|
|
189
188
|
if len(display_state.current_trick) == 4:
|
|
190
|
-
play_sound("trick")
|
|
191
189
|
# Non-skippable minimum dwell so all four cards are always visible
|
|
192
190
|
# before the trick clears, even when the user has skipped earlier
|
|
193
191
|
# animations or is on the "instant" speed preset.
|
|
@@ -215,7 +213,6 @@ def run_play(
|
|
|
215
213
|
)
|
|
216
214
|
is not None
|
|
217
215
|
):
|
|
218
|
-
play_sound("capot")
|
|
219
216
|
announce(
|
|
220
217
|
"CAPOT!", duration=trick_pause * 1.2 if not skip_anims else 0, reader=reader
|
|
221
218
|
)
|
|
@@ -228,7 +225,6 @@ def run_play(
|
|
|
228
225
|
if len(display_state.current_trick) == 4 and len(current.completed_tricks) == 0:
|
|
229
226
|
for decl in current.declarations:
|
|
230
227
|
if decl.kind in ("sequence", "carre"):
|
|
231
|
-
play_sound("declaration")
|
|
232
228
|
msg = f"{decl.seat.name}: {decl.kind.upper()}"
|
|
233
229
|
if decl.kind == "sequence":
|
|
234
230
|
# Sequence length (3=tierce, 4=quarte, 5=quinte)
|
|
@@ -264,8 +260,6 @@ def run_play(
|
|
|
264
260
|
a11y.announce_trick_won(current.last_trick_winner, pts)
|
|
265
261
|
|
|
266
262
|
if current.announced:
|
|
267
|
-
if "Belote" in current.announced:
|
|
268
|
-
play_sound("belote")
|
|
269
263
|
announce(
|
|
270
264
|
current.announced,
|
|
271
265
|
duration=max(0.5, trick_pause * 0.6) if not skip_anims else 0,
|
|
@@ -377,8 +371,6 @@ def run_round(
|
|
|
377
371
|
# Scoring Phase
|
|
378
372
|
if current.phase == Phase.SCORING:
|
|
379
373
|
breakdown = score_round(current)
|
|
380
|
-
if breakdown.is_failed:
|
|
381
|
-
play_sound("chute")
|
|
382
374
|
display(current, None)
|
|
383
375
|
sys.stdout.write(f"\r\n{'=' * 50}\r\n")
|
|
384
376
|
sys.stdout.write(" Round Results:\r\n")
|
|
@@ -24,7 +24,6 @@ class Key(Enum):
|
|
|
24
24
|
QUIT = "QUIT"
|
|
25
25
|
HELP = "HELP"
|
|
26
26
|
SORT = "SORT"
|
|
27
|
-
MUTE = "MUTE"
|
|
28
27
|
THEME = "THEME"
|
|
29
28
|
HIST = "HIST"
|
|
30
29
|
OVERLAY = "OVERLAY"
|
|
@@ -151,8 +150,6 @@ class _UnixKeyReader:
|
|
|
151
150
|
return KeyEvent(Key.THEME)
|
|
152
151
|
if ch.lower() == "o":
|
|
153
152
|
return KeyEvent(Key.SORT)
|
|
154
|
-
if ch.lower() == "m":
|
|
155
|
-
return KeyEvent(Key.MUTE)
|
|
156
153
|
if ch.lower() == "i" or ch.lower() == "v":
|
|
157
154
|
return KeyEvent(Key.OVERLAY)
|
|
158
155
|
|
|
@@ -272,8 +269,6 @@ if os.name == "nt":
|
|
|
272
269
|
return KeyEvent(Key.THEME)
|
|
273
270
|
if ch.lower() == b"o":
|
|
274
271
|
return KeyEvent(Key.SORT)
|
|
275
|
-
if ch.lower() == b"m":
|
|
276
|
-
return KeyEvent(Key.MUTE)
|
|
277
272
|
if ch.lower() in (b"i", b"v"):
|
|
278
273
|
return KeyEvent(Key.OVERLAY)
|
|
279
274
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .announce import animate_score_update, announce,
|
|
1
|
+
from .announce import animate_score_update, announce, show_stats
|
|
2
2
|
from .menu import show_ai_config, show_final_screen, show_main_menu
|
|
3
3
|
from .prompts import prompt_bid, prompt_card, show_help, show_history, show_rules
|
|
4
4
|
from .render import display, get_term_size, patch_trick_card, render
|
|
@@ -17,9 +17,6 @@ __all__ = [
|
|
|
17
17
|
"show_ai_config",
|
|
18
18
|
"show_final_screen",
|
|
19
19
|
"announce",
|
|
20
|
-
"play_sound",
|
|
21
|
-
"toggle_mute",
|
|
22
20
|
"show_stats",
|
|
23
21
|
"animate_score_update",
|
|
24
|
-
"is_muted",
|
|
25
22
|
]
|
|
@@ -19,21 +19,12 @@ from ..ansi import (
|
|
|
19
19
|
move,
|
|
20
20
|
white_fg,
|
|
21
21
|
)
|
|
22
|
-
from ..context import AUDIO
|
|
23
22
|
from ..game import GameState
|
|
24
23
|
from ..input import KeyReader, interruptible_sleep
|
|
25
24
|
from ..stats import get_session_stats, load_stats
|
|
26
25
|
from .render import display_hud, get_term_size
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
def is_muted() -> bool:
|
|
30
|
-
return AUDIO.is_muted()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def toggle_mute() -> bool:
|
|
34
|
-
return AUDIO.toggle_mute()
|
|
35
|
-
|
|
36
|
-
|
|
37
28
|
def announce(message: str, duration: float = 2.0, reader: KeyReader | None = None) -> None:
|
|
38
29
|
"""Display a transient announcement banner.
|
|
39
30
|
|
|
@@ -55,29 +46,6 @@ def announce(message: str, duration: float = 2.0, reader: KeyReader | None = Non
|
|
|
55
46
|
time.sleep(duration)
|
|
56
47
|
|
|
57
48
|
|
|
58
|
-
def play_sound(kind: str) -> None:
|
|
59
|
-
"""Enhanced terminal sounds using frequency tones (where supported) or bells."""
|
|
60
|
-
if AUDIO.is_muted():
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
# Use XTerm OSC 777 or simple bells for now to keep it cross-terminal
|
|
64
|
-
if kind == "trick":
|
|
65
|
-
sys.stdout.write("\a")
|
|
66
|
-
elif kind == "belote":
|
|
67
|
-
sys.stdout.write("\a\a")
|
|
68
|
-
elif kind == "declaration":
|
|
69
|
-
sys.stdout.write("\a")
|
|
70
|
-
elif kind == "chute":
|
|
71
|
-
sys.stdout.write("\a\a\a")
|
|
72
|
-
elif kind == "capot":
|
|
73
|
-
# Arpeggio-like bell sequence
|
|
74
|
-
for _ in range(3):
|
|
75
|
-
sys.stdout.write("\a")
|
|
76
|
-
sys.stdout.flush()
|
|
77
|
-
time.sleep(0.1)
|
|
78
|
-
sys.stdout.flush()
|
|
79
|
-
|
|
80
|
-
|
|
81
49
|
def show_stats(reader: KeyReader) -> None:
|
|
82
50
|
"""Display global game statistics."""
|
|
83
51
|
stats = load_stats()
|
|
@@ -19,7 +19,6 @@ from ..ansi import (
|
|
|
19
19
|
from ..game import GameState, Seat
|
|
20
20
|
from ..input import Key, KeyReader
|
|
21
21
|
from ..themes import THEMES, theme_manager
|
|
22
|
-
from .announce import toggle_mute
|
|
23
22
|
from .prompts import show_help
|
|
24
23
|
from .render import get_term_size
|
|
25
24
|
|
|
@@ -195,8 +194,6 @@ def show_ai_config(reader: KeyReader, current_diffs: dict[Seat, str]) -> dict[Se
|
|
|
195
194
|
return current_diffs
|
|
196
195
|
case Key.HELP:
|
|
197
196
|
show_help(reader)
|
|
198
|
-
case Key.MUTE:
|
|
199
|
-
toggle_mute()
|
|
200
197
|
case Key.UP:
|
|
201
198
|
sel = (sel - 1) % len(seats)
|
|
202
199
|
case Key.DOWN:
|
|
@@ -279,8 +276,6 @@ def show_main_menu(
|
|
|
279
276
|
return "Quit", curr_diffs, curr_target, curr_speed
|
|
280
277
|
case Key.HELP:
|
|
281
278
|
show_help(reader)
|
|
282
|
-
case Key.MUTE:
|
|
283
|
-
toggle_mute()
|
|
284
279
|
case Key.THEME:
|
|
285
280
|
themes_list = list(THEMES.keys())
|
|
286
281
|
curr_theme = theme_manager.current_name
|
|
@@ -11,7 +11,6 @@ from ..ansi import (
|
|
|
11
11
|
ansi_center,
|
|
12
12
|
clear_screen,
|
|
13
13
|
gold_fg,
|
|
14
|
-
green_fg,
|
|
15
14
|
hide_cursor,
|
|
16
15
|
red_fg,
|
|
17
16
|
visible_len,
|
|
@@ -28,7 +27,6 @@ from ..game import (
|
|
|
28
27
|
from ..input import Key, KeyReader
|
|
29
28
|
from ..rules import RULES_CONTENT, RulesPage
|
|
30
29
|
from ..themes import THEMES, theme_manager
|
|
31
|
-
from .announce import is_muted, toggle_mute # Need to implement these or import correctly
|
|
32
30
|
from .render import display, get_term_size
|
|
33
31
|
|
|
34
32
|
|
|
@@ -91,9 +89,6 @@ def prompt_card(
|
|
|
91
89
|
# Re-find selection index
|
|
92
90
|
sel = next((i for i, c in enumerate(hand) if c == selected_card), 0)
|
|
93
91
|
continue
|
|
94
|
-
case Key.MUTE:
|
|
95
|
-
toggle_mute()
|
|
96
|
-
continue
|
|
97
92
|
case Key.THEME:
|
|
98
93
|
themes_list = list(THEMES.keys())
|
|
99
94
|
curr_theme = theme_manager.current_name
|
|
@@ -156,9 +151,6 @@ def prompt_bid(state: GameState, reader: KeyReader) -> Suit | str | None:
|
|
|
156
151
|
case Key.HELP:
|
|
157
152
|
show_help(reader)
|
|
158
153
|
continue
|
|
159
|
-
case Key.MUTE:
|
|
160
|
-
toggle_mute()
|
|
161
|
-
continue
|
|
162
154
|
case Key.THEME:
|
|
163
155
|
themes_list = list(THEMES.keys())
|
|
164
156
|
curr_theme = theme_manager.current_name
|
|
@@ -195,7 +187,6 @@ def prompt_bid(state: GameState, reader: KeyReader) -> Suit | str | None:
|
|
|
195
187
|
def show_help(reader: KeyReader) -> None:
|
|
196
188
|
"""Display a quick keyboard shortcut reference."""
|
|
197
189
|
term_w, term_h = get_term_size()
|
|
198
|
-
sound_status = f"{red_fg()}OFF{RESET}" if is_muted() else f"{green_fg()}ON{RESET}"
|
|
199
190
|
|
|
200
191
|
lines = [
|
|
201
192
|
f"{BOLD}{gold_fg()}KEYBOARD SHORTCUTS{RESET}",
|
|
@@ -204,8 +195,6 @@ def show_help(reader: KeyReader) -> None:
|
|
|
204
195
|
f"{white_fg()}General:{RESET}",
|
|
205
196
|
" [?] Show this help screen",
|
|
206
197
|
" [Q] Quit to menu / Exit",
|
|
207
|
-
" [M] Toggle Sound Effects",
|
|
208
|
-
f" (Currently: {sound_status})",
|
|
209
198
|
" [T] Cycle Theme",
|
|
210
199
|
" [Esc] Cancel / Back",
|
|
211
200
|
"",
|
|
@@ -295,8 +284,6 @@ def show_rules(reader: KeyReader) -> None:
|
|
|
295
284
|
return
|
|
296
285
|
case Key.HELP:
|
|
297
286
|
show_help(reader)
|
|
298
|
-
case Key.MUTE:
|
|
299
|
-
toggle_mute()
|
|
300
287
|
case Key.UP:
|
|
301
288
|
scroll = max(0, scroll - 1)
|
|
302
289
|
case Key.DOWN:
|