belote-cli 2.9.2__tar.gz → 3.0.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.2 → belote_cli-3.0.2}/.claude/settings.local.json +2 -1
- {belote_cli-2.9.2 → belote_cli-3.0.2}/CHANGELOG.md +138 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/DEVELOPMENT.md +45 -8
- {belote_cli-2.9.2 → belote_cli-3.0.2}/PKG-INFO +32 -9
- {belote_cli-2.9.2 → belote_cli-3.0.2}/README.md +31 -8
- {belote_cli-2.9.2 → belote_cli-3.0.2}/pyproject.toml +1 -1
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/__init__.py +1 -1
- belote_cli-3.0.2/src/belote/a11y.py +93 -0
- belote_cli-3.0.2/src/belote/achievements.py +104 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ai.py +48 -14
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ansi.py +42 -18
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/scoring.py +38 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/modifier_patch.py +2 -1
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/round_driver.py +26 -0
- belote_cli-3.0.2/src/belote/belatro/ghost_run.py +83 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/base.py +19 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/registry.py +27 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/main.py +34 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/boss.py +50 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/shop.py +37 -2
- belote_cli-3.0.2/src/belote/belatro/run_summary.py +71 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/announce.py +28 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/hud.py +49 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/config.py +8 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/game.py +21 -4
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/gameflow.py +52 -1
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/input.py +12 -8
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/main.py +2 -2
- belote_cli-3.0.2/src/belote/replay.py +69 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/scoring.py +171 -20
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/stats.py +21 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/themes.py +35 -2
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/menu.py +8 -3
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/prompts.py +118 -33
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/render.py +207 -62
- belote_cli-3.0.2/tests/belatro/test_dead_flag_fixes.py +780 -0
- belote_cli-3.0.2/tests/belatro/test_ghost_run.py +49 -0
- belote_cli-3.0.2/tests/belatro/test_hud_synergy.py +61 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase0_coverage.py +27 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_progression.py +63 -0
- belote_cli-3.0.2/tests/test_a11y.py +135 -0
- belote_cli-3.0.2/tests/test_achievements.py +58 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_ai.py +66 -0
- belote_cli-3.0.2/tests/test_ansi_helpers.py +162 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_belote.py +91 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_extended.py +44 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_layout.py +2 -2
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_official_rules.py +3 -2
- belote_cli-3.0.2/tests/test_replay.py +48 -0
- belote_cli-2.9.2/tests/belatro/test_dead_flag_fixes.py +0 -290
- {belote_cli-2.9.2 → belote_cli-3.0.2}/.gitignore +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/.python-version +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/LICENSE +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/scripts/benchmark.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/core/run_state.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/context.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/deck.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/rules.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/announce.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/src/belote/ui/layout.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/__init__.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_belatro.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_boss_modifiers_integration.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/belatro/test_round_driver.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_game_logic.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_gameflow.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_new_coverage.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_properties.py +0 -0
- {belote_cli-2.9.2 → belote_cli-3.0.2}/tests/test_undo.py +0 -0
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"Bash(python3 -m pytest tests/ -q --tb=no)",
|
|
11
11
|
"Bash(python3 -m pytest tests/ -x -q)",
|
|
12
12
|
"Bash(PYTHONPATH=src python3 *)",
|
|
13
|
-
"Bash(.venv/bin/python -m mypy src/)"
|
|
13
|
+
"Bash(.venv/bin/python -m mypy src/)",
|
|
14
|
+
"Bash(PYTHONPATH=src python -m pytest --tb=short -q)"
|
|
14
15
|
]
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -5,6 +5,144 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.0.2] - 2026-05-08
|
|
9
|
+
|
|
10
|
+
Audit pass — wired two previously-dead 3.0.0 modules behind opt-in env vars, removed redundant work from `score_round()`, and pinned every boss modifier's patch keys against typo regressions.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`src/belote/belatro/main.py` + `src/belote/belatro/engine/round_driver.py`** — `GhostRecorder` (`src/belote/belatro/ghost_run.py`) was imported nowhere outside its own tests since it shipped in 3.0.0; ghost recording silently never happened. Wired through `drive_round()` via a new optional `recorder` param so bids, plays, and round-end breakdowns are now captured. Gated on `BELOTE_GHOST=1` so default play is unchanged. Saves to `~/.local/share/belote/ghosts/<label>-<seed>.json` on run end.
|
|
15
|
+
- **`src/belote/gameflow.py`** — `replay.analyze_round()` / `summarize()` (`src/belote/replay.py`) was never called from any runtime path since it shipped in 3.0.0; the post-round Hard-AI comparison the README advertised silently never fired. `run_play()` now optionally accumulates `(state, played_card)` pairs for South; `run_round()` reads `BELOTE_REPLAY=1` once per round, runs the analyzer post-scoring, and prints a one-line `Replay: Optimal plays: N/M (P%)` summary. UNDO clears the buffer so the report matches the play that actually finished the round.
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
|
|
19
|
+
- **`src/belote/scoring.py::score_round`** — pre-computes the per-trick winner list once at the top and threads it into `_calculate_base_points` and `_apply_scoring_modifiers`. Both helpers used to re-call `trick_winner_seat()` for every completed trick; under `separate_scoring` + `queen_spades_penalty` the 8-trick walk could run 3× per round.
|
|
20
|
+
- **`src/belote/belatro/items/registry.py::register_all_items`** — now idempotent. A module-level `_registered` flag short-circuits subsequent calls; second clause `and registry.jokers` re-runs when a caller has swapped the global to a fresh empty `ItemRegistry` (the test-suite pattern at `tests/belatro/test_belatro.py::TestItemRegistry.setup_method`). Saves the 4× `dir(mod)` walk on every `BelAtroRun` after the first.
|
|
21
|
+
- **`src/belote/ai.py::_special_bid`** — `_suit_lengths(hand)` is now computed once and threaded into `_easy_special` / `_medium_special` / `_hard_special`. The three branches each used to rebuild it from scratch.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`tests/belatro/test_phase0_coverage.py::test_every_boss_modifier_actually_patches_a_flag`** — pin against a typo'd `state.patch("_misspelled", True)` key silently producing a no-op boss. Iterates `ALL_BOSS_MODIFIERS`, asserts each `.flags()` differs from default `BossModifiers()`. The `boss_fields` allow-list in `engine/modifier_patch.py` is now load-bearing for correctness — this test surfaces drift.
|
|
26
|
+
- **`DEVELOPMENT.md`** — new "Optional Runtime Flags" section documenting `BELOTE_REPLAY` and `BELOTE_GHOST` next to the existing `BELOTE_A11Y` entry.
|
|
27
|
+
|
|
28
|
+
### Internal
|
|
29
|
+
|
|
30
|
+
- **Tests**: 509 → 510 (+1).
|
|
31
|
+
- **Strict gates**: pytest 510/510, mypy 0 errors, ruff 0 violations.
|
|
32
|
+
|
|
33
|
+
### Known issue (not fixed in this cut)
|
|
34
|
+
|
|
35
|
+
- `src/belote/belatro/engine/event_bus.py::EventBus.emit` is never called anywhere in the source. `unlock_tracker.subscribe_to(bus)` registers `on_event` but receives no events, so `_handle_round_end`'s unlocks for **L'Exécuteur** (first Capot), **L'Idéologue** (Sans Atout NS win), and **Le Fanatique** (Tout Atout NS win) silently never fire under normal play. Out of scope for 3.0.2 (fix would change ordering with `acc.update_state` in `round_driver._emit`); flagged for a follow-up.
|
|
36
|
+
|
|
37
|
+
## [3.0.1] - 2026-05-07
|
|
38
|
+
|
|
39
|
+
Post-3.0.0 audit pass — four player-visible / correctness bugs introduced or missed by the 3.0.0 cut, plus a batch of test-coverage and small-correctness improvements. No behaviour changes for code paths that were already correct.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`src/belote/game.py:860-873`** — `play_card()`'s per-trick running total honoured `kings_zero` and `tens_zero` but ignored the new 3.0.0 `aces_zero` and `jacks_zero` flags. Final scoring (`scoring.py::_calculate_base_points`) was correct, but the live HUD running total under Le Sauvage / L'Iconoclaste was wrong until the round ended. Mirrored the canonical `scoring.py` zero-rank pattern; `bm` aliased once for readability.
|
|
44
|
+
- **`src/belote/belatro/ui/hud.py::_SYNERGY_PAIRS`** — referenced four joker IDs (`le_finisseur`, `le_dix_de_der`, `le_marseillais`, `carre_aces_x2`) that don't exist in the registry. Two of the four pairs were dead code that could never fire. Removed; `validate_synergy_ids()` now exposes a self-check, and `register_all_items()` asserts every synergy ID resolves so future typos surface at import time.
|
|
45
|
+
- **`src/belote/gameflow.py:248-258`** — the a11y trick-winner announcement used raw `card_points()` and ignored every boss zero-rank flag, so screen-reader users heard inflated trick scores under Le Sauvage / L'Iconoclaste / Le Roi Mort / Les Dix Maudits / Les Clubs Bannis. New canonical helper `scoring.trick_card_points(state, trick)` is now the single source of truth for per-trick boss-aware totals.
|
|
46
|
+
- **`src/belote/ai.py::AIMemory.last_voids_key`** — the new-round reset in `update_memory()` cleared `played`, `known_voids`, and `processed_tricks_count` but not the cache key. After the prior round's final key (e.g. `(7, 4)`), a fresh-round `(0, 0)` or `(0, 1)` could coincidentally match a key seen during round 1 and cause `_update_voids` to skip processing. Added `last_voids_key = None` to the reset.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **`src/belote/scoring.py::trick_card_points`** — public helper for "card-point sum of one trick under all active boss zero-rank flags." Used by gameflow's a11y hook.
|
|
51
|
+
- **`src/belote/belatro/ui/hud.py::validate_synergy_ids`** — return the synergy IDs that aren't registered as jokers. Used by `register_all_items()` for the new startup self-check.
|
|
52
|
+
- **Tests**: `tests/test_a11y.py` (8 cases for `trick_card_points` + a11y stderr emit), `tests/belatro/test_hud_synergy.py` (5 cases for the synergy registry), HOLO/POLYCHROME edition tests + four `separate_scoring × zero-flag` composition tests in `tests/belatro/test_dead_flag_fixes.py`, cross-round void-cache regression test in `tests/test_ai.py`. Tests grew 489 → 509 (+20).
|
|
53
|
+
|
|
54
|
+
### Improved
|
|
55
|
+
|
|
56
|
+
- **`src/belote/belatro/run_summary.py`** — resolved path is cached at module level after the first call, so `mkdir` is no longer re-attempted on every BelAtro exit.
|
|
57
|
+
- **`src/belote/a11y.py`** — `BELOTE_A11Y` env var resolved once at module import (`_ENABLED` module variable). Tests use `_refresh_enabled_from_env()` to re-read after `monkeypatch.setenv`. Saves ~30 environ lookups per round in the disabled path.
|
|
58
|
+
- **`src/belote/scoring.py:649-668`** — Sans Atout Capot branch now asserts `taker_belote == 0 and defender_belote == 0`. Belote/Rebelote requires a unique trump suit, so this invariant should always hold under SA — the assertion documents it and surfaces any future regression that leaks belote points into the SA path.
|
|
59
|
+
- **`src/belote/belatro/run/boss.py::LeMime`** — docstring notes the redundancy between `declarations_zero` and `separate_scoring` and points to the regression test that pins their composition.
|
|
60
|
+
|
|
61
|
+
### Internal
|
|
62
|
+
|
|
63
|
+
- **Tests**: 489 → 509 (+20). All new modules covered: a11y boss-aware pts (8), HUD synergy registry (5), HOLO/POLYCHROME editions (2), separate_scoring × zero-flag matrix (4), cross-round void cache (1).
|
|
64
|
+
- **Strict gates**: pytest 509/509, mypy 0 errors, ruff 0 violations.
|
|
65
|
+
- **Perf baseline**: unchanged from 3.0.0 (sub-millisecond throughout).
|
|
66
|
+
|
|
67
|
+
## [3.0.0] - 2026-05-07
|
|
68
|
+
|
|
69
|
+
Bug-hunt + audit pass — three player-visible features that were registered but silently no-ops are now wired, the Capot scoring under Sans Atout / Tout Atout has been corrected, mypy is once again strict-clean, and a batch of P3 features lands behind the new BelAtro Endless mode flow.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **`src/belote/scoring.py::score_round`** — Capot reward used a flat `CAPOT_BASE = 252` for every contract, over-paying SA Capots (252 vs the contract-correct 220) and under-paying TA Capots (252 vs 348). New `CAPOT_BASE_SANS_ATOUT = 220` and `CAPOT_BASE_TOUT_ATOUT = 348` constants in `config.py`; scoring now branches on `state.contract`. `tests/test_belote.py::TestCapotPerContract` covers all three contracts; `tests/test_official_rules.py::test_sans_atout_score_round_baseline` updated from 252 → 220.
|
|
74
|
+
- **`src/belote/belatro/items/planets.py::TheSun`** — `level_up_reward()` returned `{"bonus_mult_per_trick": 1.0}` but no consumer ever read the key. Wired into `belatro/core/scoring.py::ScoreAccumulator` on `TrickWonEvent` when `event.trump == Suit.TOUT_ATOUT and event.trick_number > 4`.
|
|
75
|
+
- **`src/belote/belatro/items/planets.py::Libra`** — `coinche_multiplier` was set on the contract level but never read. Now consumed at `RoundEndEvent` time, scaled by `event.coinche_level`, and gated on the round being a non-failed taker win for NS.
|
|
76
|
+
- **`src/belote/scoring.py`** — `RoundScore` was constructed via `**common_kwargs` splat; mypy lost the per-field types and reported 8 errors. Inlined into both branches.
|
|
77
|
+
- **`src/belote/ui/render.py::_slot_frame_row`** — variable shadowing (`for c in range(...)` then `for c in cells`) made mypy infer `cells[i]` as `int`. Renamed the inner loop variables (`c → i`, `c → cell`).
|
|
78
|
+
- **`src/belote/ui/prompts.py`** — three untyped helpers (`_hist_taker_label`, `_hist_contract_label`, `_hist_status`) now annotated with `RoundScore`.
|
|
79
|
+
- **`src/belote/ui/prompts.py::show_history`** — N806 lint on `W_RD/W_TKR/...` constants resolved by lowercasing the locals; behaviour unchanged.
|
|
80
|
+
- **`src/belote/ai.py::_process_trick_voids`** — under the `republicain_wild` flag (Le Républicain deck / boss reuse), playing a 7 or 8 off-suit no longer falsely flags the player as void in the lead suit. Hard AI's void inference now skips wild ranks when the flag is active.
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- **`src/belote/belatro/main.py`** — post-Ante-8 endless prompt. After winning Ante 8, the run offers `Continue into Endless Mode? (Ante 9+ scales ×2.2)`. Built on the existing `BelAtroRun.endless` / `endless_ante_offset` infrastructure plus a new `BelAtroAnnounce.yes_no` helper.
|
|
85
|
+
- **`src/belote/belatro/items/base.py::Edition`** — new enum (NONE/FOIL/HOLO/POLYCHROME/NEGATIVE). Shop generation rolls per-joker editions; Foil adds +50 chips per trigger, Holo +10 mult, Polychrome ×1.5 mult, Negative grants an extra joker slot at purchase. Wiring lives in `belatro/run/shop.py` and `belatro/core/scoring.py::_apply_edition`.
|
|
86
|
+
- **`src/belote/belatro/run/boss.py`** — three new boss blinds (Le Sauvage / L'Iconoclaste / Le Mime) and three new `BossModifiers` flags (`aces_zero`, `jacks_zero`, `declarations_zero`) read by the existing scoring path.
|
|
87
|
+
- **`src/belote/belatro/ui/hud.py::detect_synergies`** — small registry of known joker pair combos; renders a `★ SYN×N` badge on the HUD when any pair (or 3+ jokers) is held.
|
|
88
|
+
- **`src/belote/achievements.py`** + **`src/belote/stats.py::Statistics.achievements`** — six classic-mode achievements (first Capot, 3 Capots in a session, 2 Capot streak, 300+ point round, hard win, ten games played) auto-evaluated post-round / post-game.
|
|
89
|
+
- **`src/belote/themes.py::THEMES["colorblind"]`** — deuteranopia/protanopia-friendly palette using blue/cyan/orange instead of red/green.
|
|
90
|
+
- **`src/belote/a11y.py`** — screen-reader hints. Cards played, trick winners, and round results emit one-line plain-text descriptions to stderr when the env var `BELOTE_A11Y=1` is set.
|
|
91
|
+
- **`src/belote/replay.py`** — `analyze_round()` runs the just-played decisions through the Hard AI and reports per-decision agreement; `summarize()` produces a one-line `Optimal: N/M (X%)` string.
|
|
92
|
+
- **`src/belote/belatro/ghost_run.py`** — `GhostRecorder` accumulates seed + bid + play events and serializes them to JSONL under the user data dir. Foundation for a future ghost-replay viewer; the JSON format is versioned (v1).
|
|
93
|
+
- **`src/belote/belatro/run_summary.py`** — appends a one-line per-run summary (deck, ante, jokers, won) to `~/.local/share/belote/run_history.jsonl` on BelAtro exit. Best-effort, OSError-swallowed.
|
|
94
|
+
- **`src/belote/gameflow.py::show_hand_preview`** — short `Dealing…` flourish before the hand preview, gated on the existing speed setting and skippable via Space/Esc.
|
|
95
|
+
|
|
96
|
+
### Improved
|
|
97
|
+
|
|
98
|
+
- **`src/belote/ansi.py`** — 16 palette accessors (`felt_bg()`, `red_fg()`, …) previously hit `theme_manager.get_current()` per call. Cached the active `Theme` at module level and registered an invalidation callback with `theme_manager`. Lower per-render dict-lookup overhead.
|
|
99
|
+
- **`src/belote/themes.py::ThemeManager`** — redundant class-level `_current_theme_name` removed; new public `current_name` property; `_initialized` guard now uses `getattr` for clarity. Eight call sites in `ui/menu.py`, `ui/prompts.py`, and `ui/render.py` migrated off `_current_theme_name`.
|
|
100
|
+
- **`src/belote/ui/render.py`** — `_LAST_RENDER_KEY` list-of-one singleton replaced with a module-level variable + `global` declaration; same behaviour, less Python idiom drag.
|
|
101
|
+
- **`src/belote/ai.py::AIMemory.last_voids_key`** — caches `(completed_count, current_trick_len)` so `_update_voids` short-circuits on repeat calls within the same trick decision (lookahead exploration triggered redundant scans).
|
|
102
|
+
|
|
103
|
+
### Internal
|
|
104
|
+
|
|
105
|
+
- **Performance baseline (post-fix)**: `scripts/benchmark.py` reports render 0.271ms (±0.044), AI Easy/Med/Hard 0.015 / 0.030 / 0.026 ms, BelAtro update 0.032 ms, scoring 0.169 ms, deal 0.071 ms, legal_cards 0.012 ms.
|
|
106
|
+
- **Tests**: 446 → 489 (+43). New files: `tests/test_ansi_helpers.py`, `tests/test_achievements.py`, `tests/test_replay.py`, `tests/belatro/test_ghost_run.py`. Existing files extended with Capot-per-contract, Sun/Libra wiring, TA→le_fanatique unlock, republicain-void edge case, and three new boss tests.
|
|
107
|
+
- **Strict mode**: README's "0 mypy errors / 0 ruff violations" claim was inaccurate at 2.9.5 (18 mypy + 10 ruff at audit time). Both gates are now actually clean.
|
|
108
|
+
|
|
109
|
+
## [2.9.5] - 2026-05-07
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
|
|
115
|
+
- **`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.
|
|
116
|
+
- **`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.
|
|
117
|
+
- **`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.
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- **`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.
|
|
122
|
+
- **`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.
|
|
123
|
+
- **`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.
|
|
124
|
+
- **`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.
|
|
125
|
+
- **`src/belote/ui/render.py::_card_face_internal`** — full GRIMAUD-1898-inspired redraw:
|
|
126
|
+
- Both corners now carry a 3-cell `rank+suit` index (`A♠` top-left, `♠A` bottom-right). The index padding scales with `inner_w`.
|
|
127
|
+
- Pip cards (7-10) at `card_h ≥ 7` get a recognisable pip arrangement instead of a single centred suit symbol.
|
|
128
|
+
- Court cards J/Q/K each get a distinct multi-row motif (sword, jewelled headdress, crown).
|
|
129
|
+
- Aces get a decorative `╭─◆─╮` / `╰─◆─╯` wreath around the central suit.
|
|
130
|
+
- Compact 6×5 layout keeps the single inner row but still benefits from both-corner indices.
|
|
131
|
+
- 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.
|
|
132
|
+
- **`tests/test_extended.py::test_round_score_history_extra_fields`** — pins the new `RoundScore` fields end-to-end through `apply_round_score`.
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- **`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.
|
|
137
|
+
- **`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.
|
|
138
|
+
- **`src/belote/ui/prompts.py::show_help`** — help-screen text rewritten to match the new bindings.
|
|
139
|
+
- **`README.md`** — Controls section rewritten; theme section now lists all six themes by name.
|
|
140
|
+
- **`tests/test_layout.py::test_hud_compact_omits_help_hints_and_theme`** — assertion updated for the new compact-HUD hint substring.
|
|
141
|
+
|
|
142
|
+
### Notes
|
|
143
|
+
|
|
144
|
+
436/436 tests pass. No gameplay, scoring, or AI-decision changes — all updates are UX (keys, slot framing, dwell, history depth, card glyphs).
|
|
145
|
+
|
|
8
146
|
## [2.9.2] - 2026-05-07
|
|
9
147
|
|
|
10
148
|
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.
|
|
@@ -78,21 +78,28 @@ PYTHONPATH=src pytest --cov=belote --cov-report=term-missing
|
|
|
78
78
|
The project maintains zero lint and type-check violations. Run all checks with:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
# Type checking (0 errors expected)
|
|
82
|
-
PYTHONPATH=src mypy
|
|
81
|
+
# Type checking (0 errors expected, strict mode)
|
|
82
|
+
PYTHONPATH=src mypy --strict src/
|
|
83
83
|
|
|
84
84
|
# Linting (0 violations expected)
|
|
85
|
-
ruff check
|
|
86
|
-
# Full test suite (435 tests expected)
|
|
87
|
-
PYTHONPATH=src pytest
|
|
85
|
+
ruff check src/ tests/
|
|
88
86
|
|
|
89
|
-
#
|
|
87
|
+
# Full test suite (510 tests expected)
|
|
88
|
+
PYTHONPATH=src pytest
|
|
89
|
+
```
|
|
90
90
|
|
|
91
|
-
Current baseline:
|
|
91
|
+
Current baseline (3.0.2):
|
|
92
92
|
- **mypy**: 0 errors (strict mode)
|
|
93
93
|
- **ruff**: 0 violations
|
|
94
|
-
- **pytest**:
|
|
94
|
+
- **pytest**: 510 tests, 0 failures
|
|
95
95
|
|
|
96
|
+
Run all gates before committing:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
PYTHONPATH=src python -m pytest --tb=short -q && \
|
|
100
|
+
python -m mypy --strict src/ && \
|
|
101
|
+
python -m ruff check src/ tests/
|
|
102
|
+
```
|
|
96
103
|
|
|
97
104
|
## Benchmarking
|
|
98
105
|
|
|
@@ -101,6 +108,36 @@ A benchmarking script is provided to measure rendering and AI performance:
|
|
|
101
108
|
PYTHONPATH=src python scripts/benchmark.py
|
|
102
109
|
```
|
|
103
110
|
|
|
111
|
+
3.0.0 baseline numbers (Linux, Python 3.10+, 1000 iterations):
|
|
112
|
+
- Render: 0.27 ms (±0.04)
|
|
113
|
+
- AI Hard decide_card: 0.026 ms (±0.003)
|
|
114
|
+
- BelAtro state update: 0.032 ms (±0.004)
|
|
115
|
+
- score_round: 0.169 ms
|
|
116
|
+
- legal_cards: 0.012 ms
|
|
117
|
+
|
|
118
|
+
Use these as a regression-detection floor for future changes.
|
|
119
|
+
|
|
120
|
+
## Accessibility
|
|
121
|
+
|
|
122
|
+
Set `BELOTE_A11Y=1` to emit one-line plain-text descriptions of card plays,
|
|
123
|
+
trick winners, and round results to stderr — readable by terminal screen
|
|
124
|
+
readers (Orca, NVDA over WSL, VoiceOver via iTerm2).
|
|
125
|
+
|
|
126
|
+
## Optional Runtime Flags
|
|
127
|
+
|
|
128
|
+
The following environment variables enable opt-in features. Each is read
|
|
129
|
+
once at startup; toggling mid-run has no effect.
|
|
130
|
+
|
|
131
|
+
- `BELOTE_REPLAY=1` — after every Classic round, print a one-line summary
|
|
132
|
+
of how often South's plays matched the Hard-AI's preferred line
|
|
133
|
+
(e.g. `Replay: Optimal plays: 6/8 (75%)`). Educational only — never
|
|
134
|
+
affects scoring. Backed by `src/belote/replay.py`.
|
|
135
|
+
- `BELOTE_GHOST=1` — silently record every BelAtro run (seed, deck,
|
|
136
|
+
bids, plays, round outcomes) to
|
|
137
|
+
`~/.local/share/belote/ghosts/<label>-<seed>.json`. The file is written
|
|
138
|
+
once when the run ends. Useful for sharing or replaying interesting
|
|
139
|
+
runs. Backed by `src/belote/belatro/ghost_run.py`.
|
|
140
|
+
|
|
104
141
|
## Releasing a New Version
|
|
105
142
|
|
|
106
143
|
### Code-only update (push to GitHub without releasing a new PyPI version)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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
|
|
@@ -45,7 +45,30 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
|
|
46
46
|
Complete implementation of the French card game Belote for the terminal, with a full-screen green felt table and full card graphics at compass positions (N/W/E/S).
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## What's new in 3.0.2
|
|
49
|
+
|
|
50
|
+
- **Replay analyzer + Ghost run wired up** — both shipped in 3.0.0 as code modules but were never called from the running game. Now opt-in behind `BELOTE_REPLAY=1` (post-round Hard-AI comparison) and `BELOTE_GHOST=1` (per-run JSON dump to `~/.local/share/belote/ghosts/`). See DEVELOPMENT.md › Optional Runtime Flags.
|
|
51
|
+
- **Performance** — `score_round()` now caches per-trick winners once instead of recomputing them in each boss-modifier helper (2-3× walks → 1× walk per round). `register_all_items()` is now idempotent so test setup no longer re-walks every items module per `BelAtroRun`. Bidding's special-bid path (TA / SA) hoists `_suit_lengths` out of the per-difficulty branches.
|
|
52
|
+
- **Defensive pin** — every entry in `ALL_BOSS_MODIFIERS` is now asserted to actually toggle a `BossModifiers` field via `.flags()`. Catches typo'd `state.patch("_misspelled", True)` keys at test time rather than letting the boss silently no-op.
|
|
53
|
+
- **Test coverage** — 510 tests (up from 509).
|
|
54
|
+
|
|
55
|
+
## What's new in 3.0.1
|
|
56
|
+
|
|
57
|
+
- **Bug fixes** — `play_card()` running total now honours `aces_zero` / `jacks_zero` (Le Sauvage / L'Iconoclaste); the screen-reader trick-winner pts are now boss-aware; the HUD synergy registry no longer references nonexistent joker IDs; the AI void-cache key is now reset across rounds.
|
|
58
|
+
- **Test coverage** — 509 tests (up from 489): HOLO/POLYCHROME editions, multi-boss composition (`separate_scoring × zero-flag`), a11y boss-aware pts, synergy registry self-check.
|
|
59
|
+
|
|
60
|
+
## What's new in 3.0.0
|
|
61
|
+
|
|
62
|
+
- **Endless mode** — beat Ante 8 in BelAtro and the run offers a continuation: targets scale ×2.2 per ante and a furthest-ante leaderboard tracks how deep you go.
|
|
63
|
+
- **Joker editions** — Foil (+50 chips), Holo (+10 mult), Polychrome (×1.5 mult), Negative (extra slot) randomly stamp shop jokers.
|
|
64
|
+
- **Three new boss blinds** — Le Sauvage (Aces = 0), L'Iconoclaste (Jacks = 0, even trump-J), Le Mime (Declarations = 0).
|
|
65
|
+
- **Achievements** — six classic-mode milestones tracked across sessions.
|
|
66
|
+
- **Colorblind palette** + **screen-reader hints** (`BELOTE_A11Y=1`) for accessibility.
|
|
67
|
+
- **Replay analyzer** — module added (post-round Hard-AI comparison). User-facing wiring landed in 3.0.2 behind `BELOTE_REPLAY=1`.
|
|
68
|
+
- **Ghost run recording** + **run summary log** — modules added (serialize a run / append per-run JSON). Run-summary fires automatically; ghost-run user-facing wiring landed in 3.0.2 behind `BELOTE_GHOST=1`.
|
|
69
|
+
- **Bug fixes** — Capot under Sans Atout / Tout Atout now uses the correct base (220 / 348, not 252); The Sun and Libra planets actually do something now; AI void inference no longer mis-flags voids under Le Républicain wild 7/8.
|
|
70
|
+
|
|
71
|
+
## BelAtro Expansion
|
|
49
72
|
|
|
50
73
|
**BelAtro** is a major roguelite expansion inspired by *Balatro*. Play through 8 Antes of escalating difficulty, build a deck of powerful Jokers, and use Tarot cards and Planets to break the game!
|
|
51
74
|
|
|
@@ -170,12 +193,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
170
193
|
## Controls
|
|
171
194
|
|
|
172
195
|
**General:**
|
|
173
|
-
-
|
|
196
|
+
- `?`: Show keyboard shortcut help
|
|
174
197
|
- `M`: Toggle sound effects on/off
|
|
175
|
-
- `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
198
|
+
- `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
176
199
|
- `Q`: Quit to main menu or exit
|
|
177
|
-
- `
|
|
178
|
-
- `T`:
|
|
200
|
+
- `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
|
|
201
|
+
- `T`: Cycle UI Theme
|
|
179
202
|
|
|
180
203
|
**Classic Belote:**
|
|
181
204
|
- `↑` `↓`: Navigate options
|
|
@@ -183,7 +206,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
183
206
|
- `Enter`: Select option / Enter submenu
|
|
184
207
|
|
|
185
208
|
**BelAtro (Roguelite):**
|
|
186
|
-
- `S`: View current Run State and Jokers
|
|
187
209
|
- `1`-`5`: Inspect specific Jokers in the Shop
|
|
188
210
|
- `U`: Use a consumable (Tarot/Planet) during gameplay
|
|
189
211
|
|
|
@@ -194,6 +216,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
194
216
|
- `O`: Sort hand by suit and rank
|
|
195
217
|
- `Z`: Undo last move
|
|
196
218
|
- `Space` or `Esc`: Skip animations
|
|
219
|
+
- During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
|
|
197
220
|
|
|
198
221
|
## Features
|
|
199
222
|
|
|
@@ -204,7 +227,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
204
227
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
205
228
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
206
229
|
- **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
|
|
207
|
-
- **Customizable Themes:** Switch between
|
|
230
|
+
- **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
231
|
- **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
|
|
209
232
|
- **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
|
|
210
233
|
- **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
|
|
@@ -267,7 +290,7 @@ belote/
|
|
|
267
290
|
PYTHONPATH=src pytest
|
|
268
291
|
```
|
|
269
292
|
|
|
270
|
-
Currently **
|
|
293
|
+
Currently **510 tests** passing with 100% coverage on game-logic modules.
|
|
271
294
|
|
|
272
295
|
## Technical Integrity
|
|
273
296
|
|
|
@@ -2,7 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Complete implementation of the French card game Belote for the terminal, with a full-screen green felt table and full card graphics at compass positions (N/W/E/S).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What's new in 3.0.2
|
|
6
|
+
|
|
7
|
+
- **Replay analyzer + Ghost run wired up** — both shipped in 3.0.0 as code modules but were never called from the running game. Now opt-in behind `BELOTE_REPLAY=1` (post-round Hard-AI comparison) and `BELOTE_GHOST=1` (per-run JSON dump to `~/.local/share/belote/ghosts/`). See DEVELOPMENT.md › Optional Runtime Flags.
|
|
8
|
+
- **Performance** — `score_round()` now caches per-trick winners once instead of recomputing them in each boss-modifier helper (2-3× walks → 1× walk per round). `register_all_items()` is now idempotent so test setup no longer re-walks every items module per `BelAtroRun`. Bidding's special-bid path (TA / SA) hoists `_suit_lengths` out of the per-difficulty branches.
|
|
9
|
+
- **Defensive pin** — every entry in `ALL_BOSS_MODIFIERS` is now asserted to actually toggle a `BossModifiers` field via `.flags()`. Catches typo'd `state.patch("_misspelled", True)` keys at test time rather than letting the boss silently no-op.
|
|
10
|
+
- **Test coverage** — 510 tests (up from 509).
|
|
11
|
+
|
|
12
|
+
## What's new in 3.0.1
|
|
13
|
+
|
|
14
|
+
- **Bug fixes** — `play_card()` running total now honours `aces_zero` / `jacks_zero` (Le Sauvage / L'Iconoclaste); the screen-reader trick-winner pts are now boss-aware; the HUD synergy registry no longer references nonexistent joker IDs; the AI void-cache key is now reset across rounds.
|
|
15
|
+
- **Test coverage** — 509 tests (up from 489): HOLO/POLYCHROME editions, multi-boss composition (`separate_scoring × zero-flag`), a11y boss-aware pts, synergy registry self-check.
|
|
16
|
+
|
|
17
|
+
## What's new in 3.0.0
|
|
18
|
+
|
|
19
|
+
- **Endless mode** — beat Ante 8 in BelAtro and the run offers a continuation: targets scale ×2.2 per ante and a furthest-ante leaderboard tracks how deep you go.
|
|
20
|
+
- **Joker editions** — Foil (+50 chips), Holo (+10 mult), Polychrome (×1.5 mult), Negative (extra slot) randomly stamp shop jokers.
|
|
21
|
+
- **Three new boss blinds** — Le Sauvage (Aces = 0), L'Iconoclaste (Jacks = 0, even trump-J), Le Mime (Declarations = 0).
|
|
22
|
+
- **Achievements** — six classic-mode milestones tracked across sessions.
|
|
23
|
+
- **Colorblind palette** + **screen-reader hints** (`BELOTE_A11Y=1`) for accessibility.
|
|
24
|
+
- **Replay analyzer** — module added (post-round Hard-AI comparison). User-facing wiring landed in 3.0.2 behind `BELOTE_REPLAY=1`.
|
|
25
|
+
- **Ghost run recording** + **run summary log** — modules added (serialize a run / append per-run JSON). Run-summary fires automatically; ghost-run user-facing wiring landed in 3.0.2 behind `BELOTE_GHOST=1`.
|
|
26
|
+
- **Bug fixes** — Capot under Sans Atout / Tout Atout now uses the correct base (220 / 348, not 252); The Sun and Libra planets actually do something now; AI void inference no longer mis-flags voids under Le Républicain wild 7/8.
|
|
27
|
+
|
|
28
|
+
## BelAtro Expansion
|
|
6
29
|
|
|
7
30
|
**BelAtro** is a major roguelite expansion inspired by *Balatro*. Play through 8 Antes of escalating difficulty, build a deck of powerful Jokers, and use Tarot cards and Planets to break the game!
|
|
8
31
|
|
|
@@ -127,12 +150,12 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
127
150
|
## Controls
|
|
128
151
|
|
|
129
152
|
**General:**
|
|
130
|
-
-
|
|
153
|
+
- `?`: Show keyboard shortcut help
|
|
131
154
|
- `M`: Toggle sound effects on/off
|
|
132
|
-
- `I`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
155
|
+
- `I` or `V`: Toggle BelAtro score overlay (per-trick breakdown popup)
|
|
133
156
|
- `Q`: Quit to main menu or exit
|
|
134
|
-
- `
|
|
135
|
-
- `T`:
|
|
157
|
+
- `H`: View Game History (round-by-round, with contract / taker / tricks / declarations)
|
|
158
|
+
- `T`: Cycle UI Theme
|
|
136
159
|
|
|
137
160
|
**Classic Belote:**
|
|
138
161
|
- `↑` `↓`: Navigate options
|
|
@@ -140,7 +163,6 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
140
163
|
- `Enter`: Select option / Enter submenu
|
|
141
164
|
|
|
142
165
|
**BelAtro (Roguelite):**
|
|
143
|
-
- `S`: View current Run State and Jokers
|
|
144
166
|
- `1`-`5`: Inspect specific Jokers in the Shop
|
|
145
167
|
- `U`: Use a consumable (Tarot/Planet) during gameplay
|
|
146
168
|
|
|
@@ -151,6 +173,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
151
173
|
- `O`: Sort hand by suit and rank
|
|
152
174
|
- `Z`: Undo last move
|
|
153
175
|
- `Space` or `Esc`: Skip animations
|
|
176
|
+
- During bidding round 2: `P` = Pass, `A` = Tout Atout, `S` = Sans Atout
|
|
154
177
|
|
|
155
178
|
## Features
|
|
156
179
|
|
|
@@ -161,7 +184,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
161
184
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
162
185
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
163
186
|
- **Enhanced Hard AI**: Advanced void inference and 2-ply lookahead for critical tricks (Dix de Der).
|
|
164
|
-
- **Customizable Themes:** Switch between
|
|
187
|
+
- **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
188
|
- **Incremental Rendering:** High-performance cursor-based updates for zero-flicker gameplay even at high speeds.
|
|
166
189
|
- **Hand Sorting:** Strategic "play value" organization (honors grouped together) for better tactical awareness.
|
|
167
190
|
- **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
|
|
@@ -224,7 +247,7 @@ belote/
|
|
|
224
247
|
PYTHONPATH=src pytest
|
|
225
248
|
```
|
|
226
249
|
|
|
227
|
-
Currently **
|
|
250
|
+
Currently **510 tests** passing with 100% coverage on game-logic modules.
|
|
228
251
|
|
|
229
252
|
## Technical Integrity
|
|
230
253
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""3.0.0: optional screen-reader hints.
|
|
2
|
+
|
|
3
|
+
When the env var ``BELOTE_A11Y`` is truthy, key in-game events emit a plain-text
|
|
4
|
+
line to stderr — readable by terminal screen readers such as Orca, NVDA in WSL,
|
|
5
|
+
or VoiceOver via iTerm2. Disabled by default so it doesn't pollute output for
|
|
6
|
+
sighted players.
|
|
7
|
+
|
|
8
|
+
Hooked from gameflow.py (card plays, trick winners, round results) and from
|
|
9
|
+
belatro/main.py (boss reveal, ante advance, run won/lost). Each hook is a
|
|
10
|
+
single line — no rich formatting — so the screen reader can speak it cleanly.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .deck import Card
|
|
21
|
+
from .game import Seat
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 3.0.1: resolve the env var once at import. The flag is read on every card
|
|
25
|
+
# play (~32 times/round); a dict lookup is cheap but bypassing it keeps
|
|
26
|
+
# `speak()` essentially free in the disabled path.
|
|
27
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
28
|
+
_ENABLED: bool = os.environ.get("BELOTE_A11Y", "").lower() in _TRUTHY
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_enabled() -> bool:
|
|
32
|
+
"""Return whether a11y hints are enabled.
|
|
33
|
+
|
|
34
|
+
Reads the cached module-level value (set at import). Tests that use
|
|
35
|
+
`monkeypatch.setenv("BELOTE_A11Y", ...)` should call
|
|
36
|
+
`_refresh_enabled_from_env()` after the patch.
|
|
37
|
+
"""
|
|
38
|
+
return _ENABLED
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _refresh_enabled_from_env() -> None:
|
|
42
|
+
"""Re-read BELOTE_A11Y from the live environment. Public for tests."""
|
|
43
|
+
global _ENABLED
|
|
44
|
+
_ENABLED = os.environ.get("BELOTE_A11Y", "").lower() in _TRUTHY
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def speak(line: str) -> None:
|
|
48
|
+
"""Emit one line to stderr — only when BELOTE_A11Y is enabled."""
|
|
49
|
+
if _ENABLED:
|
|
50
|
+
sys.stderr.write(line + "\n")
|
|
51
|
+
sys.stderr.flush()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ── Convenience formatters ────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _suit_word(suit_symbol: str) -> str:
|
|
58
|
+
return {
|
|
59
|
+
"♠": "spades",
|
|
60
|
+
"♥": "hearts",
|
|
61
|
+
"♦": "diamonds",
|
|
62
|
+
"♣": "clubs",
|
|
63
|
+
}.get(suit_symbol, suit_symbol)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def card_word(card: Card) -> str:
|
|
67
|
+
rank = card.rank.value
|
|
68
|
+
rank_word = {
|
|
69
|
+
"7": "seven",
|
|
70
|
+
"8": "eight",
|
|
71
|
+
"9": "nine",
|
|
72
|
+
"10": "ten",
|
|
73
|
+
"J": "jack",
|
|
74
|
+
"Q": "queen",
|
|
75
|
+
"K": "king",
|
|
76
|
+
"A": "ace",
|
|
77
|
+
}.get(rank, rank)
|
|
78
|
+
return f"{rank_word} of {_suit_word(card.suit.symbol)}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def announce_play(seat: Seat, card: Card) -> None:
|
|
82
|
+
speak(f"{seat.name.lower()} plays {card_word(card)}.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def announce_trick_won(winner: Seat, points: int) -> None:
|
|
86
|
+
speak(f"{winner.name.lower()} wins the trick worth {points} points.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def announce_round_result(taker_total: int, defender_total: int, taker_team_label: str) -> None:
|
|
90
|
+
speak(
|
|
91
|
+
f"round complete. {taker_team_label} taker scores {taker_total}; "
|
|
92
|
+
f"defenders score {defender_total}."
|
|
93
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""3.0.0: lightweight achievement registry for classic Belote.
|
|
2
|
+
|
|
3
|
+
Achievements are defined statically here, evaluated against the running
|
|
4
|
+
``Statistics`` object after each round/game, and persisted via the existing
|
|
5
|
+
``stats.save_stats()`` path. BelAtro has its own unlock system in
|
|
6
|
+
``progression/save.py``; this module is for the classic mode only.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from .stats import Statistics
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Achievement:
|
|
18
|
+
id: str
|
|
19
|
+
title: str
|
|
20
|
+
description: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Catalog. New achievements: add a row here AND a check in
|
|
24
|
+
# `evaluate_round` / `evaluate_game`. IDs are stable strings — never rename.
|
|
25
|
+
ACHIEVEMENTS: tuple[Achievement, ...] = (
|
|
26
|
+
Achievement(
|
|
27
|
+
"first_capot",
|
|
28
|
+
"Le Grand Chelem",
|
|
29
|
+
"Score your first Capot.",
|
|
30
|
+
),
|
|
31
|
+
Achievement(
|
|
32
|
+
"capot_x3",
|
|
33
|
+
"Triple Coup",
|
|
34
|
+
"Score 3 Capots in a single session.",
|
|
35
|
+
),
|
|
36
|
+
Achievement(
|
|
37
|
+
"capot_streak_2",
|
|
38
|
+
"Vague de Capots",
|
|
39
|
+
"Capot two rounds in a row.",
|
|
40
|
+
),
|
|
41
|
+
Achievement(
|
|
42
|
+
"high_round_300",
|
|
43
|
+
"Cartes Pleines",
|
|
44
|
+
"Score 300+ points in a single round (declarations + Capot).",
|
|
45
|
+
),
|
|
46
|
+
Achievement(
|
|
47
|
+
"win_hard",
|
|
48
|
+
"Le Maître",
|
|
49
|
+
"Win a game on Hard difficulty.",
|
|
50
|
+
),
|
|
51
|
+
Achievement(
|
|
52
|
+
"ten_games_played",
|
|
53
|
+
"Habitué",
|
|
54
|
+
"Play 10 games.",
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def evaluate_round(stats: Statistics, *, points_scored: int, was_capot: bool) -> list[Achievement]:
|
|
60
|
+
"""Check post-round triggers; return list of newly unlocked achievements.
|
|
61
|
+
|
|
62
|
+
Mutates ``stats.achievements`` to record unlocks.
|
|
63
|
+
"""
|
|
64
|
+
newly: list[Achievement] = []
|
|
65
|
+
|
|
66
|
+
def _try(aid: str) -> None:
|
|
67
|
+
if stats.unlock_achievement(aid):
|
|
68
|
+
for a in ACHIEVEMENTS:
|
|
69
|
+
if a.id == aid:
|
|
70
|
+
newly.append(a)
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
if was_capot:
|
|
74
|
+
if stats.capots_achieved == 1:
|
|
75
|
+
_try("first_capot")
|
|
76
|
+
if stats.capots_achieved >= 3:
|
|
77
|
+
_try("capot_x3")
|
|
78
|
+
if stats.current_capot_streak >= 2:
|
|
79
|
+
_try("capot_streak_2")
|
|
80
|
+
|
|
81
|
+
if points_scored >= 300:
|
|
82
|
+
_try("high_round_300")
|
|
83
|
+
|
|
84
|
+
return newly
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def evaluate_game(
|
|
88
|
+
stats: Statistics, *, won: bool, difficulty: str
|
|
89
|
+
) -> list[Achievement]:
|
|
90
|
+
newly: list[Achievement] = []
|
|
91
|
+
|
|
92
|
+
def _try(aid: str) -> None:
|
|
93
|
+
if stats.unlock_achievement(aid):
|
|
94
|
+
for a in ACHIEVEMENTS:
|
|
95
|
+
if a.id == aid:
|
|
96
|
+
newly.append(a)
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
if won and difficulty == "hard":
|
|
100
|
+
_try("win_hard")
|
|
101
|
+
if stats.games_played >= 10:
|
|
102
|
+
_try("ten_games_played")
|
|
103
|
+
|
|
104
|
+
return newly
|