belote-cli 2.9.0__tar.gz → 2.9.5__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 (106) hide show
  1. {belote_cli-2.9.0 → belote_cli-2.9.5}/CHANGELOG.md +75 -0
  2. {belote_cli-2.9.0 → belote_cli-2.9.5}/PKG-INFO +33 -25
  3. {belote_cli-2.9.0 → belote_cli-2.9.5}/README.md +32 -24
  4. {belote_cli-2.9.0 → belote_cli-2.9.5}/pyproject.toml +1 -1
  5. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/__init__.py +1 -1
  6. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/game.py +8 -0
  7. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/gameflow.py +10 -0
  8. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/input.py +12 -8
  9. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/main.py +2 -2
  10. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/scoring.py +56 -8
  11. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ui/announce.py +15 -2
  12. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ui/menu.py +44 -34
  13. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ui/prompts.py +121 -95
  14. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ui/render.py +296 -68
  15. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_extended.py +44 -0
  16. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_layout.py +2 -2
  17. {belote_cli-2.9.0 → belote_cli-2.9.5}/.claude/settings.local.json +0 -0
  18. {belote_cli-2.9.0 → belote_cli-2.9.5}/.gitignore +0 -0
  19. {belote_cli-2.9.0 → belote_cli-2.9.5}/.python-version +0 -0
  20. {belote_cli-2.9.0 → belote_cli-2.9.5}/DEVELOPMENT.md +0 -0
  21. {belote_cli-2.9.0 → belote_cli-2.9.5}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
  22. {belote_cli-2.9.0 → belote_cli-2.9.5}/LICENSE +0 -0
  23. {belote_cli-2.9.0 → belote_cli-2.9.5}/scripts/benchmark.py +0 -0
  24. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/__init__.py +0 -0
  25. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ai.py +0 -0
  26. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ansi.py +0 -0
  27. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/__init__.py +0 -0
  28. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/core/__init__.py +0 -0
  29. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/core/economy.py +0 -0
  30. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/core/run_state.py +0 -0
  31. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/core/scoring.py +0 -0
  32. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/engine/__init__.py +0 -0
  33. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/engine/event_bus.py +0 -0
  34. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/engine/modifier_patch.py +0 -0
  35. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/engine/round_driver.py +0 -0
  36. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/__init__.py +0 -0
  37. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/base.py +0 -0
  38. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/__init__.py +0 -0
  39. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/annonces.py +0 -0
  40. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/coinche.py +0 -0
  41. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/contract.py +0 -0
  42. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/corrupted.py +0 -0
  43. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/economy.py +0 -0
  44. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
  45. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
  46. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
  47. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
  48. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
  49. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
  50. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/planets.py +0 -0
  51. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/registry.py +0 -0
  52. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/tarots.py +0 -0
  53. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/items/vouchers.py +0 -0
  54. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/main.py +0 -0
  55. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/partner/__init__.py +0 -0
  56. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/partner/partner_state.py +0 -0
  57. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/partner/personality.py +0 -0
  58. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/partner/trust.py +0 -0
  59. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/progression/__init__.py +0 -0
  60. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/progression/save.py +0 -0
  61. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/progression/unlocks.py +0 -0
  62. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/run/__init__.py +0 -0
  63. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/run/ante.py +0 -0
  64. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/run/ante_themes.py +0 -0
  65. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/run/boss.py +0 -0
  66. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/run/decks.py +0 -0
  67. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/run/shop.py +0 -0
  68. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/__init__.py +0 -0
  69. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/announce.py +0 -0
  70. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/collection.py +0 -0
  71. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/hud.py +0 -0
  72. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/menu.py +0 -0
  73. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/rules.py +0 -0
  74. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/shop.py +0 -0
  75. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/belatro/ui/trust_bar.py +0 -0
  76. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/config.py +0 -0
  77. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/context.py +0 -0
  78. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/deck.py +0 -0
  79. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/rules.py +0 -0
  80. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/stats.py +0 -0
  81. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/themes.py +0 -0
  82. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ui/__init__.py +0 -0
  83. {belote_cli-2.9.0 → belote_cli-2.9.5}/src/belote/ui/layout.py +0 -0
  84. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/__init__.py +0 -0
  85. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/__init__.py +0 -0
  86. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_belatro.py +0 -0
  87. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_boss_modifiers_integration.py +0 -0
  88. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_collection_logic.py +0 -0
  89. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_contract_unlocks.py +0 -0
  90. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_dead_flag_fixes.py +0 -0
  91. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_deck_variants.py +0 -0
  92. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_partner_trust.py +0 -0
  93. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_phase0_coverage.py +0 -0
  94. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_phase1_plumbing.py +0 -0
  95. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_phase2_content.py +0 -0
  96. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_phase3_meta.py +0 -0
  97. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_progression.py +0 -0
  98. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/belatro/test_round_driver.py +0 -0
  99. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_ai.py +0 -0
  100. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_belote.py +0 -0
  101. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_game_logic.py +0 -0
  102. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_gameflow.py +0 -0
  103. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_new_coverage.py +0 -0
  104. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_official_rules.py +0 -0
  105. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_properties.py +0 -0
  106. {belote_cli-2.9.0 → belote_cli-2.9.5}/tests/test_undo.py +0 -0
