belote-cli 2.9.2__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.
- {belote_cli-2.9.2 → belote_cli-2.9.5}/CHANGELOG.md +37 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/PKG-INFO +7 -7
- {belote_cli-2.9.2 → belote_cli-2.9.5}/README.md +6 -6
- {belote_cli-2.9.2 → belote_cli-2.9.5}/pyproject.toml +1 -1
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/__init__.py +1 -1
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/game.py +8 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/gameflow.py +10 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/input.py +12 -8
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/main.py +2 -2
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/scoring.py +56 -8
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ui/menu.py +5 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ui/prompts.py +115 -31
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ui/render.py +200 -56
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_extended.py +44 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_layout.py +2 -2
- {belote_cli-2.9.2 → belote_cli-2.9.5}/.claude/settings.local.json +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/.gitignore +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/.python-version +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/DEVELOPMENT.md +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/LICENSE +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/scripts/benchmark.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ai.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ansi.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/core/run_state.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/core/scoring.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/engine/modifier_patch.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/engine/round_driver.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/registry.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/main.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/hud.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/config.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/context.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/deck.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/rules.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/stats.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/themes.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ui/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ui/announce.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/src/belote/ui/layout.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_belatro.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_boss_modifiers_integration.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_dead_flag_fixes.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_phase0_coverage.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_progression.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/belatro/test_round_driver.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_ai.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_belote.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_game_logic.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_gameflow.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_new_coverage.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_official_rules.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_properties.py +0 -0
- {belote_cli-2.9.2 → belote_cli-2.9.5}/tests/test_undo.py +0 -0
|
@@ -5,6 +5,43 @@ 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
|
+
|
|
8
45
|
## [2.9.2] - 2026-05-07
|
|
9
46
|
|
|
10
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 2.9.
|
|
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
|
|
@@ -170,12 +170,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
170
170
|
## Controls
|
|
171
171
|
|
|
172
172
|
**General:**
|
|
173
|
-
-
|
|
173
|
+
- `?`: Show keyboard shortcut help
|
|
174
174
|
- `M`: Toggle sound effects on/off
|
|
175
|
-
- `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
175
|
+
- `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
176
176
|
- `Q`: Quit to main menu or exit
|
|
177
|
-
- `
|
|
178
|
-
- `T`:
|
|
177
|
+
- `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
|
|
178
|
+
- `T`: Cycle UI Theme
|
|
179
179
|
|
|
180
180
|
**Classic Belote:**
|
|
181
181
|
- `↑` `↓`: Navigate options
|
|
@@ -183,7 +183,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
183
183
|
- `Enter`: Select option / Enter submenu
|
|
184
184
|
|
|
185
185
|
**BelAtro (Roguelite):**
|
|
186
|
-
- `S`: View current Run State and Jokers
|
|
187
186
|
- `1`-`5`: Inspect specific Jokers in the Shop
|
|
188
187
|
- `U`: Use a consumable (Tarot/Planet) during gameplay
|
|
189
188
|
|
|
@@ -194,6 +193,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
194
193
|
- `O`: Sort hand by suit and rank
|
|
195
194
|
- `Z`: Undo last move
|
|
196
195
|
- `Space` or `Esc`: Skip animations
|
|
196
|
+
- During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
|
|
197
197
|
|
|
198
198
|
## Features
|
|
199
199
|
|
|
@@ -204,7 +204,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
204
204
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
205
205
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
206
206
|
- **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
|
|
207
|
-
- **Customizable Themes:** Switch between
|
|
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.
|
|
208
208
|
- **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
|
|
209
209
|
- **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
|
|
210
210
|
- **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
|
|
@@ -127,12 +127,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
127
127
|
## Controls
|
|
128
128
|
|
|
129
129
|
**General:**
|
|
130
|
-
-
|
|
130
|
+
- `?`: Show keyboard shortcut help
|
|
131
131
|
- `M`: Toggle sound effects on/off
|
|
132
|
-
- `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
132
|
+
- `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
133
133
|
- `Q`: Quit to main menu or exit
|
|
134
|
-
- `
|
|
135
|
-
- `T`:
|
|
134
|
+
- `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
|
|
135
|
+
- `T`: Cycle UI Theme
|
|
136
136
|
|
|
137
137
|
**Classic Belote:**
|
|
138
138
|
- `↑` `↓`: Navigate options
|
|
@@ -140,7 +140,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
140
140
|
- `Enter`: Select option / Enter submenu
|
|
141
141
|
|
|
142
142
|
**BelAtro (Roguelite):**
|
|
143
|
-
- `S`: View current Run State and Jokers
|
|
144
143
|
- `1`-`5`: Inspect specific Jokers in the Shop
|
|
145
144
|
- `U`: Use a consumable (Tarot/Planet) during gameplay
|
|
146
145
|
|
|
@@ -151,6 +150,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
151
150
|
- `O`: Sort hand by suit and rank
|
|
152
151
|
- `Z`: Undo last move
|
|
153
152
|
- `Space` or `Esc`: Skip animations
|
|
153
|
+
- During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
|
|
154
154
|
|
|
155
155
|
## Features
|
|
156
156
|
|
|
@@ -161,7 +161,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
161
161
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
162
162
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
163
163
|
- **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
|
|
164
|
-
- **Customizable Themes:** Switch between
|
|
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.
|
|
165
165
|
- **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
|
|
166
166
|
- **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
|
|
167
167
|
- **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
|
|
@@ -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
|
|
146
|
+
if ch == "?":
|
|
147
147
|
return KeyEvent(Key.HELP)
|
|
148
|
-
if ch.lower() == "
|
|
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
|
|
267
|
+
if ch == b"?":
|
|
266
268
|
return KeyEvent(Key.HELP)
|
|
267
|
-
if ch.lower() == b"
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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,)
|
|
@@ -281,6 +281,11 @@ def show_main_menu(
|
|
|
281
281
|
show_help(reader)
|
|
282
282
|
case Key.MUTE:
|
|
283
283
|
toggle_mute()
|
|
284
|
+
case Key.THEME:
|
|
285
|
+
themes_list = list(THEMES.keys())
|
|
286
|
+
curr_theme = theme_manager._current_theme_name
|
|
287
|
+
new_idx = (themes_list.index(curr_theme) + 1) % len(themes_list)
|
|
288
|
+
theme_manager.set_current(themes_list[new_idx])
|
|
284
289
|
case Key.UP:
|
|
285
290
|
sel = (sel - 1) % len(options_labels)
|
|
286
291
|
case Key.DOWN:
|
|
@@ -200,11 +200,11 @@ def show_help(reader: KeyReader) -> None:
|
|
|
200
200
|
"=" * 20,
|
|
201
201
|
"",
|
|
202
202
|
f"{white_fg()}General:{RESET}",
|
|
203
|
-
" [?]
|
|
203
|
+
" [?] Show this help screen",
|
|
204
204
|
" [Q] Quit to menu / Exit",
|
|
205
205
|
" [M] Toggle Sound Effects",
|
|
206
206
|
f" (Currently: {sound_status})",
|
|
207
|
-
" [
|
|
207
|
+
" [T] Cycle Theme",
|
|
208
208
|
" [Esc] Cancel / Back",
|
|
209
209
|
"",
|
|
210
210
|
f"{white_fg()}Gameplay:{RESET}",
|
|
@@ -213,7 +213,7 @@ def show_help(reader: KeyReader) -> None:
|
|
|
213
213
|
" [1-8] Quick card select",
|
|
214
214
|
" [O] Sort hand by suit/rank",
|
|
215
215
|
" [Space] Skip animations",
|
|
216
|
-
" [
|
|
216
|
+
" [H] View Game History",
|
|
217
217
|
" [Z] Undo last move",
|
|
218
218
|
"",
|
|
219
219
|
f"{white_fg()}Bidding:{RESET}",
|
|
@@ -222,7 +222,7 @@ def show_help(reader: KeyReader) -> None:
|
|
|
222
222
|
"",
|
|
223
223
|
f"{white_fg()}Menus:{RESET}",
|
|
224
224
|
" [R] Rematch (Game Over)",
|
|
225
|
-
" [
|
|
225
|
+
" [H] View Game History",
|
|
226
226
|
"",
|
|
227
227
|
f"{DIM}Press [Any Key] to Return{RESET}",
|
|
228
228
|
]
|
|
@@ -305,6 +305,46 @@ def show_rules(reader: KeyReader) -> None:
|
|
|
305
305
|
scroll = 0
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
def _hist_taker_label(rs) -> str:
|
|
309
|
+
team = "NS" if rs.taker_team == 0 else "EW"
|
|
310
|
+
if rs.taker_seat is None:
|
|
311
|
+
return team
|
|
312
|
+
return f"{rs.taker_seat.name[0]} ({team})"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _hist_contract_label(rs) -> str:
|
|
316
|
+
if rs.contract == "sans_atout":
|
|
317
|
+
return "SA"
|
|
318
|
+
if rs.contract == "tout_atout":
|
|
319
|
+
return "TA"
|
|
320
|
+
sym = rs.trump.symbol if rs.trump is not None and hasattr(rs.trump, "symbol") else "?"
|
|
321
|
+
return f"NORM {sym}"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _hist_status(rs) -> str:
|
|
325
|
+
if rs.is_capot:
|
|
326
|
+
return f"{gold_fg()}CAPOT{RESET}"
|
|
327
|
+
if rs.is_failed:
|
|
328
|
+
return f"{red_fg()}CHUTE{RESET}"
|
|
329
|
+
if rs.is_litige:
|
|
330
|
+
return f"{DIM}LITIGE{RESET}"
|
|
331
|
+
return "─"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _hist_decl_str(items: tuple[str, ...], width: int) -> str:
|
|
335
|
+
if not items:
|
|
336
|
+
return "─"
|
|
337
|
+
s = " ".join(items)
|
|
338
|
+
if len(s) > width:
|
|
339
|
+
s = s[: max(0, width - 1)] + "…"
|
|
340
|
+
return s
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _ljust_visible(s: str, width: int) -> str:
|
|
344
|
+
pad = max(0, width - visible_len(s))
|
|
345
|
+
return s + " " * pad
|
|
346
|
+
|
|
347
|
+
|
|
308
348
|
def show_history(state: GameState, reader: KeyReader) -> None:
|
|
309
349
|
"""Display a scrollable overlay of round-by-round scores."""
|
|
310
350
|
scroll = 0
|
|
@@ -312,7 +352,7 @@ def show_history(state: GameState, reader: KeyReader) -> None:
|
|
|
312
352
|
while True:
|
|
313
353
|
term_w, term_h = get_term_size()
|
|
314
354
|
|
|
315
|
-
lines = []
|
|
355
|
+
lines: list[str] = []
|
|
316
356
|
lines.append(f"{BOLD}{gold_fg()}GAME HISTORY{RESET}")
|
|
317
357
|
lines.append("=" * 12)
|
|
318
358
|
lines.append("")
|
|
@@ -320,32 +360,76 @@ def show_history(state: GameState, reader: KeyReader) -> None:
|
|
|
320
360
|
if not state.score_history:
|
|
321
361
|
lines.append(f"{DIM}No rounds completed yet.{RESET}")
|
|
322
362
|
else:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
363
|
+
wide = term_w >= 78
|
|
364
|
+
if wide:
|
|
365
|
+
# Single-row layout. Column widths sum to ~76 with separators.
|
|
366
|
+
W_RD, W_TKR, W_CON, W_TRK, W_DECL, W_NS, W_EW, W_ST = 3, 7, 8, 7, 16, 5, 5, 7
|
|
367
|
+
header_cells = [
|
|
368
|
+
_ljust_visible("RD", W_RD),
|
|
369
|
+
_ljust_visible("TAKER", W_TKR),
|
|
370
|
+
_ljust_visible("CONTRACT", W_CON),
|
|
371
|
+
_ljust_visible("TRICKS", W_TRK),
|
|
372
|
+
_ljust_visible("DECLARATIONS", W_DECL),
|
|
373
|
+
_ljust_visible("NS", W_NS),
|
|
374
|
+
_ljust_visible("EW", W_EW),
|
|
375
|
+
_ljust_visible("STATUS", W_ST),
|
|
376
|
+
]
|
|
377
|
+
header = " │ ".join(header_cells)
|
|
378
|
+
lines.append(f"{BOLD}{white_fg()}{header}{RESET}")
|
|
379
|
+
lines.append("─" * visible_len(header))
|
|
380
|
+
|
|
381
|
+
for i, rs in enumerate(state.score_history):
|
|
382
|
+
rd = f"{i + 1:02d}"
|
|
383
|
+
taker = _hist_taker_label(rs)
|
|
384
|
+
contract = _hist_contract_label(rs)
|
|
385
|
+
tricks = f"{rs.tricks_ns} / {rs.tricks_ew}"
|
|
386
|
+
decl_ns = _hist_decl_str(rs.decl_summary_ns, W_DECL // 2 - 1)
|
|
387
|
+
decl_ew = _hist_decl_str(rs.decl_summary_ew, W_DECL // 2 - 1)
|
|
388
|
+
if rs.decl_summary_ns and rs.decl_summary_ew:
|
|
389
|
+
decls = f"{decl_ns} / {decl_ew}"
|
|
390
|
+
elif rs.decl_summary_ns:
|
|
391
|
+
decls = decl_ns
|
|
392
|
+
elif rs.decl_summary_ew:
|
|
393
|
+
decls = decl_ew
|
|
394
|
+
else:
|
|
395
|
+
decls = "─"
|
|
396
|
+
if visible_len(decls) > W_DECL:
|
|
397
|
+
decls = decls[: W_DECL - 1] + "…"
|
|
398
|
+
ns = f"{BOLD}{rs.ns_total}{RESET}"
|
|
399
|
+
ew = f"{BOLD}{rs.ew_total}{RESET}"
|
|
400
|
+
status = _hist_status(rs)
|
|
401
|
+
|
|
402
|
+
row_cells = [
|
|
403
|
+
_ljust_visible(rd, W_RD),
|
|
404
|
+
_ljust_visible(taker, W_TKR),
|
|
405
|
+
_ljust_visible(contract, W_CON),
|
|
406
|
+
_ljust_visible(tricks, W_TRK),
|
|
407
|
+
_ljust_visible(decls, W_DECL),
|
|
408
|
+
_ljust_visible(ns, W_NS),
|
|
409
|
+
_ljust_visible(ew, W_EW),
|
|
410
|
+
_ljust_visible(status, W_ST),
|
|
411
|
+
]
|
|
412
|
+
lines.append(" │ ".join(row_cells))
|
|
413
|
+
else:
|
|
414
|
+
# Compact two-line-per-round layout for narrow terminals.
|
|
415
|
+
lines.append(f"{BOLD}{white_fg()}{'RD':<3} {'TAKER':<7} {'CON':<8} {'TRICKS':<7} STATUS{RESET}")
|
|
416
|
+
lines.append("─" * 40)
|
|
417
|
+
for i, rs in enumerate(state.score_history):
|
|
418
|
+
rd = f"{i + 1:02d}"
|
|
419
|
+
taker = _hist_taker_label(rs)
|
|
420
|
+
contract = _hist_contract_label(rs)
|
|
421
|
+
tricks = f"{rs.tricks_ns}/{rs.tricks_ew}"
|
|
422
|
+
status = _hist_status(rs)
|
|
423
|
+
lines.append(
|
|
424
|
+
f"{rd:<3} {_ljust_visible(taker, 7)} {contract:<8} {tricks:<7} {status}"
|
|
425
|
+
)
|
|
426
|
+
decl_n = _hist_decl_str(rs.decl_summary_ns, 14)
|
|
427
|
+
decl_e = _hist_decl_str(rs.decl_summary_ew, 14)
|
|
428
|
+
lines.append(
|
|
429
|
+
f" NS:{BOLD}{rs.ns_total:>4}{RESET} EW:{BOLD}{rs.ew_total:>4}{RESET} "
|
|
430
|
+
f"decl: {decl_n} / {decl_e}"
|
|
431
|
+
)
|
|
432
|
+
lines.append("")
|
|
349
433
|
|
|
350
434
|
lines.append("")
|
|
351
435
|
lines.append(f"{DIM}[↑↓] Scroll [Any Key] Return{RESET}")
|