belote-cli 2.5.2__tar.gz → 2.5.5__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 (93) hide show
  1. {belote_cli-2.5.2 → belote_cli-2.5.5}/CHANGELOG.md +21 -0
  2. {belote_cli-2.5.2 → belote_cli-2.5.5}/DEVELOPMENT.md +1 -0
  3. {belote_cli-2.5.2 → belote_cli-2.5.5}/PKG-INFO +1 -1
  4. {belote_cli-2.5.2 → belote_cli-2.5.5}/pyproject.toml +1 -1
  5. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/__init__.py +1 -1
  6. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/run_state.py +2 -0
  7. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/round_driver.py +16 -12
  8. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/trick_timing.py +2 -6
  9. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/main.py +1 -1
  10. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/game.py +1 -3
  11. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/input.py +32 -5
  12. {belote_cli-2.5.2 → belote_cli-2.5.5}/.claude/settings.local.json +0 -0
  13. {belote_cli-2.5.2 → belote_cli-2.5.5}/.gitignore +0 -0
  14. {belote_cli-2.5.2 → belote_cli-2.5.5}/GRIMAUD Standard Playing-Cards-1898.png +0 -0
  15. {belote_cli-2.5.2 → belote_cli-2.5.5}/LICENSE +0 -0
  16. {belote_cli-2.5.2 → belote_cli-2.5.5}/README.md +0 -0
  17. {belote_cli-2.5.2 → belote_cli-2.5.5}/scripts/benchmark.py +0 -0
  18. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/__init__.py +0 -0
  19. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ai.py +0 -0
  20. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ansi.py +0 -0
  21. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/__init__.py +0 -0
  22. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/__init__.py +0 -0
  23. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/economy.py +0 -0
  24. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/core/scoring.py +0 -0
  25. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/__init__.py +0 -0
  26. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/event_bus.py +0 -0
  27. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/engine/modifier_patch.py +0 -0
  28. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/__init__.py +0 -0
  29. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/base.py +0 -0
  30. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/__init__.py +0 -0
  31. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/contract.py +0 -0
  32. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/corrupted.py +0 -0
  33. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/economy.py +0 -0
  34. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/jokers/hand_comp.py +0 -0
  35. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/__init__.py +0 -0
  36. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/passive.py +0 -0
  37. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/risky.py +0 -0
  38. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/partner_jokers/shaper.py +0 -0
  39. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/planets.py +0 -0
  40. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/registry.py +0 -0
  41. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/tarots.py +0 -0
  42. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/items/vouchers.py +0 -0
  43. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/__init__.py +0 -0
  44. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/partner_state.py +0 -0
  45. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/personality.py +0 -0
  46. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/partner/trust.py +0 -0
  47. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/progression/__init__.py +0 -0
  48. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/progression/save.py +0 -0
  49. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/progression/unlocks.py +0 -0
  50. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/__init__.py +0 -0
  51. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/ante.py +0 -0
  52. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/boss.py +0 -0
  53. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/decks.py +0 -0
  54. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/run/shop.py +0 -0
  55. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/__init__.py +0 -0
  56. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/announce.py +0 -0
  57. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/collection.py +0 -0
  58. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/hud.py +0 -0
  59. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/menu.py +0 -0
  60. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/rules.py +0 -0
  61. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/shop.py +0 -0
  62. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/belatro/ui/trust_bar.py +0 -0
  63. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/config.py +0 -0
  64. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/context.py +0 -0
  65. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/deck.py +0 -0
  66. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/gameflow.py +0 -0
  67. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/main.py +0 -0
  68. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/rules.py +0 -0
  69. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/scoring.py +0 -0
  70. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/stats.py +0 -0
  71. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/themes.py +0 -0
  72. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/__init__.py +0 -0
  73. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/announce.py +0 -0
  74. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/menu.py +0 -0
  75. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/prompts.py +0 -0
  76. {belote_cli-2.5.2 → belote_cli-2.5.5}/src/belote/ui/render.py +0 -0
  77. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/__init__.py +0 -0
  78. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/__init__.py +0 -0
  79. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_belatro.py +0 -0
  80. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_boss_modifiers_integration.py +0 -0
  81. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_collection_logic.py +0 -0
  82. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_deck_variants.py +0 -0
  83. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_partner_trust.py +0 -0
  84. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_progression.py +0 -0
  85. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/belatro/test_round_driver.py +0 -0
  86. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_ai.py +0 -0
  87. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_belote.py +0 -0
  88. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_extended.py +0 -0
  89. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_game_logic.py +0 -0
  90. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_gameflow.py +0 -0
  91. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_new_coverage.py +0 -0
  92. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_official_rules.py +0 -0
  93. {belote_cli-2.5.2 → belote_cli-2.5.5}/tests/test_properties.py +0 -0
