belote-cli 3.3.3__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.
Files changed (119) hide show
  1. {belote_cli-3.3.3 → belote_cli-3.3.4}/CHANGELOG.md +25 -0
  2. {belote_cli-3.3.3 → belote_cli-3.3.4}/DEVELOPMENT.md +3 -3
  3. {belote_cli-3.3.3 → belote_cli-3.3.4}/PKG-INFO +7 -4
  4. {belote_cli-3.3.3 → belote_cli-3.3.4}/README.md +6 -3
  5. {belote_cli-3.3.3 → belote_cli-3.3.4}/pyproject.toml +1 -1
  6. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/__init__.py +1 -1
  7. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/context.py +0 -14
  8. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/gameflow.py +0 -8
  9. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/input.py +0 -5
  10. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ui/__init__.py +1 -4
  11. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ui/announce.py +0 -32
  12. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ui/menu.py +0 -5
  13. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ui/prompts.py +0 -13
  14. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_gameflow.py +0 -1
  15. {belote_cli-3.3.3 → belote_cli-3.3.4}/.claude/settings.local.json +0 -0
  16. {belote_cli-3.3.3 → belote_cli-3.3.4}/.gitignore +0 -0
  17. {belote_cli-3.3.3 → belote_cli-3.3.4}/.python-version +0 -0
  18. {belote_cli-3.3.3 → belote_cli-3.3.4}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
  19. {belote_cli-3.3.3 → belote_cli-3.3.4}/LICENSE +0 -0
  20. {belote_cli-3.3.3 → belote_cli-3.3.4}/scripts/benchmark.py +0 -0
  21. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/__init__.py +0 -0
  22. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/a11y.py +0 -0
  23. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/achievements.py +0 -0
  24. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ai.py +0 -0
  25. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ansi.py +0 -0
  26. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/__init__.py +0 -0
  27. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/core/__init__.py +0 -0
  28. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/core/economy.py +0 -0
  29. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/core/run_state.py +0 -0
  30. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/core/scoring.py +0 -0
  31. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/engine/__init__.py +0 -0
  32. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/engine/event_bus.py +0 -0
  33. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/engine/modifier_patch.py +0 -0
  34. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/engine/round_driver.py +0 -0
  35. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ghost_run.py +0 -0
  36. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/__init__.py +0 -0
  37. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/base.py +0 -0
  38. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/__init__.py +0 -0
  39. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/annonces.py +0 -0
  40. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/coinche.py +0 -0
  41. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/contract.py +0 -0
  42. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/corrupted.py +0 -0
  43. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/economy.py +0 -0
  44. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
  45. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
  46. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
  47. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
  48. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
  49. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
  50. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/planets.py +0 -0
  51. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/registry.py +0 -0
  52. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/tarots.py +0 -0
  53. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/items/vouchers.py +0 -0
  54. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/main.py +0 -0
  55. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/partner/__init__.py +0 -0
  56. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/partner/partner_state.py +0 -0
  57. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/partner/personality.py +0 -0
  58. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/partner/trust.py +0 -0
  59. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/progression/__init__.py +0 -0
  60. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/progression/save.py +0 -0
  61. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/progression/unlocks.py +0 -0
  62. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/run/__init__.py +0 -0
  63. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/run/ante.py +0 -0
  64. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/run/ante_themes.py +0 -0
  65. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/run/boss.py +0 -0
  66. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/run/decks.py +0 -0
  67. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/run/shop.py +0 -0
  68. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/run_summary.py +0 -0
  69. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/__init__.py +0 -0
  70. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/announce.py +0 -0
  71. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/collection.py +0 -0
  72. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/history.py +0 -0
  73. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/hud.py +0 -0
  74. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/menu.py +0 -0
  75. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/rules.py +0 -0
  76. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/shop.py +0 -0
  77. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/belatro/ui/trust_bar.py +0 -0
  78. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/config.py +0 -0
  79. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/deck.py +0 -0
  80. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/game.py +0 -0
  81. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/main.py +0 -0
  82. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/replay.py +0 -0
  83. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/rules.py +0 -0
  84. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/scoring.py +0 -0
  85. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/stats.py +0 -0
  86. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/themes.py +0 -0
  87. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ui/layout.py +0 -0
  88. {belote_cli-3.3.3 → belote_cli-3.3.4}/src/belote/ui/render.py +0 -0
  89. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/__init__.py +0 -0
  90. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/__init__.py +0 -0
  91. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_belatro.py +0 -0
  92. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_boss_modifiers_integration.py +0 -0
  93. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_collection_logic.py +0 -0
  94. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_contract_unlocks.py +0 -0
  95. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_dead_flag_fixes.py +0 -0
  96. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_deck_variants.py +0 -0
  97. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_ghost_run.py +0 -0
  98. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_history_overlay.py +0 -0
  99. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_hud_synergy.py +0 -0
  100. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_partner_trust.py +0 -0
  101. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_phase0_coverage.py +0 -0
  102. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_phase1_plumbing.py +0 -0
  103. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_phase2_content.py +0 -0
  104. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_phase3_meta.py +0 -0
  105. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_progression.py +0 -0
  106. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/belatro/test_round_driver.py +0 -0
  107. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_a11y.py +0 -0
  108. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_achievements.py +0 -0
  109. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_ai.py +0 -0
  110. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_ansi_helpers.py +0 -0
  111. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_belote.py +0 -0
  112. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_extended.py +0 -0
  113. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_game_logic.py +0 -0
  114. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_layout.py +0 -0
  115. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_new_coverage.py +0 -0
  116. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_official_rules.py +0 -0
  117. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_properties.py +0 -0
  118. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_replay.py +0 -0
  119. {belote_cli-3.3.3 → belote_cli-3.3.4}/tests/test_undo.py +0 -0
@@ -5,6 +5,31 @@ 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
+
8
33
  ## [3.3.3] - 2026-05-10
9
34
 
10
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`.
@@ -88,11 +88,11 @@ ruff check src/ tests/
88
88
  PYTHONPATH=src pytest
89
89
  ```
90
90
 
91
- Current baseline (3.3.3):
92
- - **mypy**: 0 errors (strict mode, 76 files)
91
+ Current baseline (3.3.4):
92
+ - **mypy**: 0 errors (strict mode, 75 files)
93
93
  - **ruff**: 0 violations
94
94
  - **pytest**: 549 tests, 0 failures
95
- - 3.3.3 covered: boss-RNG seeding, Tout Atout hand sort, Le Jugement Common-only filter, plus three new invariant suites (scoring conservation, replay round-trip, HUD synergy negative test).
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.
96
96
 
97
97
  Run all gates before committing:
98
98
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: belote-cli
3
- Version: 3.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,11 @@ 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
+
48
53
  ## What's new in 3.3.3
49
54
 
50
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.
@@ -243,7 +248,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
243
248
 
244
249
  **General:**
245
250
  - `?`: Show keyboard shortcut help
246
- - `M`: Toggle sound effects on/off
247
251
  - `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
248
252
  - `Q`: Quit to main menu or exit
249
253
  - `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
@@ -284,7 +288,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
284
288
  - **Statistics:** unified global tracking of games played, win rates, best rounds, and BelAtro expansion milestones.
285
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.
286
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.
287
- - **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
288
291
  - **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
289
292
  - **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
290
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`).
@@ -316,7 +319,7 @@ belote/
316
319
  │ ├── scoring.py # Declarations, round scoring, capot
317
320
  │ ├── ai.py # Three-tier AI (easy/medium/hard)
318
321
  │ ├── config.py # Global configuration and timings
319
- │ ├── context.py # Global managers (Audio, Terminal)
322
+ │ ├── context.py # Global managers (Terminal)
320
323
  │ ├── themes.py # Color theme management
321
324
  │ ├── ui/ # Modular UI package
322
325
  │ ├── ansi.py # ANSI escape helpers (colors, cursor)
@@ -2,6 +2,11 @@
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
+
5
10
  ## What's new in 3.3.3
6
11
 
7
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.
@@ -200,7 +205,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
200
205
 
201
206
  **General:**
202
207
  - `?`: Show keyboard shortcut help
203
- - `M`: Toggle sound effects on/off
204
208
  - `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
205
209
  - `Q`: Quit to main menu or exit
206
210
  - `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
@@ -241,7 +245,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
241
245
  - **Statistics:** unified global tracking of games played, win rates, best rounds, and BelAtro expansion milestones.
242
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.
243
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.
244
- - **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
245
248
  - **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
246
249
  - **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
247
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`).
@@ -273,7 +276,7 @@ belote/
273
276
  │ ├── scoring.py # Declarations, round scoring, capot
274
277
  │ ├── ai.py # Three-tier AI (easy/medium/hard)
275
278
  │ ├── config.py # Global configuration and timings
276
- │ ├── context.py # Global managers (Audio, Terminal)
279
+ │ ├── context.py # Global managers (Terminal)
277
280
  │ ├── themes.py # Color theme management
278
281
  │ ├── ui/ # Modular UI package
279
282
  │ ├── ansi.py # ANSI escape helpers (colors, cursor)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "belote-cli"
7
- version = "3.3.3"
7
+ version = "3.3.4"
8
8
  description = "A 4-player terminal card game"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
- __version__ = "3.3.3"
1
+ __version__ = "3.3.4"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -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()
@@ -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, is_muted, play_sound, show_stats, toggle_mute
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:
@@ -51,7 +51,6 @@ def test_run_play_8_tricks() -> None:
51
51
  with (
52
52
  unittest.mock.patch("belote.gameflow.display"),
53
53
  unittest.mock.patch("belote.gameflow.patch_trick_card"),
54
- unittest.mock.patch("belote.gameflow.play_sound"),
55
54
  unittest.mock.patch("belote.gameflow.announce"),
56
55
  unittest.mock.patch("belote.gameflow.prompt_card") as mock_prompt,
57
56
  ):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes