Trump card: draw from the stack, no trump for wizards and jesters

- Deal the hands from the shuffled stack and draw the trump card from it,
  so the trump card is in nobody's hand and cannot be played
- A wizard or jester as trump card means no trump suit (previously a
  wizard let the dealer choose); remove the trump selection phase, the
  ChooseTrump action, the trump picker UI and the AI trump actions
- Completed tricks carry their round's trump card, so the table keeps
  showing the matching trump card while the last trick of a round is
  displayed
- Card corners are no longer rotated, so 6 and 9 cannot be confused
- Trump label explains why there is no trump (wizard, jester, last round)
- Tests: trump card never dealt, special cards give no trump, tricks keep
  their trump card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:28:53 +02:00
co-authored by Claude Opus 5.5
parent fde8e2fb2e
commit 21a2b044d9
16 changed files with 135 additions and 190 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ provides everything around it.
- `agents/` — `Agent` interface (`act(observation) -> action`) plus `RandomAgent`,
`HeuristicAgent` (baseline / fallback) and `ModelAgent` (wraps a `Policy`)
- `model/encoding.py` — observation → fixed-size feature vector (`FEATURE_SIZE`), and the fixed
action space (21 bids + 60 cards + 4 trump suits) with legal-action masks; works for 3–6 players
action space (21 bids + 60 cards) with legal-action masks; works for 3–6 players
- `model/policy.py` — **the place to plug in the model**: subclass `Policy`, implement
`select`, `update`, `save_weights`/`load`, register with `@register_policy`.
`UniformPolicy` is a placeholder so the pipeline runs end to end.
+2 -14
View File
@@ -6,9 +6,7 @@ as the fallback in production while no trained model is available.
from __future__ import annotations
from collections import Counter
from wizard_game import SUITS, Action, Bid, Card, ChooseTrump, Observation, Phase, PlayCard, Suit
from wizard_game import Action, Bid, Card, Observation, Phase, PlayCard, Suit
from wizard_game.rules import trick_winner
from .base import Agent
@@ -18,21 +16,11 @@ class HeuristicAgent(Agent):
name = "heuristic"
def act(self, observation: Observation) -> Action:
if observation.phase is Phase.TRUMP_SELECTION:
return ChooseTrump(self._best_trump(observation.hand))
if observation.phase is Phase.BIDDING:
return self._bid(observation)
return self._play(observation)
# --- Trump & bidding ----------------------------------------------------
@staticmethod
def _best_trump(hand: tuple[Card, ...]) -> Suit:
strength = Counter({suit: 0.0 for suit in SUITS})
for card in hand:
if card.is_number and card.suit is not None:
strength[card.suit] += 1 + card.rank / 13
return strength.most_common(1)[0][0]
# --- Bidding ------------------------------------------------------------
@staticmethod
def _expected_tricks(observation: Observation) -> float:
+3 -9
View File
@@ -17,7 +17,6 @@ from wizard_game import (
SUITS,
Action,
Bid,
ChooseTrump,
Observation,
Phase,
PlayCard,
@@ -32,12 +31,11 @@ SCORE_SCALE = 100.0
class ActionSpace:
"""Fixed action indexing: bids 0-20, then the 60 cards, then 4 trump suits."""
"""Fixed action indexing: bids 0-20, then the 60 cards."""
BID_OFFSET = 0
CARD_OFFSET = BID_OFFSET + MAX_BID + 1
TRUMP_OFFSET = CARD_OFFSET + DECK_SIZE
SIZE = TRUMP_OFFSET + len(SUITS)
SIZE = CARD_OFFSET + DECK_SIZE
@classmethod
def index(cls, action: Action) -> int:
@@ -45,16 +43,12 @@ class ActionSpace:
return cls.BID_OFFSET + action.value
if isinstance(action, PlayCard):
return cls.CARD_OFFSET + action.card.id
if isinstance(action, ChooseTrump):
return cls.TRUMP_OFFSET + action.suit.index
raise TypeError(f"Unknown action {action!r}")
@classmethod
def action(cls, index: int) -> Action:
if not 0 <= index < cls.SIZE:
raise ValueError(f"Action index out of range: {index}")
if index >= cls.TRUMP_OFFSET:
return ChooseTrump(SUITS[index - cls.TRUMP_OFFSET])
if index >= cls.CARD_OFFSET:
return PlayCard(card_by_id(index - cls.CARD_OFFSET))
return Bid(index - cls.BID_OFFSET)
@@ -86,7 +80,7 @@ def _cards(cards) -> list[float]:
return vector
PHASES = (Phase.TRUMP_SELECTION, Phase.BIDDING, Phase.PLAYING)
PHASES = (Phase.BIDDING, Phase.PLAYING)
def encode_observation(obs: Observation) -> list[float]: