belote-cli 2.5.3__tar.gz → 2.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-2.5.3 → belote_cli-2.7.1}/.claude/settings.local.json +2 -1
- belote_cli-2.7.1/.python-version +1 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/CHANGELOG.md +195 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/DEVELOPMENT.md +3 -2
- {belote_cli-2.5.3 → belote_cli-2.7.1}/PKG-INFO +12 -8
- {belote_cli-2.5.3 → belote_cli-2.7.1}/README.md +11 -7
- {belote_cli-2.5.3 → belote_cli-2.7.1}/pyproject.toml +1 -1
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/__init__.py +1 -1
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/ai.py +20 -12
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/core/economy.py +3 -2
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/core/run_state.py +52 -4
- belote_cli-2.7.1/src/belote/belatro/core/scoring.py +216 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/engine/event_bus.py +9 -2
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/engine/modifier_patch.py +3 -3
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/engine/round_driver.py +83 -18
- belote_cli-2.7.1/src/belote/belatro/items/base.py +161 -0
- belote_cli-2.7.1/src/belote/belatro/items/jokers/annonces.py +88 -0
- belote_cli-2.7.1/src/belote/belatro/items/jokers/coinche.py +67 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/jokers/contract.py +5 -7
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/jokers/corrupted.py +19 -1
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/jokers/hand_comp.py +2 -10
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/jokers/trick_timing.py +6 -12
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/partner_jokers/shaper.py +8 -5
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/registry.py +27 -6
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/tarots.py +32 -12
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/vouchers.py +29 -8
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/main.py +75 -20
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/partner/personality.py +1 -1
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/partner/trust.py +28 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/progression/save.py +4 -1
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/progression/unlocks.py +20 -4
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/run/ante.py +13 -2
- belote_cli-2.7.1/src/belote/belatro/run/ante_themes.py +92 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/run/boss.py +20 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/run/decks.py +27 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/run/shop.py +6 -10
- belote_cli-2.7.1/src/belote/belatro/ui/hud.py +118 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/config.py +7 -3
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/deck.py +29 -5
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/game.py +9 -39
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/gameflow.py +6 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/input.py +102 -28
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/main.py +19 -7
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/scoring.py +24 -19
- belote_cli-2.7.1/src/belote/ui/layout.py +103 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/ui/render.py +291 -145
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/belatro/test_belatro.py +6 -3
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/belatro/test_boss_modifiers_integration.py +57 -9
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/belatro/test_collection_logic.py +6 -8
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/belatro/test_deck_variants.py +4 -4
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/belatro/test_partner_trust.py +9 -10
- belote_cli-2.7.1/tests/belatro/test_phase0_coverage.py +288 -0
- belote_cli-2.7.1/tests/belatro/test_phase1_plumbing.py +114 -0
- belote_cli-2.7.1/tests/belatro/test_phase2_content.py +282 -0
- belote_cli-2.7.1/tests/belatro/test_phase3_meta.py +181 -0
- belote_cli-2.7.1/tests/belatro/test_progression.py +97 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/belatro/test_round_driver.py +27 -25
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_belote.py +20 -19
- belote_cli-2.7.1/tests/test_layout.py +255 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_new_coverage.py +2 -2
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_official_rules.py +21 -9
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_properties.py +11 -4
- belote_cli-2.7.1/tests/test_undo.py +109 -0
- belote_cli-2.5.3/src/belote/belatro/core/scoring.py +0 -139
- belote_cli-2.5.3/src/belote/belatro/items/base.py +0 -95
- belote_cli-2.5.3/src/belote/belatro/ui/hud.py +0 -64
- belote_cli-2.5.3/tests/belatro/test_progression.py +0 -34
- {belote_cli-2.5.3 → belote_cli-2.7.1}/.gitignore +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/LICENSE +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/scripts/benchmark.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/ansi.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/belatro/ui/trust_bar.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/context.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/rules.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/stats.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/themes.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/ui/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/ui/announce.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/ui/menu.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/src/belote/ui/prompts.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/belatro/__init__.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_ai.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_extended.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_game_logic.py +0 -0
- {belote_cli-2.5.3 → belote_cli-2.7.1}/tests/test_gameflow.py +0 -0
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"Bash(python3 -m pytest tests/ -q --tb=short)",
|
|
10
10
|
"Bash(python3 -m pytest tests/ -q --tb=no)",
|
|
11
11
|
"Bash(python3 -m pytest tests/ -x -q)",
|
|
12
|
-
"Bash(PYTHONPATH=src python3 *)"
|
|
12
|
+
"Bash(PYTHONPATH=src python3 *)",
|
|
13
|
+
"Bash(.venv/bin/python -m mypy src/)"
|
|
13
14
|
]
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13.5
|
|
@@ -5,6 +5,201 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.7.1] - 2026-05-04
|
|
9
|
+
|
|
10
|
+
Audit follow-up: cleared the last `getattr(state, "_X", False)` boss-flag reads, removed the now-redundant property aliases on `GameState`, and closed three coverage gaps the v2.7 audit flagged.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`src/belote/scoring.py`** — replaced 12 `getattr(state, "_kings_zero" / "_seven_eight_trump" / "_separate_scoring" / …, False)` reads with direct `state.boss_modifiers.X` access. mypy can now type-check these branches; renaming a flag will surface as an error instead of silently returning `False`. `_calculate_base_points`, `_apply_scoring_modifiers`, and the 10-de-der branch in `score_round` are all migrated.
|
|
15
|
+
- **`src/belote/game.py`** — deleted the 17 underscore property aliases on `GameState` (`_no_belote` … `_separate_scoring`) that previously delegated to `boss_modifiers.*`. `PatchedGameState` keeps its `__getattr__` fallback so `state.patch("_kings_zero", True)` style boss applications and the existing tests at `tests/belatro/test_belatro.py:1496+` continue to work unchanged.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`tests/belatro/test_phase0_coverage.py::test_boss_dynamic_trump_changes_trump_every_two_tricks`** — plays a full 8-trick round under L'Anarchie and asserts trump rotates after tricks 2/4/6 and stays put after trick 8 (`game.py:752`'s `tricks_count < 8` guard). Pairs with the existing seed-determinism test.
|
|
20
|
+
- **`tests/belatro/test_progression.py`** — five endless-mode tests: 2.2× per-offset target scaling (`ante.py:26`), `advance_blind` ante-8 → endless transition, `run_won` flip when not in endless, and `current_blind` dispatch to `endless_ante()` vs the static `ANTE_TABLE`.
|
|
21
|
+
- **`tests/test_undo.py`** (new file, 3 tests) — pins the gameflow history-stack contract: snapshot/pop equality, `stack_base` round-boundary semantics, and round-to-round isolation. Exercises the rollback logic at `src/belote/gameflow.py:264-299` without booting the interactive input layer.
|
|
22
|
+
|
|
23
|
+
### Audit claims explicitly rejected
|
|
24
|
+
|
|
25
|
+
The v2.7 audit also recommended five changes that turned out to be wrong or already done; documenting here so future reviewers don't reopen them:
|
|
26
|
+
|
|
27
|
+
- `@cached_property` on the 17 boss-flag properties — **infeasible**: `GameState` is `@dataclass(frozen=True, slots=True)`; `cached_property` requires `__dict__`, which `slots=True` removes. Verified locally (`TypeError: No '__dict__' attribute…`).
|
|
28
|
+
- "Stale legal-cards cache when `dynamic_trump` swaps trump mid-round" — **not a bug**: `_calculate_legal_cards_impl` (`game.py:457`) takes `trump` as a key parameter, so trump changes naturally produce a different cache entry.
|
|
29
|
+
- "Phantom card fallback in `ai.py:128`" — **already fixed** (the fallback is `legal = hand`).
|
|
30
|
+
- "Cache key uses `Seat` object directly at `game.py:461`" — **wrong**: parameter is `seat_val: int` and `legal_cards()` passes `seat.value`.
|
|
31
|
+
- `belatro/main.py:248` "f-string consistency" — **bogus**: that line is a literal string with no interpolation.
|
|
32
|
+
|
|
33
|
+
### Test count
|
|
34
|
+
|
|
35
|
+
381 → **390** (+9: 1 dynamic-trump, 5 endless, 3 undo).
|
|
36
|
+
|
|
37
|
+
## [2.7.0] - 2026-05-04
|
|
38
|
+
|
|
39
|
+
A responsive-layout overhaul: the game now adapts to any terminal between **80×32** (compact) and arbitrarily large (spacious), picking the largest preset that fits and re-detecting on every render so resize-during-play just works.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Layout system** (`src/belote/ui/layout.py` — new): `LayoutPreset` dataclass and three presets (`COMPACT` 80×32 / `STANDARD` 96×38 / `SPACIOUS` 120×48). `choose_layout(cols, rows)` picks the largest fitting preset. Each preset drives card dimensions, side-column widths, HUD verbosity, and whether the W/E "Last Trick" sidebar is shown.
|
|
44
|
+
- **Adaptive card art** in `render.py`: `_card_face_internal` now keys its cache on `(card_w, card_h)`. Standard / spacious render the full Art Nouveau face art; compact (5-row cards) drops to a clean rank-corners-and-suit design that fits the tighter inner area.
|
|
45
|
+
- **Per-tier HUD formatter**: spacious widths get the verbose form (full labels + help hints + theme name on the right). Compact and standard widths get an abbreviated single-line bar (`BELOTE T:♥ NS:200(+50) EW:80(+30) 5/8 Tk:S`) — fits cleanly in 80 cols. The previous unconditional ~132-char HUD was already overflowing at 96 cols pre-2.7.
|
|
46
|
+
- **BelAtro HUD compact mode** (`belatro/ui/hud.py`): drops the joker-name list for `J:N/M [J]` placeholder, abbreviates Ante/Blind/Target labels, adds a partner-mood glyph (`✗ · ○ ● ★`).
|
|
47
|
+
- **Vertical centering**: when the terminal is taller than the rendered content, `render()` pads top + bottom equally so the game centres vertically instead of clinging to the top.
|
|
48
|
+
- **Tests** (`tests/test_layout.py`, 20 tests): preset-selection boundaries, fits-minimum gate, card-cache layout separation, HUD verbosity per tier, vertical centering math, trick-mat dimension consistency, KeyReader factory regression.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- **Minimum terminal size**: 90×32 → **80×32**. A 1366×768 monitor at default font (~150×40) now uses the compact preset comfortably; the previous 90-col floor cut off the bottom rows on many practical setups.
|
|
53
|
+
- **Layout-aware redraw**: `render()` clears the screen on layout flavour change (`compact` → `standard`, etc.) so a mid-game resize doesn't leak stale artifacts from the previous layout.
|
|
54
|
+
- **`patch_trick_card` and trick-mat row offsets** are now computed from the active layout (`_trick_row_offsets(layout)` returns the seat→row map), instead of being baked-in at standard-preset dimensions.
|
|
55
|
+
- **Alt-screen mode is entered before the menu loop**, not when "Start Game" is picked. Without this, every menu redraw frame wrote to the regular terminal scrollback; on exit the user saw a wall of cup-template frames in their shell history. Game-start and BelAtro-mode entry points now stay in alt-screen and just clear when transitioning.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **`KeyReader()` constructor regression** introduced in 2.6.0's `input.py` refactor. The `__new__`-based factory returned a `_UnixKeyReader` instance via `cls.__new__(cls)`, which Python treats as "not a `KeyReader` subclass" and therefore skips `__init__`. Result: `_stdin_fd` was never set and `__enter__` crashed with `AttributeError`. Factory now calls the concrete reader as a normal constructor (`_UnixKeyReader()`), so its `__init__` always runs.
|
|
60
|
+
- **"Infinite main menu" cosmetic bug**: the menu's animation loop was redrawing 3× per second outside of alt-screen mode, so each `clear_screen() + redraw` cycle pushed the previous frame into the terminal scrollback. By the time the user quit the menu, their shell history was filled with overlapping cup templates. Fixed by entering alt-screen mode at the start of the `KeyReader` context, not at game-start.
|
|
61
|
+
|
|
62
|
+
### Migration notes
|
|
63
|
+
|
|
64
|
+
- Code that imported `CARD_W`, `CARD_H`, `CARD_GAP`, or `SIDE_COL_W` directly from `belote.ui.render` still works — those constants are now derived from the `STANDARD` preset for back-compat. New code should call `belote.ui.layout.choose_layout(cols, rows)` and read dimensions from the returned `LayoutPreset`.
|
|
65
|
+
- The legacy `partner_jokers_double` flag still forces +1 apply for back-compat with tests setting it directly. New partner-joker scaling reads `ScoreAccumulator.partner_tier` (set from `TrustTrack.tier`).
|
|
66
|
+
|
|
67
|
+
## [2.6.0] - 2026-05-04
|
|
68
|
+
|
|
69
|
+
A large BelAtro expansion landed in one release: a Phase 0 audit pass plus six feature areas (see `plans/let-do-all-of-radiant-pizza.md`).
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
**Critical bugs (audit Phase 0.1)**
|
|
74
|
+
|
|
75
|
+
- `game.py:750` — `random.choice(possible)` for L'Anarchie's `dynamic_trump` boss used the global RNG, ignoring `--seed`. `GameState` now carries a private `_rng: random.Random` field that `start_round` populates from the seeded RNG, and the dynamic-trump branch reads from it. L'Anarchie rounds are now seed-deterministic.
|
|
76
|
+
- `gameflow.py:125–126` — `if not isinstance(card, Card): return "UNDO"` silently coerced every non-Card return value (including `"OVERLAY"` from the info-toggle key) into UNDO, restarting the round when the user pressed `I`. `run_play` now handles `"OVERLAY"` explicitly (re-prompts; classic mode has no overlay UI). Same bug existed in `run_bidding` — pressing `I` during bidding hit `if isinstance(res, str): return None` and quit the game; now also handled explicitly.
|
|
77
|
+
- `scoring.py:616` — chute scoring hardcoded `162` instead of using `GLOBAL_CONFIG.TOTAL_POINTS + GLOBAL_CONFIG.LAST_TRICK_BONUS`. Replaced; tweaks to either config constant now propagate.
|
|
78
|
+
- `scoring.py:319, 326, 628` — `getattr(state, "_seven_eight_trump", False)` silently returned False if the property got renamed. All three sites now read `state.boss_modifiers.seven_eight_trump` directly (matches the boss-flag pattern).
|
|
79
|
+
- `ai.py:110–112` — `current_trick in sabotage_tricks` operated on an `object`-typed value pulled from `_joker_state` (mypy errored), and `trick_rank(c, state.trump)` could be called with `trump=None`. Now reads `state.boss_modifiers.agent_double_active` (consistent with the rest), narrows `sabotage_tricks` to `frozenset[int]`, and guards on `trump is not None`.
|
|
80
|
+
- `input.py:106–109` — UTF-8 multi-byte handler used `n` for the count of *continuation* bytes, then read `n + 1` bytes. Behaviour was correct but naming was misleading; renamed to `continuation_bytes` / `total_bytes`.
|
|
81
|
+
|
|
82
|
+
**Test suite hygiene (audit Phase 0.2)**
|
|
83
|
+
|
|
84
|
+
- `tests/test_official_rules.py::test_chute_declaration_transfer` — first scenario's `breakdown` was overwritten before any assertion, making it dead code. Split into two real tests (one for capot+declaration-transfer, one for defender-belote on taker success).
|
|
85
|
+
- `tests/belatro/test_boss_modifiers_integration.py::test_boss_invert_scoring` — body was just `pass`. Now actually tests La Malédiction zeroing the taker's total when NS wins more tricks.
|
|
86
|
+
- Same file — `(TrickCard(...),) * 4` patterns built four-identical-cards tricks (invalid). Helper `_trick_won_by_south` builds proper four-seat tricks instead.
|
|
87
|
+
- `tests/test_belote.py::test_must_trump_when_void_partner_not_winning` actually tested the partner-winning *exception*. Renamed to `test_void_can_discard_when_partner_winning`.
|
|
88
|
+
- `tests/test_belote.py::test_card_points_sum_152` was a duplicate of `test_total_points_consistency`. Dropped.
|
|
89
|
+
- `tests/test_new_coverage.py::test_sort_south_hand_persists_across_plays` never called `play_card`. Renamed to `test_sort_south_hand_orders_trump_first_and_is_idempotent`.
|
|
90
|
+
- `tests/test_properties.py::test_legal_moves_never_empty` silently `break`-ed on empty hands instead of failing. Now asserts the invariant explicitly.
|
|
91
|
+
|
|
92
|
+
**Lint/types (audit Phase 0.3)**
|
|
93
|
+
|
|
94
|
+
- `ruff check`: 72 → 0 errors.
|
|
95
|
+
- `mypy --strict`: 15 → 0 errors. Notably refactored `input.py::KeyReader` from a stub-class with runtime `KeyReader = _UnixKeyReader` reassignment to a polymorphic `__new__`-dispatching base, eliminating attribute-error and type-assignment errors and exposing `_restored` / `restore()` cleanly to callers.
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
**Phase 0.4 — Coverage backstop** (`tests/belatro/test_phase0_coverage.py`, 13 tests)
|
|
100
|
+
|
|
101
|
+
Happy-path tests for jokers, partner personalities, and boss modifiers that subsequent Phase 1+ refactors touch: `LeFanatique`, `LeDiplomate`, `LEconome`, `LeFlambeur`, `LeSacrifie`, `LeFantome`, `LeStratege`, `seven_eight_trump`, `dynamic_trump` seed determinism, `no_dix_de_der`, `agent_double_active`, plus a regression guard on `RoundEndEvent` payload.
|
|
102
|
+
|
|
103
|
+
**Phase 1 — Plumbing foundations** (`tests/belatro/test_phase1_plumbing.py`, 10 tests)
|
|
104
|
+
|
|
105
|
+
- `Suit.TOUT_ATOUT` is now a real enum value alongside the four card suits, with an `is_card_suit` property and a `CARD_SUITS` tuple. Every `for suit in Suit:` iteration in `game.py`, `ai.py`, and `belatro/partner/personality.py` is gated on `is_card_suit` so cards/decks are never built with TOUT_ATOUT. Under TOUT_ATOUT every card is treated as trump (`card_points` and `trick_rank` updated). `_SUIT_TO_CONTRACT` includes the new suit. `LeFanatique`'s unlock path is re-enabled in `unlocks.py` (was deferred in 2.5.6).
|
|
106
|
+
- Player-facing **coinche / surcoinche**. After bidding, if the taker is on the EW team, `RoundUICallbacks.prompt_coinche` is called; the AI rolls a seeded 30% surcoinche on top. `BidMadeEvent` and `RoundEndEvent` now carry `coinche_level: int` and `contract: str`; jokers and the economy can react to it.
|
|
107
|
+
- `BeloteAnnouncedEvent` is now actually *emitted* in `round_driver` whenever `state.belote_tracker` flips, with `is_rebelote` set correctly. The event class existed since 2.5.x but was never fired.
|
|
108
|
+
- `Rarity` enum on item base (`COMMON / UNCOMMON / RARE / LEGENDARY`); `Joker` gains `fusable: bool = True` for Phase 3 fusion gating. All existing items default to `Rarity.COMMON`.
|
|
109
|
+
- New `BelAtroRun` fields: `tierce_charges`, `legendary_unlocked`, `endless`, `endless_ante_offset`, `ante_theme`, `capot_insurance`, `partner_mood`.
|
|
110
|
+
|
|
111
|
+
**Phase 2 — Content** (`tests/belatro/test_phase2_content.py`, 20 tests)
|
|
112
|
+
|
|
113
|
+
- *Contract jokers* (`items/jokers/coinche.py`): `CoincheStack` (+4 Mult/coinche level on win), `ToutStreak` (consecutive Tout-Atout wins ramp Mult by ×0.5/streak; resets on Tout failure).
|
|
114
|
+
- *Annonce jokers* (`items/jokers/annonces.py`): `TierceCharger` (+5 chips and +1 charge per sequence announced), `RebeloteEcho` (×3 Mult on Rebelote play), `QuinteRoyale` (Legendary — quinte arms a ×4 round multiplier).
|
|
115
|
+
- *Vouchers* (`items/vouchers.py`): `CapotInsurance` (one-shot — chute next round survived without run-over), `TierceForge` (placeholder for shop-side charge spending).
|
|
116
|
+
- *Tarots* (`items/tarots.py`): `LaMaisonDieu` (sets `disable_next_boss` flag), `LeDiable` (sets `partner_overcut_round` flag).
|
|
117
|
+
- *Decks* (`run/decks.py`): `marseille` (annonces ×2, no Belote/Rebelote), `coinche` (+50 starting chips, pre-coinched rounds).
|
|
118
|
+
- *Trust as a real second axis* (`partner/trust.py`): `TrustTrack.tier` (0–4 buckets), `TrustTrack.mood()` returning HUD strings. `ScoreAccumulator.partner_tier` replaces the binary `partner_jokers_double` for partner-joker effect scaling — extra applies follow `(0,0,1,1,2)[tier]`. Legacy `partner_jokers_double` still forces +1 apply for back-compat.
|
|
119
|
+
- *Betrayal Arc boss* (`run/boss.py::BetrayalArc`): forces `lock_trust_zero` and `agent_double_active` for the round, registered in `ALL_BOSS_MODIFIERS`.
|
|
120
|
+
- `_play_blind` (`belatro/main.py`) wires Capot Insurance consumption (one-shot halve on chute) and drains pending Tierce charges into `run.tierce_charges`. `run.partner_mood` is refreshed each blind from `trust.mood()`.
|
|
121
|
+
|
|
122
|
+
**Phase 3 — Meta** (`tests/belatro/test_phase3_meta.py`, 15 tests)
|
|
123
|
+
|
|
124
|
+
- *Ante themes* (`run/ante_themes.py`): `AnteTheme` base + `CafeAnte` (+25 chips at ante start, +1 trust on blind-1 win, target 5% softer on boss blind) and `TournoiAnte` (always offers coinche, +money per blind win). `roll_theme(rng_value)` picks one with 30% probability.
|
|
125
|
+
- *Endless mode (La Belote Infinie)*: `BelAtroRun.advance_blind` no longer terminates at ante 8 / blind 2 when `run.endless` is True — instead increments `endless_ante_offset` and restarts the blind cycle. `BelAtroRun.enter_endless()` toggles the flag and clears `run_won`. `calculate_target` accepts `endless_offset` for ×2.2-per-loop super-exponential scaling. `BelAtroRun.current_blind` returns dynamically-built `endless_ante` instances when offset > 0.
|
|
126
|
+
- *Joker fusion* (`items/base.py::fuse_jokers`): two jokers → one with rarity bumped one tier (clamped at RARE — never auto-promotes to LEGENDARY), `fusable=False` on the result so fused jokers can't be re-fused, names concatenated. Rejects legendary inputs and `fusable=False` inputs with `FusionError`.
|
|
127
|
+
|
|
128
|
+
### Changed
|
|
129
|
+
|
|
130
|
+
- `GameState` gains a private `_rng: random.Random` field (`compare=False, repr=False`) so seeded randomness is reachable mid-play. Existing call sites that don't pass an RNG continue to work — the field defaults to a fresh `random.Random()`.
|
|
131
|
+
- `BidMadeEvent` and `RoundEndEvent` payload extensions (`coinche_level`, `contract`, `trump`).
|
|
132
|
+
- `make_deck()` builds from `CARD_SUITS` instead of `list(Suit)`; `deck.py` exposes `CARD_SUITS` for clean iteration in BelAtro code.
|
|
133
|
+
- Test count: 303 → 361 (+58 across Phase 0.4, Phase 1, Phase 2, and Phase 3 suites).
|
|
134
|
+
|
|
135
|
+
## [2.5.6] - 2026-05-03
|
|
136
|
+
|
|
137
|
+
### Fixed
|
|
138
|
+
|
|
139
|
+
**Critical crashes (BelAtro module)**
|
|
140
|
+
- `belatro/items/tarots.py`: `random` was never imported — `NameError` on any use of `LeJugement`, `LaPretresse`, or `LeFou`.
|
|
141
|
+
- `belatro/core/run_state.py`: `BelAtroRun` had no `consumables` list — `AttributeError` whenever tarots or the shop tried to add a Planet or Tarot to the player's inventory.
|
|
142
|
+
- `belatro/run/shop.py`: `run.consumables` crash removed; the double-append fallback in the overflow branch also removed.
|
|
143
|
+
- `belatro/items/registry.py`: `get_available_jokers(None)` and `get_available_vouchers(None)` crashed with `AttributeError` — both now accept `Profile | None` and treat `None` as "show all non-unlockable items".
|
|
144
|
+
- `belatro/engine/round_driver.py`: `Suit` was referenced in type annotations but never imported — `NameError` on module load.
|
|
145
|
+
- `belatro/progression/save.py`: `stats=data.get("stats", {})` returned an empty dict on profiles missing any stat key, causing `KeyError` when `unlocks.py` accessed `stats["total_capots"]`. Load now merges against the full default dict.
|
|
146
|
+
- `belatro/engine/event_bus.py`: `EventBus.unsubscribe` called `list.remove` unconditionally — `ValueError` if the handler was never subscribed. Now uses `contextlib.suppress(ValueError)`.
|
|
147
|
+
|
|
148
|
+
**Logic bugs (jokers / progression)**
|
|
149
|
+
- `LaSentinelle` (`hand_comp.py`): detection loop read `state.get("trump")` which is never written to the joker state dict, so it was always `None` and the joker never fired. Fixed to use `event.trump`.
|
|
150
|
+
- `LeFanatique` (`contract.py`): checked `state.get("contract") != "tout"` but `"contract"` was never injected into joker state, so the joker never activated. `ScoreAccumulator` now injects `event.contract` into joker state on every `BidMadeEvent`. Key corrected to `"tout_atout"` to match actual contract identifiers.
|
|
151
|
+
- `sans_atout_wins` stat (`unlocks.py`): incremented on every Sans Atout round regardless of outcome. Now only counts rounds where the NS team declared Sans Atout and succeeded.
|
|
152
|
+
- `LePuriste` (`contract.py`): `getattr(event.breakdown, "is_failed", True)` — wrong default (`True` = assume failed) silently blocked the joker from ever triggering. Default changed to `False`.
|
|
153
|
+
- `LaSentinelleP` (`shaper.py`): fired `trump_led = True` whenever North won any trick containing trump, including when North was *following* suit. `TrickWonEvent` now carries `leader_seat: Seat` (default `Seat.SOUTH` for backwards compatibility); the joker correctly sets the flag only when `leader_seat == Seat.NORTH` and the lead card was trump.
|
|
154
|
+
- `LeBanquier` (`economy.py`): `state.get("target_score", 80)` hardcoded to 80 because the key was never injected. `ScoreAccumulator.trigger_round_start` now writes `target_score` into the joker state dict.
|
|
155
|
+
- `LeDernierMot` (`trick_timing.py`): hard-coded `add_chips=-10` to cancel the Dix de Der bonus regardless of whether the `no_dix_de_der` boss modifier was active. Now reads `state.get("no_dix_de_der", False)` (injected at round start) and only subtracts when the bonus was actually applied.
|
|
156
|
+
|
|
157
|
+
**Boss modifier enforcement gaps**
|
|
158
|
+
- `LAvocat` (`l_avocat`): `auto_coinche` flag was set on `BossModifiers` but never read. `_play_blind` now doubles `acc.target_score` before the round; if the player wins, the base payout is tripled (2× added on top of `economy.process_round_end`).
|
|
159
|
+
- `LeDivorce` (`le_divorce`): `lock_trust_zero` flag was set but `TrustTrack` was never frozen. Trust is now temporarily set to 0 before `drive_round` (suppressing partner joker doubling and AI difficulty bonuses), restored to its original value after, and all post-round trust updates (`blind_beaten`, `big_margin_win`, `blind_failed`, `chute`, `capot_together`) are skipped.
|
|
160
|
+
- `LAgentDoubleBoss` (`l_agent_double_boss`): `_agent_double_active` caused North to sabotage for all 8 tricks despite the description stating "a random 3 tricks". `round_driver.drive_round` now picks 3 random trick numbers via the round RNG and stores them in `_joker_state["agent_double_tricks"]`; `ai.py` gates the sabotage behavior on the current trick number.
|
|
161
|
+
- `LeBrouillard` (`le_brouillard`): `hide_hud` flag was set but `BelAtroHUD.render` always drew the chips×mult score line. The score row is now skipped when `state.boss_modifiers.hide_hud` is True.
|
|
162
|
+
- `LeFantomePartenaire` (`le_fantome_partenaire`): `hide_partner_hand` flag was set but `show_north` in `_play_blind` was computed only from `run.show_north_hand` and `trust.shares_void_info`. `show_north` is now forced to `False` when this boss is active, overriding both.
|
|
163
|
+
|
|
164
|
+
**Joker accuracy**
|
|
165
|
+
- `LePuriste` (`contract.py`): `on_round_end` returned a hard-coded `add_money=10` regardless of the actual round payout. It now sets `joker_state["puriste_triggered"] = True` instead; `_play_blind` reads the flag after `economy.process_round_end` and adds an equal extra amount, correctly doubling the base payout.
|
|
166
|
+
|
|
167
|
+
### Added
|
|
168
|
+
|
|
169
|
+
**Incomplete features now implemented**
|
|
170
|
+
- `BelAtroRun` new fields: `permanent_chips`, `permanent_mult`, `guarantee_tarot_in_shop`, `show_partner_bid_tendency`, `tie_breaks_for_taker`, `partner_throws_trick`.
|
|
171
|
+
- `Economy.bonus_per_round`: flat per-round cash bonus, incremented by vouchers.
|
|
172
|
+
- `Joker.on_purchase(run)`: lifecycle hook called once at buy time for permanent run-level effects. `Shop._apply_item` now calls it after adding a joker.
|
|
173
|
+
- `Planet.use(run)`: applies `level_up_reward()` into `run.contract_levels`, stacking numerically on repeated use.
|
|
174
|
+
- **Tarot cards**: `LaRoue` (`+1.0 permanent Mult`) and `LaForce` (`+20 permanent chips`) fully implemented.
|
|
175
|
+
- **Vouchers**: `LaTelescope` (`+$1/round via bonus_per_round`), `LeGrimoire` (sets `guarantee_tarot_in_shop`), `LEncyclopedie` (sets `show_partner_bid_tendency`), `LesCartesDorees` (`+1 interest rate, +5 interest cap`), and `LaBalance` (sets `tie_breaks_for_taker`) all implemented.
|
|
176
|
+
- **Corrupted joker negative effects**: `LeTraitre.on_purchase` sets `run.partner_throws_trick = True`; `LeDemon.on_purchase` reduces partner trust by 3; `LAgentDouble` tracks a per-round sabotage window in joker state.
|
|
177
|
+
- **Planet contract bonuses consumed**: `ScoreAccumulator` now applies `contract_levels` rewards during play — per-trick chip bonuses (Saturn), per-trick Mult bonuses (Venus), Jack/9 capture bonuses (Jupiter), honor bonuses in Sans Atout (The Moon), capot chip bonus (Pluto), and round-win money bonus (Mercury).
|
|
178
|
+
- **Permanent Tarot bonuses applied**: `ScoreAccumulator.trigger_round_start` applies `permanent_chips` and `permanent_mult` from the run to the initial `GameState` before the round begins.
|
|
179
|
+
- `ScoreAccumulator` wired in `BelAtroGame._play_blind`: `target_score`, `contract_levels`, `permanent_chips`, and `permanent_mult` are now set from the active run before each blind.
|
|
180
|
+
|
|
181
|
+
### Changed
|
|
182
|
+
- `TrickWonEvent` gains `leader_seat: Seat = Seat.SOUTH` field. `round_driver.drive_round` populates it from `last_trick[0].seat`. All existing event construction and tests remain compatible via the keyword-argument default.
|
|
183
|
+
- `round_driver.py` import cleanup: removed unused `Rank`, `card_points`, `clear_announced`, and `BeloteAnnouncedEvent`; added the missing `Suit`.
|
|
184
|
+
- `Shop.generate_inventory` respects the `guarantee_tarot_in_shop` flag set by `LeGrimoire`.
|
|
185
|
+
- `LeFou` (tarot) no longer adds a copy of itself to consumables; also respects the consumable slot limit.
|
|
186
|
+
|
|
187
|
+
## [2.5.5] - 2026-05-03
|
|
188
|
+
|
|
189
|
+
### Fixed
|
|
190
|
+
- **BelAtro Run Loop**: Fixed multiple critical crashes, including a `TypeError` in `drive_round` due to signature mismatch and an `IndexError` when tricks were accessed prematurely.
|
|
191
|
+
- **Event Bus Integrity**: Fixed `TrickWonEvent` instantiation error by providing missing `trick_number` and `trump` context.
|
|
192
|
+
- **Run State Consistency**: Added missing `consumable_slots` to `BelAtroRun` to prevent crashes when applying certain Vouchers (e.g., Le Couteau).
|
|
193
|
+
- **Cache Clear Bug**: Fixed an `AttributeError` when clearing the legal cards cache by ensuring the implementation function is properly decorated with `@lru_cache`.
|
|
194
|
+
- **Input & HUD**: Fixed the `[I]` key mapping for the score overlay HUD and synchronized the Windows `KeyReader` to support all game-specific keys.
|
|
195
|
+
|
|
196
|
+
## [2.5.4] - 2026-05-03
|
|
197
|
+
|
|
198
|
+
### Fixed
|
|
199
|
+
- **Missing read_timeout**: Fixed an `AttributeError` in the main menu by implementing the `read_timeout` method in the `KeyReader` class.
|
|
200
|
+
- **IndentationError in trick_timing.py**: Fixed a critical syntax error in the BelAtro items module.
|
|
201
|
+
- **LePremierSang Logic**: Fixed `LePremierSang` joker to correctly apply +2 Mult (additive) and track its active state.
|
|
202
|
+
|
|
8
203
|
## [2.5.3] - 2026-05-03
|
|
9
204
|
|
|
10
205
|
### Fixed
|
|
@@ -37,6 +37,7 @@ Or via python:
|
|
|
37
37
|
```bash
|
|
38
38
|
python -m belote.main
|
|
39
39
|
python -m belote.belatro.main
|
|
40
|
+
PYTHONPATH=src python3 -m belote.main
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
## Testing
|
|
@@ -82,7 +83,7 @@ PYTHONPATH=src mypy .
|
|
|
82
83
|
|
|
83
84
|
# Linting (0 violations expected)
|
|
84
85
|
ruff check .
|
|
85
|
-
# Full test suite (
|
|
86
|
+
# Full test suite (390 tests expected)
|
|
86
87
|
PYTHONPATH=src pytest
|
|
87
88
|
|
|
88
89
|
# ...
|
|
@@ -90,7 +91,7 @@ PYTHONPATH=src pytest
|
|
|
90
91
|
Current baseline:
|
|
91
92
|
- **mypy**: 0 errors (strict mode)
|
|
92
93
|
- **ruff**: 0 violations
|
|
93
|
-
- **pytest**:
|
|
94
|
+
- **pytest**: 390 tests, 0 failures
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
## Benchmarking
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.1
|
|
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
|
|
@@ -78,6 +78,10 @@ belatro
|
|
|
78
78
|
| **Le Carnet** | See partner's full hand every round. +1 Mult each time South wins a trick |
|
|
79
79
|
| La Voûte | Earn $1 interest per $5 held, up to $5/round |
|
|
80
80
|
| La Double Donne | +1 Joker slot |
|
|
81
|
+
| **La Télescope** | +$1 flat bonus after every round |
|
|
82
|
+
| **Le Grimoire** | Shop always stocks at least one Tarot card |
|
|
83
|
+
| **Les Cartes Dorées** | +1 interest rate and +5 interest cap permanently |
|
|
84
|
+
| **La Balance** | Your team wins automatically on a card-point tie |
|
|
81
85
|
| La Surcoinche | Unlocks the Surcoinche contract *(unlockable)* |
|
|
82
86
|
|
|
83
87
|
## Showcase
|
|
@@ -134,7 +138,7 @@ belatro
|
|
|
134
138
|
|
|
135
139
|
- Python >= 3.10
|
|
136
140
|
- No third-party dependencies (stdlib only)
|
|
137
|
-
- Terminal with >=
|
|
141
|
+
- Terminal with >= **80 columns × 32 rows** (compact preset). Recommended: 96×38 (standard) or 120×48 (spacious) for the full Art Nouveau card art and verbose HUD. The game auto-selects the best fit and adapts on resize.
|
|
138
142
|
- UTF-8 support (for card symbols: ♠♥♦♣)
|
|
139
143
|
|
|
140
144
|
## Quick Start
|
|
@@ -187,7 +191,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
187
191
|
|
|
188
192
|
## Features
|
|
189
193
|
|
|
190
|
-
- **BelAtro Roguelite Mode:** A massive expansion featuring 50+ Jokers,
|
|
194
|
+
- **BelAtro Roguelite Mode:** A massive expansion featuring 50+ Jokers, 10 Tarot cards, and permanent upgrades.
|
|
191
195
|
- **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
|
|
192
196
|
- **Full Boss Blind Suite:** All 17 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
|
|
193
197
|
- **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
|
|
@@ -200,8 +204,8 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
200
204
|
- **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
|
|
201
205
|
- **Undo/Redo:** Press `Z` to undo your last move during bidding or play.
|
|
202
206
|
- **Statistics:** unified global tracking of games played, win rates, best rounds, and BelAtro expansion milestones.
|
|
203
|
-
- **
|
|
204
|
-
- **Alternate Screen Buffer:** BelAtro
|
|
207
|
+
- **Responsive Layout (3 tiers):** Three preset layouts — **compact** (80×32, fits 1366×768), **standard** (96×38), **spacious** (120×48+). The game picks the largest preset that fits your terminal on every render, so resizing mid-game adapts automatically; cards, side columns, and HUD verbosity all scale with the preset. Vertical centering pads tall terminals so the game never clings to the top.
|
|
208
|
+
- **Alternate Screen Buffer:** Both classic Belote and BelAtro run in a dedicated terminal buffer for a clean, non-overlapping interface — your shell scrollback stays untouched after you quit.
|
|
205
209
|
- **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
|
|
206
210
|
- **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
|
|
207
211
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
@@ -241,7 +245,7 @@ belote/
|
|
|
241
245
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
242
246
|
│ ├── stats.py # Global and session statistics tracking
|
|
243
247
|
│ └── rules.py # Game rules content
|
|
244
|
-
├── tests/ # Comprehensive test suite (
|
|
248
|
+
├── tests/ # Comprehensive test suite (390 tests)
|
|
245
249
|
├── scripts/ # Performance benchmarks
|
|
246
250
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
247
251
|
├── LICENSE # MIT License
|
|
@@ -257,14 +261,14 @@ belote/
|
|
|
257
261
|
PYTHONPATH=src pytest
|
|
258
262
|
```
|
|
259
263
|
|
|
260
|
-
Currently **
|
|
264
|
+
Currently **390 tests** passing with 100% coverage on core logic.
|
|
261
265
|
|
|
262
266
|
## Technical Integrity
|
|
263
267
|
|
|
264
268
|
The codebase is strictly validated with the following tools:
|
|
265
269
|
- **mypy**: 0 errors (strict type safety)
|
|
266
270
|
- **ruff**: 0 violations (linting & formatting)
|
|
267
|
-
- **pytest**:
|
|
271
|
+
- **pytest**: 390/390 passed
|
|
268
272
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
269
273
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
270
274
|
|
|
@@ -35,6 +35,10 @@ belatro
|
|
|
35
35
|
| **Le Carnet** | See partner's full hand every round. +1 Mult each time South wins a trick |
|
|
36
36
|
| La Voûte | Earn $1 interest per $5 held, up to $5/round |
|
|
37
37
|
| La Double Donne | +1 Joker slot |
|
|
38
|
+
| **La Télescope** | +$1 flat bonus after every round |
|
|
39
|
+
| **Le Grimoire** | Shop always stocks at least one Tarot card |
|
|
40
|
+
| **Les Cartes Dorées** | +1 interest rate and +5 interest cap permanently |
|
|
41
|
+
| **La Balance** | Your team wins automatically on a card-point tie |
|
|
38
42
|
| La Surcoinche | Unlocks the Surcoinche contract *(unlockable)* |
|
|
39
43
|
|
|
40
44
|
## Showcase
|
|
@@ -91,7 +95,7 @@ belatro
|
|
|
91
95
|
|
|
92
96
|
- Python >= 3.10
|
|
93
97
|
- No third-party dependencies (stdlib only)
|
|
94
|
-
- Terminal with >=
|
|
98
|
+
- Terminal with >= **80 columns × 32 rows** (compact preset). Recommended: 96×38 (standard) or 120×48 (spacious) for the full Art Nouveau card art and verbose HUD. The game auto-selects the best fit and adapts on resize.
|
|
95
99
|
- UTF-8 support (for card symbols: ♠♥♦♣)
|
|
96
100
|
|
|
97
101
|
## Quick Start
|
|
@@ -144,7 +148,7 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
144
148
|
|
|
145
149
|
## Features
|
|
146
150
|
|
|
147
|
-
- **BelAtro Roguelite Mode:** A massive expansion featuring 50+ Jokers,
|
|
151
|
+
- **BelAtro Roguelite Mode:** A massive expansion featuring 50+ Jokers, 10 Tarot cards, and permanent upgrades.
|
|
148
152
|
- **Collection (Almanac):** Persistent tracker to browse every Joker, Planet, and Voucher you've discovered across your runs.
|
|
149
153
|
- **Full Boss Blind Suite:** All 17 unique bosses implemented, including complex mechanics like *L'Anarchie* (dynamic trump) and *La Rupture* (no consecutive wins).
|
|
150
154
|
- **Multiplier Scoring:** Use items to stack Multipliers and reach scores in the millions.
|
|
@@ -157,8 +161,8 @@ belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
|
157
161
|
- **Main Menu:** Simple single-player entry point with configurable AI difficulty, Target Score, and Speed.
|
|
158
162
|
- **Undo/Redo:** Press `Z` to undo your last move during bidding or play.
|
|
159
163
|
- **Statistics:** unified global tracking of games played, win rates, best rounds, and BelAtro expansion milestones.
|
|
160
|
-
- **
|
|
161
|
-
- **Alternate Screen Buffer:** BelAtro
|
|
164
|
+
- **Responsive Layout (3 tiers):** Three preset layouts — **compact** (80×32, fits 1366×768), **standard** (96×38), **spacious** (120×48+). The game picks the largest preset that fits your terminal on every render, so resizing mid-game adapts automatically; cards, side columns, and HUD verbosity all scale with the preset. Vertical centering pads tall terminals so the game never clings to the top.
|
|
165
|
+
- **Alternate Screen Buffer:** Both classic Belote and BelAtro run in a dedicated terminal buffer for a clean, non-overlapping interface — your shell scrollback stays untouched after you quit.
|
|
162
166
|
- **Sound Effects:** Enhanced auditory feedback for trick wins, Belote, and Capot, with a built-in mute toggle.
|
|
163
167
|
- **Declarations:** Automatic detection and announcement of sequences (Tierce, Quarte, etc.) and Carrés after the first trick.
|
|
164
168
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
@@ -198,7 +202,7 @@ belote/
|
|
|
198
202
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
199
203
|
│ ├── stats.py # Global and session statistics tracking
|
|
200
204
|
│ └── rules.py # Game rules content
|
|
201
|
-
├── tests/ # Comprehensive test suite (
|
|
205
|
+
├── tests/ # Comprehensive test suite (390 tests)
|
|
202
206
|
├── scripts/ # Performance benchmarks
|
|
203
207
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
204
208
|
├── LICENSE # MIT License
|
|
@@ -214,14 +218,14 @@ belote/
|
|
|
214
218
|
PYTHONPATH=src pytest
|
|
215
219
|
```
|
|
216
220
|
|
|
217
|
-
Currently **
|
|
221
|
+
Currently **390 tests** passing with 100% coverage on core logic.
|
|
218
222
|
|
|
219
223
|
## Technical Integrity
|
|
220
224
|
|
|
221
225
|
The codebase is strictly validated with the following tools:
|
|
222
226
|
- **mypy**: 0 errors (strict type safety)
|
|
223
227
|
- **ruff**: 0 violations (linting & formatting)
|
|
224
|
-
- **pytest**:
|
|
228
|
+
- **pytest**: 390/390 passed
|
|
225
229
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
226
230
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
227
231
|
|
|
@@ -103,12 +103,15 @@ class AIPlayer:
|
|
|
103
103
|
hand = state.hand_of(self.seat)
|
|
104
104
|
legal = legal_cards(state, self.seat)
|
|
105
105
|
|
|
106
|
-
# Boss: L'Agent Double (Partner sabotages)
|
|
107
|
-
if
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
# Boss: L'Agent Double (Partner sabotages on 3 random tricks)
|
|
107
|
+
if state.boss_modifiers.agent_double_active and self.seat == partner(Seat.SOUTH):
|
|
108
|
+
current_trick = len(state.completed_tricks) + 1
|
|
109
|
+
raw_tricks = state._joker_state.get("agent_double_tricks", frozenset())
|
|
110
|
+
sabotage_tricks: frozenset[int] = (
|
|
111
|
+
raw_tricks if isinstance(raw_tricks, frozenset) else frozenset()
|
|
112
|
+
)
|
|
110
113
|
trump = state.trump
|
|
111
|
-
if trump:
|
|
114
|
+
if current_trick in sabotage_tricks and trump is not None:
|
|
112
115
|
return min(legal, key=lambda c: trick_rank(c, trump))
|
|
113
116
|
|
|
114
117
|
if not hand:
|
|
@@ -136,7 +139,7 @@ class AIPlayer:
|
|
|
136
139
|
"""Bid if hand has >= 2 trump honors (J, 9, A) in any suit."""
|
|
137
140
|
honors = {Rank.JACK, Rank.NINE, Rank.ACE}
|
|
138
141
|
for suit in Suit:
|
|
139
|
-
if suit == exclude:
|
|
142
|
+
if suit == exclude or not suit.is_card_suit:
|
|
140
143
|
continue
|
|
141
144
|
count = sum(1 for c in hand if c.suit == suit and c.rank in honors)
|
|
142
145
|
if count >= 2:
|
|
@@ -173,7 +176,7 @@ class AIPlayer:
|
|
|
173
176
|
if state.bidding_round == 2 and state.bidder_index == 3:
|
|
174
177
|
aggression = 1.0
|
|
175
178
|
|
|
176
|
-
avail = [s for s in Suit if s != exclude]
|
|
179
|
+
avail = [s for s in Suit if s != exclude and s.is_card_suit]
|
|
177
180
|
best_suit = max(avail, key=lambda s: suit_scores[s])
|
|
178
181
|
if suit_scores[best_suit] + personality + aggression >= 4:
|
|
179
182
|
return best_suit
|
|
@@ -263,7 +266,11 @@ class AIPlayer:
|
|
|
263
266
|
# 2. Lead from longest non-trump suit (to establish it)
|
|
264
267
|
non_trumps = [c for c in legal if c.suit != trump]
|
|
265
268
|
if non_trumps:
|
|
266
|
-
suit_counts = {
|
|
269
|
+
suit_counts = {
|
|
270
|
+
s: sum(1 for c in non_trumps if c.suit == s)
|
|
271
|
+
for s in Suit
|
|
272
|
+
if s != trump and s.is_card_suit
|
|
273
|
+
}
|
|
267
274
|
best_suit = max(suit_counts, key=lambda s: suit_counts[s])
|
|
268
275
|
if suit_counts[best_suit] > 1:
|
|
269
276
|
suit_cards = [c for c in non_trumps if c.suit == best_suit]
|
|
@@ -290,10 +297,11 @@ class AIPlayer:
|
|
|
290
297
|
self, hand: tuple[Card, ...], state: GameState, exclude: Suit | None = None
|
|
291
298
|
) -> Suit | None:
|
|
292
299
|
"""Monte-Carlo-lite bidding evaluation with personality."""
|
|
293
|
-
|
|
300
|
+
card_suits = [s for s in Suit if s.is_card_suit]
|
|
301
|
+
suit_scores: dict[Suit, float] = dict.fromkeys(card_suits, 0.0)
|
|
294
302
|
personality = self._rng.uniform(-0.8, 0.8)
|
|
295
303
|
|
|
296
|
-
for suit in
|
|
304
|
+
for suit in card_suits:
|
|
297
305
|
trump_cards = [c for c in hand if c.suit == suit]
|
|
298
306
|
honor_count = sum(1 for c in trump_cards if c.rank in (Rank.JACK, Rank.NINE, Rank.ACE))
|
|
299
307
|
point_total = sum(card_points_fn(c, suit) for c in trump_cards)
|
|
@@ -306,7 +314,7 @@ class AIPlayer:
|
|
|
306
314
|
if state.bidding_round == 2 and state.bidder_index >= 2:
|
|
307
315
|
suit_scores[suit] += 1.5
|
|
308
316
|
|
|
309
|
-
for other in
|
|
317
|
+
for other in card_suits:
|
|
310
318
|
if other != suit:
|
|
311
319
|
other_count = sum(1 for c in hand if c.suit == other)
|
|
312
320
|
if other_count == 0:
|
|
@@ -314,7 +322,7 @@ class AIPlayer:
|
|
|
314
322
|
elif other_count == 1:
|
|
315
323
|
suit_scores[suit] += 1
|
|
316
324
|
|
|
317
|
-
avail = [s for s in
|
|
325
|
+
avail = [s for s in card_suits if s != exclude]
|
|
318
326
|
best_suit = max(avail, key=lambda s: suit_scores[s])
|
|
319
327
|
if suit_scores[best_suit] + personality >= 6:
|
|
320
328
|
return best_suit
|
|
@@ -10,6 +10,7 @@ class Economy:
|
|
|
10
10
|
money: int = 0
|
|
11
11
|
interest_rate: int = 0 # default 0, becomes 1 with La Voute voucher
|
|
12
12
|
max_interest: int = 0 # default 0, becomes 5 with La Voute voucher
|
|
13
|
+
bonus_per_round: int = 0 # flat bonus paid each round end (La Télescope)
|
|
13
14
|
|
|
14
15
|
def add_money(self, amount: int) -> None:
|
|
15
16
|
self.money += amount
|
|
@@ -27,9 +28,9 @@ class Economy:
|
|
|
27
28
|
return min(interest, self.max_interest)
|
|
28
29
|
|
|
29
30
|
def process_round_end(self, points_over_target: int) -> int:
|
|
30
|
-
"""Calculate payout: $1 per 10pts over target + interest."""
|
|
31
|
+
"""Calculate payout: $1 per 10pts over target + interest + flat bonus."""
|
|
31
32
|
base_payout = max(0, points_over_target // 10)
|
|
32
33
|
interest = self.calculate_interest()
|
|
33
|
-
total = base_payout + interest
|
|
34
|
+
total = base_payout + interest + self.bonus_per_round
|
|
34
35
|
self.add_money(total)
|
|
35
36
|
return total
|
|
@@ -12,6 +12,7 @@ from ..partner.partner_state import PartnerState
|
|
|
12
12
|
from .economy import Economy
|
|
13
13
|
|
|
14
14
|
MAX_JOKER_SLOTS = 5
|
|
15
|
+
DEFAULT_CONSUMABLE_SLOTS = 2
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
@dataclass
|
|
@@ -28,7 +29,28 @@ class BelAtroRun:
|
|
|
28
29
|
# ── Collectibles ───────────────────────────────────────
|
|
29
30
|
jokers: list[Joker] = field(default_factory=list)
|
|
30
31
|
vouchers: list[Voucher] = field(default_factory=list)
|
|
32
|
+
consumables: list[Any] = field(default_factory=list) # Tarot/Planet instances
|
|
31
33
|
joker_slots: int = MAX_JOKER_SLOTS
|
|
34
|
+
consumable_slots: int = DEFAULT_CONSUMABLE_SLOTS
|
|
35
|
+
|
|
36
|
+
# ── Permanent run bonuses (from Tarot cards) ───────────
|
|
37
|
+
permanent_chips: int = 0
|
|
38
|
+
permanent_mult: float = 1.0
|
|
39
|
+
|
|
40
|
+
# ── Voucher flags ───────────────────────────────────────
|
|
41
|
+
guarantee_tarot_in_shop: bool = False
|
|
42
|
+
show_partner_bid_tendency: bool = False
|
|
43
|
+
tie_breaks_for_taker: bool = False
|
|
44
|
+
partner_throws_trick: bool = False
|
|
45
|
+
capot_insurance: bool = False # one-shot: halve a chute loss
|
|
46
|
+
|
|
47
|
+
# ── Phase 1+ feature flags ──────────────────────────────
|
|
48
|
+
tierce_charges: int = 0
|
|
49
|
+
legendary_unlocked: set[str] = field(default_factory=set)
|
|
50
|
+
endless: bool = False
|
|
51
|
+
endless_ante_offset: int = 0
|
|
52
|
+
ante_theme: str | None = None
|
|
53
|
+
partner_mood: str = "neutral"
|
|
32
54
|
|
|
33
55
|
# ── Economy ────────────────────────────────────────────
|
|
34
56
|
economy: Economy = field(default_factory=Economy)
|
|
@@ -72,11 +94,23 @@ class BelAtroRun:
|
|
|
72
94
|
planet_instance = planet_cls()
|
|
73
95
|
self.contract_levels[planet_instance.contract_id] = planet_instance.level_up_reward()
|
|
74
96
|
|
|
97
|
+
# Phase 2.4 deck mods
|
|
98
|
+
if deck.deck_modifications.get("start_chips_bonus"):
|
|
99
|
+
self.permanent_chips += int(deck.deck_modifications["start_chips_bonus"])
|
|
100
|
+
if deck.deck_modifications.get("start_coinched"):
|
|
101
|
+
self.card_enhancements["start_coinched"] = True
|
|
102
|
+
if deck.deck_modifications.get("announce_x2"):
|
|
103
|
+
self.card_enhancements["announce_x2"] = True
|
|
104
|
+
if deck.deck_modifications.get("no_belote_rebelote"):
|
|
105
|
+
self.card_enhancements["no_belote_rebelote"] = True
|
|
106
|
+
|
|
75
107
|
# ── Current blind target ───────────────────────────────
|
|
76
108
|
@property
|
|
77
109
|
def current_blind(self) -> Ante:
|
|
78
|
-
from ..run.ante import ANTE_TABLE
|
|
110
|
+
from ..run.ante import ANTE_TABLE, endless_ante
|
|
79
111
|
|
|
112
|
+
if self.endless and self.endless_ante_offset > 0:
|
|
113
|
+
return endless_ante(self.ante_number, self.blind_index, self.endless_ante_offset)
|
|
80
114
|
return ANTE_TABLE[self.ante_number - 1][self.blind_index]
|
|
81
115
|
|
|
82
116
|
@property
|
|
@@ -86,8 +120,22 @@ class BelAtroRun:
|
|
|
86
120
|
def advance_blind(self) -> None:
|
|
87
121
|
if self.blind_index < 2:
|
|
88
122
|
self.blind_index += 1
|
|
89
|
-
|
|
123
|
+
return
|
|
124
|
+
# End of an ante.
|
|
125
|
+
if self.ante_number < 8:
|
|
90
126
|
self.ante_number += 1
|
|
91
127
|
self.blind_index = 0
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
return
|
|
129
|
+
# End of ante 8.
|
|
130
|
+
if self.endless:
|
|
131
|
+
# Stay at ante 8, increment endless offset, restart blind cycle.
|
|
132
|
+
self.endless_ante_offset += 1
|
|
133
|
+
self.blind_index = 0
|
|
134
|
+
return
|
|
135
|
+
# Standard run completion.
|
|
136
|
+
self.run_won = True
|
|
137
|
+
|
|
138
|
+
def enter_endless(self) -> None:
|
|
139
|
+
"""Toggle endless mode after beating ante 8."""
|
|
140
|
+
self.endless = True
|
|
141
|
+
self.run_won = False # endless overrides run-won state
|