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.
Files changed (26) hide show
  1. {belote_cli-0.9.2 → belote_cli-0.9.4}/CHANGELOG.md +19 -0
  2. belote_cli-0.9.4/GRIMAUD_French-Standard-Playing-Cards_Supreme-Quality-Lithograph_Tarot-Nouveau-1898_Lord-Henfield.png +0 -0
  3. {belote_cli-0.9.2 → belote_cli-0.9.4}/PKG-INFO +11 -4
  4. {belote_cli-0.9.2 → belote_cli-0.9.4}/README.md +9 -2
  5. {belote_cli-0.9.2 → belote_cli-0.9.4}/pyproject.toml +2 -2
  6. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/ansi.py +13 -13
  7. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/game.py +41 -16
  8. belote_cli-0.9.4/src/belote/main.py +425 -0
  9. belote_cli-0.9.4/src/belote/replay.py +31 -0
  10. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/rules.py +41 -21
  11. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/scoring.py +39 -5
  12. belote_cli-0.9.4/src/belote/stats.py +64 -0
  13. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/ui.py +370 -182
  14. {belote_cli-0.9.2 → belote_cli-0.9.4}/tests/test_belote.py +10 -0
  15. belote_cli-0.9.4/tests/test_extended.py +71 -0
  16. belote_cli-0.9.2/src/belote/main.py +0 -352
  17. {belote_cli-0.9.2 → belote_cli-0.9.4}/.gitignore +0 -0
  18. {belote_cli-0.9.2 → belote_cli-0.9.4}/DEVELOPMENT.md +0 -0
  19. {belote_cli-0.9.2 → belote_cli-0.9.4}/LICENSE +0 -0
  20. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/__init__.py +0 -0
  21. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/__init__.py +0 -0
  22. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/ai.py +0 -0
  23. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/bidding.py +0 -0
  24. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/deck.py +0 -0
  25. {belote_cli-0.9.2 → belote_cli-0.9.4}/src/belote/input.py +0 -0
  26. {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
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: belote-cli
3
- Version: 0.9.2
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: Benjamin <benjamin@example.com>
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.2"
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 = "Benjamin", email = "benjamin@example.com" },
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(20, 90, 50)
93
+ return bg(25, 75, 45) # Deeper, more muted green
94
94
 
95
95
 
96
96
  def red_fg() -> str:
97
- return fg(220, 60, 60)
97
+ return fg(190, 45, 45) # Muted crimson
98
98
 
99
99
 
100
100
  def black_fg() -> str:
101
- return fg(20, 20, 20)
101
+ return fg(40, 40, 40) # Slightly softer black
102
102
 
103
103
 
104
104
  def card_face_bg() -> str:
105
- return bg(245, 245, 235)
105
+ return bg(248, 245, 230) # Richer cream/parchment
106
106
 
107
107
 
108
108
  def face_card_bg() -> str:
109
- return bg(255, 245, 180)
109
+ return bg(250, 240, 200) # Golden-aged parchment
110
110
 
111
111
 
112
112
  def card_back_bg() -> str:
113
- return bg(120, 30, 30)
113
+ return bg(110, 35, 35) # Deep burgundy
114
114
 
115
115
 
116
116
  def highlight_bg() -> str:
117
- return bg(240, 200, 80)
117
+ return bg(230, 190, 70) # Brass/Gold highlight
118
118
 
119
119
 
120
120
  def gold_fg() -> str:
121
- return fg(240, 200, 80)
121
+ return fg(210, 170, 60) # Antique gold
122
122
 
123
123
 
124
124
  def white_fg() -> str:
125
- return fg(240, 240, 240)
125
+ return fg(235, 235, 230) # Off-white
126
126
 
127
127
 
128
128
  def light_gray_fg() -> str:
129
- return fg(180, 180, 180)
129
+ return fg(160, 160, 155) # Muted stone gray
130
130
 
131
131
 
132
132
  def green_fg() -> str:
133
- return fg(80, 220, 120)
133
+ return fg(60, 160, 90) # Sage green
134
134
 
135
135
 
136
136
  def banner_bg() -> str:
137
- return bg(60, 60, 180)
137
+ return bg(50, 65, 120) # Muted royal blue
138
138
 
139
139
 
140
140
  def banner_fg() -> str:
141
- return fg(255, 255, 100)
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