From 21a2b044d956700a52c992888ad44523e117539a Mon Sep 17 00:00:00 2001 From: Marvin Date: Sun, 27 Sep 2026 10:28:53 +0200 Subject: [PATCH] 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 --- wizard_ai/README.md | 2 +- wizard_ai/wizard_ai/agents/heuristic_agent.py | 16 +--- wizard_ai/wizard_ai/model/encoding.py | 12 +-- wizard_server/game_logic/README.md | 2 +- wizard_server/game_logic/tests/test_game.py | 83 ++++++++++++++----- wizard_server/game_logic/tests/test_rules.py | 9 ++ .../game_logic/wizard_game/__init__.py | 3 +- .../game_logic/wizard_game/actions.py | 15 +--- wizard_server/game_logic/wizard_game/game.py | 50 ++++------- wizard_server/game_logic/wizard_game/rules.py | 11 +++ wizard_server/game_logic/wizard_game/state.py | 5 +- wizard_website/src/api/types.ts | 5 +- .../PlayingCard/PlayingCard.module.css | 2 +- .../src/screens/GameScreen/GameScreen.tsx | 31 ++++--- .../screens/GameScreen/TrumpPicker.module.css | 57 ------------- .../src/screens/GameScreen/TrumpPicker.tsx | 22 ----- 16 files changed, 135 insertions(+), 190 deletions(-) delete mode 100644 wizard_website/src/screens/GameScreen/TrumpPicker.module.css delete mode 100644 wizard_website/src/screens/GameScreen/TrumpPicker.tsx diff --git a/wizard_ai/README.md b/wizard_ai/README.md index 263fe40..c31d619 100644 --- a/wizard_ai/README.md +++ b/wizard_ai/README.md @@ -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. diff --git a/wizard_ai/wizard_ai/agents/heuristic_agent.py b/wizard_ai/wizard_ai/agents/heuristic_agent.py index 385d76e..8079be9 100644 --- a/wizard_ai/wizard_ai/agents/heuristic_agent.py +++ b/wizard_ai/wizard_ai/agents/heuristic_agent.py @@ -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: diff --git a/wizard_ai/wizard_ai/model/encoding.py b/wizard_ai/wizard_ai/model/encoding.py index 780377c..98a3810 100644 --- a/wizard_ai/wizard_ai/model/encoding.py +++ b/wizard_ai/wizard_ai/model/encoding.py @@ -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]: diff --git a/wizard_server/game_logic/README.md b/wizard_server/game_logic/README.md index fc08155..73841d4 100644 --- a/wizard_server/game_logic/README.md +++ b/wizard_server/game_logic/README.md @@ -16,7 +16,7 @@ rng = random.Random(1) while not game.is_finished: player = game.current_player obs = game.observation(player) # what this player may see - action = rng.choice(obs.legal_actions) # Bid / PlayCard / ChooseTrump + action = rng.choice(obs.legal_actions) # Bid / PlayCard events = game.apply(player, action) # TrickCompleted, RoundCompleted, ... print(game.scores, game.winners()) ``` diff --git a/wizard_server/game_logic/tests/test_game.py b/wizard_server/game_logic/tests/test_game.py index 07c4d6e..6e9560d 100644 --- a/wizard_server/game_logic/tests/test_game.py +++ b/wizard_server/game_logic/tests/test_game.py @@ -4,8 +4,8 @@ import random import pytest from wizard_game import ( + DECK_SIZE, Bid, - ChooseTrump, GameFinished, GameOptions, IllegalActionError, @@ -13,7 +13,8 @@ from wizard_game import ( Phase, PlayCard, RoundCompleted, - Suit, + TrickCompleted, + TrickResult, WizardGame, action_from_dict, card_by_id, @@ -59,8 +60,8 @@ def test_deal_sizes_and_starter(): game = WizardGame(4, seed=3, first_dealer=2) assert game.round_number == 1 assert all(len(game.hand(p)) == 1 for p in range(4)) - if game.phase is Phase.BIDDING: - assert game.current_player == 3 + assert game.phase is Phase.BIDDING + assert game.current_player == 3 def test_wrong_player_rejected(): @@ -72,31 +73,69 @@ def test_wrong_player_rejected(): def test_illegal_bid_rejected(): game = WizardGame(3, seed=1, first_dealer=0) - if game.phase is Phase.TRUMP_SELECTION: - game.apply(game.current_player, ChooseTrump(Suit.RED)) with pytest.raises(IllegalActionError): game.apply(game.current_player, Bid(5)) -def test_wizard_trump_card_lets_dealer_choose(): - for seed in range(200): - game = WizardGame(4, seed=seed, first_dealer=1) - if game.round.trump_card and game.round.trump_card.is_wizard: - break - else: - pytest.fail("no seed produced a wizard as trump card") - assert game.phase is Phase.TRUMP_SELECTION - assert game.current_player == 1 - game.apply(1, ChooseTrump(Suit.GREEN)) - assert game.round.trump_suit is Suit.GREEN +@pytest.mark.parametrize("num_players", [3, 4, 5, 6]) +def test_trump_card_is_taken_from_the_stack(num_players): + """The trump card is dealt to nobody and every card exists only once.""" + game = WizardGame(num_players, seed=num_players) + rng = random.Random(num_players) + rounds_seen = 0 + while not game.is_finished: + if game.phase is Phase.BIDDING and all(bid is None for bid in game.round.bids): + rounds_seen += 1 + hands = [card for player in range(num_players) for card in game.hand(player)] + assert len(hands) == len(set(hands)) == game.round_number * num_players + trump = game.round.trump_card + if game.round_number * num_players < DECK_SIZE: + assert trump is not None and trump not in hands + else: + assert trump is None # last round: every card was dealt + game.apply(game.current_player, rng.choice(game.legal_actions())) + assert rounds_seen == game.total_rounds + + +def find_game(predicate, num_players=4): + for seed in range(500): + game = WizardGame(num_players, seed=seed, first_dealer=1) + if predicate(game.round.trump_card): + return game + pytest.fail("no seed produced the wanted trump card") + + +def test_number_trump_card_defines_trump_suit(): + game = find_game(lambda card: card is not None and card.is_number) + assert game.round.trump_suit is game.round.trump_card.suit + assert game.observation(0).trump_suit is game.round.trump_card.suit + + +def test_completed_trick_keeps_its_rounds_trump_card(): + game = WizardGame(3, seed=4) + rng = random.Random(4) + for _ in range(200): + trump = game.round.trump_card + events = game.apply(game.current_player, rng.choice(game.legal_actions())) + tricks = [e.trick for e in events if isinstance(e, TrickCompleted)] + for trick in tricks: + assert trick.trump_card == trump + assert TrickResult.from_dict(trick.to_dict()) == trick + + +@pytest.mark.parametrize("kind", ["wizard", "jester"]) +def test_special_trump_card_means_no_trump(kind): + game = find_game(lambda card: card is not None and card.kind.value == kind) + obs = game.observation(0) + assert obs.trump_card == game.round.trump_card + assert obs.trump_suit is None + # Bidding starts right away with the player left of the dealer. assert game.phase is Phase.BIDDING assert game.current_player == 2 def test_sum_of_bids_never_equals_cards_in_hand(): game = WizardGame(3, seed=11, first_dealer=0) - if game.phase is Phase.TRUMP_SELECTION: - game.apply(game.current_player, ChooseTrump(Suit.RED)) game.apply(1, Bid(0)) game.apply(2, Bid(0)) assert game.legal_actions(0) == [Bid(0)] # bidding 1 would make the sum equal 1 @@ -104,8 +143,6 @@ def test_sum_of_bids_never_equals_cards_in_hand(): def test_hidden_bids(): game = WizardGame(3, GameOptions(hidden_bids=True), seed=5, first_dealer=0) - if game.phase is Phase.TRUMP_SELECTION: - game.apply(game.current_player, ChooseTrump(Suit.RED)) game.apply(1, Bid(1)) assert game.observation(1).bids[1] == 1 assert game.observation(2).bids[1] is None @@ -130,8 +167,10 @@ def test_observation_hides_other_hands_and_round_trips(): def test_action_round_trip(): - for action in (Bid(3), PlayCard(card_by_id(WIZARD_ID_OFFSET)), ChooseTrump(Suit.BLUE)): + for action in (Bid(3), PlayCard(card_by_id(WIZARD_ID_OFFSET))): assert action_from_dict(action.to_dict()) == action + with pytest.raises(ValueError): + action_from_dict({"type": "trump", "suit": "red"}) with pytest.raises(ValueError): action_from_dict({"type": "bid", "value": True}) with pytest.raises(ValueError): diff --git a/wizard_server/game_logic/tests/test_rules.py b/wizard_server/game_logic/tests/test_rules.py index eccd5e4..57c1534 100644 --- a/wizard_server/game_logic/tests/test_rules.py +++ b/wizard_server/game_logic/tests/test_rules.py @@ -77,3 +77,12 @@ def test_round_score(): assert round_score(3, 3) == 50 assert round_score(2, 0) == -20 assert round_score(1, 4) == -30 + + +def test_trump_suit_from_trump_card(): + from wizard_game.rules import trump_suit + + assert trump_suit(num(Suit.YELLOW, 4)) is Suit.YELLOW + assert trump_suit(WIZ1) is None + assert trump_suit(JES1) is None + assert trump_suit(None) is None diff --git a/wizard_server/game_logic/wizard_game/__init__.py b/wizard_server/game_logic/wizard_game/__init__.py index d40dd38..1e6fa77 100644 --- a/wizard_server/game_logic/wizard_game/__init__.py +++ b/wizard_server/game_logic/wizard_game/__init__.py @@ -4,7 +4,7 @@ This package is independent of the game server so that it can also be used to train AI players. """ -from .actions import Action, Bid, ChooseTrump, PlayCard, action_from_dict, action_to_dict +from .actions import Action, Bid, PlayCard, action_from_dict, action_to_dict from .cards import ALL_CARDS, DECK_SIZE, SUITS, Card, CardKind, Suit, card_by_id from .game import IllegalActionError, WizardGame from .observation import Observation @@ -32,7 +32,6 @@ __all__ = [ "Bid", "Card", "CardKind", - "ChooseTrump", "Event", "GameFinished", "GameOptions", diff --git a/wizard_server/game_logic/wizard_game/actions.py b/wizard_server/game_logic/wizard_game/actions.py index bf7afa8..6220375 100644 --- a/wizard_server/game_logic/wizard_game/actions.py +++ b/wizard_server/game_logic/wizard_game/actions.py @@ -5,7 +5,6 @@ frontend, the game server and the AI player: - ``{"type": "bid", "value": 2}`` - ``{"type": "play", "card": 17}`` -- ``{"type": "trump", "suit": "red"}`` """ from __future__ import annotations @@ -13,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from .cards import Card, Suit, card_by_id +from .cards import Card, card_by_id @dataclass(frozen=True, slots=True) @@ -32,15 +31,7 @@ class PlayCard: return {"type": "play", "card": self.card.id} -@dataclass(frozen=True, slots=True) -class ChooseTrump: - suit: Suit - - def to_dict(self) -> dict: - return {"type": "trump", "suit": self.suit.value} - - -Action = Bid | PlayCard | ChooseTrump +Action = Bid | PlayCard def action_to_dict(action: Action) -> dict: @@ -66,8 +57,6 @@ def action_from_dict(data: dict[str, Any]) -> Action: if not isinstance(card_id, int) or isinstance(card_id, bool): raise ValueError("Card must be an integer id") return PlayCard(card_by_id(card_id)) - if kind == "trump": - return ChooseTrump(Suit(data["suit"])) except KeyError as exc: raise ValueError(f"Missing field {exc} in action") from exc raise ValueError(f"Unknown action type: {kind!r}") diff --git a/wizard_server/game_logic/wizard_game/game.py b/wizard_server/game_logic/wizard_game/game.py index 8baa2bf..d3dd835 100644 --- a/wizard_server/game_logic/wizard_game/game.py +++ b/wizard_server/game_logic/wizard_game/game.py @@ -19,8 +19,8 @@ import copy import random from . import rules -from .actions import Action, Bid, ChooseTrump, PlayCard -from .cards import SUITS, Card, new_shuffled_deck +from .actions import Action, Bid, PlayCard +from .cards import Card, new_shuffled_deck from .observation import Observation from .state import ( Event, @@ -99,9 +99,6 @@ class WizardGame: if player is None or player != self.current_player: return [] - if self.phase is Phase.TRUMP_SELECTION: - return [ChooseTrump(suit) for suit in SUITS] - if self.phase is Phase.BIDDING: return [Bid(value) for value in self._allowed_bids(player)] @@ -127,8 +124,6 @@ class WizardGame: if not self.is_legal(player, action): raise IllegalActionError(f"Illegal action {action} for player {player}") - if isinstance(action, ChooseTrump): - return self._choose_trump(action) if isinstance(action, Bid): return self._bid(player, action) if isinstance(action, PlayCard): @@ -183,39 +178,32 @@ class WizardGame: return (player + 1) % self.num_players def _deal(self, round_number: int, dealer: int) -> RoundState: - deck = new_shuffled_deck(self._rng) + stack = new_shuffled_deck(self._rng) hands: list[list[Card]] = [[] for _ in range(self.num_players)] - # Deal one card at a time starting left of the dealer. + # Deal one card at a time from the top of the stack, starting left of + # the dealer. for i in range(round_number * self.num_players): - hands[(dealer + 1 + i) % self.num_players].append(deck[i]) - remaining = deck[round_number * self.num_players:] + hands[(dealer + 1 + i) % self.num_players].append(stack.pop()) - # The next card defines the trump suit. In the last round no cards - # are left, so there is no trump. - trump_card = remaining[0] if remaining else None + # The next card of the stack is turned up as trump card. It is taken + # off the stack and belongs to nobody's hand. In the last round the + # stack is empty, so there is no trump card. + trump_card = stack.pop() if stack else None starter = self._next(dealer) - rnd = RoundState( + self.phase = Phase.BIDDING + self.current_player = starter + return RoundState( round_number=round_number, dealer=dealer, hands=hands, trump_card=trump_card, - trump_suit=trump_card.suit if trump_card and trump_card.is_number else None, + trump_suit=rules.trump_suit(trump_card), bids=[None] * self.num_players, tricks_won=[0] * self.num_players, trick_leader=starter, ) - if trump_card is not None and trump_card.is_wizard: - # A wizard as trump card: the dealer chooses the trump suit. - self.phase = Phase.TRUMP_SELECTION - self.current_player = dealer - else: - # No trump card or a jester: the round is played without trump. - self.phase = Phase.BIDDING - self.current_player = starter - return rnd - def _allowed_bids(self, player: int) -> list[int]: """Bids 0..cards in hand, except that the last bidder may not make the sum of all bids equal the number of cards each player holds this round. @@ -229,12 +217,6 @@ class WizardGame: allowed = [bid for bid in allowed if bid != forbidden] return allowed - def _choose_trump(self, action: ChooseTrump) -> list[Event]: - self.round.trump_suit = action.suit - self.phase = Phase.BIDDING - self.current_player = self._next(self.round.dealer) - return [] - def _bid(self, player: int, action: Bid) -> list[Event]: rnd = self.round rnd.bids[player] = action.value @@ -263,7 +245,9 @@ class WizardGame: winner = rnd.current_trick[rules.trick_winner(cards, rnd.trump_suit)].player rnd.tricks_won[winner] += 1 - result = TrickResult(rnd.round_number, rnd.trick_number, tuple(rnd.current_trick), winner) + result = TrickResult( + rnd.round_number, rnd.trick_number, tuple(rnd.current_trick), winner, rnd.trump_card + ) self.last_trick = result events: list[Event] = [TrickCompleted(result)] diff --git a/wizard_server/game_logic/wizard_game/rules.py b/wizard_server/game_logic/wizard_game/rules.py index 247839b..1570866 100644 --- a/wizard_server/game_logic/wizard_game/rules.py +++ b/wizard_server/game_logic/wizard_game/rules.py @@ -28,6 +28,17 @@ def validate_player_count(num_players: int) -> None: ) +def trump_suit(trump_card: Card | None) -> Suit | None: + """The trump suit defined by the turned up trump card. + + Only a number card defines a trump suit. A wizard, a jester or no trump + card at all (last round) means the round is played without trump. + """ + if trump_card is None or not trump_card.is_number: + return None + return trump_card.suit + + def lead_suit(trick: Sequence[Card]) -> Suit | None: """The suit that has to be followed in a trick. diff --git a/wizard_server/game_logic/wizard_game/state.py b/wizard_server/game_logic/wizard_game/state.py index 8e21627..0bca7a9 100644 --- a/wizard_server/game_logic/wizard_game/state.py +++ b/wizard_server/game_logic/wizard_game/state.py @@ -10,7 +10,6 @@ from .cards import Card, Suit, card_by_id class Phase(str, Enum): - TRUMP_SELECTION = "trump_selection" BIDDING = "bidding" PLAYING = "playing" FINISHED = "finished" @@ -51,6 +50,8 @@ class TrickResult: trick_number: int cards: tuple[PlayedCard, ...] winner: int + # The trump card of the round the trick was played in. + trump_card: Card | None = None def to_dict(self) -> dict: return { @@ -58,6 +59,7 @@ class TrickResult: "trick_number": self.trick_number, "cards": [played.to_dict() for played in self.cards], "winner": self.winner, + "trump_card": self.trump_card.to_dict() if self.trump_card else None, } @classmethod @@ -67,6 +69,7 @@ class TrickResult: trick_number=data["trick_number"], cards=tuple(PlayedCard.from_dict(c) for c in data["cards"]), winner=data["winner"], + trump_card=card_by_id(data["trump_card"]["id"]) if data.get("trump_card") else None, ) diff --git a/wizard_website/src/api/types.ts b/wizard_website/src/api/types.ts index db3c68d..852d84a 100644 --- a/wizard_website/src/api/types.ts +++ b/wizard_website/src/api/types.ts @@ -14,9 +14,8 @@ export interface Card { export type Action = | { type: 'bid'; value: number } | { type: 'play'; card: number } - | { type: 'trump'; suit: Suit } -export type Phase = 'trump_selection' | 'bidding' | 'playing' | 'finished' +export type Phase = 'bidding' | 'playing' | 'finished' export interface GameOptions { hidden_bids: boolean @@ -32,6 +31,8 @@ export interface TrickResult { trick_number: number cards: PlayedCard[] winner: number + // Trump card of the round the trick was played in. + trump_card: Card | null } export interface RoundResult { diff --git a/wizard_website/src/components/PlayingCard/PlayingCard.module.css b/wizard_website/src/components/PlayingCard/PlayingCard.module.css index 3d1e2e5..c40e738 100644 --- a/wizard_website/src/components/PlayingCard/PlayingCard.module.css +++ b/wizard_website/src/components/PlayingCard/PlayingCard.module.css @@ -80,12 +80,12 @@ line-height: 1; } +/* Not rotated: an upside down 6 would read as 9 and vice versa. */ .cornerBottom { top: auto; right: 8%; bottom: 4%; left: auto; - transform: rotate(180deg); } .center { diff --git a/wizard_website/src/screens/GameScreen/GameScreen.tsx b/wizard_website/src/screens/GameScreen/GameScreen.tsx index 50f1e57..5f8e961 100644 --- a/wizard_website/src/screens/GameScreen/GameScreen.tsx +++ b/wizard_website/src/screens/GameScreen/GameScreen.tsx @@ -13,7 +13,6 @@ import { PlayerSeat } from './PlayerSeat' import { RoundSummary } from './RoundSummary' import { Scoreboard } from './Scoreboard' import { TrickArea } from './TrickArea' -import { TrumpPicker } from './TrumpPicker' interface GameScreenProps { lobby: Lobby @@ -39,9 +38,6 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou let status: string switch (obs.phase) { - case 'trump_selection': - status = myTurn ? 'Choose the trump suit' : `${name(obs.current_player)} is choosing the trump suit…` - break case 'bidding': status = myTurn ? 'Your turn to bid' : `Waiting for ${name(obs.current_player)} to bid…` break @@ -52,9 +48,19 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou status = 'Game over' } - let trumpText = 'No trump this round' - if (obs.trump_suit) trumpText = `Trump: ${SUIT_NAMES[obs.trump_suit]}` - else if (obs.phase === 'trump_selection') trumpText = 'Wizard turned up — dealer chooses' + // While the last trick of the previous round is still on the table, show the + // trump card that trick was played with, not the next round's one. + const showingPreviousRound = + completedTrick !== null && + obs.current_trick.length === 0 && + completedTrick.round_number !== obs.round_number + const trumpCard = showingPreviousRound ? completedTrick.trump_card : obs.trump_card + const trumpSuit = trumpCard?.kind === 'number' ? trumpCard.suit : null + + let trumpText = 'Last round — no trump' + if (trumpSuit) trumpText = `Trump: ${SUIT_NAMES[trumpSuit]}` + else if (trumpCard?.kind === 'wizard') trumpText = 'Wizard turned up — no trump' + else if (trumpCard?.kind === 'jester') trumpText = 'Jester turned up — no trump' return (
@@ -84,8 +90,12 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou
- {obs.trump_card ? :
—
} - {trumpText} + {trumpCard ? ( + + ) : ( +
—
+ )} + {trumpText}
@@ -96,9 +106,6 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou
{myTurn && obs.phase === 'bidding' && actions.play({ type: 'bid', value })} />} - {myTurn && obs.phase === 'trump_selection' && ( - actions.play({ type: 'trump', suit })} /> - )}
actions.play({ type: 'play', card })} /> diff --git a/wizard_website/src/screens/GameScreen/TrumpPicker.module.css b/wizard_website/src/screens/GameScreen/TrumpPicker.module.css deleted file mode 100644 index e9fe402..0000000 --- a/wizard_website/src/screens/GameScreen/TrumpPicker.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.panel { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - padding: 14px 18px; - border: 1px solid var(--gold); - border-radius: var(--radius); - background: rgb(26 22 64 / 95%); -} - -.question { - margin: 0; - font-weight: 700; - text-align: center; -} - -.suits { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 8px; -} - -.suit { - min-width: 88px; - padding: 10px 14px; - border: 0; - border-radius: 10px; - color: #fff; - font: inherit; - font-weight: 700; - cursor: pointer; - transition: transform 0.1s, filter 0.15s; -} - -.suit:hover { - filter: brightness(1.15); - transform: translateY(-2px); -} - -.red { - background: var(--suit-red); -} - -.yellow { - background: var(--suit-yellow); - color: #2b2200; -} - -.green { - background: var(--suit-green); -} - -.blue { - background: var(--suit-blue); -} diff --git a/wizard_website/src/screens/GameScreen/TrumpPicker.tsx b/wizard_website/src/screens/GameScreen/TrumpPicker.tsx deleted file mode 100644 index 2879fce..0000000 --- a/wizard_website/src/screens/GameScreen/TrumpPicker.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Suit } from '../../api/types' -import { SUIT_NAMES, SUITS } from '../../util/cards' -import styles from './TrumpPicker.module.css' - -interface TrumpPickerProps { - onChoose: (suit: Suit) => void -} - -export function TrumpPicker({ onChoose }: TrumpPickerProps) { - return ( -
-

A wizard was turned up — choose the trump suit.

-
- {SUITS.map((suit) => ( - - ))} -
-
- ) -}