belote-cli 3.4.2__tar.gz → 3.7.1__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.4.2 → belote_cli-3.7.1}/CHANGELOG.md +131 -0
- belote_cli-3.7.1/DEVELOPMENT.md +197 -0
- belote_cli-3.7.1/PKG-INFO +300 -0
- belote_cli-3.7.1/README.md +257 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/pyproject.toml +1 -1
- {belote_cli-3.4.2 → belote_cli-3.7.1}/scripts/benchmark.py +90 -5
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/__init__.py +1 -1
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/a11y.py +5 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/achievements.py +6 -9
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ai.py +4 -6
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/core/run_state.py +25 -5
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/core/scoring.py +61 -12
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/engine/event_bus.py +48 -4
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/engine/modifier_patch.py +10 -3
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/engine/round_driver.py +69 -39
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/annonces.py +1 -3
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/coinche.py +2 -2
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/contract.py +1 -1
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/hand_comp.py +2 -2
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/registry.py +10 -7
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/main.py +13 -1
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/progression/unlocks.py +2 -2
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/run_summary.py +5 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/announce.py +2 -2
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/collection.py +1 -1
- belote_cli-3.7.1/src/belote/belatro/ui/consumables.py +89 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/menu.py +2 -2
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/rules.py +1 -1
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/shop.py +17 -5
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/deck.py +30 -3
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/game.py +215 -130
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/gameflow.py +2 -2
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/input.py +9 -1
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/main.py +1 -1
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/scoring.py +394 -242
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ui/menu.py +2 -2
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ui/prompts.py +7 -3
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ui/render.py +7 -5
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_belatro.py +77 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_boss_modifiers_integration.py +26 -0
- belote_cli-3.7.1/tests/belatro/test_consumables_ui.py +125 -0
- belote_cli-3.7.1/tests/belatro/test_event_bus.py +96 -0
- belote_cli-3.7.1/tests/belatro/test_partner_jokers.py +346 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_round_driver.py +241 -0
- belote_cli-3.7.1/tests/belatro/test_run_summary.py +65 -0
- belote_cli-3.7.1/tests/test_declaration_tiebreak.py +115 -0
- belote_cli-3.7.1/tests/test_input_eof.py +101 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_properties.py +94 -0
- belote_cli-3.4.2/DEVELOPMENT.md +0 -180
- belote_cli-3.4.2/PKG-INFO +0 -399
- belote_cli-3.4.2/README.md +0 -356
- {belote_cli-3.4.2 → belote_cli-3.7.1}/.claude/settings.local.json +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/.gitignore +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/.python-version +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/LICENSE +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ansi.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ghost_run.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/partner/trust.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/history.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/hud.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/config.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/context.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/replay.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/rules.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/stats.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/themes.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ui/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ui/announce.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/src/belote/ui/layout.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/__init__.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_dead_flag_fixes.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_ghost_run.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_history_overlay.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_hud_synergy.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_phase0_coverage.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/belatro/test_progression.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_a11y.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_achievements.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_ai.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_ansi_helpers.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_belote.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_extended.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_game_logic.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_gameflow.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_layout.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_new_coverage.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_official_rules.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_replay.py +0 -0
- {belote_cli-3.4.2 → belote_cli-3.7.1}/tests/test_undo.py +0 -0
|
@@ -5,6 +5,137 @@ 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.7.1] - 2026-05-13
|
|
9
|
+
|
|
10
|
+
Bug-hunt, performance, and logic audit pass plus the three items 3.6.0 deferred to 3.7.0. Three Explore agents ran in parallel against the documented false-positive catalogue. The classic-engine sweep returned **no novel findings** — the 3.4.x → 3.6.0 audits have absorbed the available correctness surface. The BelAtro layer produced **2 confirmed bugs** (one HIGH, one MEDIUM) and **1 polish item**. The deferred 3.7.0 items — `score_round` / `play_card` refactor, partner-joker test coverage, player-facing NS-taker surcoinche — all land here. **+36 regression tests** (599 → 635). Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-sequential-map.md`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`src/belote/belatro/items/jokers/hand_comp.py:88` (BA-L2, HIGH) — L'Accumulateur now credits team trick wins, not just South.** Pre-3.7.1 the joker gated on `event.winner == Seat.SOUTH`, silently dropping +5 chips per 7/8 whenever the partner (NORTH) won the trick. The description says *"For every 7 or 8 **you** win in a trick"* and in BelAtro "you" = team (NS) — same convention applied to Le Patriote / Le Premier Sang / Le Sergent in 3.5.0. L'Accumulateur was missed in that pass. Fixed: `if team_of(event.winner) == 0:`. 6 regression tests in `tests/belatro/test_belatro.py::TestLAccumulateurTeamCredit` covering NORTH credit, EAST/WEST non-credit, mixed-round accumulation, and rank-8 parity with rank-7.
|
|
15
|
+
- **`src/belote/belatro/core/scoring.py:43,48,54` (BA-L1, MEDIUM) — `ContractReward` TypedDict now correctly annotates float fields as `float`.** `add_mult`, `bonus_mult_per_trick`, and `coinche_multiplier` were annotated `int` but populated with `0.3` / `1.0` / `1.0` by `planets.py`. Python's numeric coercion masked this at runtime, but the TypedDict was introduced explicitly to catch planet-reward key typos at type-check time — broken annotations defeated the purpose. Fixed; `mypy --strict` stays green (after pinning `libra_bonus: float` on the consumer side to keep the inference path explicit).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`src/belote/scoring.py:599-878` (D1) — `score_round` extracted behind `_ScoringContext`.** ~280-LOC / ~30-branch monolith split into `_compute_belote_points`, `_compute_declaration_points`, `_score_capot_outcome`, `_score_normal_outcome`, with a `_ScoringContext` frozen+slotted dataclass threading pre-computed values (trump, taker, winners, tricks_ns/ew) into the helpers. Behaviour unchanged: zero test edits required. 3.6.0 deferred this because the natural extraction passed 15+ parameters between siblings; the context dataclass collapses that to one.
|
|
20
|
+
- **`src/belote/game.py:857-1018` (D1) — `play_card` extracted behind `_PlayContext`.** ~163-LOC / ~18-branch function split into `_record_belote_announcement`, `_resolve_trick_winner`, `_compute_live_round_points`, `_rotate_dynamic_trump`. The mid-trick early-return is now visually adjacent to the trick-complete branch instead of the trick-complete branch swallowing the entire function body. Zero test edits.
|
|
21
|
+
- **`src/belote/achievements.py` (P1-1) — achievement lookup via dict; `Achievement` gets `slots=True`.** Two `for a in ACHIEVEMENTS: if a.id == aid:` loops collapse to `_ACHIEVEMENT_BY_ID[aid]`. Catalog is 6 items so the perf win is microscopic; the readability win is real. `slots=True` brings the dataclass in line with every other frozen dataclass in the codebase.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`tests/belatro/test_partner_jokers.py` (D2) — focused matrix for 9 partner jokers.** Pre-3.7.1 the partner-joker modules (`passive` / `risky` / `shaper`) carried shallow smoke-tests in `test_belatro.py`. New file covers happy-path / non-trigger / round-boundary-reset for `LeMiroir`, `LaSymbiose`, `LeRelais`, `LAventurier`, `LeMartyr`, `LeParasite`, `LeGenereux`, `LaSentinelleP`, `LeCalculateur` — 26 tests, **100% line coverage** for the three modules (was effectively 0% direct coverage despite import-time references). Audit note pinned in the docstring: partner jokers correctly key on seat (NORTH), not team — they are the deliberate complement of L'Accumulateur, NOT subject to the BA-L2 fix.
|
|
26
|
+
- **`src/belote/belatro/engine/round_driver.py:89-98` + `belatro/main.py` (D3) — `prompt_surcoinche` callback on `RoundUICallbacks`; NS-taker player surcoinche.** Pre-3.7.1 the NS-taker branch only consulted the EW-AI heuristic; when EW coinched the player had no way to surcoinche back. New optional callback (default returns False, preserves backward compatibility for any third-party `RoundUICallbacks` impl), wired into `round_driver.py:268-279` so the player gets first refusal before the existing 30% AI surcoinche fallback fires. `BelAtroMain`'s `UICallbacks` implements it via `BelAtroAnnounce.yes_no`. 4 regression tests in `test_round_driver.py` (accept / decline-AI-skips / decline-AI-takes / default-no-op).
|
|
27
|
+
|
|
28
|
+
### Verified clean — audit findings rejected after source verification
|
|
29
|
+
|
|
30
|
+
Documented so the next cycle doesn't re-investigate:
|
|
31
|
+
|
|
32
|
+
- **Classic engine** (`game.py`, `scoring.py`, `deck.py`, `ai.py`, `gameflow.py`) — three-Explore-agent pass returned no novel findings. The 3.4.x → 3.6.0 audits absorbed the correctness surface.
|
|
33
|
+
- **`visible_len()` "duplicated" in `_build_hud()`** (`ui/render.py:669-670, 705-706`) — calls are on different strings in different layout branches; not redundant.
|
|
34
|
+
- **`detect_synergies()` recomputed per HUD render** (`belatro/ui/hud.py:67-82`) — O(joker_count × 6 pairs), microseconds per render; caching adds invalidation surface for no user-visible win.
|
|
35
|
+
- **`_slot_anchors()` called 3× per trick-mat render** (`ui/render.py:379/409/446`) — pure arithmetic, sub-microsecond.
|
|
36
|
+
- **`announce()` / `BelAtroAnnounce.banner()` duplication** — modal off-the-critical-path code with intentionally different positioning semantics.
|
|
37
|
+
- **Two history-overlay code paths (wide vs narrow term)** — intentional split for narrow-terminal readability.
|
|
38
|
+
- **Round-2 bid prompt ANSI redundancy** (`ui/render.py:751-753`) — visually correct (REVERSE wraps the segment); cosmetic only.
|
|
39
|
+
|
|
40
|
+
### Deferred to 3.7.2
|
|
41
|
+
|
|
42
|
+
- **Player surcoinche when EW is taker** — the symmetric mirror of D3. Today the EW-taker branch lets the player coinche but cannot surcoinche when partner-AI surcoinches the bid. Out of scope for the current pass; the `prompt_surcoinche` callback added in D3 can be reused.
|
|
43
|
+
|
|
44
|
+
### Internal
|
|
45
|
+
|
|
46
|
+
- **Tests**: 599 → 635 (+36). Two new test files: `tests/belatro/test_partner_jokers.py` (26), 4 new D3 tests in `tests/belatro/test_round_driver.py`, 6 new BA-L2 tests in `tests/belatro/test_belatro.py`.
|
|
47
|
+
- **Strict gates**: pytest 635/635 green, mypy --strict 0 errors (77 files), ruff 0 violations.
|
|
48
|
+
- **Version markers bumped**: `pyproject.toml`, `src/belote/__init__.py`.
|
|
49
|
+
|
|
50
|
+
## [3.6.0] - 2026-05-12
|
|
51
|
+
|
|
52
|
+
Verified bug-hunt and refactor pass over both the classic Belote engine and the BelAtro roguelite layer. A three-Explore-agent audit produced ~50 candidate findings; verification against current source rejected several as **false positives** (notably "dix-de-der double counting" — independent counters; "underscore-boss-attr anti-pattern" — already pinned; "M5 `last_voids_key` cross-round bleed" — already fixed). The items below are the ones confirmed against current code and shipped. **+4 regression tests** (595 → 599). Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-functional-naur.md`.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **`src/belote/belatro/engine/round_driver.py:210-289` (H1) — EW AI can now coinche an NS taker; Libra planet is reachable in natural play.** Pre-3.6.0 the coinche flow only branched on `state.taker in (Seat.EAST, Seat.WEST)`. When NS was taker there was no path that set `coinche_level > 0` outside of L'Avocat's `auto_coinche` or Le Coincheur's `start_coinched`. The `Libra` planet at `belatro/core/scoring.py:237-250` is gated on `event.coinche_level > 0 AND event.taker_seat in _NS_TEAM AND not failed`, so its content was effectively unreachable. New `_ew_should_coinche(state, rng)` heuristic (baseline 20 %, +15 % if either defender holds 2+ honour cards) gives the EW AI defenders a seeded chance; AI surcoinche from NS follows the existing 30 % pattern gated by `surcoinche_unlocked`. The branch also collapses the previous duplicated `auto_coinche` re-emit so the boss path lives in one site. 2 regression tests in `tests/belatro/test_round_driver.py` (one for the heuristic in isolation, one end-to-end via `ScoreAccumulator` joker capture).
|
|
57
|
+
- **`src/belote/belatro/items/registry.py:184-194` (H2) — synergy-ID validation now survives `python -O`.** The 3.0.1 check used `assert not missing, ...`. Under `PYTHONOPTIMIZE=1` (the default for packaged installs and `python -O`) the assertion is stripped and a typo in `belatro/ui/hud.py::_SYNERGY_PAIRS` would silently break every HUD synergy badge for that pair. Replaced with `if missing: raise RuntimeError(...)`. Verified by importing the registry in a subprocess under `-O`.
|
|
58
|
+
- **`src/belote/belatro/engine/event_bus.py:emit` (H3) — handler exceptions no longer halt remaining subscribers.** `emit()` now wraps each handler call in `try/except Exception`, logs via `logging.exception`, and continues iterating. `BaseException` (KeyboardInterrupt etc.) still propagates so a user Ctrl-C tears down the round cleanly. Pre-3.6.0 a single raising joker `on_event` would skip every subsequent subscriber for the rest of the round. 1 regression test in `tests/belatro/test_event_bus.py::test_raising_subscriber_does_not_skip_siblings`.
|
|
59
|
+
- **`src/belote/belatro/engine/modifier_patch.py:patch` (M3) — `PatchedGameState` no longer rejects legitimate `_`-prefixed sets.** The 3.1.0 anti-shim raised on **any** leading-underscore patch key, but `GameState` has legitimate `_chips`, `_mult`, `_joker_state`, `_rng` fields. Narrowed the guard to reject only `_X` where `X` IS a `BossModifiers` field — the precise 3.0.x anti-pattern target. A future joker / boss effect that needs to adjust accumulator scalars no longer hits a confusing "3.0.x shim was removed" error. 1 regression test in `tests/belatro/test_boss_modifiers_integration.py::test_patched_state_rejects_only_underscore_boss_attrs`.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **`src/belote/scoring.py` (M1+M2) — zero-rank / `ban_clubs` flag logic extracted to module-level helpers.** Three sites previously inlined the same `kings_zero` / `tens_zero` / `aces_zero` / `jacks_zero` / `ban_clubs` table (`trick_card_points`, `_calculate_base_points`, `_apply_scoring_modifiers`). New `_card_points_with_zero_ranks(card, trump, bm)`, `_trick_zeroed_by_ban_clubs(trick, bm)`, and `_trick_points_with_modifiers(trick, trump, bm)` are the single source of truth. Adding a new zero-rank boss flag is now one edit instead of three (the audit's drift-risk concern). No behaviour change; full suite green.
|
|
64
|
+
- **`src/belote/deck.py:Contract` (M4/R1) — added `class Contract(str, Enum)`** with values `NORMAL` / `SANS_ATOUT` / `TOUT_ATOUT` / `COINCHE` / `SURCOINCHE`. Inherits from `str` so values ARE plain strings — existing comparisons (`state.contract == "sans_atout"`) and JSON serialisation are unaffected. Migrated the dense comparison sites in `scoring.py`, `game.py`, `ai.py`, `gameflow.py`, and `belatro/engine/round_driver.py` to `state.contract == Contract.SANS_ATOUT`. UI label strings and joker / planet registry keys left as plain strings — `StrEnum`-style equality means they're interchangeable.
|
|
65
|
+
- **`src/belote/game.py:sort_hand` (P4) — now `@lru_cache(maxsize=512)`.** Bench: ~34 % wall-clock win on the UI render-loop access pattern (same `(hand, trump)` requested across consecutive frames). P2 (`deck.card_points` caching) was tested with the same harness and **rejected** — the function is too small for `lru_cache` overhead to amortise (1.86× slower with cache).
|
|
66
|
+
- **`src/belote/belatro/core/scoring.py:ContractReward` (R4) — TypedDict for `contract_levels` entries.** Documents the known keys (`add_chips`, `add_mult`, `jack_9_bonus`, `honor_bonus`, `bonus_mult_per_trick`, `add_money`, `capot_bonus`, `coinche_multiplier`) so mypy catches planet-reward key typos at type-check time. `BelAtroRun.contract_levels` stays as the wider `dict[str, dict[str, Any]]` to avoid an import cycle; the cast happens at the consumer boundary in `belatro/main.py`.
|
|
67
|
+
- **`src/belote/game.py` belote detection (L1)** — single-pass per hand. Previously rebuilt a `(rank, suit)` set and looped 4 suits per seat; now tracks two `set[Suit]` (kings, queens) in one pass and intersects.
|
|
68
|
+
- **`src/belote/deck.py` (L2)** — `card_points` and `trick_rank` type annotations widened to `trump: Suit | None` to match the SA call sites at `scoring.py` and elsewhere. Runtime behaviour was already correct.
|
|
69
|
+
- **`src/belote/scoring.py:_carre_points` (L3)** — uses `.get(..., 0)` for symmetry with `get_declaration_points` and `_sequence_points`. The dict is currently complete so this is fail-soft only; protects against a future `Rank` extension crashing scoring mid-round.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **`tests/test_properties.py` (T1) — three new invariants.**
|
|
74
|
+
- `test_chute_and_capot_are_mutually_exclusive`: under capot, credited points always live on exactly one side.
|
|
75
|
+
- `test_dynamic_trump_never_overrides_sans_atout`: La Anarchie's per-2-trick trump rotation never fires under SA (`state.trump` stays None for the whole round).
|
|
76
|
+
- `test_no_consecutive_team_wins_invariant_when_rupture_active`: under La Rupture, no team sweeps all 8 tricks (30 seeded rounds).
|
|
77
|
+
- **`tests/belatro/test_round_driver.py` (H1 backfill)** — `test_ew_should_coinche_baseline_rate` and `test_ew_ai_can_coinche_ns_taker_under_seed`.
|
|
78
|
+
- **`tests/belatro/test_event_bus.py` (H3 backfill)** — `test_raising_subscriber_does_not_skip_siblings`.
|
|
79
|
+
- **`tests/belatro/test_boss_modifiers_integration.py` (M3 backfill)** — `test_patched_state_rejects_only_underscore_boss_attrs`.
|
|
80
|
+
|
|
81
|
+
### Verified clean (audit false positives — do not re-investigate)
|
|
82
|
+
|
|
83
|
+
- **Dix-de-der is NOT double-counted.** `game.py:play_card` writes the live HUD's `current_round_points` on the 8th trick; `score_round` derives from `state.completed_tricks` independently. The two counters are independent — verified by reading both code paths.
|
|
84
|
+
- **The `getattr(state, "_X", False)` boss-flag anti-pattern is already pinned.** `tests/belatro/test_boss_modifiers_integration.py::test_invariant_no_underscore_boss_attrs` covers it. No leading-underscore boss attribute resolves on a vanilla GameState.
|
|
85
|
+
- **`AIMemory.last_voids_key` cross-round reset is already in place.** `ai.py:78` clears the cache key in the new-round branch alongside `played` / `known_voids` / `processed_tricks_count`. Covered by `tests/test_ai.py::test_void_cache_invalidates_across_rounds`.
|
|
86
|
+
- **Negative-edition joker slot growth is intentionally irreversible.** No sell mechanism exists today; the asymmetry is by design and documented at the increment site (`belatro/run/shop.py:166-168`).
|
|
87
|
+
|
|
88
|
+
### Deferred to 3.7.0
|
|
89
|
+
|
|
90
|
+
- Full `score_round()` and `play_card()` helper splits (L4/L5/R2/R3). Both functions are long (~280 LOC / ~30 branches; ~130 LOC / ~18 branches respectively) but the natural extraction passes 15+ parameters between siblings; a clean split needs an intermediate `ScoringContext` / `PlayContext` dataclass, which is its own refactor.
|
|
91
|
+
- Partner-jokers test coverage (T5) — `belatro/items/partner_jokers/{passive,risky,shaper}.py` are at ~0 % coverage per the perf audit. Out of scope for a single audit pass.
|
|
92
|
+
- Player-facing coinche / surcoinche prompts when NS is taker. Today only the AI surcoinches NS-taker rounds; the `RoundUICallbacks` layer doesn't expose a `prompt_surcoinche` callback yet.
|
|
93
|
+
|
|
94
|
+
## [3.5.0] - 2026-05-12
|
|
95
|
+
|
|
96
|
+
Comprehensive bug-hunt, game-mechanic audit, and performance pass over both the classic Belote engine and the BelAtro roguelite layer. A three-Explore-agent audit produced ~30 candidate findings; verification against the source rejected several as false positives (documented below) and confirmed **15 actionable items** plus **1 latent bug surfaced during implementation**. **24 new regression tests** land here (592 total, up from 568). Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-tidy-meerkat.md`.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **`src/belote/belatro/core/run_state.py:90-117` + new `src/belote/belatro/ui/consumables.py` (C1) — BelAtro consumables can now be activated.** Pre-3.5.0 `BelAtroRun.consume()` was defined but never called from any UI: every Tarot bought from the shop and every directly-purchased Planet accumulated in `run.consumables` with no way to use them. Only the voucher-gated Forge-Tierce path could level a planet. New `ConsumablesOverlay` is reachable from the shop via the `C` key, listing the tray and dispatching to `run.consume(item, context=run)` on a digit press. Hint line in the shop now shows `C: Consumables (N)`. **Also fixes a latent Le Fou bug**: `consume()` was advancing `last_consumable_id` to the *current* item BEFORE calling `item.use()`, so Le Fou's `use()` read its own id and fell through to the fallback path. Reordered to call `item.use()` first; Le Fou is special-cased as transparent so a second Le Fou keeps copying the same source rather than itself. 7 regression tests in `tests/belatro/test_consumables_ui.py`.
|
|
101
|
+
- **`src/belote/belatro/run_summary.py:67-74` (H1) — JSONL appends are now durable.** Added `f.flush() + os.fsync(f.fileno())` inside the `with` block, mirroring the atomic-save pattern in `progression/save.py:81-82`. A crash or power-loss mid-write no longer leaves a truncated final line that breaks downstream `jq` processing. 3 regression tests in `tests/belatro/test_run_summary.py`.
|
|
102
|
+
- **`src/belote/input.py:31-37,74` + ~20 consumer sites (H2) — EOF on stdin is distinct from ESC.** `KeyReader.read()` returned `KeyEvent(Key.ESC)` when `os.read()` returned empty bytes. A closed stdin (broken pipe, headless harness, Ctrl-D) made every prompt loop spin: ESC popped one menu level, the loop fell through and re-read stdin, got another "ESC", popped again — burning CPU until the outermost loop happened to exit. New `Key.EOF` enum value is returned on empty-read. Every `Key.ESC` consumer was updated to also accept `Key.EOF` (semantically equivalent: "back / cancel"); `prompt_card` and `prompt_bid` exit cleanly on EOF instead of spinning. 5 regression tests in `tests/test_input_eof.py`.
|
|
103
|
+
- **`src/belote/belatro/engine/event_bus.py` (H3) — `EventBus` round-scope invariant documented + `clear()` added.** The bus is created fresh per round in `round_driver.drive_round` and subscribers are released with it; no explicit unsubscribe was needed. The invariant was silent — if anyone moved the bus to a longer scope, every subscription would double-fire on round 2. Module docstring now spells out the round-scope contract; a new `clear()` method exists for the future where a longer-lived bus might be desirable. 3 regression tests in `tests/belatro/test_event_bus.py`.
|
|
104
|
+
- **`src/belote/scoring.py:228-330` (M1) — tied carrés / sequences go to the first announcer.** Standard Belote-Coinché awards a tied declaration to the team whose seat declared first (announcement order: taker → clockwise). Pre-3.5.0 the resolver returned `scoring_team=None` (cancel), which was defensive but non-standard. `resolve_declarations` gains an optional `taker: Seat | None = None` parameter; when supplied, tied carrés/sequences are awarded by walking the announce order. Legacy "cancel" behaviour preserved when `taker` is not provided. Both call sites updated to pass `state.taker`. 6 regression tests in `tests/test_declaration_tiebreak.py`.
|
|
105
|
+
- **`src/belote/belatro/engine/event_bus.py` + ~10 consumer sites (M3) — `RoundEndEvent.breakdown` is properly typed.** Pre-3.5.0 the field was `breakdown: Any` and every consumer wrote `getattr(event.breakdown, "is_failed", False)` — defensive noise that hid field-rename regressions until runtime. Now typed as `ScoringBreakdown` (TYPE_CHECKING forward-ref to avoid import cycle); all 9 `getattr` patterns replaced with direct attribute access. `taker_seat` correctly annotated `Seat | None` (was `Seat`, but the all-pass emitter at `round_driver.py:298` actually passes None).
|
|
106
|
+
- **`src/belote/scoring.py:750-763` (M5) — SA belote invariant pinned at contract level.** The `assert taker_belote == 0 and defender_belote == 0` for Sans Atout rounds was only inside the capot branch — a non-capot SA round with stray belote points would silently mis-score instead of surfacing the bug. Hoisted to a contract-level post-condition that covers both capot and non-capot paths.
|
|
107
|
+
|
|
108
|
+
### Changed
|
|
109
|
+
|
|
110
|
+
- **`src/belote/belatro/core/scoring.py:125-142` (M4) — `partner_jokers_double` legacy flag is now deprecated.** When both the tier scaling and the legacy boolean flag are set, a one-shot `DeprecationWarning` fires. Behaviour unchanged (`max()` of the two still wins); the flag is slated for removal in 4.0.
|
|
111
|
+
- **`src/belote/ui/render.py:988-997` (L1) — `patch_trick_card` batches its writes.** Pre-3.5.0 each card-face line + the HUD update were separate `sys.stdout.write` calls; signal-interruptible terminals could paint half the card before the HUD landed. Now one `write()` per repaint, mirroring the pattern at `render.py:923,933`.
|
|
112
|
+
- **`src/belote/a11y.py:1-20` (L2) — module docstring spells out the env-var invariant.** `BELOTE_A11Y` is read once at import; toggling mid-session has no effect on production code. Tests use `_refresh_enabled_from_env()`. No behaviour change, just documentation.
|
|
113
|
+
- **`src/belote/belatro/core/run_state.py:30-41` (L3) — `consumables` / `jokers` / `vouchers` mutation contract documented.** These lists are intentionally mutable; any future replay / ghost-run snapshot path must deep-copy at the snapshot boundary. No behaviour change.
|
|
114
|
+
|
|
115
|
+
### Performance
|
|
116
|
+
|
|
117
|
+
- **`scripts/benchmark.py` (M2) — three new micro-benchmarks for real hot paths.** `benchmark_legal_cards_cached` (warm-cache path; production gameplay reuses across 8 tricks), `benchmark_trick_scoring` (`trick_card_points`, called 16× per round), `benchmark_ai_legality_filter` (the legal-move filter step inside `AIPlayer.decide_card`). The pre-existing `benchmark_legal_cards` was clarified as the cache-cleared cold path.
|
|
118
|
+
- **`src/belote/scoring.py:439-477` (P1) — `trick_card_points` hoists boss-flag reads.** Same pattern `_calculate_base_points` (lines 489-493) uses. Saves 4 dataclass-attr lookups per card per trick. Sub-microsecond gain; the function was already micro-optimized at ~2μs per call. **Memoization rejected**: at 2μs × 16 calls = 32μs per round, the cache-key overhead would exceed the gain.
|
|
119
|
+
- **`src/belote/game.py:490-512` (P2) — `legal_cards` cache-key analysis documented.** Benchmark shows cold ~9μs / warm ~6μs; the 33% gap is dominated by key-build cost in `legal_cards()`, not lru_cache lookup. The key already uses small-int IDs (not Card objects) which is the minimal hashable surface. Slimming further would require caching `hand_ids` on the hand tuple itself, which isn't reachable without changing the hand representation. **Documented "no actionable optimization without larger refactor"** in the cache-impl docstring so a future audit doesn't re-investigate the same dead end.
|
|
120
|
+
- **`src/belote/belatro/core/scoring.py:70-89` (P3) — `ScoreAccumulator.update_state` profiling note.** cProfile of 10k calls shows `dataclasses.replace` is 65% of the cost — frozen-GameState invariant is load-bearing, so the replace cost stays. At ~19μs per event × ~25 events per round = ~0.5ms per round, the accumulator is well under the budget.
|
|
121
|
+
|
|
122
|
+
### Verified clean — agent claims that did NOT survive source verification
|
|
123
|
+
|
|
124
|
+
Catalogued so they aren't re-investigated next cycle.
|
|
125
|
+
|
|
126
|
+
- **"Belote/Rebelote not announced when partner holds K+Q"** (`game.py:863-876`) — The condition `state.belote_holders.get(trump) == state.turn` fires when the holder *plays* the K/Q, which is exactly when the announcement should fire. `state.turn` equals the holder at the moment of play regardless of partnership. **Correct as-is.**
|
|
127
|
+
- **"Capot false-positive under La Rupture on the 8th-trick announcement"** (`gameflow.py:215`) — `is_capot()` already routes through `compute_trick_winners()`, which honours Rupture for both the live-announce path and the final scoring path. The docstring at `scoring.py:317-326` explicitly calls this out. **Correct as-is.**
|
|
128
|
+
- **"AI `partner_hand` not cleared on undo path"** (`ai.py:79-92`) — `update_memory()` always clears `partner_hand` at line 104 and re-fills it from the current state, regardless of which earlier branch ran (new-round / undo / normal). **Correct as-is.**
|
|
129
|
+
- **"Negative-edition slot rollback on purchase failure"** (`run/shop.py:166-168`) — `_can_accept()` returns True unconditionally for Negative jokers, and `_apply_item` runs only after `spend_money()` succeeded. No failure path exists. **Correct as-is.**
|
|
130
|
+
- **"`boss.id == \"…\"` string-branching in pre-round setup"** — `grep -rn 'boss\.id\s*==' src/belote/belatro/` returns zero results. Cleaned up in the May 2026 audit. **Correct as-is.**
|
|
131
|
+
|
|
132
|
+
### Internal
|
|
133
|
+
|
|
134
|
+
- **Tests**: 568 → 592 (+24). Six new test files: `tests/belatro/test_consumables_ui.py` (7), `tests/belatro/test_run_summary.py` (3), `tests/test_input_eof.py` (5), `tests/belatro/test_event_bus.py` (3), `tests/test_declaration_tiebreak.py` (6). 0 existing tests modified.
|
|
135
|
+
- **Strict gates**: pytest 592/592 green.
|
|
136
|
+
- **Version markers bumped**: `pyproject.toml`, `src/belote/__init__.py`.
|
|
137
|
+
- **Docs bumped**: `CHANGELOG.md` (this entry), `README.md` "What's new in 3.5.0".
|
|
138
|
+
|
|
8
139
|
## [3.4.2] - 2026-05-11
|
|
9
140
|
|
|
10
141
|
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`.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
Welcome to the Belote development guide. This project is structured as a standard Python package.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. **Clone the repository:**
|
|
8
|
+
```bash
|
|
9
|
+
git clone <repository-url>
|
|
10
|
+
cd belote
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. **Create a virtual environment:**
|
|
14
|
+
```bash
|
|
15
|
+
python -m venv .venv
|
|
16
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. **Install in editable mode:**
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Running the Game
|
|
25
|
+
|
|
26
|
+
After installing, you can run the game using the `belote` command for Classic mode:
|
|
27
|
+
```bash
|
|
28
|
+
belote
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or the `belatro` command for the Roguelite expansion:
|
|
32
|
+
```bash
|
|
33
|
+
belatro
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or via python:
|
|
37
|
+
```bash
|
|
38
|
+
python -m belote.main
|
|
39
|
+
python -m belote.belatro.main
|
|
40
|
+
PYTHONPATH=src python3 -m belote.main
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Testing
|
|
44
|
+
|
|
45
|
+
We use `pytest` for testing. Install it if you haven't already:
|
|
46
|
+
```bash
|
|
47
|
+
pip install pytest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run tests:
|
|
51
|
+
```bash
|
|
52
|
+
# Run all tests (Classic + BelAtro)
|
|
53
|
+
PYTHONPATH=src pytest
|
|
54
|
+
|
|
55
|
+
# Run only Classic Belote tests
|
|
56
|
+
PYTHONPATH=src pytest tests/
|
|
57
|
+
|
|
58
|
+
# Run only BelAtro tests
|
|
59
|
+
PYTHONPATH=src pytest tests/belatro/
|
|
60
|
+
|
|
61
|
+
# Run a single test file
|
|
62
|
+
PYTHONPATH=src pytest tests/test_game.py
|
|
63
|
+
PYTHONPATH=src pytest tests/belatro/test_scoring.py
|
|
64
|
+
|
|
65
|
+
# Run a single test by name
|
|
66
|
+
PYTHONPATH=src pytest tests/test_game.py::test_play_card_legal
|
|
67
|
+
PYTHONPATH=src pytest -k "test_scoring"
|
|
68
|
+
|
|
69
|
+
# Run with verbose output
|
|
70
|
+
PYTHONPATH=src pytest -v
|
|
71
|
+
|
|
72
|
+
# Run with coverage report
|
|
73
|
+
PYTHONPATH=src pytest --cov=belote --cov-report=term-missing
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Code Quality
|
|
77
|
+
|
|
78
|
+
The project maintains zero lint and type-check violations. Run all checks with:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Type checking (0 errors expected, strict mode)
|
|
82
|
+
PYTHONPATH=src mypy --strict src/
|
|
83
|
+
|
|
84
|
+
# Linting (0 violations expected)
|
|
85
|
+
ruff check src/ tests/
|
|
86
|
+
|
|
87
|
+
# Full test suite (635 tests expected)
|
|
88
|
+
PYTHONPATH=src pytest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Current baseline (3.7.1):
|
|
92
|
+
- **mypy**: 0 errors (strict mode, 77 files)
|
|
93
|
+
- **ruff**: 0 violations
|
|
94
|
+
- **pytest**: 635 tests, 0 failures
|
|
95
|
+
- 3.7.1 lands the deferred 3.7.0 items plus a fresh audit pass. Three Explore agents ran in parallel against the documented false-positive catalogue. The classic-engine sweep returned no novel findings (3.4.x → 3.6.0 absorbed the surface); the BelAtro layer produced **BA-L2** (L'Accumulateur team→seat bug, HIGH) and **BA-L1** (`ContractReward` TypedDict float annotations, MEDIUM). Deferred items: **D1** — `score_round` and `play_card` extracted behind `_ScoringContext` / `_PlayContext` (zero test edits, behaviour-preserving); **D2** — `tests/belatro/test_partner_jokers.py` adds 26 tests, **100% coverage** for `passive` / `risky` / `shaper` partner-joker modules; **D3** — `prompt_surcoinche` callback on `RoundUICallbacks` plus NS-taker player-surcoinche path in `round_driver.py:268-283`. **+36 regression tests** (599 → 635). Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-sequential-map.md`.
|
|
96
|
+
- 3.6.0 lands a verified bug-hunt and refactor pass over the classic engine and the BelAtro roguelite layer. Three Explore agents produced ~50 candidate findings; verification against current code rejected several as false positives (notably "dix-de-der double counting" — separate counters; "underscore-boss-attr anti-pattern" — already pinned by tests) and confirmed the items shipped here. **+4 regression tests** (595 → 599). Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-functional-naur.md`.
|
|
97
|
+
- **H1** (`belatro/engine/round_driver.py:210-289`) — EW AI can now coinche an NS taker via a new `_ew_should_coinche` heuristic. Pre-3.6.0 there was no path that set `coinche_level > 0` when NS was taker (outside `auto_coinche` / `start_coinched`), making the Libra planet effectively unreachable in natural play.
|
|
98
|
+
- **H2** (`belatro/items/registry.py:184-194`) — synergy-ID validation converted from `assert` to a real `raise RuntimeError(...)` so the check survives `python -O` / `PYTHONOPTIMIZE=1` in packaged installs.
|
|
99
|
+
- **H3** (`belatro/engine/event_bus.py:emit`) — handler exceptions are now logged and the remaining subscribers still fire. A single buggy joker's `on_event` can no longer halt mid-round accumulator/unlock dispatch.
|
|
100
|
+
- **M1+M2** (`scoring.py`) — extracted `_card_points_with_zero_ranks`, `_trick_zeroed_by_ban_clubs`, and `_trick_points_with_modifiers`. Three sites that previously inlined the zero-rank flag table (drift-prone) now route through one helper. New zero-rank boss flags need one edit, not three.
|
|
101
|
+
- **M3** (`belatro/engine/modifier_patch.py:patch`) — narrowed the leading-underscore guard to reject ONLY `_<boss_field>` (the actual 3.0.x anti-pattern). Legitimate scalar GameState fields (`_chips`, `_mult`, `_joker_state`, `_rng`) are now patchable through `PatchedGameState`.
|
|
102
|
+
- **M4/R1** (`deck.py:Contract`, plus `scoring.py`/`game.py`/`ai.py`/`gameflow.py`/`belatro/engine/round_driver.py`) — `class Contract(str, Enum)` defines `NORMAL`/`SANS_ATOUT`/`TOUT_ATOUT`/`COINCHE`/`SURCOINCHE` and the dense comparison sites switched from string literals. Values remain plain strings so JSON serialisation and legacy comparisons keep working.
|
|
103
|
+
- **P4** (`game.py:sort_hand`) — `@lru_cache(maxsize=512)`. Benchmark showed ~34 % wall-clock win on the UI render's repeated `(hand, trump)` access pattern. P2 (`deck.card_points` caching) was **rejected** by benchmark — the function is too small for `lru_cache` to win against call overhead (1.86× slower with cache).
|
|
104
|
+
- **R4** (`belatro/core/scoring.py:ContractReward`) — `TypedDict` schema for `contract_levels` entries; catches planet-reward key typos (e.g. `bonus_per_trick` vs `bonus_mult_per_trick`) at type-check time. `BelAtroRun.contract_levels` keeps its wider `dict[str, dict[str, Any]]` type to avoid an import cycle; the consumer site casts at the boundary.
|
|
105
|
+
- **L1** (`game.py:place_bid` belote detection) — single-pass per hand: previously rebuilt a `(rank, suit)` set and re-iterated 4 suits.
|
|
106
|
+
- **L2** (`deck.py`) — `card_points`/`trick_rank` signatures widened to `trump: Suit | None` to match the SA call sites.
|
|
107
|
+
- **L3** (`scoring.py:_carre_points`) — switched from dict `[]` access to `.get(..., 0)` for asymmetry with sibling lookups; the dict is complete today so this is fail-soft only.
|
|
108
|
+
- **T1** (`tests/test_properties.py`) — three new invariants: `test_chute_and_capot_are_mutually_exclusive`, `test_dynamic_trump_never_overrides_sans_atout`, `test_no_consecutive_team_wins_invariant_when_rupture_active`.
|
|
109
|
+
- **Verified clean (NOT bugs)**: the "dix-de-der double counting" the audit initially flagged turned out to be two independent counters (`play_card` writes the HUD's `current_round_points`; `score_round` derives from `completed_tricks`). The "underscore-boss-attr anti-pattern" is already pinned by `tests/belatro/test_boss_modifiers_integration.py::test_invariant_no_underscore_boss_attrs`. `AIMemory.last_voids_key` reset on new round is already correct and covered by `tests/test_ai.py::test_void_cache_invalidates_across_rounds`.
|
|
110
|
+
- **Deferred to 3.7.0**: full `score_round()` / `play_card()` helper splits (L4/L5/R2/R3) — would need an intermediate `ScoringContext` dataclass to avoid 15-param helper signatures, which is its own refactor. Partner-jokers test coverage (T5) — broader surface than this audit's scope. Player-side coinche / surcoinche UI prompts when NS is taker (currently AI-only).
|
|
111
|
+
- 3.5.0 lands a 15-fix audit pass over the classic engine + BelAtro layer: C1 (consumables UI + Le Fou ordering), H1 (run-summary fsync), H2 (Key.EOF distinct from ESC), H3 (EventBus round-scope docs + `clear()`), M1 (declaration first-announcer tie-break), M3 (typed `RoundEndEvent.breakdown`), M4 (deprecate `partner_jokers_double`), M5 (SA belote invariant hoisted), L1 (`patch_trick_card` single-write), L2/L3 (doc pins), M2 (3 new benchmark micro-tests), P1/P2/P3 (perf hoist + cache-key + accumulator-profile analyses). +24 regression tests across 5 new files; 0 existing tests modified. Plan file at `/home/mrrobot/.claude/plans/bug-hunt-code-performance-tidy-meerkat.md`.
|
|
112
|
+
- 3.4.2 closed 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`) shipped 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.
|
|
113
|
+
- 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.
|
|
114
|
+
- 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.
|
|
115
|
+
|
|
116
|
+
Run all gates before committing:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
PYTHONPATH=src python -m pytest --tb=short -q && \
|
|
120
|
+
python -m mypy --strict src/ && \
|
|
121
|
+
python -m ruff check src/ tests/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Benchmarking
|
|
125
|
+
|
|
126
|
+
A benchmarking script is provided to measure rendering and AI performance:
|
|
127
|
+
```bash
|
|
128
|
+
PYTHONPATH=src python scripts/benchmark.py
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
3.0.0 baseline numbers (Linux, Python 3.10+, 1000 iterations):
|
|
132
|
+
- Render: 0.27 ms (±0.04)
|
|
133
|
+
- AI Hard decide_card: 0.026 ms (±0.003)
|
|
134
|
+
- BelAtro state update: 0.032 ms (±0.004)
|
|
135
|
+
- score_round: 0.169 ms
|
|
136
|
+
- legal_cards: 0.012 ms
|
|
137
|
+
|
|
138
|
+
Use these as a regression-detection floor for future changes.
|
|
139
|
+
|
|
140
|
+
## Accessibility
|
|
141
|
+
|
|
142
|
+
Set `BELOTE_A11Y=1` to emit one-line plain-text descriptions of card plays,
|
|
143
|
+
trick winners, and round results to stderr — readable by terminal screen
|
|
144
|
+
readers (Orca, NVDA over WSL, VoiceOver via iTerm2).
|
|
145
|
+
|
|
146
|
+
## Optional Runtime Flags
|
|
147
|
+
|
|
148
|
+
The following environment variables enable opt-in features. Each is read
|
|
149
|
+
once at startup; toggling mid-run has no effect.
|
|
150
|
+
|
|
151
|
+
- `BELOTE_REPLAY=1` — after every Classic round, print a one-line summary
|
|
152
|
+
of how often South's plays matched the Hard-AI's preferred line
|
|
153
|
+
(e.g. `Replay: Optimal plays: 6/8 (75%)`). Educational only — never
|
|
154
|
+
affects scoring. Backed by `src/belote/replay.py`.
|
|
155
|
+
- `BELOTE_GHOST=1` — silently record every BelAtro run (seed, deck,
|
|
156
|
+
bids, plays, round outcomes) to
|
|
157
|
+
`~/.local/share/belote/ghosts/<label>-<seed>.json`. The file is written
|
|
158
|
+
once when the run ends. Useful for sharing or replaying interesting
|
|
159
|
+
runs. Backed by `src/belote/belatro/ghost_run.py`.
|
|
160
|
+
|
|
161
|
+
## Releasing a New Version
|
|
162
|
+
|
|
163
|
+
### Code-only update (push to GitHub without releasing a new PyPI version)
|
|
164
|
+
|
|
165
|
+
If you're just iterating on code, fixing typos, updating docs, etc., and don't want to cut a new PyPI release yet:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
git add <files>
|
|
169
|
+
git commit -m "<what changed>"
|
|
170
|
+
git push origin master
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Releasing a New Version (Manual)
|
|
174
|
+
|
|
175
|
+
1. **Bump the version** in `pyproject.toml`.
|
|
176
|
+
2. **Add a CHANGELOG entry** at the top of `CHANGELOG.md`.
|
|
177
|
+
3. **Clean stale build artifacts:**
|
|
178
|
+
```bash
|
|
179
|
+
rm -rf dist/ build/ *.egg-info/
|
|
180
|
+
```
|
|
181
|
+
4. **Build, validate, and upload:**
|
|
182
|
+
```bash
|
|
183
|
+
pipx run build --sdist --wheel
|
|
184
|
+
pipx run twine check dist/*
|
|
185
|
+
pipx run twine upload dist/*
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
*Note: `twine upload` will prompt for your PyPI credentials or use your `~/.pypirc` file.*
|
|
189
|
+
|
|
190
|
+
5. **Commit and tag in git:**
|
|
191
|
+
```bash
|
|
192
|
+
git add pyproject.toml CHANGELOG.md
|
|
193
|
+
git commit -m "Release vX.Y.Z"
|
|
194
|
+
git tag -a vX.Y.Z -m "vX.Y.Z"
|
|
195
|
+
git push origin master --tags
|
|
196
|
+
```
|
|
197
|
+
```
|