@@ -5,6 +5,81 @@ 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
+ ## [2.9.5] - 2026-05-07
9
+
10
+ In-game keyboard shortcuts cleaned up, the trick mat now anchors every played card inside a visible per-seat slot, the round history overlay carries the full per-round picture, and the cards have been redrawn in a GRIMAUD-1898 style with both-corner indices and patterned pip layouts.
11
+
12
+ ### Fixed
13
+
14
+ - **`src/belote/input.py`** — `Key.THEME` was defined but never mapped to a keystroke; the help text falsely advertised `Shift+T` (which terminals can't generally distinguish in raw mode). Theme cycling during gameplay was unreachable. New mapping: `t/T → Key.THEME`, `h/H → Key.HIST`, `?` → `Key.HELP`, `o/O → Key.SORT`. Both the Unix and Windows readers updated.
15
+ - **`src/belote/input.py`** — `s` was previously stolen by `Key.SORT`, so pressing `S` in round-2 bidding triggered a sort instead of the **Sans Atout** quick-bid the help text promised. Sort now lives on `O` (matching what the help screen always claimed) and `s` falls through to `Key.CHAR` so SA bidding works.
16
+ - **`src/belote/gameflow.py::run_play`** — when the user had already pressed any key earlier in the round (which sets `skip_anims`), the post-trick pause was skipped entirely and the 4th card vanished before it could be read. New `MIN_TRICK_DWELL = 0.5s` non-skippable hold runs after every completed trick (even on the `instant` speed preset) so the player always sees all four cards before the mat clears.
17
+
18
+ ### Added
19
+
20
+ - **`src/belote/ui/render.py`** — visible **slot frames** drawn around each compass position on the trick mat. Implemented via three new helpers (`_slot_anchors`, `_slot_frame_row`, `_felt_pad_ns`, `_we_row`) that paint thin `─`/`│` borders on the felt cells immediately surrounding each card slot, in the felt-placeholder dim colour. Total mat dimensions (`6 + 3*card_h`) are unchanged, so `_calculate_base_row` and `patch_trick_card` continue to work without any coordinate adjustments — patched cards land exactly inside the existing frame.
21
+ - **`src/belote/game.py::RoundScore`** — eight new optional fields (`contract`, `trump`, `taker_seat`, `tricks_ns`, `tricks_ew`, `last_trick_winner`, `decl_summary_ns`, `decl_summary_ew`) populated from `state` and `state.completed_tricks` at scoring time. All fields default so existing test fixtures and any historical `RoundScore` constructions remain valid.
22
+ - **`src/belote/scoring.py::apply_round_score`** — now computes per-team trick counts via `trick_winner_seat`/`team_of`, builds short declaration labels (`"100♥"`, `"Belote"`, `"Carré-J"`, …) gated on the team's `*_decl_pts > 0` so only the *scored* declarations appear, and threads everything into `RoundScore`. New helper `_decl_short_label` covers belote/rebelote/sequence/carre.
23
+ - **`src/belote/ui/prompts.py::show_history`** — rewritten as an 8-column table (`RD | TAKER | CONTRACT | TRICKS | DECLARATIONS | NS | EW | STATUS`) for terminals ≥78 cols, with a 2-line-per-round fallback for narrower terminals. Status colouring: gold `CAPOT`, red `CHUTE`, dim `LITIGE`. Existing scrolling, view-height clamp, and exit-on-any-key behaviour preserved.
24
+ - **`src/belote/ui/render.py::_card_face_internal`** — full GRIMAUD-1898-inspired redraw:
25
+ - Both corners now carry a 3-cell `rank+suit` index (`A♠` top-left, `♠A` bottom-right). The index padding scales with `inner_w`.
26
+ - Pip cards (7-10) at `card_h ≥ 7` get a recognisable pip arrangement instead of a single centred suit symbol.
27
+ - Court cards J/Q/K each get a distinct multi-row motif (sword, jewelled headdress, crown).
28
+ - Aces get a decorative `╭─◆─╮` / `╰─◆─╯` wreath around the central suit.
29
+ - Compact 6×5 layout keeps the single inner row but still benefits from both-corner indices.
30
+ - All variants honour the active theme (`face_card_bg`, `card_face_bg`, `highlight_bg`, `red_fg`/`black_fg`) and the `DIM` prefix for illegal cards. ASCII fallback paths preserved.
31
+ - **`tests/test_extended.py::test_round_score_history_extra_fields`** — pins the new `RoundScore` fields end-to-end through `apply_round_score`.
32
+
33
+ ### Changed
34
+
35
+ - **`src/belote/ui/menu.py`** — main-menu loop now handles `Key.THEME` (cycles forward through `THEMES`), so the new in-game `T` shortcut works at the menu too.
36
+ - **`src/belote/main.py`**, **`src/belote/ui/render.py`** — game-over hint and HUD compact hint updated to advertise the new `[H] History` / `[T] Theme` shortcuts.
37
+ - **`src/belote/ui/prompts.py::show_help`** — help-screen text rewritten to match the new bindings.
38
+ - **`README.md`** — Controls section rewritten; theme section now lists all six themes by name.
39
+ - **`tests/test_layout.py::test_hud_compact_omits_help_hints_and_theme`** — assertion updated for the new compact-HUD hint substring.
40
+
41
+ ### Notes
42
+
43
+ 436/436 tests pass. No gameplay, scoring, or AI-decision changes — all updates are UX (keys, slot framing, dwell, history depth, card glyphs).
44
+
45
+ ## [2.9.2] - 2026-05-07
46
+
47
+ Render-pipeline fix for Konsole (KDE/Kubuntu) and other strict ANSI terminals where UI elements visibly stacked on top of each other — the top HUD repeating ~6 times, "Theme: Sepia Vintage" duplicating in the right column, "Partner" doubling, the bid prompt repainting below itself, and bid history accumulating between frames. The bug existed in the code on every terminal but VTE-based emulators (LXTerminal, GNOME Terminal, xterm) auto-blanked the leaking cells, masking it. Konsole's Vt102Emulation does not, so the leakage was visible.
48
+
49
+ ### Fixed
50
+
51
+ - **`src/belote/ui/render.py::render`** — the bidding selector (Round 1 inline highlighted Take/Pass; Round 2 boxed grid; optional `partner_bid_tendency_text` line) is now painted inside the main render frame via the new `bid_selection: int | None` parameter on `render()` and `display()`. Previously `prompt_bid` paid display() and then wrote 8+ extra `\r\n` lines, which scrolled the alt-screen on the bottom row and left stale rows that the next frame's blank padding never repainted.
52
+ - **`src/belote/ui/render.py::render`** — every line in the rendered frame now ends with `clear_to_eol()` (`\x1b[K`), including blank vertical-centering padding rows. The previous `line + clear_to_eol() if line else line` branch skipped the escape on padding rows, betting the previous frame's content area had already cleared them — but that bet broke whenever an external write (announcement, prompt artifact) deposited debris on a padding row. Cost is one 3-byte escape per row; benefit is correct rendering on any ANSI-compliant terminal regardless of strictness.
53
+ - **`src/belote/ui/prompts.py::prompt_bid`** — gutted of all post-`display()` `sys.stdout.write` calls. Each loop iteration is now a single `display(state, None, bid_selection=sel)` + `reader.read()`. Removed unused `REVERSE` and `black_fg` imports (the box-rendering code that consumed them moved to `render.py::_build_bid_prompt_lines`).
54
+ - **`src/belote/ui/announce.py::announce`** — replaced the `\r\n`-bracketed banner with absolute cursor positioning (`move(term_h - 1, 1) + clear_line()`) so the banner can never trigger a scroll, even when the cursor is parked on the bottom row of the alt-screen.
55
+
56
+ ### Added
57
+
58
+ - **`src/belote/ui/render.py::_build_bid_prompt_lines`** — pure helper that returns the in-frame bidding selector lines. Encapsulates the Round 1 inline form, the Round 2 60-column boxed form, and the optional partner-tendency line so the main render loop reads as one coherent paint.
59
+
60
+ ### Notes
61
+
62
+ No gameplay, scoring, or AI changes. 435/435 tests pass. The fix is observable when running under Konsole — start a Belote round, force a Round 2 bid (pass on the up-card), and arrow-navigate the selector: previously the box stacked between iterations; now it repaints in place.
63
+
64
+ ## [2.9.1] - 2026-05-06
65
+
66
+ UI polish patch on top of 2.9.0. Two pieces of menu art had drifted; this release fixes both and tightens the menu plumbing so the cup walls hold for every label combination.
67
+
68
+ ### Fixed
69
+
70
+ - **`src/belote/ui/menu.py::get_cards_art`** — the croissant Braille had a corrupted line 8 (`⠘⢿⡿⠋⣠⣾⣿⣿⣿⠟⠁⣿⣿⣿⣿⣿⠟⢁⣀`, 21 cells) that broke the right curl, and was missing the 13th-line `⠉⠉⠉` crumb under the tip entirely. Restored to the canonical 13-line, 25-cell-wide reference. Each line now uses U+2800 Braille blanks for its leading indent (uniform 25-cell width) rather than mixed ASCII spaces, so callers don't have to re-pad.
71
+ - **`src/belote/ui/menu.py::CUP_TEMPLATE`** — body inner width was 47 chars (29-char opt + 16 trailing dead space + 2 gutter), much wider than the 23-char lid and saucer. Now 29 chars flush so menu options sit between the cup walls with no padding. Steam line 2 now correctly shows the two-puff frame (`) (`) — previously the template's leading-indent disagreement with `STEAMS[..][1]` pushed the second puff out of view.
72
+ - **`src/belote/ui/menu.py::_render_main_menu_art`** — selected-row markers tightened from ` > {label} < ` (6-char overhead) to `> {label} <` (4-char overhead) so the selected row uses the same width budget as unselected (` {label} `, also 4-char overhead). Without this, a selected `Theme: < Sepia Vintage >` would bust the right cup wall.
73
+ - **`src/belote/ui/menu.py::show_main_menu`** — settings labels shortened to fit the new 25-char usable label width: `AI Config:` → `AI:`, `Target Score:` → `Target:`. `Speed:` and `Theme:` keep their names with tightened spacing. All four `<` markers still column-align.
74
+
75
+ ### Changed
76
+
77
+ - **`src/belote/ui/menu.py::_render_main_menu_art`** — opt-slot loop and assertion bound dropped from 12 → 9 to match the new template. The error message ("add opt slots to CUP_TEMPLATE") still points at the right fix if the menu ever grows past 9 entries.
78
+
79
+ ### Notes
80
+
81
+ No gameplay or scoring changes; classic and BelAtro behavior is bit-for-bit identical. 435/435 tests pass; ruff and mypy clean on `src/`. Pre-existing lint/type debt in `tests/` and `scripts/benchmark.py` (18 ruff, 69 mypy strict-mode annotations) is untouched and tracked separately.
82
+
8
83
  ## [2.9.0] - 2026-05-06
9
84
 
10
85
  Audit-driven sweep on top of 2.8.0: four engine bugs fixed and the long-deferred Tout Atout / Sans Atout bidding affordance shipped end-to-end. The README's "future work" line for those contracts is gone — both contracts are now bidable in classic Belote and BelAtro, and the two jokers / two unlock counters that had been waiting on the affordance now fire in real play. Plan: `plans/bug-hunt-code-performance-elegant-starlight.md`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: belote-cli
3
- Version: 2.9.0
3
+ Version: 2.9.5
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
@@ -88,18 +88,19 @@ belatro
88
88
 
89
89
  ### Main Menu
90
90
  ```text
91
- ⢠⣴⣶⣶⣶⣄
92
- ⣿⣿⣿⣿⣿⣿⣦
93
- ⢰⣿⣿⣿⣿⡿⠟⠁⣠⣴⣶⣦⠄
94
- ⢸⣿⣿⠟⠉⣠⣴⣿⣿⣿⠟⠁⣠⣾⣿⣦⡀
95
- ⠉⣀⣴⣾⣿⣿⣿⠟⢁⣤⣾⣿⣿⣿⣿⣿⡆
96
- ⢀⣤⣾⣿⣿⣿⡿⠛⢁⣴⣿⣿⣿⡿⠛⢁⣴⣿⣿⣿⣿⣿⣿⣿⠟⠁⡀
97
- ⢼⣿⣿⣿⡿⠋⣀⣴⣿⣿⣿⣿⣿⣿⣿⡿⠉⣠⣾⣿⡆
98
- ⠘⢿⡿⠋⣠⣾⣿⣿⣿⠟⠁⣿⣿⣿⣿⣿⠟⢁⣀
99
- ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⠏⢀⣴⣿⣿⣿⠋⢠⣾⣿⣷⣦⡀
100
- ⢻⣿⣿⣿⣿⣿⣿⣿⠟⢁⣴⣿⣿⣿⡿⠁⣰⣿⣿⣿⣿⣿⣿
101
- ⠹⢿⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⠟⢀⣼⣿⣿⣿⣿⣿⣿⡟
102
- ⠉⠉⠉⠀⢾⣿⣿⣿⣿⠋⠀⠚⠛⠛⠛⠛⠛⠛⠁
91
+ ⢠⣴⣶⣶⣶⣄
92
+ ⣿⣿⣿⣿⣿⣿⣦
93
+ ⢰⣿⣿⣿⣿⡿⠟⠁⣠⣴⣶⣦⠄
94
+ ⢸⣿⣿⠟⠉⣠⣴⣿⣿⣿⠟⠁⣠⣾⣿⣦⡀
95
+ ⠉⣀⣴⣾⣿⣿⣿⠟⢁⣤⣾⣿⣿⣿⣿⣿⡆
96
+ ⢀⣤⣾⣿⣿⣿⡿⠛⢁⣴⣿⣿⣿⣿⣿⣿⣿⠟⠁⡀
97
+ ⢼⣿⣿⣿⡿⠋⣀⣴⣿⣿⣿⣿⣿⣿⣿⡿⠉⣠⣾⣿⡆
98
+ ⠘⢿⡿⠋⣠⣾⣿⣿⣿⣿⣿⣿⣿⡿⠋⢀⣾⣿⣿⠟⢁⣀
99
+ ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⠏⢀⣴⣿⣿⣿⠋⢠⣾⣿⣷⣦⡀
100
+ ⢻⣿⣿⣿⣿⣿⣿⣿⠟⢁⣴⣿⣿⣿⡿⠁⣰⣿⣿⣿⣿⣿⣿
101
+ ⠹⢿⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⠟⢀⣼⣿⣿⣿⣿⣿⣿⡟
102
+ ⠉⠉⠉ ⢾⣿⣿⣿⣿⠋ ⠚⠛⠛⠛⠛⠛⠛⠁
103
+ ⠉⠉⠉
103
104
 
104
105
  (
105
106
  ) (
@@ -107,12 +108,17 @@ belatro
107
108
  .-'' ) ( ''-.
108
109
  .-'``'|-._ ) _.-|
109
110
  / .--.| `''---...........---''` |
110
- / / | > Start Game < |
111
- | | | Difficulty: Medium |
112
- \ \ | Target Score: 1000 |
113
- `\ `\ | Speed: Normal |
114
- `\ `| Rules & History |
115
- _/ /\ Quit /
111
+ / / | BelAtro |
112
+ | | | > Start Game < |
113
+ | | | AI: < Hard > |
114
+ | | | Target: < 1000 > |
115
+ | | | Speed: < Normal > |
116
+ | | | Theme: < Classic Green > |
117
+ | | | Rules & History |
118
+ \ \ | Statistics |
119
+ `\ `\ | Quit |
120
+ `\ `| |
121
+ _/ /\ /
116
122
  (__/ \ /
117
123
  _..---''` \ /`''---.._
118
124
  .-' \ / '-.
@@ -164,12 +170,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
164
170
  ## Controls
165
171
 
166
172
  **General:**
167
- - `?` or `H`: Show keyboard shortcut help
173
+ - `?`: Show keyboard shortcut help
168
174
  - `M`: Toggle sound effects on/off
169
- - `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
175
+ - `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
170
176
  - `Q`: Quit to main menu or exit
171
- - `t`: View Game History (Round-by-round)
172
- - `T`: Switch UI Theme
177
+ - `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
178
+ - `T`: Cycle UI Theme
173
179
 
174
180
  **Classic Belote:**
175
181
  - `↑` `↓`: Navigate options
@@ -177,7 +183,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
177
183
  - `Enter`: Select option / Enter submenu
178
184
 
179
185
  **BelAtro (Roguelite):**
180
- - `S`: View current Run State and Jokers
181
186
  - `1`-`5`: Inspect specific Jokers in the Shop
182
187
  - `U`: Use a consumable (Tarot/Planet) during gameplay
183
188
 
@@ -188,6 +193,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
188
193
  - `O`: Sort hand by suit and rank
189
194
  - `Z`: Undo last move
190
195
  - `Space` or `Esc`: Skip animations
196
+ - During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
191
197
 
192
198
  ## Features
193
199
 
@@ -198,7 +204,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
198
204
  - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
199
205
  - **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
200
206
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
201
- - **Customizable Themes:** Switch between different color palettes (e.g., Classic Green, Dark Blue, Royal Purple) using the `T` key during gameplay.
207
+ - **Customizable Themes:** Switch between six color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast) using the `T` key during gameplay.
202
208
  - **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