@@ -5,6 +5,27 @@ 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.5.5] - 2026-05-03
9
+
10
+ ### Fixed
11
+ - **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.
12
+ - **Event Bus Integrity**: Fixed `TrickWonEvent` instantiation error by providing missing `trick_number` and `trump` context.
13
+ - **Run State Consistency**: Added missing `consumable_slots` to `BelAtroRun` to prevent crashes when applying certain Vouchers (e.g., Le Couteau).
14
+ - **Cache Clear Bug**: Fixed an `AttributeError` when clearing the legal cards cache by ensuring the implementation function is properly decorated with `@lru_cache`.
15
+ - **Input & HUD**: Fixed the `[I]` key mapping for the score overlay HUD and synchronized the Windows `KeyReader` to support all game-specific keys.
16
+
17
+ ## [2.5.4] - 2026-05-03
18
+
19
+ ### Fixed
20
+ - **Missing read_timeout**: Fixed an `AttributeError` in the main menu by implementing the `read_timeout` method in the `KeyReader` class.
21
+ - **IndentationError in trick_timing.py**: Fixed a critical syntax error in the BelAtro items module.
22
+ - **LePremierSang Logic**: Fixed `LePremierSang` joker to correctly apply +2 Mult (additive) and track its active state.
23
+
24
+ ## [2.5.3] - 2026-05-03
25
+
26
+ ### Fixed
27
+ - **IndentationError in game.py**: Fixed a critical syntax error that prevented the game from starting.
28
+
8
29
  ## [2.5.2] - 2026-05-02
9
30
 
10
31
  ### 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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: belote-cli
3
- Version: 2.5.2
3
+ Version: 2.5.5
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "belote-cli"
7
- version = "2.5.2"
7
+ version = "2.5.5"
8
8
  description = "A 4-player terminal card game"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
- __version__ = "2.4.1"
1
+ __version__ = "2.5.5"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -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
@@ -29,6 +30,7 @@ class BelAtroRun:
29
30
  jokers: list[Joker] = field(default_factory=list)
30
31
  vouchers: list[Voucher] = field(default_factory=list)
31
32
  joker_slots: int = MAX_JOKER_SLOTS
33
+ consumable_slots: int = DEFAULT_CONSUMABLE_SLOTS
32
34
 
33
35
  # ── Economy ────────────────────────────────────────────
34
36
  economy: Economy = field(default_factory=Economy)
@@ -40,9 +40,18 @@ if TYPE_CHECKING:
40
40
  class RoundUICallbacks(ABC):
41
41
  """Interface for UI interaction during a round."""
42
42
 
43
+ @abstractmethod
44
+ def prompt_bid(self, state: GameState) -> Suit | None: ...
45
+
46
+ @abstractmethod
47
+ def prompt_card(self, state: GameState) -> tuple[Card, GameState]: ...
48
+
43
49
  @abstractmethod
44
50
  def on_card_played(self, state: GameState, seat: Seat, card: Card) -> None: ...
45
51
 
52
+ @abstractmethod
53
+ def on_trick_end(self, state: GameState, winner: Seat, points: int) -> None: ...
54
+
46
55
  @abstractmethod
47
56
  def on_round_end(self, breakdown: object) -> None: ...
48
57
 
