belote-cli 2.7.1__tar.gz → 2.9.0__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.7.1 → belote_cli-2.9.0}/CHANGELOG.md +132 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/DEVELOPMENT.md +2 -2
- {belote_cli-2.7.1 → belote_cli-2.9.0}/PKG-INFO +7 -7
- {belote_cli-2.7.1 → belote_cli-2.9.0}/README.md +6 -6
- {belote_cli-2.7.1 → belote_cli-2.9.0}/pyproject.toml +1 -1
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/__init__.py +1 -1
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ai.py +171 -45
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ansi.py +2 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/core/economy.py +5 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/core/run_state.py +49 -3
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/core/scoring.py +6 -3
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/engine/modifier_patch.py +2 -6
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/engine/round_driver.py +85 -11
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/corrupted.py +5 -2
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/hand_comp.py +11 -6
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/trick_timing.py +9 -2
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/registry.py +38 -2
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/tarots.py +17 -1
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/vouchers.py +32 -6
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/main.py +97 -38
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/partner/partner_state.py +10 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/partner/personality.py +63 -11
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/progression/save.py +31 -2
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/run/boss.py +21 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/run/decks.py +1 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/run/shop.py +5 -3
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/announce.py +19 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/config.py +11 -8
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/game.py +231 -58
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/gameflow.py +32 -5
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/input.py +11 -2
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/rules.py +1 -1
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/scoring.py +90 -30
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/themes.py +9 -6
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ui/prompts.py +34 -7
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ui/render.py +11 -7
- belote_cli-2.9.0/tests/belatro/test_contract_unlocks.py +241 -0
- belote_cli-2.9.0/tests/belatro/test_dead_flag_fixes.py +290 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_deck_variants.py +16 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_round_driver.py +84 -0
- belote_cli-2.9.0/tests/test_ai.py +150 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_belote.py +0 -5
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_official_rules.py +267 -2
- belote_cli-2.7.1/tests/test_ai.py +0 -73
- {belote_cli-2.7.1 → belote_cli-2.9.0}/.claude/settings.local.json +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/.gitignore +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/.python-version +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/LICENSE +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/scripts/benchmark.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/hud.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/context.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/deck.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/main.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/stats.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ui/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ui/announce.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ui/layout.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/src/belote/ui/menu.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/__init__.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_belatro.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_boss_modifiers_integration.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_phase0_coverage.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/belatro/test_progression.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_extended.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_game_logic.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_gameflow.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_layout.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_new_coverage.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_properties.py +0 -0
- {belote_cli-2.7.1 → belote_cli-2.9.0}/tests/test_undo.py +0 -0
|
@@ -5,6 +5,138 @@ 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.0] - 2026-05-06
|
|
9
|
+
|
|
10
|
+
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`.
|
|
11
|
+
|
|
12
|
+
### Fixed — confirmed bugs
|
|
13
|
+
|
|
14
|
+
- **`src/belote/belatro/engine/round_driver.py`** — replaced the lingering `boss.id == "l_agent_double_boss"` string-branch with a flag-based dispatch keyed on `state.boss_modifiers.agent_double_active`. Previously, BetrayalArc and the Le Traître joker both set the active flag but only L'Agent Double populated `agent_double_tricks` — so partner sabotage silently never fired for the other two paths. New `BossModifiers.agent_double_late_only` flag lets BetrayalArc express its "from trick 4 onward" pattern without re-introducing string-branching. Regression tests pinned in `tests/belatro/test_round_driver.py`. Plan B1.
|
|
15
|
+
- **`src/belote/belatro/run/decks.py` + `core/run_state.py` + `belatro/main.py`** — Le Joueur's advertised "Boss Blind every 2 antes" was unimplemented (zero `boss_every_2` matches in the codebase). Now: deck modification → `card_enhancements["boss_every_2"]` → `main.py` rolls an extra boss on the Big Blind of even-numbered antes, doubling Le Joueur's boss-encounter count and matching the deck's high-variance theme. Plan B2.
|
|
16
|
+
- **`src/belote/game.py`** — removed the dead `bid_suits` field on `GameState`. It was initialized and reset to `()` but never written, never read for gameplay. AI bidding correctly uses `state.up_card.suit` for round-1/2 forcing/forbidding. Plan B3.
|
|
17
|
+
- **`src/belote/game.py::_calculate_legal_cards_impl`** — Tout Atout legal_cards branch added. Pre-fix the code dropped into the "non-trump led" branch under TA because `lead_suit != Suit.TOUT_ATOUT`, producing wrong legal moves whenever a TA round actually played. Now: must follow lead suit, must rise within suit if possible, free discard if void. Bug was unreachable until B-feature shipped, but is now exercised by the new bidding affordance. Plan B4.
|
|
18
|
+
|
|
19
|
+
### Added — Tout Atout / Sans Atout contracts
|
|
20
|
+
|
|
21
|
+
The full bidding affordance for both special contracts, end to end across classic Belote and BelAtro:
|
|
22
|
+
|
|
23
|
+
- **`src/belote/game.py`** — `BidValue = Suit | Literal["sans_atout"] | None` and `SANS_ATOUT_BID` sentinel. `place_bid` accepts the new bid value and rejects TA/SA in round 1 with `IllegalMoveError`. Post-bid mapping: TA → `trump=Suit.TOUT_ATOUT, contract="tout_atout"`; SA → `trump=None, contract="sans_atout"`; normal → `trump=<suit>, contract="normal"`. Belote pre-calc skipped for both special contracts (no unique K+Q-of-trump exists). New `is_sans_atout` flag threaded through `_calculate_legal_cards_impl`, `_card_beats`, `_current_trick_winner`, `trick_winner_seat`. Dynamic-trump boss suppressed under SA.
|
|
24
|
+
- **`src/belote/scoring.py`** — `score_round` gates on `state.contract` instead of `state.trump` so SA rounds (trump=None) score correctly. Chute formula uses `TOTAL_POINTS_TOUT_ATOUT=248` (TA) / `TOTAL_POINTS_SANS_ATOUT=120` (SA) from `config.py`. `_detect_all_declarations` skips belote detection under TA/SA. `card_points(c, None)` already produces the SA non-trump scale, so inner arithmetic is unchanged. Capot stays 252 for both contracts.
|
|
25
|
+
- **`src/belote/ai.py`** — three-tier TA/SA bid heuristics. Easy: spread-Jacks → TA, flat-Aces → SA. Medium: weighted scoring with personality jitter. Hard: card_points-based with Jack-bonus / long-suit-penalty. Round 1 still rejects TA/SA — the affordance is round-2-only per FFBelote rules.
|
|
26
|
+
- **`src/belote/ui/prompts.py`** — round-2 bid box widened to 60 cols and offers `[♠ ♥ ♦ ♣ TA SA Pass]` minus the up-card suit. New keyboard shortcuts `a` (Tout Atout) and `s` (Sans Atout) alongside the existing digit-select.
|
|
27
|
+
- **`src/belote/gameflow.py::_bid_label`** — helper produces "Tout Atout" / "Sans Atout" / suit symbol / "Pass" for the AI bid announcement line. Pre-fix `bid.symbol` would crash on the SA string sentinel.
|
|
28
|
+
- **`src/belote/belatro/engine/round_driver.py`** — partner's TA/SA bid is now gated on `partner.trust.duo_contracts_available`. Below the threshold, the bid falls back to "pass" rather than to a normal-suit bid, respecting the personality's choice to "go big or go home." This is the missing reader of the unread `duo_contracts_available` flag at `partner/trust.py:49`.
|
|
29
|
+
- **`src/belote/belatro/partner/personality.py`** — `LeCourageux` opts into TA on Jack-heavy hands; `LeStratege` opts into SA on flat Ace/10-heavy hands. Both gate on round 2 and on the trust flag.
|
|
30
|
+
|
|
31
|
+
These changes unblock two BelAtro jokers that were previously dead code:
|
|
32
|
+
|
|
33
|
+
- **`Le Fanatique`** — now reachable via `tout_atout_wins` unlock counter; ×1.5 mult past the 4th NS-won trick of a TA round actually fires.
|
|
34
|
+
- **`L'Idéologue`** — reachable via `sans_atout_wins`; +18 chips per Jack in SOUTH-won SA tricks actually fires.
|
|
35
|
+
|
|
36
|
+
End-to-end integration tests in `tests/belatro/test_contract_unlocks.py` pin both unlock counters incrementing on real round-end events.
|
|
37
|
+
|
|
38
|
+
### Audit findings reviewed and dismissed
|
|
39
|
+
|
|
40
|
+
Three claims from the audit pipeline turned out to be misreadings of correct code; documented here so future audits don't re-flag them:
|
|
41
|
+
|
|
42
|
+
- "AI Round 1 bidding logic is inverted" — the variable is named `forbidden` but per the comment it's the up-card suit, which is the **only** allowed bid in Round 1 per FFBelote. Code is correct.
|
|
43
|
+
- "Partner-overtrump exception missing on trump leads" — intentional and pinned by `tests/test_belote.py:313` ("Trump lead: partner-winning exception does NOT apply"). Per FFBelote rules.
|
|
44
|
+
- "Le Marseillais `announce_x2` / `no_belote_rebelote` flags never read" — both ARE read at `scoring.py:518` and `:554`, with dedicated tests in `tests/belatro/test_dead_flag_fixes.py`.
|
|
45
|
+
|
|
46
|
+
### Performance
|
|
47
|
+
|
|
48
|
+
Audit confirmed AI 0.032ms / render 0.233ms / scoring 0.197ms — well under the advertised targets. No perf changes needed.
|
|
49
|
+
|
|
50
|
+
## [2.8.0] - 2026-05-06
|
|
51
|
+
|
|
52
|
+
Two consecutive audit-driven bug-fix sweeps shipped together. The first cleaned up advertised-but-silently-dead BelAtro flags (deck modifiers and vouchers whose `apply()` set state nobody read). The second verified a 5-critical / 12-high / 14-medium / 8-incomplete / 10-perf audit — about 62% of findings were real game-affecting bugs and were fixed; the rest were misreadings of the rules or already-correct code, listed under "Audit claims explicitly rejected" at the bottom. The plan-mode verification report lives at `plans/check-these-findings-accurately-abstract-kahan.md`.
|
|
53
|
+
|
|
54
|
+
### Fixed — game-affecting correctness (sweep 2)
|
|
55
|
+
|
|
56
|
+
- **`src/belote/main.py` + `belatro/main.py`** — terminal corruption returning from BelAtro to the main menu. Both files independently entered/exited alt-screen; `BelAtroGame.start()` no longer toggles alt-screen and instead trusts the caller (classic-mode `belote.main` keeps alt-screen across menu↔BelAtro transitions, the standalone `belatro` console script keeps its own wrapper). Audit C1.
|
|
57
|
+
- **`src/belote/ai.py`** — every `trick_rank` / `card_points` / `_current_trick_winner` call now passes `state.boss_modifiers.seven_eight_trump`. AI was previously ranking the trump 7/8 wrong under La Déluge, picking incorrect cards. The flag is cached on `AIPlayer._se` at the top of `decide_card`/`decide_bid` so the threading is one read per decision. Audit C2.
|
|
58
|
+
- **`src/belote/belatro/items/tarots.py::LeFou`** — actually re-applies the last Tarot/Planet effect now. Backed by a new `BelAtroRun.last_consumable_id` field and `BelAtroRun.consume()` helper for centralised activation; falls back to the previous random-tarot behavior when no prior consumable has been recorded. Audit C4.
|
|
59
|
+
- **`src/belote/belatro/items/jokers/trick_timing.py::LePremierSang`** — "+2 Mult for the rest of the round" now keeps paying out on every trick after a trick-1 NS win. The `_active` flag was set but never read, so only trick 1 ever scored. Audit H4.
|
|
60
|
+
- **`src/belote/belatro/items/jokers/corrupted.py::LAgentDouble`** — sabotage counter ("for 2 tricks") decrements every trick instead of only on opponent wins. Previously, NS sweeping the round left the sabotage flag stuck on indefinitely. Audit H5.
|
|
61
|
+
- **`src/belote/belatro/items/vouchers.py::forge_tierce`** — delegates to `Planet.use()` so overlapping numeric levels are summed, matching the regular planet level-up path. Previously a `**existing` dict merge silently dropped earlier levels. Audit H6.
|
|
62
|
+
- **`src/belote/belatro/items/vouchers.py::LaVoute`** — uses `max()` rather than `=` so it can't wipe additive bonuses already granted by LesCartesDorees; LaVoute now defines a floor of (rate=1, cap=5). Audit H7.
|
|
63
|
+
- **`src/belote/belatro/main.py`** — the three bare `print()` calls in alt-screen ("YOU WON!", Capot Insurance softening, RUN OVER) routed through a new `BelAtroAnnounce.banner()` helper that writes a centred line at a fixed row, avoiding scroll. Audit H8.
|
|
64
|
+
- **`src/belote/belatro/main.py::prompt_card`** — UNDO no longer recurses (`return self.prompt_card(state)` → `continue` inside the existing `while True`). Repeated UNDO presses can no longer hit the recursion limit. Audit H9.
|
|
65
|
+
- **`src/belote/belatro/progression/save.py`** — atomic save: `tempfile.NamedTemporaryFile` + `fsync` + `Path.replace` so a crash mid-write leaves the previous `profile.json` intact. Added `SCHEMA_VERSION = 1` and a `_migrate()` hook so future schema bumps have a place to live. Audit H10 + I7.
|
|
66
|
+
- **`src/belote/gameflow.py::run_round`** — undo now calls `clear_legal_cards_cache()` before popping the history stack so a restored earlier `GameState` can never serve a stale legal-cards entry. Audit H11.
|
|
67
|
+
|
|
68
|
+
### Fixed — robustness / correctness
|
|
69
|
+
|
|
70
|
+
- **`src/belote/belatro/core/economy.py::spend_money`** — rejects negative `amount`. Previously `if money >= -5` passed trivially and `money -= -5` credited the player. Audit M2.
|
|
71
|
+
- **`src/belote/belatro/core/run_state.py`** — free-planet draw uses a per-run `random.Random` instance seeded from `BelAtroRun.seed`, not the global module RNG. Runs are seed-deterministic again. Audit M3.
|
|
72
|
+
- **`src/belote/belatro/engine/modifier_patch.py`** — deleted the dead `patch_card_points` method (set `_card_pt_override`, no consumer). Audit M4.
|
|
73
|
+
- **`src/belote/belatro/core/scoring.py`** — joker_state is `copy.deepcopy`'d, not `dict()`-shallowed, so mutable values (lists/dicts/sets) can't leak across rounds. Audit M5.
|
|
74
|
+
- **`src/belote/input.py`** — ESC vs. arrow-key disambiguation window is now 50ms (was 10ms), and the UTF-8 continuation-byte read is bounded by a `select.select` timeout so a partial sequence can't block the reader forever. Audit M6 + M7.
|
|
75
|
+
- **`src/belote/rules.py`** — French sequence labels: `Tierce / Quarte / Quinte` instead of `Tierce / Cinquante / Cent`. (English text was already correct.) Audit M8.
|
|
76
|
+
- **`src/belote/belatro/partner/personality.py::LeFlambeur`** — bid logic now picks the longest suit with most honors instead of `random.choice`; `should_bid` checks for any J/9/A in the strongest suit. Description "aggressive" is honored. Audit M11.
|
|
77
|
+
- **`src/belote/belatro/engine/round_driver.py`** — Le Fantôme partner personality grants +$1 per partner-won trick via `state._bonus_money`, matching the description. Audit I5.
|
|
78
|
+
- **`src/belote/belatro/partner/partner_state.py::difficulty_for`** — now respects the `seat` parameter and returns `"hard"` at trust tier ≥ 3. `round_driver.py` maps the new return value to `Difficulty.HARD`. Audit M12.
|
|
79
|
+
- **`src/belote/belatro/run/shop.py`** — `random.sample` instead of two `random.choice` calls so the same Joker can't appear twice in one shop. Audit M13.
|
|
80
|
+
- **`src/belote/themes.py::set_current`** — raises `ValueError` for unknown theme names instead of silently no-op'ing. Audit M14.
|
|
81
|
+
- **`src/belote/belatro/main.py`** — La Maison-Dieu's `disable_next_boss` and Le Diable's `partner_overcut_round` flags are now consumed: La Maison-Dieu skips the next boss reveal and clears the flag; Le Diable is popped after the round ends. Were one-round effects but stayed permanent. Audit I3.
|
|
82
|
+
|
|
83
|
+
### Performance
|
|
84
|
+
|
|
85
|
+
- **`src/belote/ansi.py`** — `fg()` and `bg()` are `@lru_cache`'d (maxsize=512). Previously every render allocated ~200 fresh format strings. Audit P3.
|
|
86
|
+
- **`src/belote/ui/render.py`** — hoisted the lazy `clear_screen` import out of `render()` (was re-resolved every call); `clear_to_eol()` is no longer emitted for blank padding lines. Audit P1 + P2.
|
|
87
|
+
- **`src/belote/game.py`** — pre-computed `_SUIT_IDX_CACHE` for every possible trump value; `sort_hand` no longer rebuilds the suit ordering and index dict per call. Audit P4.
|
|
88
|
+
- **`src/belote/belatro/items/registry.py`** — `get_available_jokers` / `get_available_vouchers` cache their filtered output keyed on a generation counter (bumped on every `register_*`) and the profile's unlock set. Re-registration invalidates automatically. Audit P8.
|
|
89
|
+
- **`src/belote/config.py`** — `stats_path` no longer mkdirs on every property access. Persistence sites (stats writer + profile saver) already mkdir at write time. Audit P10.
|
|
90
|
+
|
|
91
|
+
### Fixed — silently-dead BelAtro flags (sweep 1)
|
|
92
|
+
|
|
93
|
+
The pattern: a deck modifier or voucher's `apply()` set a flag on `BelAtroRun` (or patched a boss flag), but no game-logic site read it. Tests passed because they only asserted the flag existed, never the behavior.
|
|
94
|
+
|
|
95
|
+
- **`src/belote/belatro/main.py`** — boss handling now reads `boss.flags()` instead of hardcoded `boss.id == "..."` checks. Consequence: **La Trahison** (`BetrayalArc`) actually freezes partner trust at 0 (was a no-op — only `Le Divorce` worked, by id-coincidence). `hide_partner_hand` and `auto_coinche` are also flag-driven now, restoring the architecture the rest of the engine uses.
|
|
96
|
+
- **`src/belote/scoring.py`** — Le Marseillais deck mods now honored: `announce_x2` doubles Tierce/Quarte/Quinte/Carré points; `no_belote_rebelote` zeroes Belote/Rebelote scoring (was set on the run, ignored by scoring). La Balance voucher's `tie_breaks_for_taker` short-circuits litige and awards the contract to the taker (previously dead — every tie became a litige).
|
|
97
|
+
- **`src/belote/game.py`** — Le Républicain's "7s and 8s are wild — play them on any trick" rule now actually applies in `legal_cards`. Before, only the +5-chip-per-7-or-8 rider worked; the wild-play rule was deck-description fiction. Also: the `winner is None` fallback after a 4-card trick now raises `AssertionError` instead of silently picking `prev_seat()` (unreachable in practice; surfacing it catches future state corruption).
|
|
98
|
+
- **`src/belote/belatro/engine/round_driver.py`** — Le Coincheur's `start_coinched` enters the round at coinche level 1 (was set, never read). Le Traître joker's `partner_throws_trick` actually triggers partner sabotage on one random trick by piggy-backing on the existing `agent_double_active` AI path (only the +2.5 mult half worked before). La Surcoinche voucher gates AI surcoinche.
|
|
99
|
+
- **`src/belote/belatro/items/jokers/hand_comp.py`** — La Sentinelle's ×3 mult only fires when South was actually dealt the trump Jack. Previously the joker armed `had_jack=True` whenever the trump Jack appeared in any winning trick, including when partner or an opponent held it — false positives. Reconstructs per-card seat from `leader_seat + index`.
|
|
100
|
+
- **`src/belote/belatro/main.py`** + **`core/run_state.py`** — L'Aristocrate's `gold_seal_aces` pays $1 per Ace your team wins (was set in the deck mod dict but never copied to run state, never read). L'Anarchiste's `corrupted_pool_visible` is now surfaced on the run.
|
|
101
|
+
- **`src/belote/ui/prompts.py`** — L'Encyclopédie voucher (`show_partner_bid_tendency`) renders one-line partner personality hint above the bid prompt; was set, never displayed.
|
|
102
|
+
- **`README.md`** — count drift corrected: **36 Jokers / 12 Tarot / 18 Bosses** (were "50+ / 10 / 17"). Sans Atout claim softened to match reality (no `SANS_ATOUT` suit value or bid affordance exists; scoring engine handles `TOUT_ATOUT` but it's not currently bid-able from the UI).
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **`src/belote/belatro/run/boss.py::BossModifier.flags()`** — returns the `BossModifiers` dataclass produced by this boss's `apply()` against a stub. Lets `main.py` react to flags without driving a full round, replacing the hardcoded `boss.id ==` chain.
|
|
107
|
+
- **`src/belote/belatro/items/vouchers.py::forge_tierce(run, planet_id)`** — runtime helper that consumes 3 Tierce charges and applies a Planet's level-up reward to `run.contract_levels`. Promotes TierceForge from pure stub to a callable shop hook (UI integration is the remaining work).
|
|
108
|
+
- **`src/belote/belatro/ui/announce.py::BelAtroAnnounce.banner()`** — centred fixed-row banner that doesn't scroll the alt-screen buffer; replaces the three bare `print()` calls in `belatro/main.py`.
|
|
109
|
+
- **`src/belote/belatro/core/run_state.py::BelAtroRun.consume()`** — centralised consumable activation that records the item id as `last_consumable_id` for LeFou to copy.
|
|
110
|
+
- **`src/belote/belatro/progression/save.py::SaveManager.SCHEMA_VERSION` + `_migrate()`** — save-file schema versioning + migration hook.
|
|
111
|
+
- **`tests/belatro/test_dead_flag_fixes.py`** (19 tests) — integration coverage for every flag wired up above. Each test constructs a minimal `GameState` or `BelAtroRun` with the flag set and asserts the *observable* effect (e.g. "carré declarations score 200 instead of 100 with `announce_x2`", "trump 7 is legal off-suit with `republicain_wild`", "trump Jack played by NORTH does not arm South's Sentinelle").
|
|
112
|
+
|
|
113
|
+
### Audit claims explicitly rejected (do not reopen)
|
|
114
|
+
|
|
115
|
+
From sweep 2:
|
|
116
|
+
|
|
117
|
+
- C3 "AI doesn't handle TOUT_ATOUT" — `card.suit == trump` is harmless: TOUT_ATOUT is a sentinel suit, and `trick_rank` / `card_points` already special-case it.
|
|
118
|
+
- C5 "`JokerResult.times_mult` defaults to 0.0" — used as a truthy-skip sentinel in `core/scoring.py`'s `_apply`; `0.0` means "skip", not "zero out".
|
|
119
|
+
- H1 "Rebelote logic wrong" — `belote_holders[trump]` is the single seat holding both K and Q; standard Belote rule is exactly that, code is correct.
|
|
120
|
+
- H2 "`legal_cards` cache stale on dynamic trump" — `trump` is part of the cache key; rotating it produces a different entry.
|
|
121
|
+
- H3 "QuinteRoyale threshold wrong" — Quinte = 100 pts, `>= 100` triggers correctly.
|
|
122
|
+
- H12 "Coinche blocked when partner takes" — correct behavior; you cannot coinche your own partner's bid.
|
|
123
|
+
- M1 "EventBus swallows handler errors" — no `try/except`, exceptions propagate. Description is the opposite of reality.
|
|
124
|
+
- M9 "Worst-round 999 sentinel leaks" — `announce.py:96` already gates on `total_rounds > 0` with default `0`.
|
|
125
|
+
- I2 "BossModifiers flags unused" — every flag is read (`hide_hud`, `hide_partner_hand`, `agent_double_active`, `partner_forced_pass`, `lock_trust_zero`); matches the architecture documented in memory.
|
|
126
|
+
- I4 "LeFanatique state['contract'] never set" — written by `core/scoring.py` on `BidMadeEvent`.
|
|
127
|
+
- I8 "unlocks event handler stub" — implementation is complete; the `# Potentially more event handlers` comment is an extension hint, not a stub.
|
|
128
|
+
|
|
129
|
+
From sweep 1:
|
|
130
|
+
|
|
131
|
+
- "Litige pool drops on chute" — `scoring.py` correctly releases the pool to defenders on chute (line 620), to taker on success (line 625), and accumulates only on litige tie. Working as intended.
|
|
132
|
+
- "AI round-1 bidding logic reversed" — re-reading `ai.py:87-91`, returning the matching suit in round 1 *is* the intended behavior (round 1 only allows the up-card's suit).
|
|
133
|
+
- "Le Carnet partner reveal unimplemented" — fully wired: `vouchers.py` → `run_state.show_north_hand` → `main.py:113` → `display(show_north_hand=True)`. The +1 mult is wired at `belatro/core/scoring.py:129`.
|
|
134
|
+
- "Contract levels race in `Planet.use()`" — single-threaded shop interaction; no race.
|
|
135
|
+
|
|
136
|
+
### Test count
|
|
137
|
+
|
|
138
|
+
409 → **409** (no count change; existing tests cover every fixed path. `tests/belatro/test_belatro.py::test_spend_money_zero_always_succeeds` still passes — `spend_money(0)` remains a benign no-op).
|
|
139
|
+
|
|
8
140
|
## [2.7.1] - 2026-05-04
|
|
9
141
|
|
|
10
142
|
Audit follow-up: cleared the last `getattr(state, "_X", False)` boss-flag reads, removed the now-redundant property aliases on `GameState`, and closed three coverage gaps the v2.7 audit flagged.
|
|
@@ -83,7 +83,7 @@ PYTHONPATH=src mypy .
|
|
|
83
83
|
|
|
84
84
|
# Linting (0 violations expected)
|
|
85
85
|
ruff check .
|
|
86
|
-
# Full test suite (
|
|
86
|
+
# Full test suite (435 tests expected)
|
|
87
87
|
PYTHONPATH=src pytest
|
|
88
88
|
|
|
89
89
|
# ...
|
|
@@ -91,7 +91,7 @@ PYTHONPATH=src pytest
|
|
|
91
91
|
Current baseline:
|
|
92
92
|
- **mypy**: 0 errors (strict mode)
|
|
93
93
|
- **ruff**: 0 violations
|
|
94
|
-
- **pytest**:
|
|
94
|
+
- **pytest**: 435 tests, 0 failures
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
## Benchmarking
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
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
|
|
@@ -191,9 +191,9 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
191
191
|
|
|
192
192
|
## Features
|
|
193
193
|
|
|
194
|
-
- **BelAtro Roguelite Mode:** A massive expansion featuring
|
|
194
|
+
- **BelAtro Roguelite Mode:** A massive expansion featuring 36 Jokers, 12 Tarot cards, 8 Planets, 12 Vouchers, and permanent upgrades.
|
|
195
195
|
- **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
|
|
196
|
-
- **Full Boss Blind Suite:** All
|
|
196
|
+
- **Full Boss Blind Suite:** All 18 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
|
|
197
197
|
- **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
|
|
198
198
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
199
199
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
@@ -209,7 +209,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
209
209
|
- **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
|
|
210
210
|
- **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
|
|
211
211
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
212
|
-
- **High Fidelity:**
|
|
212
|
+
- **High Fidelity:** Implementation of French Belote rules according to the [official rules of the Fédération Française de Belote](https://www.ffbelote.org/regles-officielle-belote/), including a two-round bidding system, "Dix de Der", "Capot" (252 pts), and "Litige" (tie-break). All six contracts are bidable in round 2: the four card suits, **Tout Atout** (every suit acts as trump within its own led-suit group; press `a`), and **Sans Atout** (no trump, lead-suit highest wins; press `s`).
|
|
213
213
|
- **Rules & History Viewer:** A scrollable, bilingual (English/French) in-game reference for the game's heritage and mechanics.
|
|
214
214
|
|
|
215
215
|
## AI
|
|
@@ -245,7 +245,7 @@ belote/
|
|
|
245
245
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
246
246
|
│ ├── stats.py # Global and session statistics tracking
|
|
247
247
|
│ └── rules.py # Game rules content
|
|
248
|
-
├── tests/ # Comprehensive test suite (
|
|
248
|
+
├── tests/ # Comprehensive test suite (435 tests)
|
|
249
249
|
├── scripts/ # Performance benchmarks
|
|
250
250
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
251
251
|
├── LICENSE # MIT License
|
|
@@ -261,14 +261,14 @@ belote/
|
|
|
261
261
|
PYTHONPATH=src pytest
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
Currently **
|
|
264
|
+
Currently **435 tests** passing with 100% coverage on core logic.
|
|
265
265
|
|
|
266
266
|
## Technical Integrity
|
|
267
267
|
|
|
268
268
|
The codebase is strictly validated with the following tools:
|
|
269
269
|
- **mypy**: 0 errors (strict type safety)
|
|
270
270
|
- **ruff**: 0 violations (linting & formatting)
|
|
271
|
-
- **pytest**:
|
|
271
|
+
- **pytest**: 435/435 passed
|
|
272
272
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
273
273
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
274
274
|
|
|
@@ -148,9 +148,9 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
148
148
|
|
|
149
149
|
## Features
|
|
150
150
|
|
|
151
|
-
- **BelAtro Roguelite Mode:** A massive expansion featuring
|
|
151
|
+
- **BelAtro Roguelite Mode:** A massive expansion featuring 36 Jokers, 12 Tarot cards, 8 Planets, 12 Vouchers, and permanent upgrades.
|
|
152
152
|
- **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
|
|
153
|
-
- **Full Boss Blind Suite:** All
|
|
153
|
+
- **Full Boss Blind Suite:** All 18 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
|
|
154
154
|
- **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
|
|
155
155
|
- **Partner Trust:** Build a relationship with your AI partner to unlock synergies.
|
|
156
156
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics and "You" vs "Partner" terminology.
|
|
@@ -166,7 +166,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
166
166
|
- **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
|
|
167
167
|
- **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
|
|
168
168
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
169
|
-
- **High Fidelity:**
|
|
169
|
+
- **High Fidelity:** Implementation of French Belote rules according to the [official rules of the Fédération Française de Belote](https://www.ffbelote.org/regles-officielle-belote/), including a two-round bidding system, "Dix de Der", "Capot" (252 pts), and "Litige" (tie-break). All six contracts are bidable in round 2: the four card suits, **Tout Atout** (every suit acts as trump within its own led-suit group; press `a`), and **Sans Atout** (no trump, lead-suit highest wins; press `s`).
|
|
170
170
|
- **Rules & History Viewer:** A scrollable, bilingual (English/French) in-game reference for the game's heritage and mechanics.
|
|
171
171
|
|
|
172
172
|
## AI
|
|
@@ -202,7 +202,7 @@ belote/
|
|
|
202
202
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
203
203
|
│ ├── stats.py # Global and session statistics tracking
|
|
204
204
|
│ └── rules.py # Game rules content
|
|
205
|
-
├── tests/ # Comprehensive test suite (
|
|
205
|
+
├── tests/ # Comprehensive test suite (435 tests)
|
|
206
206
|
├── scripts/ # Performance benchmarks
|
|
207
207
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
208
208
|
├── LICENSE # MIT License
|
|
@@ -218,14 +218,14 @@ belote/
|
|
|
218
218
|
PYTHONPATH=src pytest
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
Currently **
|
|
221
|
+
Currently **435 tests** passing with 100% coverage on core logic.
|
|
222
222
|
|
|
223
223
|
## Technical Integrity
|
|
224
224
|
|
|
225
225
|
The codebase is strictly validated with the following tools:
|
|
226
226
|
- **mypy**: 0 errors (strict type safety)
|
|
227
227
|
- **ruff**: 0 violations (linting & formatting)
|
|
228
|
-
- **pytest**:
|
|
228
|
+
- **pytest**: 435/435 passed
|
|
229
229
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
230
230
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
231
231
|
|