203
209
  - **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
204
210
  - **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
@@ -289,3 +295,5 @@ This will wipe all global statistics and reset your discovered item Almanac in B
289
295
  ## Terminal Hygiene
290
296
 
291
297
  Signal handlers (SIGINT, SIGTERM) and atexit hooks ensure the terminal is always restored — cursor visible, colors reset, alt-screen off — even after Ctrl+C or crashes.
298
+
299
+ Every rendered row ends with `\x1b[K` (clear-to-end-of-line) and every interactive prompt (bid selector, card selector, full-screen overlays) repaints in a single in-frame pass — no `\r\n`-bracketed writes outside the render. This keeps the game free of stale-cell artifacts on strict ANSI emulators like Konsole (KDE), in addition to the more lenient VTE-based terminals (GNOME Terminal, LXTerminal, xterm).
@@ -45,18 +45,19 @@ belatro
45
45
 
46
46
  ### Main Menu
47
47
  ```text
48
- ⢠⣴⣶⣶⣶⣄
49
- ⣿⣿⣿⣿⣿⣿⣦
50
- ⢰⣿⣿⣿⣿⡿⠟⠁⣠⣴⣶⣦⠄
51
- ⢸⣿⣿⠟⠉⣠⣴⣿⣿⣿⠟⠁⣠⣾⣿⣦⡀
52
- ⠉⣀⣴⣾⣿⣿⣿⠟⢁⣤⣾⣿⣿⣿⣿⣿⡆
53
- ⢀⣤⣾⣿⣿⣿⡿⠛⢁⣴⣿⣿⣿⡿⠛⢁⣴⣿⣿⣿⣿⣿⣿⣿⠟⠁⡀
54
- ⢼⣿⣿⣿⡿⠋⣀⣴⣿⣿⣿⣿⣿⣿⣿⡿⠉⣠⣾⣿⡆
55
- ⠘⢿⡿⠋⣠⣾⣿⣿⣿⠟⠁⣿⣿⣿⣿⣿⠟⢁⣀
56
- ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⠏⢀⣴⣿⣿⣿⠋⢠⣾⣿⣷⣦⡀
57
- ⢻⣿⣿⣿⣿⣿⣿⣿⠟⢁⣴⣿⣿⣿⡿⠁⣰⣿⣿⣿⣿⣿⣿
58
- ⠹⢿⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⠟⢀⣼⣿⣿⣿⣿⣿⣿⡟
59
- ⠉⠉⠉⠀⢾⣿⣿⣿⣿⠋⠀⠚⠛⠛⠛⠛⠛⠛⠁
48
+ ⢠⣴⣶⣶⣶⣄
49
+ ⣿⣿⣿⣿⣿⣿⣦
50
+ ⢰⣿⣿⣿⣿⡿⠟⠁⣠⣴⣶⣦⠄
51
+ ⢸⣿⣿⠟⠉⣠⣴⣿⣿⣿⠟⠁⣠⣾⣿⣦⡀
52
+ ⠉⣀⣴⣾⣿⣿⣿⠟⢁⣤⣾⣿⣿⣿⣿⣿⡆
53
+ ⢀⣤⣾⣿⣿⣿⡿⠛⢁⣴⣿⣿⣿⣿⣿⣿⣿⠟⠁⡀
54
+ ⢼⣿⣿⣿⡿⠋⣀⣴⣿⣿⣿⣿⣿⣿⣿⡿⠉⣠⣾⣿⡆
55
+ ⠘⢿⡿⠋⣠⣾⣿⣿⣿⣿⣿⣿⣿⡿⠋⢀⣾⣿⣿⠟⢁⣀
56
+ ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⠏⢀⣴⣿⣿⣿⠋⢠⣾⣿⣷⣦⡀
57
+ ⢻⣿⣿⣿⣿⣿⣿⣿⠟⢁⣴⣿⣿⣿⡿⠁⣰⣿⣿⣿⣿⣿⣿
58
+ ⠹⢿⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⠟⢀⣼⣿⣿⣿⣿⣿⣿⡟
59
+ ⠉⠉⠉ ⢾⣿⣿⣿⣿⠋ ⠚⠛⠛⠛⠛⠛⠛⠁
60
+ ⠉⠉⠉
60
61
 
61
62
  (
62
63
  ) (
@@ -64,12 +65,17 @@ belatro
64
65
  .-'' ) ( ''-.
65
66
  .-'``'|-._ ) _.-|
66
67
  / .--.| `''---...........---''` |
67
- / / | > Start Game < |
68
- | | | Difficulty: Medium |
69
- \ \ | Target Score: 1000 |
70
- `\ `\ | Speed: Normal |
71
- `\ `| Rules & History |
72
- _/ /\ Quit /
68
+ / / | BelAtro |
69
+ | | | > Start Game < |
70
+ | | | AI: < Hard > |
71
+ | | | Target: < 1000 > |
72
+ | | | Speed: < Normal > |
73
+ | | | Theme: < Classic Green > |
74
+ | | | Rules & History |
75
+ \ \ | Statistics |
76
+ `\ `\ | Quit |
77
+ `\ `| |
78
+ _/ /\ /
73
79
  (__/ \ /
74
80
  _..---''` \ /`''---.._
75
81
  .-' \ / '-.
@@ -121,12 +127,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
121
127
  ## Controls
122
128
 
123
129
  **General:**
124
- - `?` or `H`: Show keyboard shortcut help
130
+ - `?`: Show keyboard shortcut help
125
131
  - `M`: Toggle sound effects on/off
126
- - `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
132
+ - `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
127
133
  - `Q`: Quit to main menu or exit
128
- - `t`: View Game History (Round-by-round)
129
- - `T`: Switch UI Theme
134
+ - `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
135
+ - `T`: Cycle UI Theme
130
136
 
