belote-cli 4.9.4__tar.gz → 4.9.6__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-4.9.4 → belote_cli-4.9.6}/CHANGELOG.md +97 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/DEVELOPMENT.md +8 -4
- {belote_cli-4.9.4 → belote_cli-4.9.6}/PKG-INFO +4 -4
- {belote_cli-4.9.4 → belote_cli-4.9.6}/README.md +3 -3
- {belote_cli-4.9.4 → belote_cli-4.9.6}/pyproject.toml +1 -1
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/__init__.py +1 -1
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ai.py +28 -19
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ansi.py +76 -5
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/partner_jokers/passive.py +4 -1
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/main.py +23 -10
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/partner/trust.py +10 -6
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/history.py +93 -74
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/hud.py +18 -4
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/trust_bar.py +4 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/config.py +6 -1
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/game.py +19 -17
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/gameflow.py +11 -5
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/scoring.py +25 -15
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/render.py +4 -1
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_jokers_4_5.py +26 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_partner_jokers.py +11 -0
- belote_cli-4.9.6/tests/belatro/test_registry_integrity.py +61 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_alt_screen_scroll.py +6 -0
- belote_cli-4.9.6/tests/test_deluge_legal_cards.py +68 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_g1_coinche_classic.py +30 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_g2_ai_signals.py +49 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_new_coverage.py +3 -2
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_official_rules.py +21 -0
- belote_cli-4.9.6/tests/test_render_golden.py +80 -0
- belote_cli-4.9.6/tests/test_scoring_totals.py +62 -0
- belote_cli-4.9.6/tests/test_wcwidth.py +88 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/uv.lock +1 -1
- {belote_cli-4.9.4 → belote_cli-4.9.6}/.claude/settings.local.json +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/.gitignore +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/.python-version +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/LICENSE +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/scripts/benchmark.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/a11y.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/achievements.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/core/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/core/economy.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/core/round_ledger.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/core/run_state.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/core/scoring.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/engine/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/engine/event_bus.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/engine/modifier_patch.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/engine/round_driver.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ghost_run.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/base.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/annonces.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/coinche.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/contract.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/corrupted.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/economy.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/jokers/trick_timing.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/planets.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/registry.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/tarots.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/items/vouchers.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/partner/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/partner/partner_state.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/partner/personality.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/progression/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/progression/save.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/progression/unlocks.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/run/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/run/ante.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/run/ante_themes.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/run/boss.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/run/decks.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/run/shop.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/run_summary.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/announce.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/collection.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/consumables.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/inventory.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/menu.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/rules.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/belatro/ui/shop.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/context.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/deck.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/input.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/main.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/replay.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/rules.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/stats.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/themes.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/anim.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/announce.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/fit_guard.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/layout.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/menu.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/src/belote/ui/prompts.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/__init__.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_belatro.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_boss_contracts.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_boss_modifiers_integration.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_collection_logic.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_competition_malediction.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_consumables_ui.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_contract_unlocks.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_dead_flag_fixes.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_deck_variants.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_decks_4_5.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_endless.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_event_bus.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_ghost_run.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_handler_registry.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_heist.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_history_overlay.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_hud_synergy.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_hud_toggle.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_inventory_overlay.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_joker_callouts.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_joker_contracts.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_partner_trust.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_phase0_coverage.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_phase1_plumbing.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_phase2_content.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_phase3_meta.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_progression.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_round_driver.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_run_summary.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_shop_animations.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_shop_empty_pools.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_slot_machine_tally.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/belatro/test_voucher_idempotency.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/perf_baselines.json +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_a11y.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_achievements.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_ai.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_ai_deluge_voids.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_ai_sans_atout.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_anim_helpers.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_announce_stats.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_ansi_helpers.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_belote.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_belote_stinger.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_benchmark_smoke.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_bidding_all_pass.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_declaration_tiebreak.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_extended.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_game_logic.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_gameflow.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_hand_auto_sort.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_history_overlay_cache.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_input_eof.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_input_wasd.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_invariants.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_layout.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_no_color.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_perf_regression.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_properties.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_render_diff.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_render_felt_polish.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_render_hand_lift.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_replay.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_undo.py +0 -0
- {belote_cli-4.9.4 → belote_cli-4.9.6}/tests/test_winner_glow.py +0 -0
|
@@ -5,6 +5,103 @@ 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
|
+
## [4.9.6] - 2026-06-01
|
|
9
|
+
|
|
10
|
+
Follow-up bug-hunt, logic, and performance audit. Three parallel Explore agents
|
|
11
|
+
re-swept the classic engine, BelAtro mechanics, and the UI/render layer; every
|
|
12
|
+
"major" finding was then traced to exact code and verified before acting. The
|
|
13
|
+
result: **no critical or major bug survived verification.** The flagged items
|
|
14
|
+
were all either false positives (Le Passeur "double-payout" — the post-coinche
|
|
15
|
+
`BidMadeEvent` re-emit carries the taker's seat + a real trump, so the
|
|
16
|
+
North-pass gate never matches; Le Carnet's +1 Mult *is* wired via
|
|
17
|
+
`acc.carnet_active`; Tout Streak / Quinte Royale re-emit guards are already
|
|
18
|
+
present; `modifier_patch` is a fresh per-instance proxy each round) or
|
|
19
|
+
intentional, test-pinned behaviour (Le Patriote's `// 2`). Benchmarks confirmed
|
|
20
|
+
the render/scoring/AI paths are already well within budget (render ≈0.4 ms, E2E
|
|
21
|
+
round ≈4–6 ms). This release therefore lands the one genuine latent gap the
|
|
22
|
+
audit surfaced plus a layer of behaviour-locking tests. Test count 1079 → 1111.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Minimal wide-character (wcwidth) support** (`src/belote/ansi.py`). `visible_len()`
|
|
27
|
+
previously counted codepoints, so `ansi_center()` / `ansi_ljust()` and the
|
|
28
|
+
render diff's per-row width would mis-align by one cell per wide glyph (emoji,
|
|
29
|
+
CJK). A new dependency-free `char_width()` (stdlib `unicodedata`) returns 0
|
|
30
|
+
for combining/zero-width marks, 2 for East-Asian Wide/Fullwidth, 1 otherwise,
|
|
31
|
+
and `visible_len()` now measures display **cells**. A pure-ASCII escape-free
|
|
32
|
+
fast path keeps the hot render path allocation-free and byte-identical, so
|
|
33
|
+
there is no performance regression. A new `ansi_truncate()` (cell-aware,
|
|
34
|
+
escape-skipping) replaces the raw byte-slices that trimmed BelAtro HUD joker
|
|
35
|
+
names and synergy lines (`belatro/ui/hud.py`), which could previously split a
|
|
36
|
+
wide glyph.
|
|
37
|
+
- **Behaviour-locking tests** that freeze what the audit confirmed correct:
|
|
38
|
+
`tests/test_render_golden.py` (full-frame snapshot + per-row cell-width
|
|
39
|
+
fingerprint — the regression guard for the wcwidth change),
|
|
40
|
+
`tests/test_wcwidth.py` (per-character widths and helper cell-correctness),
|
|
41
|
+
`tests/test_scoring_totals.py` (the canonical 152 / 120 / 248 point totals,
|
|
42
|
+
belote 20, capot bases 252 / 220 / 348, dix de der +10), and
|
|
43
|
+
`tests/belatro/test_registry_integrity.py` (item-census counts 42 jokers /
|
|
44
|
+
8 planets / 12 tarots / 12 vouchers, id↔key consistency, no duplicate display
|
|
45
|
+
names — so a duplicate-id registration that silently overwrites an item now
|
|
46
|
+
fails a test).
|
|
47
|
+
|
|
48
|
+
## [4.9.5] - 2026-05-28
|
|
49
|
+
|
|
50
|
+
Bug-hunt, logic, and performance audit pass. Three parallel Explore agents
|
|
51
|
+
swept the classic engine, BelAtro mechanics, and the UI/render layer; every
|
|
52
|
+
finding below was traced to exact code and the high-severity ones reproduced at
|
|
53
|
+
runtime before fixing. Test count 1066 → 1079.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **Crash: `legal_cards` returned an empty set under La Déluge**
|
|
58
|
+
(`src/belote/game.py`). With the `seven_eight_trump` boss active, every 7/8
|
|
59
|
+
is a trump — but the trump-follow set was built from `c.suit == lead_suit`,
|
|
60
|
+
which dropped off-suit 7/8s. A player void of the trump *suit* but holding
|
|
61
|
+
only off-suit 7/8 (which ARE trumps) got zero legal cards on a trump lead,
|
|
62
|
+
tripping `AIPlayer.decide_card`'s empty-legal assertion. A single
|
|
63
|
+
`is_trump_card` predicate now drives both the trump-led and non-trump-led
|
|
64
|
+
follow/overtrump paths. Regression tests in `tests/test_deluge_legal_cards.py`.
|
|
65
|
+
- **AI partner-signal double-count after a mid-round undo** (`src/belote/ai.py`).
|
|
66
|
+
The undo-regression branch reset the void cache and `processed_tricks_count`
|
|
67
|
+
but not the partner-signal tally; because `_update_voids` re-walks surviving
|
|
68
|
+
tricks from zero, their signals were counted twice. Both reset paths now share
|
|
69
|
+
a single `_reset_round_memory()` helper so they can't drift again.
|
|
70
|
+
- **Belote + Rebelote scored 40 instead of the official 20** (`src/belote/config.py`).
|
|
71
|
+
Holding the King and Queen of trump is worth 20 points *total* in official
|
|
72
|
+
French Belote — and the in-game Rules screen already said 20. `REBELOTE_POINTS`
|
|
73
|
+
is now 20, matching both.
|
|
74
|
+
- **Coinche round-end animation showed the pre-multiplier score**
|
|
75
|
+
(`src/belote/gameflow.py`). The score counter animated to the raw breakdown
|
|
76
|
+
total while the stored score applied `2 ** coinche_level`. Both now use the
|
|
77
|
+
shared `scoring.coinche_adjusted_credits()` helper, so the animation lands on
|
|
78
|
+
the real total.
|
|
79
|
+
- **Joker money not reconciled on a failed-but-survived round**
|
|
80
|
+
(`src/belote/belatro/main.py`). Signed `JokerResult.add_money` (e.g.
|
|
81
|
+
LePrêteur's −$5 skim, baked into the round's ×Mult) was only applied on the
|
|
82
|
+
success path, handing a free multiplier when a chute was survived via Capot
|
|
83
|
+
Insurance. A shared `_reconcile_bonus_money()` helper now runs on both paths.
|
|
84
|
+
- **La Symbiose fired ×1.2 Mult on zero-point declarations under Le Mime**
|
|
85
|
+
(`src/belote/belatro/items/partner_jokers/passive.py`). Now gated on
|
|
86
|
+
`event.points > 0`, mirroring Le Mathématicien.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- **BelAtro HUD / TrustBar now invalidate the render diff** after their direct
|
|
91
|
+
stdout writes (`belatro/ui/hud.py`, `trust_bar.py`), matching the 4.0.0
|
|
92
|
+
overlay convention so a static-state `display()` can't leave a stale top bar.
|
|
93
|
+
The convention is now enforced for both files by
|
|
94
|
+
`tests/test_alt_screen_scroll.py`.
|
|
95
|
+
- **Performance**: `_pip_at`'s LRU cache raised 4096 → 8192 so wide-terminal
|
|
96
|
+
felt mats (>5000 cells/frame) stop thrashing it (`ui/render.py`);
|
|
97
|
+
`visible_len` no longer spends cache slots on plain (no-ANSI) strings
|
|
98
|
+
(`ansi.py`); the BelAtro [H] history overlay caches its line list on
|
|
99
|
+
`(term_w, entry_count)` instead of rebuilding every keystroke
|
|
100
|
+
(`belatro/ui/history.py`).
|
|
101
|
+
- **Docs**: corrected the trust tier-0 docstring (it claimed "effects halved";
|
|
102
|
+
the implementation applies the baseline once with no penalty) and the tier
|
|
103
|
+
bonus percentages to match the `(0, 0, 1, 1, 2)` extra-applies model.
|
|
104
|
+
|
|
8
105
|
## [4.9.4] - 2026-05-26
|
|
9
106
|
|
|
10
107
|
Audit + maintenance pass. Three parallel Explore agents reviewed game-logic
|
|
@@ -84,7 +84,7 @@ PYTHONPATH=src mypy --strict src/
|
|
|
84
84
|
# Linting (0 violations expected)
|
|
85
85
|
ruff check src/ tests/
|
|
86
86
|
|
|
87
|
-
# Full test suite (
|
|
87
|
+
# Full test suite (1111 tests expected)
|
|
88
88
|
PYTHONPATH=src pytest
|
|
89
89
|
```
|
|
90
90
|
|
|
@@ -221,6 +221,10 @@ PYTHONPATH=src python scripts/benchmark.py
|
|
|
221
221
|
If you suspect a per-frame allocation regression, the canonical hot paths to
|
|
222
222
|
profile live (e.g. with `py-spy record --rate 100`) are `render()` in
|
|
223
223
|
`src/belote/ui/render.py` and `_calculate_legal_cards_impl` in
|
|
224
|
-
`src/belote/game.py`. The 4.9.4 audit
|
|
225
|
-
already landed in the 4.6.x / 4.7.x perf cleanups
|
|
226
|
-
|
|
224
|
+
`src/belote/game.py`. The 4.9.4 / 4.9.5 / 4.9.6 audit passes found only minor wins
|
|
225
|
+
beyond those already landed in the 4.6.x / 4.7.x perf cleanups (4.9.5 bumped
|
|
226
|
+
the `_pip_at` cache to 8192, trimmed `visible_len`'s cache to ANSI strings,
|
|
227
|
+
and memoised the BelAtro history overlay; 4.9.6 made `visible_len` measure
|
|
228
|
+
terminal *cells* via a minimal `char_width`, keeping the allocation-free
|
|
229
|
+
ASCII fast path so the hot path is unchanged) — start from a profiler trace,
|
|
230
|
+
not from guesswork, before adding new caches.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 4.9.
|
|
3
|
+
Version: 4.9.6
|
|
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
|
|
@@ -264,7 +264,7 @@ belote/
|
|
|
264
264
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
265
265
|
│ ├── stats.py # Global and session statistics tracking
|
|
266
266
|
│ └── rules.py # Game rules content
|
|
267
|
-
├── tests/ # Comprehensive test suite (
|
|
267
|
+
├── tests/ # Comprehensive test suite (1111 tests)
|
|
268
268
|
├── scripts/ # Performance benchmarks
|
|
269
269
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
270
270
|
├── LICENSE # MIT License
|
|
@@ -279,14 +279,14 @@ belote/
|
|
|
279
279
|
PYTHONPATH=src pytest
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
-
Currently **
|
|
282
|
+
Currently **1111 tests** passing with 100% coverage on game-logic modules (4.9.6).
|
|
283
283
|
|
|
284
284
|
## Technical Integrity
|
|
285
285
|
|
|
286
286
|
The codebase is strictly validated with the following tools:
|
|
287
287
|
- **mypy**: 0 errors (strict type safety)
|
|
288
288
|
- **ruff**: 0 violations (linting & formatting)
|
|
289
|
-
- **pytest**:
|
|
289
|
+
- **pytest**: 1111/1111 passed
|
|
290
290
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
291
291
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
292
292
|
|
|
@@ -221,7 +221,7 @@ belote/
|
|
|
221
221
|
│ ├── input.py # Platform-dispatched key reader and interruptible sleep
|
|
222
222
|
│ ├── stats.py # Global and session statistics tracking
|
|
223
223
|
│ └── rules.py # Game rules content
|
|
224
|
-
├── tests/ # Comprehensive test suite (
|
|
224
|
+
├── tests/ # Comprehensive test suite (1111 tests)
|
|
225
225
|
├── scripts/ # Performance benchmarks
|
|
226
226
|
├── pyproject.toml # Build system and dev dependencies (ruff/mypy)
|
|
227
227
|
├── LICENSE # MIT License
|
|
@@ -236,14 +236,14 @@ belote/
|
|
|
236
236
|
PYTHONPATH=src pytest
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
Currently **
|
|
239
|
+
Currently **1111 tests** passing with 100% coverage on game-logic modules (4.9.6).
|
|
240
240
|
|
|
241
241
|
## Technical Integrity
|
|
242
242
|
|
|
243
243
|
The codebase is strictly validated with the following tools:
|
|
244
244
|
- **mypy**: 0 errors (strict type safety)
|
|
245
245
|
- **ruff**: 0 violations (linting & formatting)
|
|
246
|
-
- **pytest**:
|
|
246
|
+
- **pytest**: 1111/1111 passed
|
|
247
247
|
- **Functional Architecture**: Purely immutable state transitions using `dataclasses.replace`
|
|
248
248
|
- **Performance**: High-efficiency rendering and sub-millisecond AI decision times (see `scripts/benchmark.py`)
|
|
249
249
|
|
|
@@ -74,6 +74,27 @@ class AIPlayer:
|
|
|
74
74
|
# All ranking helpers in this class read it via self._se.
|
|
75
75
|
self._se = False
|
|
76
76
|
|
|
77
|
+
def _reset_round_memory(self) -> None:
|
|
78
|
+
"""Clear all per-round derived memory back to a clean slate.
|
|
79
|
+
|
|
80
|
+
Shared by the new-round and mid-round-undo paths in ``update_memory`` so
|
|
81
|
+
the two can't drift: a partial reset (e.g. clearing the void cache but
|
|
82
|
+
not the partner-signal tally) would let ``_update_voids`` re-walk the
|
|
83
|
+
surviving tricks and double-count their signals.
|
|
84
|
+
"""
|
|
85
|
+
self.memory.played.clear()
|
|
86
|
+
for s in Seat:
|
|
87
|
+
self.memory.known_voids[s].clear()
|
|
88
|
+
self.memory.partner_hand.clear()
|
|
89
|
+
self.memory.processed_tricks_count = 0
|
|
90
|
+
self.memory.last_voids_key = None
|
|
91
|
+
self.memory.last_partner_hand_key = None
|
|
92
|
+
# 4.9.0 / G2: partner-signal tally and our emit-counter follow
|
|
93
|
+
# the same reset pattern as the void cache.
|
|
94
|
+
for suit in Suit:
|
|
95
|
+
self.memory.signals[suit] = 0
|
|
96
|
+
self.memory.signals_emitted = 0
|
|
97
|
+
|
|
77
98
|
def update_memory(self, state: GameState) -> None:
|
|
78
99
|
"""Update memory with currently visible information."""
|
|
79
100
|
completed_count = len(state.completed_tricks)
|
|
@@ -84,18 +105,7 @@ class AIPlayer:
|
|
|
84
105
|
# this a (0, 0) / (0, 1) key from the first decision of *this* round
|
|
85
106
|
# could coincidentally match a leftover from the previous round and
|
|
86
107
|
# cause _update_voids to skip processing entirely.
|
|
87
|
-
self.
|
|
88
|
-
for s in Seat:
|
|
89
|
-
self.memory.known_voids[s].clear()
|
|
90
|
-
self.memory.partner_hand.clear()
|
|
91
|
-
self.memory.processed_tricks_count = 0
|
|
92
|
-
self.memory.last_voids_key = None
|
|
93
|
-
self.memory.last_partner_hand_key = None
|
|
94
|
-
# 4.9.0 / G2: partner-signal tally and our emit-counter follow
|
|
95
|
-
# the same triple-reset pattern as the void cache.
|
|
96
|
-
for suit in Suit:
|
|
97
|
-
self.memory.signals[suit] = 0
|
|
98
|
-
self.memory.signals_emitted = 0
|
|
108
|
+
self._reset_round_memory()
|
|
99
109
|
elif (
|
|
100
110
|
self.memory.last_voids_key is not None
|
|
101
111
|
and (completed_count, current_count) < self.memory.last_voids_key
|
|
@@ -104,13 +114,12 @@ class AIPlayer:
|
|
|
104
114
|
# we've processed. `known_voids` and `processed_tricks_count`
|
|
105
115
|
# are monotonic and would carry stale inferences forward
|
|
106
116
|
# (a void inferred from a now-rolled-back trick). Rebuild from
|
|
107
|
-
# the current state instead of trying to subtract.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
self.
|
|
113
|
-
self.memory.last_partner_hand_key = None
|
|
117
|
+
# the current state instead of trying to subtract. The signal
|
|
118
|
+
# tally is rebuilt by _update_voids' re-walk (processed count is
|
|
119
|
+
# reset to 0), so it MUST be cleared here too — otherwise the
|
|
120
|
+
# surviving tricks' signals are double-counted. Same full reset
|
|
121
|
+
# as a new round.
|
|
122
|
+
self._reset_round_memory()
|
|
114
123
|
|
|
115
124
|
# Track all cards in completed tricks. N is small (max 32 cards), so
|
|
116
125
|
# the O(N) re-walk is fine and keeps this in sync with the transient
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
+
import unicodedata
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from functools import lru_cache
|
|
7
8
|
|
|
@@ -102,15 +103,52 @@ RESET = "\x1b[0m"
|
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
@lru_cache(maxsize=4096)
|
|
106
|
+
def char_width(ch: str) -> int:
|
|
107
|
+
"""Terminal display width of a single character, in cells.
|
|
108
|
+
|
|
109
|
+
4.9.6: minimal, dependency-free wcwidth using only the stdlib
|
|
110
|
+
``unicodedata``. Combining marks and zero-width codepoints occupy 0 cells;
|
|
111
|
+
East-Asian Wide/Fullwidth glyphs (CJK, many emoji) occupy 2; everything
|
|
112
|
+
else 1. Not a full Unicode-grapheme/emoji-ZWJ implementation — it is the
|
|
113
|
+
pragmatic subset the UI needs so ``visible_len`` reports *cells*, not
|
|
114
|
+
codepoints, and centering/padding/truncation stay aligned.
|
|
115
|
+
"""
|
|
116
|
+
# C0/C1 controls (other than handled escapes) have no width here.
|
|
117
|
+
if ch == "\x00":
|
|
118
|
+
return 0
|
|
119
|
+
if unicodedata.combining(ch) or unicodedata.category(ch) in ("Mn", "Me", "Cf"):
|
|
120
|
+
return 0
|
|
121
|
+
if unicodedata.east_asian_width(ch) in ("W", "F"):
|
|
122
|
+
return 2
|
|
123
|
+
return 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@lru_cache(maxsize=2048)
|
|
127
|
+
def _visible_len_ansi(s: str) -> int:
|
|
128
|
+
return sum(char_width(c) for c in _RESET_RE.sub("", s))
|
|
129
|
+
|
|
130
|
+
|
|
105
131
|
def visible_len(s: str) -> int:
|
|
106
|
-
"""Return
|
|
107
|
-
|
|
132
|
+
"""Return the terminal *cell* width of a string, ignoring ANSI escapes.
|
|
133
|
+
|
|
134
|
+
4.9.5 (P3): only the regex-stripping path is cached — plain (no-ESC)
|
|
135
|
+
strings skip the bounded cache so it stays reserved for ANSI strings that
|
|
136
|
+
benefit from memoising the regex sub (static glyphs, frames).
|
|
137
|
+
|
|
138
|
+
4.9.6: width is measured in display cells (see ``char_width``), not
|
|
139
|
+
codepoints. Pure-ASCII *escape-free* strings — the overwhelmingly common
|
|
140
|
+
case — take the allocation-free ``len(s)`` fast path since every such char
|
|
141
|
+
is exactly one cell. Anything with an ESC (note: ESC is itself ASCII, so it
|
|
142
|
+
must be excluded explicitly) or any non-ASCII char pays for the strip +
|
|
143
|
+
per-char width sum.
|
|
144
|
+
"""
|
|
145
|
+
if "\x1b" not in s and s.isascii():
|
|
108
146
|
return len(s)
|
|
109
|
-
return
|
|
147
|
+
return _visible_len_ansi(s)
|
|
110
148
|
|
|
111
149
|
|
|
112
150
|
def ansi_center(s: str, width: int) -> str:
|
|
113
|
-
"""Center a string by visible width, padding both sides."""
|
|
151
|
+
"""Center a string by visible cell width, padding both sides."""
|
|
114
152
|
vlen = visible_len(s)
|
|
115
153
|
pad = max(0, width - vlen)
|
|
116
154
|
left = pad // 2
|
|
@@ -119,7 +157,7 @@ def ansi_center(s: str, width: int) -> str:
|
|
|
119
157
|
|
|
120
158
|
|
|
121
159
|
def ansi_ljust(s: str, width: int) -> str:
|
|
122
|
-
"""Left-justify a string to `width` visible
|
|
160
|
+
"""Left-justify a string to `width` visible cells, ANSI-aware.
|
|
123
161
|
Never use str.ljust() on ANSI strings — it counts escape bytes as chars.
|
|
124
162
|
"""
|
|
125
163
|
vlen = visible_len(s)
|
|
@@ -127,6 +165,39 @@ def ansi_ljust(s: str, width: int) -> str:
|
|
|
127
165
|
return s + " " * pad
|
|
128
166
|
|
|
129
167
|
|
|
168
|
+
def ansi_truncate(s: str, width: int) -> str:
|
|
169
|
+
"""Truncate `s` to at most `width` visible cells, skipping ANSI escapes.
|
|
170
|
+
|
|
171
|
+
Cell-aware: a wide (2-cell) glyph that would straddle the boundary is
|
|
172
|
+
dropped rather than split. ASCII strings without escapes take a plain slice
|
|
173
|
+
fast path. Returns the original string untouched when it already fits.
|
|
174
|
+
"""
|
|
175
|
+
if s.isascii() and "\x1b" not in s:
|
|
176
|
+
return s if len(s) <= width else s[:width]
|
|
177
|
+
if visible_len(s) <= width:
|
|
178
|
+
return s
|
|
179
|
+
out: list[str] = []
|
|
180
|
+
used = 0
|
|
181
|
+
i = 0
|
|
182
|
+
n = len(s)
|
|
183
|
+
while i < n:
|
|
184
|
+
ch = s[i]
|
|
185
|
+
if ch == "\x1b":
|
|
186
|
+
# Copy the whole escape sequence verbatim (zero width).
|
|
187
|
+
m = _RESET_RE.match(s, i)
|
|
188
|
+
if m:
|
|
189
|
+
out.append(m.group())
|
|
190
|
+
i = m.end()
|
|
191
|
+
continue
|
|
192
|
+
w = char_width(ch)
|
|
193
|
+
if used + w > width:
|
|
194
|
+
break
|
|
195
|
+
out.append(ch)
|
|
196
|
+
used += w
|
|
197
|
+
i += 1
|
|
198
|
+
return "".join(out)
|
|
199
|
+
|
|
200
|
+
|
|
130
201
|
@lru_cache(maxsize=512)
|
|
131
202
|
def _fg_seq(r: int, g: int, b: int) -> str:
|
|
132
203
|
return f"\x1b[38;2;{r};{g};{b}m"
|
|
@@ -29,7 +29,10 @@ class LaSymbiose(Joker):
|
|
|
29
29
|
is_partner_joker = True
|
|
30
30
|
|
|
31
31
|
def on_declaration(self, event: DeclarationScoredEvent, state: dict[str, Any]) -> JokerResult | None:
|
|
32
|
-
|
|
32
|
+
# Gate on points so Le Mime (declarations_zero) — which emits
|
|
33
|
+
# declarations worth 0 — doesn't grant the multiplier for a
|
|
34
|
+
# declaration that scored nothing. Mirrors LeMathematicien's guard.
|
|
35
|
+
if event.seat == Seat.NORTH and event.points > 0:
|
|
33
36
|
return JokerResult(times_mult=1.2)
|
|
34
37
|
return None
|
|
35
38
|
|
|
@@ -326,6 +326,22 @@ class BelAtroGame:
|
|
|
326
326
|
for msg in self.unlock_tracker.drain_announcements():
|
|
327
327
|
BelAtroAnnounce.banner(msg, self.reader, hold=1.5)
|
|
328
328
|
|
|
329
|
+
def _reconcile_bonus_money(self, bonus_money: int) -> None:
|
|
330
|
+
"""Apply a round's signed joker money to the wallet.
|
|
331
|
+
|
|
332
|
+
``final_state._bonus_money`` accumulates ``JokerResult.add_money`` over
|
|
333
|
+
the round (positive = credit, negative = debit such as LePreteur's -$5
|
|
334
|
+
skim). Positive amounts credit; negative amounts route through
|
|
335
|
+
``spend_money`` (which no-ops if the wallet can't cover it) so the
|
|
336
|
+
Economy's non-negative guard on ``add_money`` is respected.
|
|
337
|
+
"""
|
|
338
|
+
if self.run is None:
|
|
339
|
+
return
|
|
340
|
+
if bonus_money > 0:
|
|
341
|
+
self.run.economy.add_money(bonus_money)
|
|
342
|
+
elif bonus_money < 0:
|
|
343
|
+
self.run.economy.spend_money(-bonus_money)
|
|
344
|
+
|
|
329
345
|
def _run_loop(self) -> None:
|
|
330
346
|
"""Main game loop: Blind -> Shop -> Next."""
|
|
331
347
|
if self.run is None:
|
|
@@ -617,6 +633,12 @@ class BelAtroGame:
|
|
|
617
633
|
color="red",
|
|
618
634
|
hold=2.5,
|
|
619
635
|
)
|
|
636
|
+
else:
|
|
637
|
+
# The run continues past this chute. Reconcile signed joker
|
|
638
|
+
# money (e.g. LePreteur's -$5 skim, which was baked into the
|
|
639
|
+
# round's ×Mult) so the player isn't handed a free multiplier
|
|
640
|
+
# for the surviving round. spend_money no-ops if broke.
|
|
641
|
+
self._reconcile_bonus_money(final_state._bonus_money)
|
|
620
642
|
if not lock_trust:
|
|
621
643
|
trust.blind_failed()
|
|
622
644
|
else:
|
|
@@ -624,16 +646,7 @@ class BelAtroGame:
|
|
|
624
646
|
payout = self.run.economy.process_round_end(total - self.run.target_score)
|
|
625
647
|
if auto_coinche_active:
|
|
626
648
|
self.run.economy.add_money(payout * 2) # L'Avocat: triple total payout
|
|
627
|
-
|
|
628
|
-
# Pre-4.6.5 this branch was `> 0`, which silently dropped LePreteur's
|
|
629
|
-
# `-5` cost (and any other negative-add_money joker) while still
|
|
630
|
-
# applying the multiplier — free ×1.2 Mult on every $50+ round.
|
|
631
|
-
if final_state._bonus_money > 0:
|
|
632
|
-
self.run.economy.add_money(final_state._bonus_money)
|
|
633
|
-
elif final_state._bonus_money < 0:
|
|
634
|
-
# Route through spend_money so the Economy negative guard fires
|
|
635
|
-
# if accumulated debit somehow exceeds wallet (caller's bug).
|
|
636
|
-
self.run.economy.spend_money(-final_state._bonus_money)
|
|
649
|
+
self._reconcile_bonus_money(final_state._bonus_money)
|
|
637
650
|
if final_state._joker_state.get("puriste_triggered"):
|
|
638
651
|
extra = max(0, (total - self.run.target_score) // 10)
|
|
639
652
|
self.run.economy.add_money(extra) # Le Puriste: double base payout
|
|
@@ -73,15 +73,19 @@ class TrustTrack:
|
|
|
73
73
|
def tier(self) -> int:
|
|
74
74
|
"""Five-tier bucketing for partner-joker effect scaling.
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
Scaling is implemented as N *extra* applications of the joker's
|
|
77
|
+
JokerResult on top of the baseline one (see
|
|
78
|
+
``ScoreAccumulator._fire_jokers``; ``tier_extras = (0, 0, 1, 1, 2)``):
|
|
79
|
+
|
|
80
|
+
0 (degraded, value 0–2) — baseline only (no bonus, no penalty)
|
|
81
|
+
1 (base, value 3–4) — baseline only
|
|
82
|
+
2 (boost, value 5–6) — +1 apply (≈ ×2 effect)
|
|
83
|
+
3 (strong, value 7–8) — +1 apply (≈ ×2 effect)
|
|
84
|
+
4 (elite, value 9–10) — +2 applies (≈ ×3 effect)
|
|
81
85
|
|
|
82
86
|
Used by partner_jokers/* to scale their JokerResult payloads. The
|
|
83
87
|
legacy `partner_jokers_double` flag (value≥7) still exists for backward
|
|
84
|
-
compatibility but
|
|
88
|
+
compatibility but tier is the new path.
|
|
85
89
|
"""
|
|
86
90
|
if self.value <= 2:
|
|
87
91
|
return 0
|