belote-cli 0.9.2__tar.gz → 0.9.4__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-0.9.2 → belote_cli-0.9.4}/CHANGELOG.md +19 -0
- belote_cli-0.9.4/GRIMAUD_French-Standard-Playing-Cards_Supreme-Quality-Lithograph_Tarot-Nouveau-1898_Lord-Henfield.png +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/PKG-INFO +11 -4
- {belote_cli-0.9.2 → belote_cli-0.9.4}/README.md +9 -2
- {belote_cli-0.9.2 → belote_cli-0.9.4}/pyproject.toml +2 -2
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/ansi.py +13 -13
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/game.py +41 -16
- belote_cli-0.9.4/src/belote/main.py +425 -0
- belote_cli-0.9.4/src/belote/replay.py +31 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/rules.py +41 -21
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/scoring.py +39 -5
- belote_cli-0.9.4/src/belote/stats.py +64 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/ui.py +370 -182
- {belote_cli-0.9.2 → belote_cli-0.9.4}/tests/test_belote.py +10 -0
- belote_cli-0.9.4/tests/test_extended.py +71 -0
- belote_cli-0.9.2/src/belote/main.py +0 -352
- {belote_cli-0.9.2 → belote_cli-0.9.4}/.gitignore +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/DEVELOPMENT.md +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/LICENSE +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/__init__.py +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/__init__.py +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/ai.py +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/bidding.py +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/deck.py +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/input.py +0 -0
- {belote_cli-0.9.2 → belote_cli-0.9.4}/tests/__init__.py +0 -0
|
@@ -5,6 +5,25 @@ 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
|
+
## [0.9.4] - 2026-04-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Detailed Round Score History pop-up (accessible via 'H').
|
|
12
|
+
- Global Statistics screen in main menu.
|
|
13
|
+
- Hotseat (2P) Mode for local multiplayer.
|
|
14
|
+
- Undo Support (accessible via 'Z' during play).
|
|
15
|
+
- Terminal Sound Effects for key events.
|
|
16
|
+
- Seat-specific AI Difficulty configuration.
|
|
17
|
+
- Support for skipping animations (Space/Esc).
|
|
18
|
+
|
|
19
|
+
### Improved
|
|
20
|
+
- Significant performance optimization for real-time HUD scoring.
|
|
21
|
+
- UI rendering speed and smooth ASCII art animations.
|
|
22
|
+
- O(1) rank lookup performance.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Indentation and template errors in `main.py` and `ui.py`.
|
|
26
|
+
|
|
8
27
|
## [0.9.2] - 2026-04-26
|
|
9
28
|
|
|
10
29
|
### Fixed
|
|
Binary file
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: belote-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
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
|
|
7
7
|
Project-URL: Issues, https://github.com/ElysiumDisc/belote/issues
|
|
8
|
-
Author-email:
|
|
8
|
+
Author-email: elysium <discelysioum@delicious.com>
|
|
9
9
|
License: MIT License
|
|
10
10
|
|
|
11
11
|
Copyright (c) 2026 Benjamin
|
|
@@ -111,9 +111,10 @@ pip install belote-cli
|
|
|
111
111
|
belote
|
|
112
112
|
|
|
113
113
|
# Custom settings
|
|
114
|
-
belote --difficulty hard --target 500
|
|
114
|
+
belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
|
|
117
118
|
## Controls
|
|
118
119
|
|
|
119
120
|
**Main Menu:**
|
|
@@ -130,12 +131,18 @@ belote --difficulty hard --target 500
|
|
|
130
131
|
## Features
|
|
131
132
|
|
|
132
133
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics, face card art (Roi ♔, Dame ♕, Valet ⚔), and distinct color palettes.
|
|
133
|
-
- **Main Menu:** Configure Difficulty, Target Score, and Game Speed, and access **Rules & History** (EN/FR) without restarting the app.
|
|
134
|
+
- **Main Menu:** Configure Mode, Difficulty, Target Score, and Game Speed, and access **Rules & History** (EN/FR) without restarting the app.
|
|
135
|
+
- **Undo/Redo:** Press `Z` to undo your last move during bidding or play.
|
|
136
|
+
- **Statistics:** Global tracking of games played/won, win rate, capots, and streaks.
|
|
137
|
+
- **Hotseat Mode:** Play with a friend locally (Human vs Human vs 2 AI).
|
|
138
|
+
- **Sound Effects:** Auditory feedback for trick wins, Belote, Chute, and Capot.
|
|
139
|
+
- **Animation Skip:** Press `Space` or `Esc` to skip AI delays and scoring animations.
|
|
134
140
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
135
141
|
- **Trick History:** Visual "Last Trick" panel helps track the flow of the game.
|
|
136
142
|
- **High Fidelity:** Full implementation of French Belote rules including a two-round bidding system, "Dix de Der", and "Capot" (250 pts) announcements.
|
|
137
143
|
- **Rules & History Viewer:** A scrollable, bilingual (English/French) in-game reference for the game's heritage and mechanics.
|
|
138
144
|
- **Robust Input:** High-performance unbuffered key reading for responsive navigation and 'q' to quit functionality.
|
|
145
|
+
- **Replayability:** Use `--seed <number>` to reproduce the exact same deck and AI choices.
|
|
139
146
|
|
|
140
147
|
## AI
|
|
141
148
|
|
|
@@ -72,9 +72,10 @@ pip install belote-cli
|
|
|
72
72
|
belote
|
|
73
73
|
|
|
74
74
|
# Custom settings
|
|
75
|
-
belote --difficulty hard --target 500
|
|
75
|
+
belote --difficulty hard --target 500 --seed 123 --speed fast
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
|
|
78
79
|
## Controls
|
|
79
80
|
|
|
80
81
|
**Main Menu:**
|
|
@@ -91,12 +92,18 @@ belote --difficulty hard --target 500
|
|
|
91
92
|
## Features
|
|
92
93
|
|
|
93
94
|
- **Rich Terminal UI:** Full-screen green felt table with detailed card graphics, face card art (Roi ♔, Dame ♕, Valet ⚔), and distinct color palettes.
|
|
94
|
-
- **Main Menu:** Configure Difficulty, Target Score, and Game Speed, and access **Rules & History** (EN/FR) without restarting the app.
|
|
95
|
+
- **Main Menu:** Configure Mode, Difficulty, Target Score, and Game Speed, and access **Rules & History** (EN/FR) without restarting the app.
|
|
96
|
+
- **Undo/Redo:** Press `Z` to undo your last move during bidding or play.
|
|
97
|
+
- **Statistics:** Global tracking of games played/won, win rate, capots, and streaks.
|
|
98
|
+
- **Hotseat Mode:** Play with a friend locally (Human vs Human vs 2 AI).
|
|
99
|
+
- **Sound Effects:** Auditory feedback for trick wins, Belote, Chute, and Capot.
|
|
100
|
+
- **Animation Skip:** Press `Space` or `Esc` to skip AI delays and scoring animations.
|
|
95
101
|
- **Live HUD:** Real-time round scoring displays points won during the current round, with a smooth "rolling" numerical animation for total scores.
|
|
96
102
|
- **Trick History:** Visual "Last Trick" panel helps track the flow of the game.
|
|
97
103
|
- **High Fidelity:** Full implementation of French Belote rules including a two-round bidding system, "Dix de Der", and "Capot" (250 pts) announcements.
|
|
98
104
|
- **Rules & History Viewer:** A scrollable, bilingual (English/French) in-game reference for the game's heritage and mechanics.
|
|
99
105
|
- **Robust Input:** High-performance unbuffered key reading for responsive navigation and 'q' to quit functionality.
|
|
106
|
+
- **Replayability:** Use `--seed <number>` to reproduce the exact same deck and AI choices.
|
|
100
107
|
|
|
101
108
|
## AI
|
|
102
109
|
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "belote-cli"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.4"
|
|
8
8
|
description = "A 4-player terminal card game"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = {file = "LICENSE"}
|
|
12
12
|
authors = [
|
|
13
|
-
{ name = "
|
|
13
|
+
{ name = "elysium", email = "discelysioum@delicious.com" },
|
|
14
14
|
]
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
@@ -90,52 +90,52 @@ def scroll_region(top: int, bottom: int) -> str:
|
|
|
90
90
|
|
|
91
91
|
# Palette constants
|
|
92
92
|
def felt_bg() -> str:
|
|
93
|
-
return bg(
|
|
93
|
+
return bg(25, 75, 45) # Deeper, more muted green
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def red_fg() -> str:
|
|
97
|
-
return fg(
|
|
97
|
+
return fg(190, 45, 45) # Muted crimson
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def black_fg() -> str:
|
|
101
|
-
return fg(
|
|
101
|
+
return fg(40, 40, 40) # Slightly softer black
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def card_face_bg() -> str:
|
|
105
|
-
return bg(
|
|
105
|
+
return bg(248, 245, 230) # Richer cream/parchment
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
def face_card_bg() -> str:
|
|
109
|
-
return bg(
|
|
109
|
+
return bg(250, 240, 200) # Golden-aged parchment
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
def card_back_bg() -> str:
|
|
113
|
-
return bg(
|
|
113
|
+
return bg(110, 35, 35) # Deep burgundy
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
def highlight_bg() -> str:
|
|
117
|
-
return bg(
|
|
117
|
+
return bg(230, 190, 70) # Brass/Gold highlight
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
def gold_fg() -> str:
|
|
121
|
-
return fg(
|
|
121
|
+
return fg(210, 170, 60) # Antique gold
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
def white_fg() -> str:
|
|
125
|
-
return fg(
|
|
125
|
+
return fg(235, 235, 230) # Off-white
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def light_gray_fg() -> str:
|
|
129
|
-
return fg(
|
|
129
|
+
return fg(160, 160, 155) # Muted stone gray
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def green_fg() -> str:
|
|
133
|
-
return fg(
|
|
133
|
+
return fg(60, 160, 90) # Sage green
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
def banner_bg() -> str:
|
|
137
|
-
return bg(
|
|
137
|
+
return bg(50, 65, 120) # Muted royal blue
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
def banner_fg() -> str:
|
|
141
|
-
return fg(
|
|
141
|
+
return fg(240, 220, 150) # Pale gold text
|
|
@@ -5,7 +5,7 @@ from dataclasses import dataclass, replace
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import Final
|
|
7
7
|
|
|
8
|
-
from .deck import Card, Rank, Suit, trick_rank
|
|
8
|
+
from .deck import Card, Rank, Suit, trick_rank, make_deck, shuffle as shuffle_deck_, deal as deal_cards_, card_points
|
|
9
9
|
|
|
10
10
|
# ---------------------------------------------------------------------------
|
|
11
11
|
# Enums
|
|
@@ -110,6 +110,21 @@ class TrickCard:
|
|
|
110
110
|
card: Card
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class RoundScore:
|
|
115
|
+
taker_team: int
|
|
116
|
+
ns_card_pts: int
|
|
117
|
+
ew_card_pts: int
|
|
118
|
+
ns_decl_pts: int
|
|
119
|
+
ew_decl_pts: int
|
|
120
|
+
ns_belote_pts: int
|
|
121
|
+
ew_belote_pts: int
|
|
122
|
+
ns_total: int
|
|
123
|
+
ew_total: int
|
|
124
|
+
is_failed: bool
|
|
125
|
+
is_capot: bool
|
|
126
|
+
|
|
127
|
+
|
|
113
128
|
@dataclass(frozen=True, slots=True)
|
|
114
129
|
class GameState:
|
|
115
130
|
hands: tuple[tuple[Card, ...], ...] # indexed by Seat.value
|
|
@@ -127,6 +142,8 @@ class GameState:
|
|
|
127
142
|
declarations_resolved: bool
|
|
128
143
|
team_scores: tuple[int, int] # (NS, EW)
|
|
129
144
|
round_scores: tuple[int, int] # this round declaration points (NS, EW)
|
|
145
|
+
current_round_points: tuple[int, int] # this round card points (NS, EW)
|
|
146
|
+
score_history: tuple[RoundScore, ...]
|
|
130
147
|
target: int
|
|
131
148
|
up_card: Card | None # The card turned up during bidding phase
|
|
132
149
|
remaining_cards: tuple[Card, ...] # The 11 cards to be dealt after bidding
|
|
@@ -170,6 +187,8 @@ def new_game(target: int = 1000) -> GameState:
|
|
|
170
187
|
declarations_resolved=False,
|
|
171
188
|
team_scores=(0, 0),
|
|
172
189
|
round_scores=(0, 0),
|
|
190
|
+
current_round_points=(0, 0),
|
|
191
|
+
score_history=(),
|
|
173
192
|
target=target,
|
|
174
193
|
up_card=None,
|
|
175
194
|
remaining_cards=(),
|
|
@@ -204,6 +223,7 @@ def start_round(state: GameState, rng: random.Random) -> GameState:
|
|
|
204
223
|
declarations=(),
|
|
205
224
|
declarations_resolved=False,
|
|
206
225
|
round_scores=(0, 0),
|
|
226
|
+
current_round_points=(0, 0),
|
|
207
227
|
up_card=up_card,
|
|
208
228
|
remaining_cards=remaining,
|
|
209
229
|
bidder_index=0,
|
|
@@ -216,26 +236,13 @@ def start_round(state: GameState, rng: random.Random) -> GameState:
|
|
|
216
236
|
|
|
217
237
|
|
|
218
238
|
def shuffle_deck(rng: random.Random) -> tuple[Card, ...]:
|
|
219
|
-
from .deck import make_deck, shuffle as shuffle_deck_
|
|
220
239
|
return shuffle_deck_(make_deck(), rng)
|
|
221
240
|
|
|
222
241
|
|
|
223
|
-
def deal_cards(deck: tuple[Card, ...]) -> tuple[tuple[Card, ...], ...]:
|
|
224
|
-
from .deck import deal as deal_cards_
|
|
242
|
+
def deal_cards(deck: tuple[Card, ...]) -> tuple[tuple[tuple[Card, ...], ...], Card, tuple[Card, ...]]:
|
|
225
243
|
return deal_cards_(deck)
|
|
226
244
|
|
|
227
245
|
|
|
228
|
-
def bidding_order(dealer: Seat) -> tuple[Seat, ...]:
|
|
229
|
-
"""Return seats in bidding order (counter-clockwise from left of dealer)."""
|
|
230
|
-
start = dealer.next_seat()
|
|
231
|
-
return tuple(start.next_seat().next_seat().next_seat() if i == 0 else
|
|
232
|
-
(start if i == 0 else
|
|
233
|
-
(start.next_seat() if i == 1 else
|
|
234
|
-
(start.next_seat().next_seat() if i == 2 else
|
|
235
|
-
start.next_seat().next_seat().next_seat())))
|
|
236
|
-
for i in range(4))
|
|
237
|
-
|
|
238
|
-
|
|
239
246
|
def get_bidder(dealer: Seat, index: int) -> Seat:
|
|
240
247
|
"""Get the seat of the bidder at the given index."""
|
|
241
248
|
start = dealer.next_seat()
|
|
@@ -481,10 +488,26 @@ def play_card(state: GameState, card: Card) -> GameState:
|
|
|
481
488
|
new_completed = state.completed_tricks + (new_trick,)
|
|
482
489
|
tricks_count = len(new_completed)
|
|
483
490
|
|
|
491
|
+
# Update current round points
|
|
492
|
+
trick_pts = sum(card_points(tc.card, state.trump) for tc in new_trick) # type: ignore[arg-type]
|
|
493
|
+
ns_pts, ew_pts = state.current_round_points
|
|
494
|
+
if team_of(winner) == 0:
|
|
495
|
+
ns_pts += trick_pts
|
|
496
|
+
else:
|
|
497
|
+
ew_pts += trick_pts
|
|
498
|
+
|
|
499
|
+
# Last trick bonus
|
|
500
|
+
if tricks_count == 8:
|
|
501
|
+
if team_of(winner) == 0:
|
|
502
|
+
ns_pts += 10
|
|
503
|
+
else:
|
|
504
|
+
ew_pts += 10
|
|
505
|
+
|
|
506
|
+
new_round_points = (ns_pts, ew_pts)
|
|
507
|
+
|
|
484
508
|
# Check if round is complete (8 tricks)
|
|
485
509
|
if tricks_count >= 8:
|
|
486
510
|
# Round over
|
|
487
|
-
ns_round, ew_round = state.round_scores
|
|
488
511
|
return replace(
|
|
489
512
|
state,
|
|
490
513
|
hands=tuple(new_hands),
|
|
@@ -497,6 +520,7 @@ def play_card(state: GameState, card: Card) -> GameState:
|
|
|
497
520
|
announced=announced,
|
|
498
521
|
belote_tracker=tuple(belote_tracker),
|
|
499
522
|
first_trick_done=True,
|
|
523
|
+
current_round_points=new_round_points,
|
|
500
524
|
)
|
|
501
525
|
else:
|
|
502
526
|
# Next trick led by winner
|
|
@@ -513,6 +537,7 @@ def play_card(state: GameState, card: Card) -> GameState:
|
|
|
513
537
|
announced=announced,
|
|
514
538
|
belote_tracker=tuple(belote_tracker),
|
|
515
539
|
first_trick_done=first_trick_done,
|
|
540
|
+
current_round_points=new_round_points,
|
|
516
541
|
)
|
|
517
542
|
else:
|
|
518
543
|
# Next player in trick
|