131
137
  **Classic Belote:**
132
138
  - `↑` `↓`: Navigate options
@@ -134,7 +140,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
134
140
  - `Enter`: Select option / Enter submenu
135
141
 
136
142
  **BelAtro (Roguelite):**
137
- - `S`: View current Run State and Jokers
138
143
  - `1`-`5`: Inspect specific Jokers in the Shop
139
144
  - `U`: Use a consumable (Tarot/Planet) during gameplay
140
145
 
@@ -145,6 +150,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
145
150
  - `O`: Sort hand by suit and rank
146
151
  - `Z`: Undo last move
147
152
  - `Space` or `Esc`: Skip animations
153
+ - During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
148
154
 
149
155
  ## Features
150
156
 
@@ -155,7 +161,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
155
161
  - **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
156
162
  - **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
157
163
  - **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
158
- - **Customizable Themes:** Switch between different color palettes (e.g., Classic Green, Dark Blue, Royal Purple) using the `T` key during gameplay.
164
+ - **Customizable Themes:** Switch between six color palettes (Classic Green, Dark Mode, Blue Velvet, Red Casino, Sepia Vintage, High Contrast) using the `T` key during gameplay.
159
165
  - **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
160
166
  - **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
161
167
  - **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
@@ -246,3 +252,5 @@ This will wipe all global statistics and reset your discovered item Almanac in B
246
252
  ## Terminal Hygiene
247
253
 
248
254
  Signal handlers (SIGINT, SIGTERM) and atexit hooks ensure the terminal is always restored — cursor visible, colors reset, alt-screen off — even after Ctrl+C or crashes.
255
+
256
+ Every rendered row ends with `\x1b[K` (clear-to-end-of-line) and every interactive prompt (bid selector, card selector, full-screen overlays) repaints in a single in-frame pass — no `\r\n`-bracketed writes outside the render. This keeps the game free of stale-cell artifacts on strict ANSI emulators like Konsole (KDE), in addition to the more lenient VTE-based terminals (GNOME Terminal, LXTerminal, xterm).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "belote-cli"
7
- version = "2.9.0"
7
+ version = "2.9.5"
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__ = "2.8.0"
1
+ __version__ = "2.9.5"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -149,6 +149,14 @@ class RoundScore:
149
149
  is_capot: bool
150
150
  is_litige: bool = False
151
151
  litige_points: int = 0
152
+ contract: str | None = None
153
+ trump: Suit | None = None
154
+ taker_seat: Seat | None = None
155
+ tricks_ns: int = 0
156
+ tricks_ew: int = 0
157
+ last_trick_winner: Seat | None = None
158
+ decl_summary_ns: tuple[str, ...] = ()
159
+ decl_summary_ew: tuple[str, ...] = ()
152
160
 
