belote-cli 0.9.0__py3-none-any.whl
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/__init__.py +0 -0
- belote/ai.py +412 -0
- belote/ansi.py +141 -0
- belote/bidding.py +68 -0
- belote/deck.py +156 -0
- belote/game.py +537 -0
- belote/input.py +151 -0
- belote/main.py +352 -0
- belote/rules.py +104 -0
- belote/scoring.py +488 -0
- belote/ui.py +825 -0
- belote_cli-0.9.0.dist-info/METADATA +177 -0
- belote_cli-0.9.0.dist-info/RECORD +16 -0
- belote_cli-0.9.0.dist-info/WHEEL +4 -0
- belote_cli-0.9.0.dist-info/entry_points.txt +2 -0
- belote_cli-0.9.0.dist-info/licenses/LICENSE +21 -0
belote/scoring.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
from .deck import Card, Rank, Suit, card_points as card_points_fn
|
|
7
|
+
from .game import (
|
|
8
|
+
GameState,
|
|
9
|
+
Seat,
|
|
10
|
+
Sequence,
|
|
11
|
+
Carre,
|
|
12
|
+
BeloteDecl,
|
|
13
|
+
Declaration,
|
|
14
|
+
team_of,
|
|
15
|
+
partner,
|
|
16
|
+
trick_winner_seat,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Rank numeric values for sequence detection (ascending order)
|
|
20
|
+
_RANK_VALUES: dict[Rank, int] = {
|
|
21
|
+
Rank.SEVEN: 1,
|
|
22
|
+
Rank.EIGHT: 2,
|
|
23
|
+
Rank.NINE: 3,
|
|
24
|
+
Rank.TEN: 4,
|
|
25
|
+
Rank.JACK: 5,
|
|
26
|
+
Rank.QUEEN: 6,
|
|
27
|
+
Rank.KING: 7,
|
|
28
|
+
Rank.ACE: 8,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Carré point values
|
|
32
|
+
_CARRE_POINTS: dict[Rank, int] = {
|
|
33
|
+
Rank.JACK: 200,
|
|
34
|
+
Rank.NINE: 150,
|
|
35
|
+
Rank.ACE: 100,
|
|
36
|
+
Rank.TEN: 100,
|
|
37
|
+
Rank.KING: 100,
|
|
38
|
+
Rank.QUEEN: 100,
|
|
39
|
+
Rank.SEVEN: 0,
|
|
40
|
+
Rank.EIGHT: 0,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Sequence point values
|
|
44
|
+
_SEQUENCE_POINTS: dict[int, int] = {
|
|
45
|
+
3: 20,
|
|
46
|
+
4: 50,
|
|
47
|
+
5: 100,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
BELOTE_POINTS: Final = 20
|
|
51
|
+
LAST_TRICK_BONUS: Final = 10
|
|
52
|
+
CAPOT_BASE: Final = 250
|
|
53
|
+
TOTAL_POINTS: Final = 162 # 152 card points + 10 last trick
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class ScoringBreakdown:
|
|
58
|
+
taker_team: int # 0 = NS, 1 = EW
|
|
59
|
+
taker_card_pts: int
|
|
60
|
+
defender_card_pts: int
|
|
61
|
+
raw_taker_card_pts: int # raw card points before failed-bid adjustment
|
|
62
|
+
raw_defender_card_pts: int # raw card points before failed-bid adjustment
|
|
63
|
+
last_trick_team: int | None # who got the +10
|
|
64
|
+
taker_declarations: int
|
|
65
|
+
defender_declarations: int
|
|
66
|
+
taker_belote: int
|
|
67
|
+
defender_belote: int
|
|
68
|
+
taker_total: int
|
|
69
|
+
defender_total: int
|
|
70
|
+
is_capot: bool
|
|
71
|
+
is_failed: bool
|
|
72
|
+
messages: tuple[str, ...]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class ResolvedDeclarations:
|
|
77
|
+
ns_sequences: tuple[Sequence, ...]
|
|
78
|
+
ew_sequences: tuple[Sequence, ...]
|
|
79
|
+
ns_carres: tuple[Carre, ...]
|
|
80
|
+
ew_carres: tuple[Carre, ...]
|
|
81
|
+
ns_belote: bool
|
|
82
|
+
ew_belote: bool
|
|
83
|
+
scoring_team: int | None # which team scores sequences/carres (0=NS, 1=EW, None=cancel)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def detect_belote(hand: tuple[Card, ...], trump: Suit) -> bool:
|
|
87
|
+
"""Check if hand contains both K and Q of trump."""
|
|
88
|
+
king = Card(trump, Rank.KING)
|
|
89
|
+
queen = Card(trump, Rank.QUEEN)
|
|
90
|
+
return king in hand and queen in hand
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def detect_sequences(hand: tuple[Card, ...]) -> list[Sequence]:
|
|
94
|
+
"""Find all maximal sequences (tierce/quarte/quinte) in a hand.
|
|
95
|
+
|
|
96
|
+
Sequences are based on rank order 7<8<9<10<J<Q<K<A within a single suit.
|
|
97
|
+
Only sequences of length >= 3 count.
|
|
98
|
+
"""
|
|
99
|
+
# Group cards by suit
|
|
100
|
+
by_suit: dict[Suit, list[int]] = {}
|
|
101
|
+
for card in hand:
|
|
102
|
+
if card.suit not in by_suit:
|
|
103
|
+
by_suit[card.suit] = []
|
|
104
|
+
by_suit[card.suit].append(_RANK_VALUES[card.rank])
|
|
105
|
+
|
|
106
|
+
sequences: list[Sequence] = []
|
|
107
|
+
for suit, ranks in by_suit.items():
|
|
108
|
+
ranks.sort()
|
|
109
|
+
# Find consecutive runs
|
|
110
|
+
if not ranks:
|
|
111
|
+
continue
|
|
112
|
+
run_start = 0
|
|
113
|
+
for i in range(1, len(ranks)):
|
|
114
|
+
if ranks[i] != ranks[i - 1] + 1:
|
|
115
|
+
run_len = i - run_start
|
|
116
|
+
if run_len >= 3:
|
|
117
|
+
top = ranks[i - 1]
|
|
118
|
+
seq_cards = tuple(
|
|
119
|
+
c for c in hand
|
|
120
|
+
if c.suit == suit and _RANK_VALUES[c.rank] >= ranks[run_start]
|
|
121
|
+
and _RANK_VALUES[c.rank] <= ranks[i - 1]
|
|
122
|
+
)
|
|
123
|
+
sequences.append(Sequence(
|
|
124
|
+
length=run_len,
|
|
125
|
+
top_rank=top,
|
|
126
|
+
suit=suit,
|
|
127
|
+
is_trump=False, # will be updated later
|
|
128
|
+
cards=seq_cards,
|
|
129
|
+
))
|
|
130
|
+
run_start = i
|
|
131
|
+
# Final run
|
|
132
|
+
run_len = len(ranks) - run_start
|
|
133
|
+
if run_len >= 3:
|
|
134
|
+
top = ranks[-1]
|
|
135
|
+
seq_cards = tuple(
|
|
136
|
+
c for c in hand
|
|
137
|
+
if c.suit == suit and _RANK_VALUES[c.rank] >= ranks[run_start]
|
|
138
|
+
and _RANK_VALUES[c.rank] <= ranks[-1]
|
|
139
|
+
)
|
|
140
|
+
sequences.append(Sequence(
|
|
141
|
+
length=run_len,
|
|
142
|
+
top_rank=top,
|
|
143
|
+
suit=suit,
|
|
144
|
+
is_trump=False,
|
|
145
|
+
cards=seq_cards,
|
|
146
|
+
))
|
|
147
|
+
return sequences
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def detect_carres(hand: tuple[Card, ...]) -> list[Carre]:
|
|
151
|
+
"""Find all carrés (four of a kind by rank) in a hand."""
|
|
152
|
+
by_rank: dict[Rank, list[Card]] = {}
|
|
153
|
+
for card in hand:
|
|
154
|
+
if card.rank not in by_rank:
|
|
155
|
+
by_rank[card.rank] = []
|
|
156
|
+
by_rank[card.rank].append(card)
|
|
157
|
+
|
|
158
|
+
carres: list[Carre] = []
|
|
159
|
+
for rank, cards in by_rank.items():
|
|
160
|
+
if len(cards) == 4:
|
|
161
|
+
carres.append(Carre(
|
|
162
|
+
rank=_RANK_VALUES[rank],
|
|
163
|
+
cards=tuple(cards),
|
|
164
|
+
))
|
|
165
|
+
return carres
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _sequence_strength(seq: Sequence) -> tuple[int, int, bool]:
|
|
169
|
+
"""Comparable tuple for sequence priority: (length, top_rank, is_trump)."""
|
|
170
|
+
return (seq.length, seq.top_rank, seq.is_trump)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _carre_strength(carre: Carre) -> tuple[int, int]:
|
|
174
|
+
"""Comparable tuple for carré priority: (points, rank)."""
|
|
175
|
+
return (_CARRE_POINTS.get(
|
|
176
|
+
next(r for r in Rank if _RANK_VALUES[r] == carre.rank), 0
|
|
177
|
+
), carre.rank)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _best_sequence(sequences: list[Sequence]) -> Sequence | None:
|
|
181
|
+
if not sequences:
|
|
182
|
+
return None
|
|
183
|
+
return max(sequences, key=_sequence_strength)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _best_carre(carres: list[Carre]) -> Carre | None:
|
|
187
|
+
if not carres:
|
|
188
|
+
return None
|
|
189
|
+
return max(carres, key=_carre_strength)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _carre_points(carre: Carre) -> int:
|
|
193
|
+
rank = next(r for r in Rank if _RANK_VALUES[r] == carre.rank)
|
|
194
|
+
return _CARRE_POINTS[rank]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _sequence_points(seq: Sequence) -> int:
|
|
198
|
+
return _SEQUENCE_POINTS.get(seq.length, 0)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def resolve_declarations(
|
|
202
|
+
decls_per_seat: dict[Seat, dict[str, object]],
|
|
203
|
+
trump: Suit,
|
|
204
|
+
) -> ResolvedDeclarations:
|
|
205
|
+
"""Resolve declarations per §3.7.
|
|
206
|
+
|
|
207
|
+
decls_per_seat maps each seat to a dict with keys:
|
|
208
|
+
'sequences': list[Sequence]
|
|
209
|
+
'carres': list[Carre]
|
|
210
|
+
'belote': bool
|
|
211
|
+
"""
|
|
212
|
+
ns_seqs: list[Sequence] = []
|
|
213
|
+
ew_seqs: list[Sequence] = []
|
|
214
|
+
ns_carres: list[Carre] = []
|
|
215
|
+
ew_carres: list[Carre] = []
|
|
216
|
+
ns_belote = False
|
|
217
|
+
ew_belote = False
|
|
218
|
+
|
|
219
|
+
for seat, decls in decls_per_seat.items():
|
|
220
|
+
seqs = [s for s in decls.get("sequences", [])]
|
|
221
|
+
updated_seqs = []
|
|
222
|
+
for s in seqs:
|
|
223
|
+
updated_seqs.append(Sequence(
|
|
224
|
+
length=s.length, top_rank=s.top_rank, suit=s.suit,
|
|
225
|
+
is_trump=(s.suit == trump), cards=s.cards,
|
|
226
|
+
))
|
|
227
|
+
carres = list(decls.get("carres", []))
|
|
228
|
+
has_belote = decls.get("belote", False)
|
|
229
|
+
|
|
230
|
+
if team_of(seat) == 0:
|
|
231
|
+
ns_seqs.extend(updated_seqs)
|
|
232
|
+
ns_carres.extend(carres)
|
|
233
|
+
ns_belote = ns_belote or has_belote
|
|
234
|
+
else:
|
|
235
|
+
ew_seqs.extend(updated_seqs)
|
|
236
|
+
ew_carres.extend(carres)
|
|
237
|
+
ew_belote = ew_belote or has_belote
|
|
238
|
+
|
|
239
|
+
# Determine which team scores sequences/carres
|
|
240
|
+
ns_best_seq = _best_sequence(ns_seqs)
|
|
241
|
+
ew_best_seq = _best_sequence(ew_seqs)
|
|
242
|
+
ns_best_carre = _best_carre(ns_carres)
|
|
243
|
+
ew_best_carre = _best_carre(ew_carres)
|
|
244
|
+
|
|
245
|
+
scoring_team: int | None = None
|
|
246
|
+
|
|
247
|
+
# Carrés outrank any sequence
|
|
248
|
+
if ns_best_carre and ew_best_carre:
|
|
249
|
+
# Both have carrés - higher carré wins
|
|
250
|
+
ns_cp = _carre_points(ns_best_carre)
|
|
251
|
+
ew_cp = _carre_points(ew_best_carre)
|
|
252
|
+
if ns_cp > ew_cp:
|
|
253
|
+
scoring_team = 0
|
|
254
|
+
elif ew_cp > ns_cp:
|
|
255
|
+
scoring_team = 1
|
|
256
|
+
else:
|
|
257
|
+
# Same carré strength - compare rank
|
|
258
|
+
if ns_best_carre.rank > ew_best_carre.rank:
|
|
259
|
+
scoring_team = 0
|
|
260
|
+
elif ew_best_carre.rank > ns_best_carre.rank:
|
|
261
|
+
scoring_team = 1
|
|
262
|
+
else:
|
|
263
|
+
scoring_team = None # cancel
|
|
264
|
+
elif ns_best_carre:
|
|
265
|
+
scoring_team = 0
|
|
266
|
+
elif ew_best_carre:
|
|
267
|
+
scoring_team = 1
|
|
268
|
+
elif ns_best_seq and ew_best_seq:
|
|
269
|
+
ns_str = _sequence_strength(ns_best_seq)
|
|
270
|
+
ew_str = _sequence_strength(ew_best_seq)
|
|
271
|
+
if ns_str > ew_str:
|
|
272
|
+
scoring_team = 0
|
|
273
|
+
elif ew_str > ns_str:
|
|
274
|
+
scoring_team = 1
|
|
275
|
+
else:
|
|
276
|
+
scoring_team = None # cancel
|
|
277
|
+
elif ns_best_seq:
|
|
278
|
+
scoring_team = 0
|
|
279
|
+
elif ew_best_seq:
|
|
280
|
+
scoring_team = 1
|
|
281
|
+
|
|
282
|
+
return ResolvedDeclarations(
|
|
283
|
+
ns_sequences=tuple(ns_seqs),
|
|
284
|
+
ew_sequences=tuple(ew_seqs),
|
|
285
|
+
ns_carres=tuple(ns_carres),
|
|
286
|
+
ew_carres=tuple(ew_carres),
|
|
287
|
+
ns_belote=ns_belote,
|
|
288
|
+
ew_belote=ew_belote,
|
|
289
|
+
scoring_team=scoring_team,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def score_round(state: GameState) -> ScoringBreakdown:
|
|
294
|
+
"""Score the completed round per §3.8."""
|
|
295
|
+
if state.trump is None or state.taker is None:
|
|
296
|
+
return ScoringBreakdown(
|
|
297
|
+
taker_team=team_of(Seat.SOUTH),
|
|
298
|
+
taker_card_pts=0, defender_card_pts=0,
|
|
299
|
+
raw_taker_card_pts=0, raw_defender_card_pts=0,
|
|
300
|
+
last_trick_team=None,
|
|
301
|
+
taker_declarations=0, defender_declarations=0,
|
|
302
|
+
taker_belote=0, defender_belote=0,
|
|
303
|
+
taker_total=0, defender_total=0,
|
|
304
|
+
is_capot=False, is_failed=False,
|
|
305
|
+
messages=(),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
trump = state.trump
|
|
309
|
+
taker_team = team_of(state.taker)
|
|
310
|
+
defender_team = 1 - taker_team
|
|
311
|
+
|
|
312
|
+
# Calculate card points per team from completed tricks
|
|
313
|
+
taker_card_pts = 0
|
|
314
|
+
defender_card_pts = 0
|
|
315
|
+
for trick in state.completed_tricks:
|
|
316
|
+
winner = trick_winner_seat(trick, trump)
|
|
317
|
+
if winner is None:
|
|
318
|
+
continue
|
|
319
|
+
trick_pts = sum(card_points_fn(tc.card, trump) for tc in trick)
|
|
320
|
+
if team_of(winner) == taker_team:
|
|
321
|
+
taker_card_pts += trick_pts
|
|
322
|
+
else:
|
|
323
|
+
defender_card_pts += trick_pts
|
|
324
|
+
|
|
325
|
+
# Last trick bonus
|
|
326
|
+
last_trick_winner = state.last_trick_winner
|
|
327
|
+
last_trick_team: int | None = None
|
|
328
|
+
if last_trick_winner is not None:
|
|
329
|
+
last_trick_team = team_of(last_trick_winner)
|
|
330
|
+
if last_trick_team == taker_team:
|
|
331
|
+
taker_card_pts += LAST_TRICK_BONUS
|
|
332
|
+
else:
|
|
333
|
+
defender_card_pts += LAST_TRICK_BONUS
|
|
334
|
+
|
|
335
|
+
# --- Detect declarations from reconstructed initial hands ---
|
|
336
|
+
# Reconstruct each player's initial hand from all cards they played
|
|
337
|
+
hands_by_seat: dict[Seat, list[Card]] = {s: [] for s in Seat}
|
|
338
|
+
for trick in state.completed_tricks:
|
|
339
|
+
for tc in trick:
|
|
340
|
+
hands_by_seat[tc.seat].append(tc.card)
|
|
341
|
+
for tc in state.current_trick:
|
|
342
|
+
hands_by_seat[tc.seat].append(tc.card)
|
|
343
|
+
initial_hands = {s: tuple(cards) for s, cards in hands_by_seat.items()}
|
|
344
|
+
|
|
345
|
+
# Detect sequences, carres, and belote for each seat
|
|
346
|
+
decls_per_seat: dict[Seat, dict[str, object]] = {}
|
|
347
|
+
for seat in Seat:
|
|
348
|
+
hand = initial_hands[seat]
|
|
349
|
+
seqs = detect_sequences(hand)
|
|
350
|
+
carres = detect_carres(hand)
|
|
351
|
+
has_belote = detect_belote(hand, trump)
|
|
352
|
+
decls_per_seat[seat] = {
|
|
353
|
+
"sequences": seqs,
|
|
354
|
+
"carres": carres,
|
|
355
|
+
"belote": has_belote,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
resolved = resolve_declarations(decls_per_seat, trump)
|
|
359
|
+
|
|
360
|
+
# Compute belote points per team
|
|
361
|
+
taker_has_belote = any(
|
|
362
|
+
decls_per_seat[s].get("belote", False)
|
|
363
|
+
for s in Seat if team_of(s) == taker_team
|
|
364
|
+
)
|
|
365
|
+
defender_has_belote = any(
|
|
366
|
+
decls_per_seat[s].get("belote", False)
|
|
367
|
+
for s in Seat if team_of(s) == defender_team
|
|
368
|
+
)
|
|
369
|
+
taker_belote = BELOTE_POINTS if taker_has_belote else 0
|
|
370
|
+
defender_belote = BELOTE_POINTS if defender_has_belote else 0
|
|
371
|
+
|
|
372
|
+
# Compute declaration points (sequences + carres) per team
|
|
373
|
+
scoring_team = resolved.scoring_team
|
|
374
|
+
taker_declarations = 0
|
|
375
|
+
defender_declarations = 0
|
|
376
|
+
|
|
377
|
+
if scoring_team is not None:
|
|
378
|
+
# Only the winning team scores their sequences and carres
|
|
379
|
+
if scoring_team == taker_team:
|
|
380
|
+
for seq in resolved.ns_sequences if taker_team == 0 else resolved.ew_sequences:
|
|
381
|
+
taker_declarations += _sequence_points(seq)
|
|
382
|
+
for carre in resolved.ns_carres if taker_team == 0 else resolved.ew_carres:
|
|
383
|
+
taker_declarations += _carre_points(carre)
|
|
384
|
+
else:
|
|
385
|
+
for seq in resolved.ns_sequences if defender_team == 0 else resolved.ew_sequences:
|
|
386
|
+
defender_declarations += _sequence_points(seq)
|
|
387
|
+
for carre in resolved.ns_carres if defender_team == 0 else resolved.ew_carres:
|
|
388
|
+
defender_declarations += _carre_points(carre)
|
|
389
|
+
|
|
390
|
+
# Check capot: taker won all 8 tricks
|
|
391
|
+
is_capot = all(
|
|
392
|
+
trick_winner_seat(trick, trump) is not None and
|
|
393
|
+
team_of(trick_winner_seat(trick, trump)) == taker_team
|
|
394
|
+
for trick in state.completed_tricks
|
|
395
|
+
) if state.completed_tricks else False
|
|
396
|
+
|
|
397
|
+
messages: list[str] = []
|
|
398
|
+
|
|
399
|
+
if is_capot:
|
|
400
|
+
messages.append("Capot!")
|
|
401
|
+
taker_total = CAPOT_BASE + taker_declarations + taker_belote
|
|
402
|
+
defender_total = defender_belote
|
|
403
|
+
return ScoringBreakdown(
|
|
404
|
+
taker_team=taker_team,
|
|
405
|
+
taker_card_pts=0, defender_card_pts=0,
|
|
406
|
+
raw_taker_card_pts=taker_card_pts, raw_defender_card_pts=defender_card_pts,
|
|
407
|
+
last_trick_team=last_trick_team,
|
|
408
|
+
taker_declarations=taker_declarations,
|
|
409
|
+
defender_declarations=defender_declarations,
|
|
410
|
+
taker_belote=taker_belote,
|
|
411
|
+
defender_belote=defender_belote,
|
|
412
|
+
taker_total=taker_total,
|
|
413
|
+
defender_total=defender_total,
|
|
414
|
+
is_capot=True, is_failed=False,
|
|
415
|
+
messages=tuple(messages),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Check bid failure
|
|
419
|
+
is_failed = taker_card_pts < defender_card_pts
|
|
420
|
+
|
|
421
|
+
if is_failed:
|
|
422
|
+
messages.append("Chute! (bid failed)")
|
|
423
|
+
taker_total = taker_belote
|
|
424
|
+
defender_total = TOTAL_POINTS + defender_declarations + defender_belote + taker_declarations
|
|
425
|
+
else:
|
|
426
|
+
taker_total = taker_card_pts + taker_declarations + taker_belote
|
|
427
|
+
defender_total = defender_card_pts + defender_declarations + defender_belote
|
|
428
|
+
|
|
429
|
+
return ScoringBreakdown(
|
|
430
|
+
taker_team=taker_team,
|
|
431
|
+
taker_card_pts=taker_card_pts if not is_failed else 0,
|
|
432
|
+
defender_card_pts=defender_card_pts if not is_failed else 0,
|
|
433
|
+
raw_taker_card_pts=taker_card_pts,
|
|
434
|
+
raw_defender_card_pts=defender_card_pts,
|
|
435
|
+
last_trick_team=last_trick_team,
|
|
436
|
+
taker_declarations=taker_declarations,
|
|
437
|
+
defender_declarations=defender_declarations,
|
|
438
|
+
taker_belote=taker_belote,
|
|
439
|
+
defender_belote=defender_belote,
|
|
440
|
+
taker_total=taker_total,
|
|
441
|
+
defender_total=defender_total,
|
|
442
|
+
is_capot=False, is_failed=is_failed,
|
|
443
|
+
messages=tuple(messages),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def apply_round_score(state: GameState, breakdown: ScoringBreakdown) -> GameState:
|
|
448
|
+
"""Apply round scoring result to team scores and advance state."""
|
|
449
|
+
ns, ew = state.team_scores
|
|
450
|
+
if breakdown.taker_team == 0:
|
|
451
|
+
ns += breakdown.taker_total
|
|
452
|
+
ew += breakdown.defender_total
|
|
453
|
+
else:
|
|
454
|
+
ew += breakdown.taker_total
|
|
455
|
+
ns += breakdown.defender_total
|
|
456
|
+
|
|
457
|
+
new_scores = (ns, ew)
|
|
458
|
+
|
|
459
|
+
# Determine if game is over
|
|
460
|
+
if ns >= state.target or ew >= state.target:
|
|
461
|
+
from .game import Phase as P
|
|
462
|
+
phase = P.GAME_OVER
|
|
463
|
+
else:
|
|
464
|
+
from .game import Phase as P
|
|
465
|
+
phase = P.DEAL
|
|
466
|
+
|
|
467
|
+
# Rotate dealer
|
|
468
|
+
from .game import replace as dataclass_replace
|
|
469
|
+
return dataclass_replace(
|
|
470
|
+
state,
|
|
471
|
+
team_scores=new_scores,
|
|
472
|
+
dealer=state.dealer.next_seat(),
|
|
473
|
+
phase=phase,
|
|
474
|
+
trump=None,
|
|
475
|
+
taker=None,
|
|
476
|
+
current_trick=(),
|
|
477
|
+
completed_tricks=(),
|
|
478
|
+
last_trick_winner=None,
|
|
479
|
+
bids=(),
|
|
480
|
+
bidder_index=0,
|
|
481
|
+
bid_suits=(),
|
|
482
|
+
round_scores=(0, 0),
|
|
483
|
+
declarations=(),
|
|
484
|
+
declarations_resolved=False,
|
|
485
|
+
announced=None,
|
|
486
|
+
belote_tracker=(False, False),
|
|
487
|
+
first_trick_done=False,
|
|
488
|
+
)
|