belote-cli 3.3.4__tar.gz → 3.4.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-3.3.4 → belote_cli-3.4.2}/CHANGELOG.md +122 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/DEVELOPMENT.md +7 -5
- {belote_cli-3.3.4 → belote_cli-3.4.2}/PKG-INFO +29 -4
- {belote_cli-3.3.4 → belote_cli-3.4.2}/README.md +28 -3
- {belote_cli-3.3.4 → belote_cli-3.4.2}/pyproject.toml +1 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/__init__.py +1 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ai.py +20 -4
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/core/run_state.py +12 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/core/scoring.py +4 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/engine/event_bus.py +6 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/engine/round_driver.py +7 -4
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/base.py +13 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/contract.py +6 -6
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/trick_timing.py +5 -5
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/main.py +3 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/partner/partner_state.py +12 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/progression/save.py +10 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/run/ante_themes.py +12 -7
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/hud.py +138 -10
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/shop.py +4 -1
- belote_cli-3.4.2/src/belote/belatro/ui/trust_bar.py +69 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/game.py +0 -5
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/gameflow.py +10 -5
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/input.py +8 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/main.py +8 -7
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ui/prompts.py +2 -1
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_belatro.py +47 -7
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_boss_modifiers_integration.py +60 -0
- belote_cli-3.4.2/tests/belatro/test_collection_logic.py +77 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_dead_flag_fixes.py +28 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_hud_synergy.py +6 -4
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_partner_trust.py +69 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_phase0_coverage.py +100 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_phase3_meta.py +31 -3
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_round_driver.py +62 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_ai.py +83 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_new_coverage.py +49 -0
- belote_cli-3.3.4/src/belote/belatro/ui/trust_bar.py +0 -44
- belote_cli-3.3.4/tests/belatro/test_collection_logic.py +0 -36
- {belote_cli-3.3.4 → belote_cli-3.4.2}/.claude/settings.local.json +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/.gitignore +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/.python-version +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/LICENSE +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/scripts/benchmark.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/a11y.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/achievements.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ansi.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/engine/modifier_patch.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ghost_run.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/registry.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/run_summary.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/history.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/config.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/context.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/deck.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/replay.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/rules.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/scoring.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/stats.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/themes.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ui/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ui/announce.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ui/layout.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ui/menu.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/src/belote/ui/render.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/__init__.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_ghost_run.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_history_overlay.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/belatro/test_progression.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_a11y.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_achievements.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_ansi_helpers.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_belote.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_extended.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_game_logic.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_gameflow.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_layout.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_official_rules.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_properties.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_replay.py +0 -0
- {belote_cli-3.3.4 → belote_cli-3.4.2}/tests/test_undo.py +0 -0
|
@@ -5,6 +5,128 @@ 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.4.2] - 2026-05-11
|
|
9
|
+
|
|
10
|
+
Implements the deferred bug roadmap from 3.4.1's verification pass. **9 fixes land here** — 3 Critical (C1/C3/C4), 4 High (H1/H4/H5/H7), 1 architectural cleanup (H10), 1 dead-code deletion (M4). Adds 17 regression tests (551 → 568). The 3.4.1 entry catalogued these against the source; this entry implements them. Plan file at `/home/mrrobot/.claude/plans/wtf-these-were-verified-shiny-flute.md`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`src/belote/ai.py:104-108` (C1) — AI no longer sees partner's hand under `hide_partner_hand`.** `AIMemory.update_memory()` now gates the `partner_hand` population on `not state.boss_modifiers.hide_partner_hand`. Under Le Fantôme Partenaire the human was blinded but the AI continued to play with perfect partner information — the boss's visibility cost was paid by one team only. The fix removes the perfect-information cheat; the AI still infers partner cards via `known_voids` and `played` like any real player. Regression test in `tests/belatro/test_dead_flag_fixes.py::test_ai_memory_respects_hide_partner_hand`.
|
|
15
|
+
- **`src/belote/gameflow.py:200-203` (C3) — Dix de Der announcement now uses the Rupture-aware winner.** Pre-3.4.2 the 8th-trick "Dix de Der (Team X)" line called raw `trick_winner_seat()` on `display_state.current_trick`, which ignores `boss_modifiers.no_consecutive_team_wins`. Under La Rupture the announcement could name a team that was *not* credited the +10 in scoring. Fix swaps to `compute_trick_winners(state, trump, is_sa, tricks=projected)[-1]` (same helper `scoring.py` uses), feeding `projected = completed_tricks + [display_state.current_trick]` since the 8th trick isn't yet pushed to `completed_tricks` when the announcement fires. Removes the now-unused `trick_winner_seat` import. Regression test in `tests/belatro/test_boss_modifiers_integration.py::test_dix_de_der_announcement_honors_rupture`.
|
|
16
|
+
- **`src/belote/ai.py:540-549` (C4) — `opp_trumps` no longer over-counts; TA total fixed.** The pre-3.4.2 formula `8 - sum(played trumps)` conflated "remaining trump anywhere" with "opponent trump", treating South's own hand and partner's visible cards as still in opponents' hands. The fix subtracts `my_trumps`, `played_trumps`, and `partner_trumps` from the total. Under Tout Atout every card is a trump and the total is `32`, not `8` — pre-3.4.2 the formula degraded to `8 - 0 = 8` always under TA because no card's `.suit` equals `Suit.TOUT_ATOUT`. Both regimes are now handled. Has the side effect of also fixing `my_trumps` under TA (used by `_score_leading_strategy` downstream). Regression tests in `tests/test_ai.py::test_opp_trumps_excludes_own_and_partner_hand` and `::test_opp_trumps_under_tout_atout_uses_32_total`.
|
|
17
|
+
- **`src/belote/belatro/items/jokers/contract.py` + `trick_timing.py` (H1) — 8 jokers now gate on team, not seat.** `LIdeologue`, `LeFanatique`, `LeDiplomate`, `LePatriote`, `LIllusionniste`, `LePremierSang` (both checks), `LeSergent`, `LExecuteur` were checking `event.winner == Seat.SOUTH` instead of `team_of(event.winner) == 0`. They silently no-opped when partner (North) took the relevant trick. Now consistent with the 3.2.0 fix that landed `LaSentinelle` and `LeDernierMot` on the same pattern. Five existing tests that asserted the broken behavior (`test_north_*_returns_none`) were rewritten to verify the new correct behavior — partner wins now fire the joker; opposing-team wins (EAST) are added as the new negative case. `LeSergent`'s "streak reset" semantics also shifted: an opposing-team trick now breaks the streak, not a partner-won one. Regression backstop in `tests/belatro/test_phase0_coverage.py::test_h1_team_aware_jokers_fire_on_north_partner_win`. **`LeRebelle` is intentionally not included** — the 3.4.1 catalogue flagged it as a probable audit hallucination; it is an `on_belote` joker, not on `on_trick_won`, and the seat-vs-team distinction there is a separate spec call deferred to a future cycle.
|
|
18
|
+
- **`src/belote/belatro/run/ante_themes.py` + `belatro/main.py:413-414` (H4) — TournoiAnte pays a real 50% of round payout.** `AnteTheme.on_blind_won` gains a `blind_payout: int` parameter (forwarded to base class and both subclasses). The call site at `belatro/main.py:412` snapshots `run.economy.money` immediately before `process_round_end` and computes `blind_payout = money_after - money_before` after all bonus paths (L'Avocat, `_bonus_money`, Le Puriste, L'Aristocrate) have run. TournoiAnte now does `add_money(max(1, blind_payout // 2))` — actually 50% of payout, with a $1 floor so blind payouts of 0 still pay something. Comment rewritten to match. Updated tests in `tests/belatro/test_phase3_meta.py` (CafeAnte test threads the new arg; TournoiAnte tests verify exact `payout // 2` math + the floor).
|
|
19
|
+
- **`src/belote/belatro/progression/save.py:97-101` (H5) — `load_profile` no longer loses starter unlocks.** Pre-3.4.2 the happy path read `data.get("unlocked_ids", [])`, defaulting to an empty list when the key was absent — wiping the Profile dataclass's `["le_classique", "le_courageux", "l_econome"]` starter unlocks for any save missing the key (legacy saves, manual edits, partial writes). The fix falls back to `Profile().unlocked_ids` when the key is missing; an explicitly empty `unlocked_ids: []` is honored unchanged (a player who has reset their unlocks stays reset). Two regression tests in `tests/belatro/test_collection_logic.py` lock both behaviors.
|
|
20
|
+
- **`src/belote/main.py:230-231` (H7) — stats line agrees with menu summary on ties.** Changed `won=(ns >= target and ns >= ew)` to `won=(ns >= target and ns > ew)` so the `update_stats_game` `won` flag aligns with `ui/menu.py:344`'s `winner = "NS" if ns > ew else "EW"`. On an exact tie at target, pre-3.4.2 the stats recorded a NS win while the visible summary said EW. Both regression tests in `tests/test_new_coverage.py` — one source-grep anti-pattern lock against `>=`, one semantic check on the formula.
|
|
21
|
+
|
|
22
|
+
### Internal
|
|
23
|
+
|
|
24
|
+
- **`src/belote/belatro/partner/partner_state.py:38-49` (H10, architectural)** — `equip_joker` signature widened to `equip_joker(self, joker: Joker, run: BelAtroRun | None = None) -> bool`. When `run` is provided, `joker.on_purchase(run)` fires after the slot append. No current partner joker defines `on_purchase`, so no behaviour changes today — this is forward-looking infrastructure. The catalogue called this "latent"; equipping through this path will now invoke purchase-time effects consistently with the main joker slot equip path. Note: `equip_joker` has zero callers in the current codebase (the shop path equips through a different surface), so this fix is doubly forward-looking. Three regression tests in `tests/belatro/test_partner_trust.py::TestEquipJokerOnPurchase`.
|
|
25
|
+
- **`src/belote/game.py::advance_turn` (M4) — dead code deleted.** Zero callers across `src/` and `tests/`. The function was a one-line `replace(state, turn=state.turn.next_seat())` helper that no live code used.
|
|
26
|
+
- **Tests**: 551 → 568 (+17). Five existing `test_north_*_returns_none` tests in `test_belatro.py` were flipped to assert the new team-aware behavior (these tests had encoded the H1 bug as a contract — they are not test regressions but contract updates).
|
|
27
|
+
- **Strict gates**: pytest 568/568, mypy 0 errors (76 source files), ruff 0 violations.
|
|
28
|
+
- **Version markers bumped**: `pyproject.toml`, `src/belote/__init__.py`.
|
|
29
|
+
- **Docs bumped**: `CHANGELOG.md` (this entry), `README.md` "What's new in 3.4.2", `DEVELOPMENT.md` baseline.
|
|
30
|
+
- **Still deferred**: **H2** (`LEgoiste` partner-trick nullification) was flagged as needing a spec decision in 3.4.1 and was not in any tier the user picked for 3.4.2 — the comment says "Partner's points are nullified" which reads as intent, so a fix would invert documented behavior. Stays on the deferred list pending a spec call. `LeRebelle` `on_belote` seat/team gating (noted above) is similarly deferred.
|
|
31
|
+
|
|
32
|
+
## [3.4.1] - 2026-05-11
|
|
33
|
+
|
|
34
|
+
Audit-verification-only release — **no code changes**. A fresh external LLM audit ("Comprehensive Audit Report — Belote CLI v3.4.0") produced 26 prioritized findings (4 Critical, 10 High, 12 Medium) plus a test-coverage section and a performance section. Direct verification against the source confirmed **7 real bugs** (3 Critical, 4 High), **1 architectural latent issue** (no current consumer but fragile for future work), and **1 disputed claim** that needs a spec call before any fix. **8 claims were false positives** under verification (intent inversion, defense-in-depth confused for bugs, or stale call-graph reading). Mediums spot-checked: 1 real (dead code), 4 false. The point of cutting a release for verification-only work is to (a) lock the false-positive catalogue against re-investigation next cycle and (b) record the confirmed-bug roadmap before any fixes land. Plan/verification file at `/home/mrrobot/.claude/plans/check-on-this-audit-polished-kite.md`. Test count, mypy, ruff results unchanged from 3.4.0 (no source code touched).
|
|
35
|
+
|
|
36
|
+
### Confirmed bugs — deferred to 3.4.2+
|
|
37
|
+
|
|
38
|
+
These were verified against current code and are real. None are fixed in 3.4.1; they are catalogued here so the next session has a vetted target list.
|
|
39
|
+
|
|
40
|
+
**Critical**
|
|
41
|
+
- **`src/belote/ai.py:104-108` (C1) — AI sees partner's full hand regardless of `hide_partner_hand`.** `AIMemory.update_memory()` unconditionally populates `self.memory.partner_hand` from `state.hand_of(partner)`. The boss flag `hide_partner_hand` (declared at `game.py:177`, set by Le Fantôme Partenaire at `belatro/run/boss.py:171`) is only read by display code at `belatro/main.py:291`, never by the AI memory path. Net effect: under Le Fantôme Partenaire the human is blinded to partner's hand but the AI continues to play with perfect partner information.
|
|
42
|
+
- **`src/belote/gameflow.py:196-198` (C3) — Dix de Der announcement uses non-Rupture-aware winner.** The announcement calls `trick_winner_seat()` directly; the La Rupture-aware helper `compute_trick_winners()` (defined at `game.py:756`, used by `scoring.py`) is the one that honours `boss_modifiers.no_consecutive_team_wins`. Under La Rupture the announced "Dix de Der goes to TEAM X" line can name a team that is not actually credited the +10 in scoring.
|
|
43
|
+
- **`src/belote/ai.py:533` (C4) — `opp_trumps` formula conflates "remaining trump" with "opponent trump".** Current line is `opp_trumps = 8 - sum(1 for c in self.memory.played if c.suit == trump)`. This counts trump still anywhere in unrevealed hands — including South's own hand (already computed as `my_trumps` on line 532) and partner's hand (visible at `self.memory.partner_hand`). The variable is then compared against `my_trumps` in `_score_leading_strategy`, so the over-count biases Hard AI's trump-coverage decisions when the AI itself is holding trump.
|
|
44
|
+
|
|
45
|
+
**High**
|
|
46
|
+
- **`src/belote/belatro/items/jokers/contract.py` + `trick_timing.py` (H1, partial) — 8 jokers still gate on `event.winner == Seat.SOUTH`.** 3.2.0 fixed La Sentinelle and Le Dernier Mot to use `team_of(event.winner) == 0`. The same anti-pattern survives in: `LIdeologue` (contract.py:21), `LeFanatique` (contract.py:45), `LeDiplomate` (contract.py:62), `LePatriote` (contract.py:81), `LIllusionniste` (contract.py:128), `LePremierSang` (trick_timing.py:26/30), `LeSergent` (trick_timing.py:46), `LExecuteur` (trick_timing.py:82). All silently no-op when North takes the relevant trick. (The audit also named `LeRebelle` but I could not find it among the South-only checks — likely a hallucinated joker name; verify before touching.)
|
|
47
|
+
- **`src/belote/belatro/run/ante_themes.py:73-76` (H4) — TournoiAnte bonus is not the advertised 50%.** Effect uses `run.economy.add_money(max(1, run.economy.bonus_per_round // 2 + 2))`. The comment claims "+50% bonus on top of whatever payout the round produced", but the formula is a flat function of `bonus_per_round` (the per-round flat-bonus economy field), not 50% of actual round payout. Either compute true 50% of the round delta or rewrite the comment.
|
|
48
|
+
- **`src/belote/belatro/progression/save.py:94` (H5) — `load_profile` loses default unlocks.** Line reads `unlocked_ids=data.get("unlocked_ids", [])`. The `Profile` dataclass default is `["le_classique", "le_courageux", "l_econome"]`. A saved profile missing the key (older saves, manual edits, partial writes) reloads with no unlocks. The exception branch correctly returns `Profile()` with defaults; only the happy path corrupts.
|
|
49
|
+
- **`src/belote/main.py:230-231` (H7) — Win operator mismatch on ties.** Main loop uses `won=(ns >= target and ns >= ew)`; menu summary at `ui/menu.py:344` uses `winner = "NS" if ns > ew else "EW"`. On an exact tie at target, main records a NS win while the visible summary attributes the round to EW.
|
|
50
|
+
|
|
51
|
+
### Architectural / latent
|
|
52
|
+
|
|
53
|
+
- **`src/belote/belatro/partner/partner_state.py:34-38` (H10) — `equip_joker` skips `on_purchase`.** The method simply appends to `self.jokers`; no `on_purchase()` hook is invoked. No current partner joker defines `on_purchase()`, so this is latent today — but any future partner joker with a purchase-time effect (a la `LeTraitre` in `corrupted.py`) will silently fail to fire when equipped through this path. Document or wire the hook before adding such a joker.
|
|
54
|
+
- **`src/belote/game.py:1007` (M4) — `advance_turn()` is dead code.** Defined, never called. Safe to delete.
|
|
55
|
+
|
|
56
|
+
### Needs spec decision before fixing
|
|
57
|
+
|
|
58
|
+
- **`src/belote/belatro/items/jokers/corrupted.py:56-62` (H2) — `LEgoiste` nullifies the entire partner trick.** On `event.winner == Seat.NORTH` the joker returns `JokerResult(add_chips=-event.card_points)`, where `event.card_points` is the FULL trick's card points (South's contribution included). The audit reads this as a bug (only partner's own contribution should be subtracted); the code comment reads "Partner's points are nullified", implying *intentional* full-trick nullification. Resolve which is canonical before changing the formula — and either way add a test that pins the intended behaviour.
|
|
59
|
+
|
|
60
|
+
### Verified clean — agent claims that did NOT survive source verification
|
|
61
|
+
|
|
62
|
+
Catalogued so they aren't re-investigated next cycle.
|
|
63
|
+
|
|
64
|
+
- **(C2) "No round-2 bid validation in `place_bid`"** — `place_bid` itself does not validate, but the human-input path filters the up-card suit out of the options menu at `ui/prompts.py:132-135`, and the AI path uses `exclude=forbidden` at `ai.py:137`. Defense-in-depth gap (a programmatic caller could pass a bad bid), not a live bug.
|
|
65
|
+
- **(H3) "`LeFou` fallback is dead code"** — `tarots.py:119` checks `if last_id and last_id != self.id`. `last_consumable_id` defaults to `None` and `getattr(item, "id", None)` can also set it to `None`, so the fallback fires on the first consumable of a run and on self-copy attempts. Live code.
|
|
66
|
+
- **(H6) "Signal handlers skip `finally` and lose stats"** — `main.py:127-129` handler does call `sys.exit(0)`, which bypasses the `finally`. But `flush_stats()` is also invoked at `main.py:160` (quit path) and `main.py:235` (game-over). The data-loss window is narrow (SIGINT before any flush in the same session). Cosmetic at best.
|
|
67
|
+
- **(H8) "`zip(..., strict=False)` causes silent data loss in scoring"** — `winners` at `scoring.py:461,498` is produced by `compute_trick_winners(state, ...)`, which is exactly one entry per completed trick by construction. The length invariant holds; `strict=False` is defensive noise, not a live bug.
|
|
68
|
+
- **(H9) "`_CARD_TO_ID[c]` will KeyError on BelAtro jokers"** — `_CARD_TO_ID` is built from `make_deck()` (32 standard cards). BelAtro jokers are `Joker` objects living in `state._joker_state`, never inserted into `hand`. No reachable code path produces the KeyError.
|
|
69
|
+
- **(M2) "Frozen `GameState` contains mutable `_rng`"** — `_rng` is declared with `field(default_factory=random.Random, compare=False, repr=False)` — the same documented pattern used for `_joker_state` (see comment at `game.py:214-217`). Intentional; the contract is "always rebuild via `dataclasses.replace`".
|
|
70
|
+
- **(M8) "`card in self.memory.partner_hand` is always False"** — `partner_hand` is populated at `ai.py:107-108` during PLAYING/SCORING phase. The check at line 660 is live and prevents double-scoring visible partner cards.
|
|
71
|
+
- **(M12) "`mult == float(int(mult))` float precision bug"** — Intentional optimisation in `belatro/core/scoring.py:254`: when the multiplier is exactly integral, take the lossless integer-multiplication path; else accept float multiplication. Correct logic.
|
|
72
|
+
|
|
73
|
+
### Audit calibration notes (for the next pass)
|
|
74
|
+
|
|
75
|
+
The audit's overall scaffolding (file:line citations, severity tiers, action plan) was well-presented but had a recurring failure mode: **flagging "suspicious-looking" patterns without verifying behaviour in context**. Concrete recurring misses worth feeding back to the auditing model:
|
|
76
|
+
|
|
77
|
+
1. **Defense-in-depth confused with bugs** (C2, H8, H9). When the audited line lacks an obvious check, the audit should trace one hop up the call graph before declaring "no validation". In all three cases the invariant is enforced at the caller.
|
|
78
|
+
2. **Intent inversion** (H2, M2, M12). When code is paired with a comment that describes the exact behaviour the audit flags as wrong, that's evidence of intent, not a bug. Reading the adjacent comment before flagging would have caught these.
|
|
79
|
+
3. **Self-cancelling dead-code claims** (H3). The audit claimed "`last_consumable_id` is always set before `use()` is called". True — but it can be set to `None`, and the consumer's guard is `if last_id and ...`. Surface-level static reasoning without tracing the guard.
|
|
80
|
+
4. **Headline metrics with no numbers**. The "Current Health" table lists Tests / Lint / Types / Version rows with no values. The version row is checkable; the others were left blank, which makes the table cosmetic.
|
|
81
|
+
5. **Joker name drift** (H1). `LeRebelle` appeared in the South-only list but is not among the actual offenders. Probable hallucination.
|
|
82
|
+
|
|
83
|
+
### Internal
|
|
84
|
+
|
|
85
|
+
- **No source code touched.** Test count, mypy strictness, ruff cleanliness all unchanged from 3.4.0: pytest 551/551, mypy 0 errors (76 files), ruff 0 violations.
|
|
86
|
+
- **Version markers bumped**: `pyproject.toml`, `src/belote/__init__.py`.
|
|
87
|
+
- **Docs bumped**: `README.md` "What's new in 3.4.1" section, `DEVELOPMENT.md` baseline.
|
|
88
|
+
- **Roadmap for 3.4.2**: the 7 confirmed bugs above (C1, C3, C4, H1×8, H4, H5, H7) plus the H10/M4 cleanups. Tier 1 (C1/C3/C4) closes the AI-fairness gap; Tier 2 (H1/H4/H5/H7) closes the remaining verified issues.
|
|
89
|
+
|
|
90
|
+
## [3.4.0] - 2026-05-10
|
|
91
|
+
|
|
92
|
+
Audit + endless-mode reliability + HUD polish release. A fresh three-agent codebase pass (classic engine / BelAtro layer / UI + I/O) produced ~80 candidate findings. Direct verification against the source rejected ~95% as false positives or by-design patterns. The five surviving issues plus two **new** bugs uncovered during follow-up verification of endless mode and classic game flow are fixed here. Two HUD features land alongside (joker pip strip with edition glow, synergy tooltip, polished trust bar). 551 tests passing (up from 549), ruff and mypy strict still clean. Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-fizzy-summit.md`.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- **`src/belote/belatro/engine/round_driver.py` (A1, HIGH)** — `BidMadeEvent` was emitted twice for the winning bid on every coinche path (player coinche → AI surcoinche, AI partner coinche, boss `auto_coinche` for EW *and* NS takers, and the `start_coinched` deck mod). Both emits ran `on_bid` joker handlers — once with `coinche_level=0`, then again with the resolved level — so any `on_bid` joker that accumulates per event was silently invoked twice for the same bid (Le Passeur and the contract-injection path were both vulnerable, future on_bid jokers more so). The fix adds a `re_emit: bool = False` field to `BidMadeEvent`; the post-coinche refreshes pass `re_emit=True`, and `ScoreAccumulator.update_state` skips `_fire_jokers("on_bid", ...)` for re-emits while still updating `joker_state["contract"]` so the HUD and contract-aware logic stay in sync. Regression test in `tests/belatro/test_round_driver.py::test_bid_made_event_does_not_double_fire_on_bid_under_auto_coinche` (registers a counting `on_bid` joker under L'Avocat and asserts no fire carries `coinche_level > 0`).
|
|
97
|
+
- **`src/belote/belatro/core/run_state.py::enter_endless` (E1, HIGH)** — Pre-3.4.0, accepting the "Continue into Endless Mode? (Ante 9+ scales ×2.2)" prompt left the run at `(ante=8, blind_index=2, endless_ante_offset=0, endless=True)`. The next `_play_blind` therefore *replayed* the Ante 8 Boss Blind at the SAME base target before the ×2.2 scaling kicked in on the second cycle — the prompt's promise of "Ante 9+ scales" was violated for one full round. The fix bumps `endless_ante_offset` to `max(offset, 1)` and resets `blind_index = 0` inside `enter_endless`, so the first endless round is Ante 8 Small Blind × 2.2 as advertised. Regression test in `tests/belatro/test_phase3_meta.py::test_enter_endless_advances_into_first_scaled_cycle`.
|
|
98
|
+
- **`src/belote/main.py` classic game-over branch (E2, HIGH)** — `apply_round_score` (scoring.py:952-953) intentionally keeps `phase=Phase.DEAL` when both teams reach `target` AND the round ended in a tie — Belote's tie-breaker rule. The classic main loop then re-checked `ns >= target or ew >= target` and unconditionally forced `phase=Phase.GAME_OVER`, overriding the scoring layer's intent: tie-breakers never played, the game just ended on the first round any team crossed target even if the score was exactly even. Fixed by replacing the redundant re-check with `if state.phase == Phase.GAME_OVER:` — the scoring layer is the single source of truth, and the unused `dataclasses.replace` import is removed.
|
|
99
|
+
- **`src/belote/input.py::_UnixKeyReader.restore` (A2, MED)** — `termios.tcsetattr` ran without exception handling. On a dropped SSH session, broken pipe, or a permission glitch it raised and left the host shell in raw/no-echo mode (the parent terminal would no longer echo keystrokes after the game crashed out). The call is now wrapped in `contextlib.suppress(termios.error, OSError)` and `_restored` is set regardless, so a follow-up restore call from `__exit__` after a prior raise is a no-op.
|
|
100
|
+
- **`src/belote/belatro/ui/shop.py` selection clamp (A3, MED)** — After reroll the index clamp was `min(self.selected, len(self.shop.inventory))`, which allows `selected == len(inventory)` — out-of-bounds for the very next render's `inventory[self.selected]`. The buy-path guard at the same site already used the correct `max(0, len(...) - 1)` form. Fixed to match.
|
|
101
|
+
- **`src/belote/ui/prompts.py::prompt_card` dead code (A5, LOW)** — The trailing `return None, state` after the `while True:` loop was unreachable (every match arm either continues or returns inside the loop). Replaced with an explicit `raise AssertionError("…")` so a future change that lets the loop fall through fails loud rather than silently returning a sentinel.
|
|
102
|
+
|
|
103
|
+
### Added — UI/HUD polish
|
|
104
|
+
|
|
105
|
+
- **`src/belote/belatro/ui/hud.py::render_joker_pip_strip` (B.3)** — Row-1 strip of 5 joker slots, each rendered as a 4-cell pip `[Xx ]` (or `[Xx*]` when the joker is in an active synergy pair). Empty slots paint as dotted `[··]` so the player sees their capacity at a glance. Edition support: `F` Foil → bright cyan, `H` Holo → magenta, `P` Polychrome → pink-violet, `N` Negative → reverse-video. The shortcode is `Joker.shortcode` — a new class property that returns the joker's manual `_shortcode_override` if set, else the first two letters of `name` upper-cased. New jokers inherit a sensible default with no extra plumbing. Hidden under Le Brouillard's `hide_hud` like the rest of the BelAtro HUD.
|
|
106
|
+
- **`src/belote/belatro/ui/hud.py::render_synergy_tooltip` (B.4)** — When at least one synergy pair is active, prints a green-pip line below the score line describing the synergy (e.g. *"♦ Coinched Tout-Atout wins ramp the streak multiplier"*). Up to two synergies render on consecutive rows; further matches collapse to a `+N more synergies` line. `_SYNERGY_PAIRS` widened from `tuple[id_a, id_b]` to `tuple[id_a, id_b, description]`; existing `detect_synergies()` callers stay compatible via a 2-tuple shim, and the new `detect_synergies_full()` returns the description too. `validate_synergy_ids()` was updated to walk the new 3-tuple format.
|
|
107
|
+
- **`src/belote/belatro/ui/trust_bar.py` polish (B.5)** — Four-tier colour ramp (cramoisi ≤2 / orange 3–4 / gold 5–7 / emeraude 8–10) replacing the previous three-tier red/gold/green. Leading tier glyph rendered from `_TIER_GLYPHS` (`✗ ♡ ♥ ♦ ★`) — Loyal/Mécène (tier ≥3) glyphs are bolded so the top tiers stand out. All four-tier transitions reuse `TrustTrack.tier`'s existing bucketing — no trust-math change.
|
|
108
|
+
- **`src/belote/belatro/items/base.py::Joker.shortcode`** — New class property used by the pip strip. Subclasses can set `_shortcode_override = "Cs"` for a custom 2-char tag; otherwise the property derives one from `name`/`id`. No subclass changes required for the existing roster — defaults are good enough.
|
|
109
|
+
|
|
110
|
+
### Verified clean — agent claims that did NOT survive source verification
|
|
111
|
+
|
|
112
|
+
These were flagged by the audit agents but verification against the current code showed they are either correct behaviour, by-design patterns, or already-handled invariants. Catalogued so they aren't re-investigated next cycle.
|
|
113
|
+
|
|
114
|
+
- **`game.py:562` "Tout Atout legal_cards downgrade" claim** — The `risers or tuple(my_suit_cards)` fallback is correct Belote: if you cannot rise within the lead suit, you may play any card *of that suit*. `my_suit_cards` is your hand filtered by lead suit, not played cards. **Not a bug.**
|
|
115
|
+
- **`game.py:947-955` "L'Anarchie unseeded `_rng`"** — The default `_rng = field(default_factory=random.Random)` IS unseeded, BUT `start_round()` at `game.py:302` always sets `_rng=rng` from the driver's seeded RNG before any round logic runs. By the time L'Anarchie consumes it at line 955 the seeded instance is in place. **Clean.**
|
|
116
|
+
- **`ai.py:73-92` "AI memory `last_voids_key` reset coverage"** — Both reset branches (new-round at line 73-78 and regression-detected at line 88-92) reset `last_voids_key` alongside the other three fields. **Clean per documented invariant.**
|
|
117
|
+
- **`run/shop.py:166-168` "Negative-edition double-fits a full inventory"** — The `joker_slots += 1; jokers.append()` sequence is the documented Negative design (see `_can_accept` docstring at line 145-147). Net effect: slot pool grows with the joker. **Not a bug.**
|
|
118
|
+
- **`round_driver.py:95-99` "Le Traître sabotage flag duplication"** — The guard `not state.boss_modifiers.agent_double_active` at line 95 and the population check `not state._joker_state.get("agent_double_tricks")` at line 120 prevent the double-population the agent feared. **Clean.**
|
|
119
|
+
- **`run_state.py:66` "`contract_levels` not reset per run"** — `BelAtroRun.contract_levels` is `field(default_factory=dict)`; each new run instance starts fresh. Within a run it intentionally accumulates so planet rewards persist. **By design.**
|
|
120
|
+
- **`registry.py:128-135` "`register_all_items` idempotency hole"** — The double-guard `_registered and registry.jokers` is *deliberate* per the docstring at line 130-133, to support test-suite registry resets. **Working as intended.**
|
|
121
|
+
|
|
122
|
+
### Internal
|
|
123
|
+
|
|
124
|
+
- **Tests**: 549 → 551 (+2 — A1 regression + E1 regression). Ruff and mypy strict still clean across all 76 source files.
|
|
125
|
+
- **Strict gates**: pytest 551/551, mypy 0 errors (76 files), ruff 0 violations.
|
|
126
|
+
- **`BidMadeEvent`** gained a `re_emit: bool = False` field. Existing call sites unchanged; only the three post-coinche refresh sites in `round_driver.py` opt into `re_emit=True`. Backward-compatible.
|
|
127
|
+
- **`_SYNERGY_PAIRS`** widened to 3-tuples. `detect_synergies()` keeps the historic `list[tuple[str, str]]` return; `detect_synergies_full()` exposes the description.
|
|
128
|
+
- **Deferred to a future release**: the larger render-pipeline features from the plan — score gutter (B.2) and trick-lane compass animation (B.1) — were scoped out because they touch `ui/render.py`'s line-assembly and vertical-centering logic, where a regression risks the classic and BelAtro display flows. They remain on the roadmap but want a dedicated session.
|
|
129
|
+
|
|
8
130
|
## [3.3.4] - 2026-05-10
|
|
9
131
|
|
|
10
132
|
Portability release — removes all terminal-bell / sound code, which was triggering SIGSYS ("Bad system call") on Alpine 23 (musl libc) the moment the first trick completed in classic Belote mode. BelAtro mode was unaffected on the same Alpine box (it never imported `play_sound`), and Kubuntu / Lubuntu 24.10 / 25.10 (glibc) were unaffected in either mode. Rather than guard the BEL writes behind a libc-detection flag, the entire sound subsystem is removed: classic Belote and BelAtro now share the same "no bells" baseline. 549 tests still passing, ruff and mypy strict still clean.
|
|
@@ -84,15 +84,17 @@ PYTHONPATH=src mypy --strict src/
|
|
|
84
84
|
# Linting (0 violations expected)
|
|
85
85
|
ruff check src/ tests/
|
|
86
86
|
|
|
87
|
-
# Full test suite (
|
|
87
|
+
# Full test suite (568 tests expected)
|
|
88
88
|
PYTHONPATH=src pytest
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Current baseline (3.
|
|
92
|
-
- **mypy**: 0 errors (strict mode,
|
|
91
|
+
Current baseline (3.4.2):
|
|
92
|
+
- **mypy**: 0 errors (strict mode, 76 files)
|
|
93
93
|
- **ruff**: 0 violations
|
|
94
|
-
- **pytest**:
|
|
95
|
-
- 3.3.4
|
|
94
|
+
- **pytest**: 568 tests, 0 failures
|
|
95
|
+
- 3.4.2 closes the 3.4.1 catalogue. All 7 confirmed bugs (C1 AI cheat under `hide_partner_hand`, C3 Dix de Der under La Rupture, C4 `opp_trumps` formula + TA total, H1 8 jokers seat→team, H4 TournoiAnte true 50%, H5 `load_profile` default unlocks, H7 classic-mode tie operator) plus H10 (`equip_joker` wires `on_purchase`) and M4 (delete dead `advance_turn`) ship in 3.4.2. +17 regression tests (551 → 568). H2 (`LEgoiste` partner-trick nullification) remains deferred — needs a spec call between code-comment intent and the audit's reading.
|
|
96
|
+
- 3.4.1 was **documentation-only** — an external LLM audit was verified against the source. 7 confirmed bugs were catalogued in `CHANGELOG.md` as deferred to 3.4.2+; 8 audit claims were rejected as false positives and are listed in the "Verified clean" section to block re-investigation. No source code changed in 3.4.1.
|
|
97
|
+
- 3.4.0 covered: A1 `BidMadeEvent` double-fire on coinche paths (HIGH), E1 endless mode replaying Ante 8 Boss instead of advancing to the first scaled cycle (HIGH), E2 classic-mode tie-breaker overridden by main loop (HIGH), A2 termios raw-mode leak on SSH drop (MED), A3 shop selection index off-by-one after reroll (MED), A5 prompts.py dead return (LOW). Plus HUD additions: joker pip strip with edition glow (B.3), synergy tooltip (B.4), four-tier trust bar with tier glyph (B.5). Score gutter (B.2) and trick-lane compass (B.1) intentionally deferred — they touch `ui/render.py`'s vertical-centering logic and want a dedicated session.
|
|
96
98
|
|
|
97
99
|
Run all gates before committing:
|
|
98
100
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.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,6 +45,31 @@ 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
|
+
## What's new in 3.4.2
|
|
49
|
+
|
|
50
|
+
- **The 3.4.1 catalogue is closed.** All 7 confirmed bugs plus the H10 architectural cleanup and the M4 dead-code deletion ship here. **C1 — AI cheating under Le Fantôme Partenaire:** AI memory now respects `hide_partner_hand`; the boss flag's visibility cost is paid by both sides. **C3 — Dix de Der announcement under La Rupture:** the 8th-trick "Team X" line now uses the Rupture-aware `compute_trick_winners` helper, so the named team matches what scoring credits. **C4 — `opp_trumps` formula:** subtracts South's own trumps, played trumps, and partner-visible trumps; under Tout Atout the total switches to 32 (every card is a trump). **H1 — 8 BelAtro jokers:** `LIdeologue`, `LeFanatique`, `LeDiplomate`, `LePatriote`, `LIllusionniste`, `LePremierSang`, `LeSergent`, `LExecuteur` now gate on `team_of(event.winner) == 0` instead of `event.winner == Seat.SOUTH`, matching the 3.2.0 fix that landed `LaSentinelle` and `LeDernierMot`. **H4 — TournoiAnte:** `on_blind_won` now receives `blind_payout` and pays a true 50% (was a flat function of `bonus_per_round`). **H5 — `load_profile`:** saves missing the `unlocked_ids` key fall back to the Profile dataclass default starter unlocks. **H7 — classic win attribution on ties:** `update_stats_game` operator aligned to `ns > ew` so the stats line agrees with `menu.py`'s visible winner on an exact tie at target.
|
|
51
|
+
- **Architectural / cleanup.** `equip_joker` accepts an optional `run` and fires `on_purchase` when provided (forward-looking — no current partner joker has a purchase hook). Dead `advance_turn()` deleted from `game.py`.
|
|
52
|
+
- **Still deferred** (need spec calls, not implementation work): H2 `LEgoiste` partner-trick nullification (comment vs. audit reading); `LeRebelle` `on_belote` seat/team gating (separate code path from H1).
|
|
53
|
+
- **Tests + gates** — 568 tests (up from 551, +17 regressions). Five existing `test_north_*_returns_none` tests in `test_belatro.py` were flipped to assert the new team-aware behaviour (they had encoded the H1 bug as a contract). Strict gates still clean: pytest 568/568, mypy 0 errors (76 files), ruff 0 violations.
|
|
54
|
+
|
|
55
|
+
## What's new in 3.4.1
|
|
56
|
+
|
|
57
|
+
- **Audit verification pass — no code changes.** An external LLM audit ("Comprehensive Audit Report — Belote CLI v3.4.0") flagged 4 Critical, 10 High, and 12 Medium issues. Direct verification against the source confirmed **7 real bugs** (3 Critical, 4 High), **1 architectural latent issue**, and **1 disputed claim** needing a spec decision; **8 claims were false positives**. The roadmap and the false-positive catalogue are recorded in `CHANGELOG.md` so the next session has a vetted target list and the rejected claims aren't re-investigated. *Note (3.4.2): the catalogue is now closed — all 7 confirmed bugs plus H10 and M4 are fixed in 3.4.2. The 3.4.1 entry is preserved as a historical record of the verification-only release.*
|
|
58
|
+
- **Confirmed bugs deferred to 3.4.2+**: AI ignoring `hide_partner_hand` (C1), Dix de Der announcement not La Rupture-aware (C3), AI `opp_trumps` over-count (C4), 8 BelAtro jokers still gated on South instead of NS team (H1), TournoiAnte bonus not actually 50% (H4), `load_profile` losing default unlocks (H5), classic-mode tie-break operator mismatch with the menu summary (H7). Plus latent: `equip_joker` skipping `on_purchase` (H10), and a dead `advance_turn()` (M4).
|
|
59
|
+
- **Audit calibration findings** — recurring failure modes documented for the next pass: defense-in-depth confused with bugs (C2/H8/H9), intent inversion against adjacent comments (H2/M2/M12), self-cancelling dead-code claims (H3), and joker-name hallucination (H1's `LeRebelle`). Useful as feedback to the auditing model.
|
|
60
|
+
- **No behavioural changes.** Test count, mypy strict, ruff all unchanged from 3.4.0: pytest 551/551, mypy 0 errors (76 files), ruff 0 violations.
|
|
61
|
+
|
|
62
|
+
## What's new in 3.4.0
|
|
63
|
+
|
|
64
|
+
- **BelAtro joker correctness** — `BidMadeEvent` no longer double-fires `on_bid` joker handlers on coinche paths. Pre-3.4.0 the player-coinche, AI-partner-coinche, `auto_coinche` boss, and `start_coinched` deck-mod paths all re-emitted the same bid event a second time with the resolved `coinche_level`, so on_bid jokers (Le Passeur today, anything new tomorrow) were silently invoked twice for the same bid. Fixed via a `re_emit: bool` field on the event; refreshes update `joker_state["contract"]` but skip joker firing.
|
|
65
|
+
- **Endless mode honours its prompt** — Accepting "Continue into Endless Mode? (Ante 9+ scales ×2.2)" used to leave the player at Ante 8 Boss Blind for one more *un-scaled* round before the ×2.2 kicked in. `enter_endless()` now advances into the first scaled cycle (offset=1, blind_index=0) immediately, so the very next round is the scaled Small Blind as advertised.
|
|
66
|
+
- **Classic mode tie-breaker actually plays** — When both teams ended a round tied at exactly the target score, the classic loop unconditionally forced GAME_OVER, overriding `apply_round_score`'s deliberate `phase=DEAL` for tie-breaker rounds. The redundant re-check is gone; the scoring layer is the single source of truth for game-over phase.
|
|
67
|
+
- **Terminal raw-mode no longer leaks on SSH drop** — `_UnixKeyReader.restore()` now wraps `termios.tcsetattr` in `contextlib.suppress(termios.error, OSError)` and marks the reader restored regardless. A dropped SSH session previously left the parent shell in no-echo mode.
|
|
68
|
+
- **Shop reroll OOB fix** — Shop selection index after reroll is now correctly clamped to `len(inventory) - 1` instead of `len(inventory)`, preventing an out-of-range access on the next render.
|
|
69
|
+
- **HUD: joker pip strip + synergy tooltip + polished trust bar** — Top-row 5-slot strip shows your jokers as compact pips with edition tint (Foil cyan / Holo magenta / Polychrome pink-violet / Negative reverse-video); slots in an active synergy pair gain a `*` marker, and a one-line tooltip below the score line describes the synergy. The trust bar gains a four-tier colour ramp (cramoisi/orange/gold/emeraude) and a leading tier glyph (`✗ ♡ ♥ ♦ ★`) — Loyal/Mécène glyphs are bolded so the top tiers pop.
|
|
70
|
+
- **Audit reconciliation** — A fresh three-agent audit pass surfaced ~80 candidate findings; verification rejected ~95% as false positives or by-design patterns. The seven survivors are the fixes above; the rejected claims are catalogued in `CHANGELOG.md` so they aren't re-investigated.
|
|
71
|
+
- **Test coverage** — 551 tests (up from 549). Strict gates still clean: pytest 551/551, mypy 0 errors (76 files), ruff 0 violations.
|
|
72
|
+
|
|
48
73
|
## What's new in 3.3.4
|
|
49
74
|
|
|
50
75
|
- **Portability fix** — Removed all terminal-bell / sound code, which was triggering SIGSYS ("Bad system call") on Alpine 23 (musl libc) the moment the first trick completed in classic Belote mode. BelAtro mode and every glibc-based distro (Kubuntu / Lubuntu 24.10 / 25.10) were unaffected, but rather than guard the BEL writes behind a libc check, the entire sound subsystem is gone — `play_sound`, `AudioManager` / `AUDIO`, `is_muted` / `toggle_mute`, the `[M]` mute key, and the help-screen mute line. Classic Belote and BelAtro now share the same "no bells" baseline.
|
|
@@ -326,7 +351,7 @@ belote/
|
|
|
326
351
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
327
352
|
│ ├── stats.py # Global and session statistics tracking
|
|
328
353
|
│ └── rules.py # Game rules content
|
|
329
|
-
├── tests/ # Comprehensive test suite (
|
|
354
|
+
├── tests/ # Comprehensive test suite (568 tests)
|
|
330
355
|
├── scripts/ # Performance benchmarks
|
|
331
356
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
332
357
|
├── LICENSE # MIT License
|
|
@@ -342,14 +367,14 @@ belote/
|
|
|
342
367
|
PYTHONPATH=src pytest
|
|
343
368
|
```
|
|
344
369
|
|
|
345
|
-
Currently **
|
|
370
|
+
Currently **568 tests** passing with 100% coverage on game-logic modules.
|
|
346
371
|
|
|
347
372
|
## Technical Integrity
|
|
348
373
|
|
|
349
374
|
The codebase is strictly validated with the following tools:
|
|
350
375
|
- **mypy**: 0 errors (strict type safety)
|
|
351
376
|
- **ruff**: 0 violations (linting & formatting)
|
|
352
|
-
- **pytest**:
|
|
377
|
+
- **pytest**: 568/568 passed
|
|
353
378
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
354
379
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
355
380
|
|
|
@@ -2,6 +2,31 @@
|
|
|
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
|
+
## What's new in 3.4.2
|
|
6
|
+
|
|
7
|
+
- **The 3.4.1 catalogue is closed.** All 7 confirmed bugs plus the H10 architectural cleanup and the M4 dead-code deletion ship here. **C1 — AI cheating under Le Fantôme Partenaire:** AI memory now respects `hide_partner_hand`; the boss flag's visibility cost is paid by both sides. **C3 — Dix de Der announcement under La Rupture:** the 8th-trick "Team X" line now uses the Rupture-aware `compute_trick_winners` helper, so the named team matches what scoring credits. **C4 — `opp_trumps` formula:** subtracts South's own trumps, played trumps, and partner-visible trumps; under Tout Atout the total switches to 32 (every card is a trump). **H1 — 8 BelAtro jokers:** `LIdeologue`, `LeFanatique`, `LeDiplomate`, `LePatriote`, `LIllusionniste`, `LePremierSang`, `LeSergent`, `LExecuteur` now gate on `team_of(event.winner) == 0` instead of `event.winner == Seat.SOUTH`, matching the 3.2.0 fix that landed `LaSentinelle` and `LeDernierMot`. **H4 — TournoiAnte:** `on_blind_won` now receives `blind_payout` and pays a true 50% (was a flat function of `bonus_per_round`). **H5 — `load_profile`:** saves missing the `unlocked_ids` key fall back to the Profile dataclass default starter unlocks. **H7 — classic win attribution on ties:** `update_stats_game` operator aligned to `ns > ew` so the stats line agrees with `menu.py`'s visible winner on an exact tie at target.
|
|
8
|
+
- **Architectural / cleanup.** `equip_joker` accepts an optional `run` and fires `on_purchase` when provided (forward-looking — no current partner joker has a purchase hook). Dead `advance_turn()` deleted from `game.py`.
|
|
9
|
+
- **Still deferred** (need spec calls, not implementation work): H2 `LEgoiste` partner-trick nullification (comment vs. audit reading); `LeRebelle` `on_belote` seat/team gating (separate code path from H1).
|
|
10
|
+
- **Tests + gates** — 568 tests (up from 551, +17 regressions). Five existing `test_north_*_returns_none` tests in `test_belatro.py` were flipped to assert the new team-aware behaviour (they had encoded the H1 bug as a contract). Strict gates still clean: pytest 568/568, mypy 0 errors (76 files), ruff 0 violations.
|
|
11
|
+
|
|
12
|
+
## What's new in 3.4.1
|
|
13
|
+
|
|
14
|
+
- **Audit verification pass — no code changes.** An external LLM audit ("Comprehensive Audit Report — Belote CLI v3.4.0") flagged 4 Critical, 10 High, and 12 Medium issues. Direct verification against the source confirmed **7 real bugs** (3 Critical, 4 High), **1 architectural latent issue**, and **1 disputed claim** needing a spec decision; **8 claims were false positives**. The roadmap and the false-positive catalogue are recorded in `CHANGELOG.md` so the next session has a vetted target list and the rejected claims aren't re-investigated. *Note (3.4.2): the catalogue is now closed — all 7 confirmed bugs plus H10 and M4 are fixed in 3.4.2. The 3.4.1 entry is preserved as a historical record of the verification-only release.*
|
|
15
|
+
- **Confirmed bugs deferred to 3.4.2+**: AI ignoring `hide_partner_hand` (C1), Dix de Der announcement not La Rupture-aware (C3), AI `opp_trumps` over-count (C4), 8 BelAtro jokers still gated on South instead of NS team (H1), TournoiAnte bonus not actually 50% (H4), `load_profile` losing default unlocks (H5), classic-mode tie-break operator mismatch with the menu summary (H7). Plus latent: `equip_joker` skipping `on_purchase` (H10), and a dead `advance_turn()` (M4).
|
|
16
|
+
- **Audit calibration findings** — recurring failure modes documented for the next pass: defense-in-depth confused with bugs (C2/H8/H9), intent inversion against adjacent comments (H2/M2/M12), self-cancelling dead-code claims (H3), and joker-name hallucination (H1's `LeRebelle`). Useful as feedback to the auditing model.
|
|
17
|
+
- **No behavioural changes.** Test count, mypy strict, ruff all unchanged from 3.4.0: pytest 551/551, mypy 0 errors (76 files), ruff 0 violations.
|
|
18
|
+
|
|
19
|
+
## What's new in 3.4.0
|
|
20
|
+
|
|
21
|
+
- **BelAtro joker correctness** — `BidMadeEvent` no longer double-fires `on_bid` joker handlers on coinche paths. Pre-3.4.0 the player-coinche, AI-partner-coinche, `auto_coinche` boss, and `start_coinched` deck-mod paths all re-emitted the same bid event a second time with the resolved `coinche_level`, so on_bid jokers (Le Passeur today, anything new tomorrow) were silently invoked twice for the same bid. Fixed via a `re_emit: bool` field on the event; refreshes update `joker_state["contract"]` but skip joker firing.
|
|
22
|
+
- **Endless mode honours its prompt** — Accepting "Continue into Endless Mode? (Ante 9+ scales ×2.2)" used to leave the player at Ante 8 Boss Blind for one more *un-scaled* round before the ×2.2 kicked in. `enter_endless()` now advances into the first scaled cycle (offset=1, blind_index=0) immediately, so the very next round is the scaled Small Blind as advertised.
|
|
23
|
+
- **Classic mode tie-breaker actually plays** — When both teams ended a round tied at exactly the target score, the classic loop unconditionally forced GAME_OVER, overriding `apply_round_score`'s deliberate `phase=DEAL` for tie-breaker rounds. The redundant re-check is gone; the scoring layer is the single source of truth for game-over phase.
|
|
24
|
+
- **Terminal raw-mode no longer leaks on SSH drop** — `_UnixKeyReader.restore()` now wraps `termios.tcsetattr` in `contextlib.suppress(termios.error, OSError)` and marks the reader restored regardless. A dropped SSH session previously left the parent shell in no-echo mode.
|
|
25
|
+
- **Shop reroll OOB fix** — Shop selection index after reroll is now correctly clamped to `len(inventory) - 1` instead of `len(inventory)`, preventing an out-of-range access on the next render.
|
|
26
|
+
- **HUD: joker pip strip + synergy tooltip + polished trust bar** — Top-row 5-slot strip shows your jokers as compact pips with edition tint (Foil cyan / Holo magenta / Polychrome pink-violet / Negative reverse-video); slots in an active synergy pair gain a `*` marker, and a one-line tooltip below the score line describes the synergy. The trust bar gains a four-tier colour ramp (cramoisi/orange/gold/emeraude) and a leading tier glyph (`✗ ♡ ♥ ♦ ★`) — Loyal/Mécène glyphs are bolded so the top tiers pop.
|
|
27
|
+
- **Audit reconciliation** — A fresh three-agent audit pass surfaced ~80 candidate findings; verification rejected ~95% as false positives or by-design patterns. The seven survivors are the fixes above; the rejected claims are catalogued in `CHANGELOG.md` so they aren't re-investigated.
|
|
28
|
+
- **Test coverage** — 551 tests (up from 549). Strict gates still clean: pytest 551/551, mypy 0 errors (76 files), ruff 0 violations.
|
|
29
|
+
|
|
5
30
|
## What's new in 3.3.4
|
|
6
31
|
|
|
7
32
|
- **Portability fix** — Removed all terminal-bell / sound code, which was triggering SIGSYS ("Bad system call") on Alpine 23 (musl libc) the moment the first trick completed in classic Belote mode. BelAtro mode and every glibc-based distro (Kubuntu / Lubuntu 24.10 / 25.10) were unaffected, but rather than guard the BEL writes behind a libc check, the entire sound subsystem is gone — `play_sound`, `AudioManager` / `AUDIO`, `is_muted` / `toggle_mute`, the `[M]` mute key, and the help-screen mute line. Classic Belote and BelAtro now share the same "no bells" baseline.
|
|
@@ -283,7 +308,7 @@ belote/
|
|
|
283
308
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
284
309
|
│ ├── stats.py # Global and session statistics tracking
|
|
285
310
|
│ └── rules.py # Game rules content
|
|
286
|
-
├── tests/ # Comprehensive test suite (
|
|
311
|
+
├── tests/ # Comprehensive test suite (568 tests)
|
|
287
312
|
├── scripts/ # Performance benchmarks
|
|
288
313
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
289
314
|
├── LICENSE # MIT License
|
|
@@ -299,14 +324,14 @@ belote/
|
|
|
299
324
|
PYTHONPATH=src pytest
|
|
300
325
|
```
|
|
301
326
|
|
|
302
|
-
Currently **
|
|
327
|
+
Currently **568 tests** passing with 100% coverage on game-logic modules.
|
|
303
328
|
|
|
304
329
|
## Technical Integrity
|
|
305
330
|
|
|
306
331
|
The codebase is strictly validated with the following tools:
|
|
307
332
|
- **mypy**: 0 errors (strict type safety)
|
|
308
333
|
- **ruff**: 0 violations (linting & formatting)
|
|
309
|
-
- **pytest**:
|
|
334
|
+
- **pytest**: 568/568 passed
|
|
310
335
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
311
336
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
312
337
|
|
|
@@ -102,8 +102,10 @@ class AIPlayer:
|
|
|
102
102
|
# In this implementation, AI tracks what it can see
|
|
103
103
|
p = partner(self.seat)
|
|
104
104
|
self.memory.partner_hand.clear()
|
|
105
|
-
if
|
|
106
|
-
|
|
105
|
+
if (
|
|
106
|
+
state.phase in (Phase.PLAYING, Phase.SCORING)
|
|
107
|
+
and not state.boss_modifiers.hide_partner_hand
|
|
108
|
+
):
|
|
107
109
|
for card in state.hand_of(p):
|
|
108
110
|
self.memory.partner_hand.add(card)
|
|
109
111
|
|
|
@@ -529,8 +531,22 @@ class AIPlayer:
|
|
|
529
531
|
|
|
530
532
|
my_hand = state.hand_of(self.seat)
|
|
531
533
|
hand_suit_counts: dict[Suit, int] = Counter(c.suit for c in my_hand)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
+
# Under Tout Atout every card is a trump; under a normal contract
|
|
535
|
+
# trump cards are those matching the trump suit. `opp_trumps` must
|
|
536
|
+
# subtract everything that is no longer in opponents' hands: my own
|
|
537
|
+
# trumps, trumps already played, and any of partner's visible
|
|
538
|
+
# trumps (empty under `hide_partner_hand`).
|
|
539
|
+
if trump is Suit.TOUT_ATOUT:
|
|
540
|
+
total_trumps = 32
|
|
541
|
+
my_trumps = len(my_hand)
|
|
542
|
+
played_trumps = len(self.memory.played)
|
|
543
|
+
partner_trumps = len(self.memory.partner_hand)
|
|
544
|
+
else:
|
|
545
|
+
total_trumps = 8
|
|
546
|
+
my_trumps = hand_suit_counts.get(trump, 0)
|
|
547
|
+
played_trumps = sum(1 for c in self.memory.played if c.suit == trump)
|
|
548
|
+
partner_trumps = sum(1 for c in self.memory.partner_hand if c.suit == trump)
|
|
549
|
+
opp_trumps = max(0, total_trumps - my_trumps - played_trumps - partner_trumps)
|
|
534
550
|
|
|
535
551
|
# Score each legal card by expected outcome
|
|
536
552
|
best_card = legal[0]
|
|
@@ -215,7 +215,18 @@ class BelAtroRun:
|
|
|
215
215
|
self.run_over = True
|
|
216
216
|
|
|
217
217
|
def enter_endless(self) -> None:
|
|
218
|
-
"""Toggle endless mode after beating ante 8.
|
|
218
|
+
"""Toggle endless mode after beating ante 8.
|
|
219
|
+
|
|
220
|
+
Pre-3.4.0 the loop continued at (ante=8, blind=2, offset=0), which made
|
|
221
|
+
the *first* endless round replay the Ante 8 Boss Blind at the same
|
|
222
|
+
target before the ×2.2 scaling kicked in on the next cycle. We now
|
|
223
|
+
advance into a fresh endless cycle here so the prompt's "Ante 9+ scales
|
|
224
|
+
×2.2" is honoured immediately.
|
|
225
|
+
"""
|
|
219
226
|
self.endless = True
|
|
220
227
|
self.run_won = False # endless overrides run-won state
|
|
221
228
|
self.run_over = False # ...and re-opens the run so the main loop continues
|
|
229
|
+
# Skip the redundant Ante 8 Boss replay: bump offset and restart the
|
|
230
|
+
# blind cycle. max(...) preserves any externally-set offset (tests).
|
|
231
|
+
self.endless_ante_offset = max(self.endless_ante_offset, 1)
|
|
232
|
+
self.blind_index = 0
|
|
@@ -230,7 +230,10 @@ class ScoreAccumulator:
|
|
|
230
230
|
elif isinstance(event, BidMadeEvent):
|
|
231
231
|
# Inject contract type into joker state so jokers can read it
|
|
232
232
|
joker_state["contract"] = event.contract
|
|
233
|
-
|
|
233
|
+
# Re-emits (post-coinche refresh) update derived state but must not
|
|
234
|
+
# re-fire on_bid jokers — those already fired for the original bid.
|
|
235
|
+
if not event.re_emit:
|
|
236
|
+
_fire_jokers("on_bid", event)
|
|
234
237
|
|
|
235
238
|
# Update GameState with new values
|
|
236
239
|
return replace(
|
|
@@ -51,6 +51,12 @@ class BidMadeEvent:
|
|
|
51
51
|
trump: Suit | None # None = pass
|
|
52
52
|
contract: str # "normal" | "tout_atout" | "sans_atout" | "coinche" | "surcoinche"
|
|
53
53
|
coinche_level: int = 0 # 0=none, 1=coinche, 2=surcoinche
|
|
54
|
+
# When True, this event is a post-coinche refresh of an already-emitted bid.
|
|
55
|
+
# Consumers should update derived state (HUD, joker_state["contract"]) but
|
|
56
|
+
# MUST NOT re-fire `on_bid` jokers — those already fired for the original
|
|
57
|
+
# bid during the bidding loop. Without this flag, jokers like Le Passeur
|
|
58
|
+
# would double-count or future on_bid-based scoring would silently overpay.
|
|
59
|
+
re_emit: bool = False
|
|
54
60
|
|
|
55
61
|
|
|
56
62
|
# ── Bus ────────────────────────────────────────────────────────────────────
|
|
@@ -238,7 +238,9 @@ def drive_round(
|
|
|
238
238
|
# L'Avocat boss forces at least coinche=1 (existing auto_coinche flag).
|
|
239
239
|
if state.boss_modifiers.auto_coinche:
|
|
240
240
|
coinche_level = max(coinche_level, 1)
|
|
241
|
-
#
|
|
241
|
+
# Refresh joker_state with the resolved coinche level via a re-emit.
|
|
242
|
+
# `re_emit=True` updates derived state (HUD, joker_state["contract"])
|
|
243
|
+
# without re-firing on_bid jokers — those already fired in the loop.
|
|
242
244
|
if coinche_level > 0:
|
|
243
245
|
state = _emit(
|
|
244
246
|
BidMadeEvent(
|
|
@@ -246,15 +248,14 @@ def drive_round(
|
|
|
246
248
|
trump=state.trump,
|
|
247
249
|
contract=state.contract or "normal",
|
|
248
250
|
coinche_level=coinche_level,
|
|
251
|
+
re_emit=True,
|
|
249
252
|
),
|
|
250
253
|
state,
|
|
251
254
|
)
|
|
252
255
|
elif state.boss_modifiers.auto_coinche and state.phase == Phase.PLAYING:
|
|
253
256
|
# Boss forces coinche even if taker is on NS team.
|
|
254
257
|
coinche_level = 1
|
|
255
|
-
# Re-emit
|
|
256
|
-
# coinche level. The EW-taker branch above does this; this NS branch
|
|
257
|
-
# used to skip it, silently dropping the event for on_bid subscribers.
|
|
258
|
+
# Re-emit refresh — see comment above; on_bid is suppressed via re_emit.
|
|
258
259
|
if state.taker is not None:
|
|
259
260
|
state = _emit(
|
|
260
261
|
BidMadeEvent(
|
|
@@ -262,6 +263,7 @@ def drive_round(
|
|
|
262
263
|
trump=state.trump,
|
|
263
264
|
contract=state.contract or "normal",
|
|
264
265
|
coinche_level=coinche_level,
|
|
266
|
+
re_emit=True,
|
|
265
267
|
),
|
|
266
268
|
state,
|
|
267
269
|
)
|
|
@@ -280,6 +282,7 @@ def drive_round(
|
|
|
280
282
|
trump=state.trump,
|
|
281
283
|
contract=state.contract or "normal",
|
|
282
284
|
coinche_level=coinche_level,
|
|
285
|
+
re_emit=True,
|
|
283
286
|
),
|
|
284
287
|
state,
|
|
285
288
|
)
|
|
@@ -61,6 +61,19 @@ class Joker(ABC):
|
|
|
61
61
|
# NONE for backward compatibility with existing tests that instantiate
|
|
62
62
|
# jokers directly.
|
|
63
63
|
edition: Edition = Edition.NONE
|
|
64
|
+
# 3.4.0: short 2-char label used by the joker pip strip in the HUD. Sub-
|
|
65
|
+
# classes may override; the default takes the first two ASCII letters of
|
|
66
|
+
# `name` for instances that don't set their own. Resolved lazily so the
|
|
67
|
+
# default doesn't snapshot during class definition before name is set.
|
|
68
|
+
_shortcode_override: str = ""
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def shortcode(self) -> str:
|
|
72
|
+
if self._shortcode_override:
|
|
73
|
+
return self._shortcode_override[:2]
|
|
74
|
+
# Strip non-letters (avoid leading "L'" or "Le " producing empty codes)
|
|
75
|
+
letters = "".join(c for c in (self.name or self.id or "??") if c.isalpha())
|
|
76
|
+
return (letters[:2] or "??").upper()
|
|
64
77
|
|
|
65
78
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
66
79
|
return None
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from belote.deck import Rank, Suit, card_points
|
|
6
|
-
from belote.game import Seat
|
|
6
|
+
from belote.game import Seat, team_of
|
|
7
7
|
|
|
8
8
|
from ...engine.event_bus import BeloteAnnouncedEvent, RoundEndEvent, TrickWonEvent
|
|
9
9
|
from ..base import Joker, JokerResult
|
|
@@ -18,7 +18,7 @@ class LIdeologue(Joker):
|
|
|
18
18
|
|
|
19
19
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
20
20
|
# Sans Atout has event.trump as None
|
|
21
|
-
if event.winner ==
|
|
21
|
+
if team_of(event.winner) == 0 and event.trump is None:
|
|
22
22
|
jacks = sum(1 for c in event.cards if c.rank == Rank.JACK)
|
|
23
23
|
if jacks > 0:
|
|
24
24
|
# In Sans Atout, Jack is worth 2. We want it to be 20.
|
|
@@ -42,7 +42,7 @@ class LeFanatique(Joker):
|
|
|
42
42
|
if state.get("contract") != "tout_atout":
|
|
43
43
|
return None
|
|
44
44
|
|
|
45
|
-
if event.winner ==
|
|
45
|
+
if team_of(event.winner) == 0:
|
|
46
46
|
wins = state.get(f"{self.id}_wins", 0) + 1
|
|
47
47
|
state[f"{self.id}_wins"] = wins
|
|
48
48
|
if wins > 4:
|
|
@@ -59,7 +59,7 @@ class LeDiplomate(Joker):
|
|
|
59
59
|
cost = 7
|
|
60
60
|
|
|
61
61
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
62
|
-
if event.winner ==
|
|
62
|
+
if team_of(event.winner) == 0:
|
|
63
63
|
suits: dict[Suit, set[Rank]] = {}
|
|
64
64
|
for c in event.cards:
|
|
65
65
|
if c.rank in (Rank.KING, Rank.QUEEN):
|
|
@@ -78,7 +78,7 @@ class LePatriote(Joker):
|
|
|
78
78
|
cost = 6
|
|
79
79
|
|
|
80
80
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
81
|
-
if event.winner ==
|
|
81
|
+
if team_of(event.winner) == 0 and event.trump:
|
|
82
82
|
trump_pts = sum(
|
|
83
83
|
card_points(c, event.trump) for c in event.cards if c.suit == event.trump
|
|
84
84
|
)
|
|
@@ -125,7 +125,7 @@ class LIllusionniste(Joker):
|
|
|
125
125
|
cost = 9
|
|
126
126
|
|
|
127
127
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
128
|
-
if event.winner ==
|
|
128
|
+
if team_of(event.winner) == 0 and event.trump:
|
|
129
129
|
extra_pts = sum(
|
|
130
130
|
18 for c in event.cards if c.rank == Rank.JACK and c.suit != event.trump
|
|
131
131
|
)
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from belote.game import
|
|
5
|
+
from belote.game import team_of
|
|
6
6
|
|
|
7
7
|
from ...engine.event_bus import TrickWonEvent
|
|
8
8
|
from ..base import Joker, JokerResult
|
|
@@ -23,11 +23,11 @@ class LePremierSang(Joker):
|
|
|
23
23
|
# subsequent NS-won trick for the rest of the round.
|
|
24
24
|
active = state.get(f"{self.id}_active", False)
|
|
25
25
|
if event.trick_number == 1:
|
|
26
|
-
if event.winner ==
|
|
26
|
+
if team_of(event.winner) == 0:
|
|
27
27
|
state[f"{self.id}_active"] = True
|
|
28
28
|
return JokerResult(add_mult=2.0)
|
|
29
29
|
return None
|
|
30
|
-
if active and event.winner ==
|
|
30
|
+
if active and team_of(event.winner) == 0:
|
|
31
31
|
return JokerResult(add_mult=2.0)
|
|
32
32
|
return None
|
|
33
33
|
|
|
@@ -43,7 +43,7 @@ class LeSergent(Joker):
|
|
|
43
43
|
return None
|
|
44
44
|
|
|
45
45
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
46
|
-
if event.winner ==
|
|
46
|
+
if team_of(event.winner) == 0:
|
|
47
47
|
streak = state.get(f"{self.id}_streak", 0) + 1
|
|
48
48
|
state[f"{self.id}_streak"] = streak
|
|
49
49
|
return JokerResult(add_mult=0.5)
|
|
@@ -79,7 +79,7 @@ class LExecuteur(Joker):
|
|
|
79
79
|
is_unlockable = True
|
|
80
80
|
|
|
81
81
|
def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
82
|
-
if event.is_last and event.winner ==
|
|
82
|
+
if event.is_last and team_of(event.winner) == 0:
|
|
83
83
|
return JokerResult(add_chips=40, times_mult=1.5)
|
|
84
84
|
return None
|
|
85
85
|
|
|
@@ -375,6 +375,7 @@ class BelAtroGame:
|
|
|
375
375
|
if not lock_trust:
|
|
376
376
|
trust.blind_failed()
|
|
377
377
|
else:
|
|
378
|
+
money_before = self.run.economy.money
|
|
378
379
|
payout = self.run.economy.process_round_end(total - self.run.target_score)
|
|
379
380
|
if auto_coinche_active:
|
|
380
381
|
self.run.economy.add_money(payout * 2) # L'Avocat: triple total payout
|
|
@@ -409,7 +410,8 @@ class BelAtroGame:
|
|
|
409
410
|
# awards bonus money, Café gives +1 trust on big-blind wins).
|
|
410
411
|
theme = self.run.get_ante_theme()
|
|
411
412
|
if theme is not None:
|
|
412
|
-
|
|
413
|
+
blind_payout = self.run.economy.money - money_before
|
|
414
|
+
theme.on_blind_won(self.run, self.run.blind_index, blind_payout)
|
|
413
415
|
|
|
414
416
|
# Partner-specific trust events (skipped under Le Divorce)
|
|
415
417
|
if not lock_trust:
|