153
161
 
154
162
  @dataclass(frozen=True, slots=True)
@@ -50,6 +50,12 @@ from .ui import (
50
50
  )
51
51
 
52
52
 
53
+ # Minimum time the four cards stay on the mat before a trick clears. This
54
+ # applies even when the user has skipped earlier animations (so a fast-paced
55
+ # session still lets the player read every completed trick).
56
+ MIN_TRICK_DWELL: float = 0.5
57
+
58
+
53
59
  def create_ai_players(diffs_map: dict[Seat, str]) -> dict[Seat, AIPlayer]:
54
60
  """Create AI players for seats not occupied by humans."""
55
61
  ai_seats = {Seat.EAST, Seat.NORTH, Seat.WEST}
@@ -177,6 +183,10 @@ def run_play(
177
183
  # 3. If this completes a trick, pause longer and show announcements
178
184
  if len(display_state.current_trick) == 4:
179
185
  play_sound("trick")
186
+ # Non-skippable minimum dwell so all four cards are always visible
187
+ # before the trick clears, even when the user has skipped earlier
188
+ # animations or is on the "instant" speed preset.
189
+ interruptible_sleep(MIN_TRICK_DWELL, None)
180
190
  if len(current.completed_tricks) == 7: # This was the 8th trick
181
191
  se_trump = current.boss_modifiers.seven_eight_trump
182
192
  is_sa = current.contract == "sans_atout"
@@ -143,14 +143,16 @@ class _UnixKeyReader:
143
143
  ch = chr(byte)
144
144
  if ch.lower() == "q":
145
145
  return KeyEvent(Key.QUIT)
146
- if ch.lower() == "h":
146
+ if ch == "?":
147
147
  return KeyEvent(Key.HELP)
148
- if ch.lower() == "s":
148
+ if ch.lower() == "h":
149
+ return KeyEvent(Key.HIST)
150
+ if ch.lower() == "t":
151
+ return KeyEvent(Key.THEME)
152
+ if ch.lower() == "o":
149
153
  return KeyEvent(Key.SORT)
150
154
  if ch.lower() == "m":
151
155
  return KeyEvent(Key.MUTE)
152
- if ch.lower() == "t":
153
- return KeyEvent(Key.HIST)
154
156
  if ch.lower() == "i" or ch.lower() == "v":
155
157
  return KeyEvent(Key.OVERLAY)
156
158
 
@@ -262,14 +264,16 @@ if os.name == "nt":
262
264
  return KeyEvent(Key.SPACE)
263
265
  if ch.lower() == b"q":
264
266
  return KeyEvent(Key.QUIT)
265
- if ch.lower() == b"h":
267
+ if ch == b"?":
266
268
  return KeyEvent(Key.HELP)
267
- if ch.lower() == b"s":
269
+ if ch.lower() == b"h":
270
+ return KeyEvent(Key.HIST)
271
+ if ch.lower() == b"t":
272
+ return KeyEvent(Key.THEME)
273
+ if ch.lower() == b"o":
268
274
  return KeyEvent(Key.SORT)
269
275
  if ch.lower() == b"m":
270
276
  return KeyEvent(Key.MUTE)
271
- if ch.lower() == b"t":
272
- return KeyEvent(Key.HIST)
273
277
  if ch.lower() in (b"i", b"v"):
274
278
  return KeyEvent(Key.OVERLAY)
275
279
 
@@ -240,7 +240,7 @@ def main() -> None:
240
240
  # Wait for Enter/R/Q/T
241
241
  sys.stdout.write(f"\n {BOLD}{gold_fg()}GAME OVER{RESET}")
242
242
  sys.stdout.write(
243
- f"\n {white_fg()}[Enter/Q] Menu [R] Rematch [T] History{RESET} "
243
+ f"\n {white_fg()}[Enter/Q] Menu [R] Rematch [H] History{RESET} "
244
244
  )
245
245
  sys.stdout.flush()
246
246
 
@@ -254,7 +254,7 @@ def main() -> None:
254
254
  show_final_screen(state)
255
255
  sys.stdout.write(f"\n {BOLD}{gold_fg()}GAME OVER{RESET}")
256
256
  sys.stdout.write(
257
- f"\n {white_fg()}[Enter/Q] Menu [R] Rematch [t] History{RESET} "
257
+ f"\n {white_fg()}[Enter/Q] Menu [R] Rematch [H] History{RESET} "
258
258
  )
259
259
  sys.stdout.flush()
260
260
  if ev.key in (Key.ENTER, Key.QUIT):
@@ -719,6 +719,20 @@ def score_round(state: GameState) -> ScoringBreakdown:
719
719
  )