@@ -103,12 +112,7 @@ def drive_round(
103
112
  bid: Suit | None = None
104
113
 
105
114
  if bidder == Seat.SOUTH:
106
- # We don't have a direct prompt here, we assume the caller
107
- # (main.py) handled South's choice or we default to Pass
108
- # Actually in drive_round we might need a callback for human input
109
- # but BelAtro drive_round is usually automated for testing or
110
- # the UI loop handles it. For now, we'll assume South passes if not handled.
111
- pass
115
+ bid = ui_callbacks.prompt_bid(state)
112
116
  elif (
113
117
  bidder == Seat.NORTH
114
118
  and partner is not None
@@ -159,9 +163,7 @@ def drive_round(
159
163
  card: Card | None = None
160
164
 
161
165
  if player == Seat.SOUTH:
162
- # Caller must provide card via some mechanism?
163
- # In a truly automated drive_round, we might need a callback.
164
- break # Wait for human in main loop
166
+ card, state = ui_callbacks.prompt_card(state)
165
167
  else:
166
168
  # AI Play
167
169
  card = ai_players[player].decide_card(state)
@@ -172,7 +174,7 @@ def drive_round(
172
174
  state = play_card(state, card)
173
175
  ui_callbacks.on_card_played(state, player, card)
174
176
 
175
- if is_last_in_trick:
177
+ if is_last_in_trick(state):
176
178
  last_trick = state.completed_tricks[-1]
177
179
 
178
180
  winner = trick_winner_seat(
@@ -203,11 +205,14 @@ def drive_round(
203
205
  TrickWonEvent(
204
206
  winner=winner,
205
207
  cards=cards,
206
- card_points=points,
208
+ trick_number=len(state.completed_tricks),
207
209
  is_last=is_last,
210
+ card_points=points,
211
+ trump=state.trump,
208
212
  ),
209
213
  state
210
214
  )
215
+ ui_callbacks.on_trick_end(state, winner, points)
211
216
 
212
217
  # Round End / Scoring
213
218
  if state.phase == Phase.SCORING:
@@ -228,7 +233,6 @@ def drive_round(
228
233
  return state
229
234
 
230
235
 
231
- @property
232
236
  def is_last_in_trick(state: GameState) -> bool:
233
237
  """Helper to check if a trick just ended."""
234
238
  return len(state.current_trick) == 0 and len(state.completed_tricks) > 0
@@ -20,7 +20,8 @@ class LePremierSang(Joker):
20
20
 
21
21
  def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
22
22
  if event.trick_number == 1 and event.winner == Seat.SOUTH:
23
- return JokerResult(times_mult=2.0)
23
+ state[f"{self.id}_active"] = True
24
+ return JokerResult(add_mult=2.0)
24
25
  return None
25
26
 
26
27
 
@@ -71,9 +72,4 @@ class LExecuteur(Joker):
71
72
  if event.is_last and event.winner == Seat.SOUTH:
72
73
  return JokerResult(add_chips=40, times_mult=1.5)
73
74
  return None
74
- s_unlockable = True
75
75
 
76
- def on_trick_won(self, event: TrickWonEvent, state: dict[str, Any]) -> JokerResult | None:
77
- if event.is_last and event.winner == Seat.SOUTH:
78
- return JokerResult(add_chips=40, times_mult=1.5)
79
- return None
@@ -196,7 +196,7 @@ class BelAtroGame:
196
196
  bus=bus,
197
197
  partner=self.run.partner,
198
198
  boss=boss,
199
- deck_id=self.run.deck_id,
199
+ target_score=self.run.target_score,
200
200
  ui_callbacks=UICallbacks(self.reader),
201
201
  acc=acc,
202
202
  )
@@ -449,6 +449,7 @@ def clear_legal_cards_cache() -> None:
449
449
  _trick_winner_seat_impl.cache_clear()
450
450
 
451
451
 
452
+ @lru_cache(maxsize=2048)
452
453
  def _calculate_legal_cards_impl(
453
454
  hand_ids: tuple[int, ...],
454
455
  trump: Suit | None,
@@ -873,7 +874,4 @@ def sort_south_hand(state: GameState) -> GameState:
873
874
  )
874
875
 
875
876
  return replace(state, hands=tuple(new_hands), initial_hands=tuple(new_initial))
876
- [Seat.SOUTH.value], state.trump
877
- )
878
877
 
879
- return replace(state, hands=tuple(new_hands), initial_hands=tuple(new_initial))
@@ -76,7 +76,7 @@ class _UnixKeyReader:
76
76
  r, _, _ = select.select([sys.stdin], [], [], 0.01)
77
77
  if not r:
78
78
  return KeyEvent(Key.ESC)
79
-
79
+
80
80
  next_byte = os.read(self._stdin_fd, 1)
81
81
  if next_byte == b"[":
82
82
  # Likely an arrow key
@@ -107,7 +107,7 @@ class _UnixKeyReader:
107
107
  elif (byte & 0xF0) == 0xE0: n = 2
108
108
  elif (byte & 0xF8) == 0xF0: n = 3
109
109
  else: return KeyEvent(Key.ESC)
110
-
110
+
111
111
  full_buf = bytes([byte])
112
112
  while len(full_buf) < n + 1:
113
113
  chunk = os.read(self._stdin_fd, n + 1 - len(full_buf))
@@ -133,13 +133,19 @@ class _UnixKeyReader:
133
133
  return KeyEvent(Key.MUTE)
134
134
  if ch.lower() == "t":
135
135
  return KeyEvent(Key.HIST)
136
- if ch.lower() == "v":
136
+ if ch.lower() == "i" or ch.lower() == "v":
137
137
  return KeyEvent(Key.OVERLAY)
138
138
 
139
139
  return KeyEvent(Key.CHAR, ch)
140
140
  except Exception:
141
141
  return KeyEvent(Key.ESC)
142
142
 
143
+ def read_timeout(self, timeout: float) -> KeyEvent | None:
144
+ """Read a key with a timeout. Returns None if no key is pressed."""
145
+ r, _, _ = select.select([sys.stdin], [], [], timeout)
146
+ if r:
147
+ return self.read()
148
+ return None
143
149
 
144
150
  def interruptible_sleep(seconds: float, reader: KeyReader | None = None) -> KeyEvent | None:
145
151
  """Sleep for some time, but return immediately if a key is pressed."""
@@ -160,7 +166,8 @@ class KeyReader:
160
166
 
161
167
  def __enter__(self) -> KeyReader: ...
162
168
  def __exit__(self, *args: Any) -> None: ...
163
- def read_key(self) -> KeyEvent: ...
169
+ def read(self) -> KeyEvent: ...
170
+ def read_timeout(self, timeout: float) -> KeyEvent | None: ...
164
171
 
165
172
 
166
173
  if os.name == "nt":
@@ -172,7 +179,7 @@ if os.name == "nt":
172
179
  def __exit__(self, *args: Any) -> None:
173
180
  pass
174
181
 
175
- def read_key(self) -> KeyEvent:
182
+ def read(self) -> KeyEvent:
176
183
  import msvcrt # type: ignore[import-not-found]
177
184
 
178
185
  ch = msvcrt.getch()
@@ -195,6 +202,16 @@ if os.name == "nt":
195
202
  return KeyEvent(Key.SPACE)
196
203
  if ch.lower() == b"q":
197
204
  return KeyEvent(Key.QUIT)
205
+ if ch.lower() == b"h":
206
+ return KeyEvent(Key.HELP)
207
+ if ch.lower() == b"s":
208
+ return KeyEvent(Key.SORT)
209
+ if ch.lower() == b"m":
210
+ return KeyEvent(Key.MUTE)
211
+ if ch.lower() == b"t":
212
+ return KeyEvent(Key.HIST)
213
+ if ch.lower() in (b"i", b"v"):
214
+ return KeyEvent(Key.OVERLAY)
198
215
 
199
216
  try:
200
217
  char = ch.decode("utf-8")
@@ -202,6 +219,16 @@ if os.name == "nt":
202
219
  except Exception:
203
220
  return KeyEvent(Key.ESC)
204
221
 
222
+ def read_timeout(self, timeout: float) -> KeyEvent | None:
223
+ import msvcrt # type: ignore[import-not-found]
224
+
225
+ start = time.time()
226
+ while time.time() - start < timeout:
227
+ if msvcrt.kbhit():
228
+ return self.read()
229
+ time.sleep(0.01)
230
+ return None
231
+
205
232
  KeyReader = _WindowsKeyReader # type: ignore[misc, assignment]
206
233
  else:
207
234
  KeyReader = _UnixKeyReader
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes