belote-cli 2.9.0__tar.gz → 2.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {belote_cli-2.9.0 → belote_cli-2.9.2}/CHANGELOG.md +38 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/PKG-INFO +27 -19
- {belote_cli-2.9.0 → belote_cli-2.9.2}/README.md +26 -18
- {belote_cli-2.9.0 → belote_cli-2.9.2}/pyproject.toml +1 -1
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/__init__.py +1 -1
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ui/announce.py +15 -2
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ui/menu.py +39 -34
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ui/prompts.py +6 -64
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ui/render.py +96 -12
- {belote_cli-2.9.0 → belote_cli-2.9.2}/.claude/settings.local.json +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/.gitignore +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/.python-version +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/DEVELOPMENT.md +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/LICENSE +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/scripts/benchmark.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ai.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ansi.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/core/run_state.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/core/scoring.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/engine/modifier_patch.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/engine/round_driver.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/registry.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/main.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/hud.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/config.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/context.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/deck.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/game.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/gameflow.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/input.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/main.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/rules.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/scoring.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/stats.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/themes.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ui/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/src/belote/ui/layout.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/__init__.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_belatro.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_boss_modifiers_integration.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_dead_flag_fixes.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_phase0_coverage.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_progression.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/belatro/test_round_driver.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_ai.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_belote.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_extended.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_game_logic.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_gameflow.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_layout.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_new_coverage.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_official_rules.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_properties.py +0 -0
- {belote_cli-2.9.0 → belote_cli-2.9.2}/tests/test_undo.py +0 -0
|
@@ -5,6 +5,44 @@ 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.2] - 2026-05-07
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`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.
|
|
15
|
+
- **`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.
|
|
16
|
+
- **`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`).
|
|
17
|
+
- **`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.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`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.
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
## [2.9.1] - 2026-05-06
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`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.
|
|
34
|
+
- **`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.
|
|
35
|
+
- **`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.
|
|
36
|
+
- **`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.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`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.
|
|
41
|
+
|
|
42
|
+
### Notes
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
8
46
|
## [2.9.0] - 2026-05-06
|
|
9
47
|
|
|
10
48
|
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.
|
|
3
|
+
Version: 2.9.2
|
|
4
4
|
Summary: A 4-player terminal card game
|
|
5
5
|
Project-URL: Homepage, https://github.com/ElysiumDisc/belote
|
|
6
6
|
Project-URL: Repository, https://github.com/ElysiumDisc/belote
|
|
@@ -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
|
-
/ / |
|
|
111
|
-
| | |
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
.-' \ / '-.
|
|
@@ -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
|
-
/ / |
|
|
68
|
-
| | |
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
.-' \ / '-.
|
|
@@ -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).
|
|
@@ -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
|
-
|
|
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)
|
|
@@ -25,43 +25,48 @@ from .render import get_term_size
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def get_cards_art() -> list[str]:
|
|
28
|
-
"""Return the
|
|
28
|
+
"""Return the croissant art with current theme colors.
|
|
29
|
+
|
|
30
|
+
Each line is 25 Braille cells wide (including U+2800 blanks for indent),
|
|
31
|
+
so callers can rely on uniform width without ASCII padding.
|
|
32
|
+
"""
|
|
29
33
|
c = menu_art_fg()
|
|
30
34
|
return [
|
|
31
|
-
f"
|
|
32
|
-
f"
|
|
33
|
-
f"
|
|
34
|
-
f"
|
|
35
|
-
f"
|
|
36
|
-
f"
|
|
37
|
-
f"
|
|
38
|
-
f"
|
|
39
|
-
f"
|
|
40
|
-
f"
|
|
41
|
-
f"
|
|
42
|
-
f"
|
|
35
|
+
f"{c}⠀⠀⢠⣴⣶⣶⣶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
|
|
36
|
+
f"{c}⠀⠀⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
|
|
37
|
+
f"{c}⠀⢰⣿⣿⣿⣿⡿⠟⠁⣠⣴⣶⣦⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
|
|
38
|
+
f"{c}⠀⢸⣿⣿⠟⠉⣠⣴⣿⣿⣿⠟⠁⣠⣾⣿⣦⡀⠀⠀⠀⠀⠀⠀⠀{RESET}",
|
|
39
|
+
f"{c}⠀⠀⠉⣀⣴⣾⣿⣿⣿⠟⢁⣤⣾⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀{RESET}",
|
|
40
|
+
f"{c}⢀⣤⣾⣿⣿⣿⡿⠛⢁⣴⣿⣿⣿⣿⣿⣿⣿⠟⠁⡀⠀⠀⠀⠀⠀{RESET}",
|
|
41
|
+
f"{c}⢼⣿⣿⣿⡿⠋⣀⣴⣿⣿⣿⣿⣿⣿⣿⡿⠉⣠⣾⣿⡆⠀⠀⠀⠀{RESET}",
|
|
42
|
+
f"{c}⠘⢿⡿⠋⣠⣾⣿⣿⣿⣿⣿⣿⣿⡿⠋⢀⣾⣿⣿⠟⢁⣀⠀⠀⠀{RESET}",
|
|
43
|
+
f"{c}⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⠏⢀⣴⣿⣿⣿⠋⢠⣾⣿⣷⣦⡀{RESET}",
|
|
44
|
+
f"{c}⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⠟⢁⣴⣿⣿⣿⡿⠁⣰⣿⣿⣿⣿⣿⣿{RESET}",
|
|
45
|
+
f"{c}⠀⠀⠀⠹⢿⣿⣿⣿⡿⠋⣠⣾⣿⣿⣿⠟⢀⣼⣿⣿⣿⣿⣿⣿⡟{RESET}",
|
|
46
|
+
f"{c}⠀⠀⠀⠀⠀⠉⠉⠉⠀⢾⣿⣿⣿⣿⠋⠀⠚⠛⠛⠛⠛⠛⠛⠁⠀{RESET}",
|
|
47
|
+
f"{c}⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{RESET}",
|
|
43
48
|
]
|
|
44
49
|
|
|
45
50
|
|
|
46
51
|
CUP_TEMPLATE = [
|
|
47
|
-
"
|
|
48
|
-
"
|
|
52
|
+
" {steam0}",
|
|
53
|
+
" {steam1}",
|
|
49
54
|
" {gold}___...(-------)-....___{reset}",
|
|
50
55
|
" {gold}.-'' ) ( ''-.{reset}",
|
|
51
56
|
" {gold}.-'``'|-._ ) _.-|{reset}",
|
|
52
57
|
" {gold}/ .--.| `''---...........---''` |{reset}",
|
|
53
|
-
" {gold}/ / |
|
|
54
|
-
" {gold}| | |
|
|
55
|
-
" {gold}| | |
|
|
56
|
-
" {gold}| | |
|
|
57
|
-
" {gold}| | |
|
|
58
|
-
" {gold}| | |
|
|
59
|
-
" {gold}| | |
|
|
60
|
-
" {gold}\\ \\ |
|
|
61
|
-
" {gold}`\\ `\\ |
|
|
62
|
-
" {gold}`\\ `|
|
|
63
|
-
" {gold}_/ /\\
|
|
64
|
-
" {gold}(__/ \\
|
|
58
|
+
" {gold}/ / |{opt0}|{reset}",
|
|
59
|
+
" {gold}| | |{opt1}|{reset}",
|
|
60
|
+
" {gold}| | |{opt2}|{reset}",
|
|
61
|
+
" {gold}| | |{opt3}|{reset}",
|
|
62
|
+
" {gold}| | |{opt4}|{reset}",
|
|
63
|
+
" {gold}| | |{opt5}|{reset}",
|
|
64
|
+
" {gold}| | |{opt6}|{reset}",
|
|
65
|
+
" {gold}\\ \\ |{opt7}|{reset}",
|
|
66
|
+
" {gold}`\\ `\\ |{opt8}|{reset}",
|
|
67
|
+
" {gold}`\\ `| |{reset}",
|
|
68
|
+
" {gold}_/ /\\ /{reset}",
|
|
69
|
+
" {gold}(__/ \\ /{reset}",
|
|
65
70
|
" {gold}_..---''` \\ /`''---.._{reset}",
|
|
66
71
|
" {gold}.-' \\ / '-.{reset}",
|
|
67
72
|
" {gold}: `-.__ __.-' :{reset}",
|
|
@@ -88,12 +93,12 @@ def _render_main_menu_art(sel: int, options: list[str], frame: int, term_h: int)
|
|
|
88
93
|
|
|
89
94
|
# Process placeholders
|
|
90
95
|
opts = {}
|
|
91
|
-
assert len(options) <=
|
|
96
|
+
assert len(options) <= 9, (
|
|
92
97
|
f"Too many menu options ({len(options)}); add opt slots to CUP_TEMPLATE"
|
|
93
98
|
)
|
|
94
|
-
for i in range(
|
|
99
|
+
for i in range(9):
|
|
95
100
|
label = options[i] if i < len(options) else ""
|
|
96
|
-
text = f"{REVERSE}
|
|
101
|
+
text = f"{REVERSE}> {label} <{RESET}" if i == sel else f" {label} "
|
|
97
102
|
opts[f"opt{i}"] = ansi_center(text, 29)
|
|
98
103
|
|
|
99
104
|
final_cup = []
|
|
@@ -231,10 +236,10 @@ def show_main_menu(
|
|
|
231
236
|
options_labels = [
|
|
232
237
|
"BelAtro",
|
|
233
238
|
"Start Game",
|
|
234
|
-
f"AI
|
|
235
|
-
f"Target
|
|
236
|
-
f"Speed:
|
|
237
|
-
f"Theme:
|
|
239
|
+
f"AI: < {diff_display} >",
|
|
240
|
+
f"Target: < {curr_target} >",
|
|
241
|
+
f"Speed: < {curr_speed.capitalize()} >",
|
|
242
|
+
f"Theme: < {theme_manager.get_current().name} >",
|
|
238
243
|
"Rules & History",
|
|
239
244
|
"Statistics",
|
|
240
245
|
"Quit",
|
|
@@ -7,9 +7,7 @@ from ..ansi import (
|
|
|
7
7
|
BOLD,
|
|
8
8
|
DIM,
|
|
9
9
|
RESET,
|
|
10
|
-
REVERSE,
|
|
11
10
|
ansi_center,
|
|
12
|
-
black_fg,
|
|
13
11
|
clear_screen,
|
|
14
12
|
gold_fg,
|
|
15
13
|
green_fg,
|
|
@@ -123,81 +121,25 @@ def prompt_bid(state: GameState, reader: KeyReader) -> Suit | str | None:
|
|
|
123
121
|
Round 2 offers Tout Atout (TA) and Sans Atout (SA) in addition to the
|
|
124
122
|
three remaining card suits. Per FFBelote rules, round 1 is "take the
|
|
125
123
|
up-card suit at the standard contract" only — TA/SA aren't offered there.
|
|
124
|
+
|
|
125
|
+
The selector UI is painted by render() (via display(..., bid_selection=sel))
|
|
126
|
+
so each frame is a single in-place repaint. Writing additional lines after
|
|
127
|
+
display() would scroll the alt-screen on stricter terminals (Konsole) and
|
|
128
|
+
leak previous frames onto blank padding rows.
|
|
126
129
|
"""
|
|
127
130
|
from ..game import SANS_ATOUT_BID
|
|
128
131
|
|
|
129
132
|
if state.bidding_round == 1:
|
|
130
|
-
# Round 1: Take (up_card suit) or Pass
|
|
131
133
|
options: list[Suit | str | None] = [state.up_card.suit, None] # type: ignore[union-attr]
|
|
132
|
-
labels = [f"Take {state.up_card.suit.symbol}", "Pass"] # type: ignore[union-attr]
|
|
133
134
|
else:
|
|
134
|
-
# Round 2: Any classic suit except up_card's, plus Tout Atout (every
|
|
135
|
-
# suit acts as trump) and Sans Atout (no trump). All round-2 specials
|
|
136
|
-
# gate to round 2 in `place_bid` — calling them in round 1 raises.
|
|
137
135
|
all_suits = [Suit.SPADES, Suit.HEARTS, Suit.DIAMONDS, Suit.CLUBS]
|
|
138
136
|
other_suits = [s for s in all_suits if s != state.up_card.suit] # type: ignore[union-attr]
|
|
139
137
|
options = [*other_suits, Suit.TOUT_ATOUT, SANS_ATOUT_BID, None]
|
|
140
|
-
labels = (
|
|
141
|
-
[s.symbol for s in other_suits]
|
|
142
|
-
+ ["TA", "SA", "Pass"]
|
|
143
|
-
)
|
|
144
138
|
|
|
145
139
|
sel = 0
|
|
146
140
|
|
|
147
141
|
while True:
|
|
148
|
-
display(state, None)
|
|
149
|
-
term_w, _ = get_term_size()
|
|
150
|
-
|
|
151
|
-
# L'Encyclopédie voucher: surface partner bidding tendency before each bid.
|
|
152
|
-
tendency = state._joker_state.get("partner_bid_tendency_text")
|
|
153
|
-
if isinstance(tendency, str) and tendency:
|
|
154
|
-
sys.stdout.write(
|
|
155
|
-
"\r\n" + ansi_center(f"{DIM}{tendency}{RESET}", term_w) + "\r\n"
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
if state.bidding_round == 2:
|
|
159
|
-
# Nice boxed UI for round 2. Widened to fit 7 options (♠/♥/♦/♣
|
|
160
|
-
# minus up-card suit + TA + SA + Pass) on one row.
|
|
161
|
-
inner_w = 60
|
|
162
|
-
box_lines = [
|
|
163
|
-
f"┌{'─' * inner_w}┐",
|
|
164
|
-
f"│{f'ROUND {state.bidding_round} BID'.center(inner_w)}│",
|
|
165
|
-
f"├{'─' * inner_w}┤",
|
|
166
|
-
f"│{' ' * inner_w}│",
|
|
167
|
-
]
|
|
168
|
-
|
|
169
|
-
# Options row
|
|
170
|
-
opt_str = ""
|
|
171
|
-
for i, opt in enumerate(options):
|
|
172
|
-
lbl = labels[i]
|
|
173
|
-
prefix = f"{BOLD}{gold_fg()}" if i == sel else ""
|
|
174
|
-
|
|
175
|
-
# Add color to suit symbols
|
|
176
|
-
color = ""
|
|
177
|
-
if isinstance(opt, Suit):
|
|
178
|
-
color = red_fg() if opt.is_red else black_fg()
|
|
179
|
-
|
|
180
|
-
entry = f"{prefix}({i + 1}) {color}{lbl}{RESET}{prefix}"
|
|
181
|
-
entry = f"{REVERSE} {entry} {RESET}" if i == sel else f" {entry} "
|
|
182
|
-
opt_str += entry + " "
|
|
183
|
-
|
|
184
|
-
box_lines.append(f"│{ansi_center(opt_str.strip(), inner_w)}│")
|
|
185
|
-
box_lines.append(f"│{' ' * inner_w}│")
|
|
186
|
-
box_lines.append(f"└{'─' * inner_w}┘")
|
|
187
|
-
|
|
188
|
-
for bl in box_lines:
|
|
189
|
-
sys.stdout.write("\r\n" + ansi_center(bl, term_w))
|
|
190
|
-
sys.stdout.write("\r\n")
|
|
191
|
-
else:
|
|
192
|
-
# Round 1 simple prompt
|
|
193
|
-
parts = [
|
|
194
|
-
f"{BOLD}{gold_fg()}({i + 1}){lbl}{RESET}" if i == sel else f"({i + 1}){lbl}"
|
|
195
|
-
for i, lbl in enumerate(labels)
|
|
196
|
-
]
|
|
197
|
-
prompt = f"{BOLD}{white_fg()}Round {state.bidding_round} Bid: {' '.join(parts)}{RESET}"
|
|
198
|
-
sys.stdout.write("\r\n" + ansi_center(prompt, term_w) + "\r\n")
|
|
199
|
-
|
|
200
|
-
sys.stdout.flush()
|
|
142
|
+
display(state, None, bid_selection=sel)
|
|
201
143
|
|
|
202
144
|
event = reader.read()
|
|
203
145
|
match event.key:
|
|
@@ -9,6 +9,7 @@ from ..ansi import (
|
|
|
9
9
|
BOLD,
|
|
10
10
|
DIM,
|
|
11
11
|
RESET,
|
|
12
|
+
REVERSE,
|
|
12
13
|
UNDERLINE,
|
|
13
14
|
ansi_center,
|
|
14
15
|
ansi_ljust,
|
|
@@ -35,8 +36,9 @@ from ..ansi import (
|
|
|
35
36
|
clear_screen as _clear_screen,
|
|
36
37
|
)
|
|
37
38
|
from ..context import TERMINAL
|
|
38
|
-
from ..deck import Card, Rank
|
|
39
|
+
from ..deck import Card, Rank, Suit
|
|
39
40
|
from ..game import (
|
|
41
|
+
SANS_ATOUT_BID,
|
|
40
42
|
GameState,
|
|
41
43
|
Phase,
|
|
42
44
|
Seat,
|
|
@@ -562,13 +564,88 @@ def _build_hud(state: GameState, term_w: int, layout: LayoutPreset = STANDARD) -
|
|
|
562
564
|
return bar + " " * pad + theme_label
|
|
563
565
|
|
|
564
566
|
|
|
565
|
-
def
|
|
567
|
+
def _build_bid_prompt_lines(state: GameState, term_w: int, bid_selection: int) -> list[str]:
|
|
568
|
+
"""Build the in-frame bidding prompt: optional tendency line, then the
|
|
569
|
+
selector (Round 1: inline highlighted Take/Pass; Round 2: boxed grid).
|
|
570
|
+
|
|
571
|
+
Painted as part of the main render so no writes happen after `display()` —
|
|
572
|
+
that's what was causing the Konsole UI to stack (post-render \\r\\n's
|
|
573
|
+
scrolled the alt-screen, leaving stale content on rows the next frame's
|
|
574
|
+
blank padding doesn't repaint).
|
|
575
|
+
"""
|
|
576
|
+
if state.bidding_round == 1:
|
|
577
|
+
up_card = state.up_card
|
|
578
|
+
assert up_card is not None # guaranteed by Phase.BIDDING + round 1
|
|
579
|
+
labels = [f"Take {up_card.suit.symbol}", "Pass"]
|
|
580
|
+
else:
|
|
581
|
+
all_suits = [Suit.SPADES, Suit.HEARTS, Suit.DIAMONDS, Suit.CLUBS]
|
|
582
|
+
up_card = state.up_card
|
|
583
|
+
assert up_card is not None
|
|
584
|
+
other_suits = [s for s in all_suits if s != up_card.suit]
|
|
585
|
+
labels = [s.symbol for s in other_suits] + ["TA", "SA", "Pass"]
|
|
586
|
+
round2_suits: list[Suit | str | None] = [
|
|
587
|
+
*other_suits,
|
|
588
|
+
Suit.TOUT_ATOUT,
|
|
589
|
+
SANS_ATOUT_BID,
|
|
590
|
+
None,
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
lines: list[str] = []
|
|
594
|
+
|
|
595
|
+
tendency = state._joker_state.get("partner_bid_tendency_text")
|
|
596
|
+
if isinstance(tendency, str) and tendency:
|
|
597
|
+
lines.append(ansi_center(f"{DIM}{tendency}{RESET}", term_w))
|
|
598
|
+
|
|
599
|
+
if state.bidding_round == 2:
|
|
600
|
+
inner_w = 60
|
|
601
|
+
opt_str = ""
|
|
602
|
+
for i, lbl in enumerate(labels):
|
|
603
|
+
prefix = f"{BOLD}{gold_fg()}" if i == bid_selection else ""
|
|
604
|
+
color = ""
|
|
605
|
+
opt = round2_suits[i]
|
|
606
|
+
if isinstance(opt, Suit):
|
|
607
|
+
color = red_fg() if opt.is_red else black_fg()
|
|
608
|
+
entry = f"{prefix}({i + 1}) {color}{lbl}{RESET}{prefix}"
|
|
609
|
+
entry = f"{REVERSE} {entry} {RESET}" if i == bid_selection else f" {entry} "
|
|
610
|
+
opt_str += entry + " "
|
|
611
|
+
|
|
612
|
+
box = [
|
|
613
|
+
f"┌{'─' * inner_w}┐",
|
|
614
|
+
f"│{f'ROUND {state.bidding_round} BID'.center(inner_w)}│",
|
|
615
|
+
f"├{'─' * inner_w}┤",
|
|
616
|
+
f"│{' ' * inner_w}│",
|
|
617
|
+
f"│{ansi_center(opt_str.strip(), inner_w)}│",
|
|
618
|
+
f"│{' ' * inner_w}│",
|
|
619
|
+
f"└{'─' * inner_w}┘",
|
|
620
|
+
]
|
|
621
|
+
lines.extend(ansi_center(bl, term_w) for bl in box)
|
|
622
|
+
else:
|
|
623
|
+
parts = [
|
|
624
|
+
f"{BOLD}{gold_fg()}({i + 1}){lbl}{RESET}" if i == bid_selection else f"({i + 1}){lbl}"
|
|
625
|
+
for i, lbl in enumerate(labels)
|
|
626
|
+
]
|
|
627
|
+
prompt = f"{BOLD}{white_fg()}Round {state.bidding_round} Bid: {' '.join(parts)}{RESET}"
|
|
628
|
+
lines.append(ansi_center(prompt, term_w))
|
|
629
|
+
|
|
630
|
+
return lines
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def render(
|
|
634
|
+
state: GameState,
|
|
635
|
+
selection: int | None = None,
|
|
636
|
+
show_north_hand: bool = False,
|
|
637
|
+
bid_selection: int | None = None,
|
|
638
|
+
) -> str:
|
|
566
639
|
"""Pure: returns a full-screen ANSI-formatted string. No I/O.
|
|
567
640
|
|
|
568
641
|
Terminal width is queried fresh on every call so resizing works correctly.
|
|
569
642
|
The layout preset (compact / standard / spacious) is also picked fresh from
|
|
570
643
|
the current dimensions, so resizing the terminal mid-game adapts on the
|
|
571
644
|
next render.
|
|
645
|
+
|
|
646
|
+
`bid_selection` paints the bidding selector with the highlighted option
|
|
647
|
+
inside the frame. Pass `None` for non-interactive renders (the simple
|
|
648
|
+
static "Bid: [P]ass [1]♠..." hint shows instead).
|
|
572
649
|
"""
|
|
573
650
|
term_w, term_h = get_term_size()
|
|
574
651
|
layout = choose_layout(term_w, term_h)
|
|
@@ -657,8 +734,11 @@ def render(state: GameState, selection: int | None = None, show_north_hand: bool
|
|
|
657
734
|
if state.phase == Phase.BIDDING and state.turn == Seat.SOUTH:
|
|
658
735
|
if term_h > 40:
|
|
659
736
|
lines.append("")
|
|
660
|
-
|
|
661
|
-
|
|
737
|
+
if bid_selection is not None:
|
|
738
|
+
lines.extend(_build_bid_prompt_lines(state, term_w, bid_selection))
|
|
739
|
+
else:
|
|
740
|
+
prompt = f"{BOLD}{gold_fg()}Bid: [P]ass [1]♠ [2]♥ [3]♦ [4]♣{RESET}"
|
|
741
|
+
lines.append(ansi_center(prompt, term_w))
|
|
662
742
|
|
|
663
743
|
# ── Vertical centering ──────────────────────────────────────────────────
|
|
664
744
|
# If the terminal is taller than the rendered content, pad top + bottom so
|
|
@@ -670,12 +750,11 @@ def render(state: GameState, selection: int | None = None, show_north_hand: bool
|
|
|
670
750
|
bottom_pad = slack - top_pad
|
|
671
751
|
lines = [""] * top_pad + lines + [""] * bottom_pad
|
|
672
752
|
|
|
673
|
-
#
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
]
|
|
753
|
+
# Always emit clear_to_eol on every row, including blank padding. Konsole
|
|
754
|
+
# (and other strict emulators) don't auto-blank cells when an empty string
|
|
755
|
+
# passes through, so any debris from external writes (announcements, etc.)
|
|
756
|
+
# would remain visible. The cost is one extra 3-byte escape per row.
|
|
757
|
+
rendered_lines = [line + clear_to_eol() for line in lines[:term_h]]
|
|
679
758
|
return "".join([out, "\r\n".join(rendered_lines), show_cursor()])
|
|
680
759
|
|
|
681
760
|
|
|
@@ -687,8 +766,13 @@ def display_hud(state: GameState) -> None:
|
|
|
687
766
|
sys.stdout.flush()
|
|
688
767
|
|
|
689
768
|
|
|
690
|
-
def display(
|
|
691
|
-
|
|
769
|
+
def display(
|
|
770
|
+
state: GameState,
|
|
771
|
+
selection: int | None = None,
|
|
772
|
+
show_north_hand: bool = False,
|
|
773
|
+
bid_selection: int | None = None,
|
|
774
|
+
) -> None:
|
|
775
|
+
sys.stdout.write(render(state, selection, show_north_hand, bid_selection=bid_selection))
|
|
692
776
|
sys.stdout.flush()
|
|
693
777
|
|
|
694
778
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|