720
720
 
721
721
 
722
+ def _decl_short_label(d: Declaration) -> str:
723
+ if d.kind == "belote":
724
+ return "Belote"
725
+ if d.kind == "rebelote":
726
+ return "Rebelote"
727
+ if d.kind == "carre" and isinstance(d.detail, Carre):
728
+ rank = _VALUE_TO_RANK.get(d.detail.rank)
729
+ return f"Carré-{rank.value}" if rank else "Carré"
730
+ if d.kind == "sequence" and isinstance(d.detail, Sequence):
731
+ pts = _SEQUENCE_POINTS.get(d.detail.length, 0)
732
+ return f"{pts}{d.detail.suit.symbol}"
733
+ return d.kind
734
+
735
+
722
736
  def apply_round_score(state: GameState, breakdown: ScoringBreakdown) -> GameState:
723
737
  """Apply round scoring result to team scores and advance state."""
724
738
  ns, ew = state.team_scores
@@ -731,6 +745,46 @@ def apply_round_score(state: GameState, breakdown: ScoringBreakdown) -> GameStat
731
745
 
732
746
  new_scores = (ns, ew)
733
747
 
748
+ # Trick counts per team across the played round.
749
+ se_trump = state.boss_modifiers.seven_eight_trump
750
+ is_sa = state.contract == "sans_atout"
751
+ tricks_ns = 0
752
+ tricks_ew = 0
753
+ for trick in state.completed_tricks:
754
+ winner = trick_winner_seat(trick, state.trump, se_trump, is_sa)
755
+ if winner is None:
756
+ continue
757
+ if team_of(winner) == 0:
758
+ tricks_ns += 1
759
+ else:
760
+ tricks_ew += 1
761
+
762
+ # Declaration summaries — only show the team(s) that actually scored decls,
763
+ # which matches Belote's "best team takes all decls" rule.
764
+ ns_decl_total = breakdown.taker_declarations if breakdown.taker_team == 0 else breakdown.defender_declarations
765
+ ew_decl_total = breakdown.defender_declarations if breakdown.taker_team == 0 else breakdown.taker_declarations
766
+ ns_decls = tuple(
767
+ _decl_short_label(d) for d in state.declarations if team_of(d.seat) == 0
768
+ ) if ns_decl_total > 0 else ()
769
+ ew_decls = tuple(
770
+ _decl_short_label(d) for d in state.declarations if team_of(d.seat) == 1
771
+ ) if ew_decl_total > 0 else ()
772
+
773
+ common_kwargs = dict(
774
+ is_failed=breakdown.is_failed,
775
+ is_capot=breakdown.is_capot,
776
+ is_litige=breakdown.is_litige,
777
+ litige_points=breakdown.litige_points_awarded,
778
+ contract=state.contract,
779
+ trump=state.trump,
780
+ taker_seat=state.taker,
781
+ tricks_ns=tricks_ns,
782
+ tricks_ew=tricks_ew,
783
+ last_trick_winner=state.last_trick_winner,
784
+ decl_summary_ns=ns_decls,
785
+ decl_summary_ew=ew_decls,
786
+ )
787
+
734
788
  # Create RoundScore for history
735
789
  if breakdown.taker_team == 0:
736
790
  round_score = RoundScore(
@@ -745,10 +799,7 @@ def apply_round_score(state: GameState, breakdown: ScoringBreakdown) -> GameStat
745
799
  ew_rebelote=breakdown.defender_rebelote,
746
800
  ns_total=breakdown.taker_total,
747
801
  ew_total=breakdown.defender_total,
748
- is_failed=breakdown.is_failed,
749
- is_capot=breakdown.is_capot,
750
- is_litige=breakdown.is_litige,
751
- litige_points=breakdown.litige_points_awarded,
802
+ **common_kwargs,
752
803
  )
753
804
  else:
754
805
  round_score = RoundScore(
@@ -763,10 +814,7 @@ def apply_round_score(state: GameState, breakdown: ScoringBreakdown) -> GameStat
763
814
  ew_rebelote=breakdown.taker_rebelote,
764
815
  ns_total=breakdown.defender_total,
765
816
  ew_total=breakdown.taker_total,
766
- is_failed=breakdown.is_failed,
767
- is_capot=breakdown.is_capot,
768
- is_litige=breakdown.is_litige,
769
- litige_points=breakdown.litige_points_awarded,
817
+ **common_kwargs,
770
818
  )
771
819
 
772
820
  new_history = state.score_history + (round_score,)
@@ -12,9 +12,11 @@ from ..ansi import (
12
12
  ansi_center,
13
13
  banner_bg,
14
14
  banner_fg,
15
+ clear_line,
15
16
  clear_screen,
16
17
  gold_fg,
17
18
  hide_cursor,
19
+ move,
18
20
  white_fg,
19
21
  )
20
22
  from ..context import AUDIO
@@ -33,8 +35,19 @@ def toggle_mute() -> bool:
33
35
 
34
36
 
35
37
  def announce(message: str, duration: float = 2.0, reader: KeyReader | None = None) -> None:
36
- """Display a transient announcement banner."""
37
- sys.stdout.write(f"\r\n{banner_bg()}{banner_fg()} {BOLD} {message} {RESET}\r\n")
38
+ """Display a transient announcement banner.
39
+
40
+ Painted with absolute cursor positioning + clear_line so it never triggers
41
+ a scroll, even when the cursor is parked on the bottom row of the alt
42
+ screen. Writing \\r\\n at the bottom row scrolls the alt-screen on Konsole
43
+ and other strict emulators, which leaks the previous frame onto rows the
44
+ next render's blank padding doesn't repaint.
45
+ """
46
+ term_w, term_h = get_term_size()
47
+ banner = ansi_center(
48
+ f"{banner_bg()}{banner_fg()} {BOLD} {message} {RESET}", term_w
49
+ )
50
+ sys.stdout.write(move(max(1, term_h - 1), 1) + clear_line() + banner)
38
51
  sys.stdout.flush()
39
52
  if reader and duration > 0:
40
53
  interruptible_sleep(duration, reader)