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
+3 -2
View File
@@ -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 {
@@ -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 {
@@ -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 (
<main className={styles.page}>
@@ -84,8 +90,12 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou
<section className={styles.table}>
<div className={styles.trump}>
{obs.trump_card ? <PlayingCard card={obs.trump_card} size="md" /> : <div className={styles.noTrump}>—</div>}
<span className={`${styles.trumpLabel} ${obs.trump_suit ? styles[obs.trump_suit] : ''}`}>{trumpText}</span>
{trumpCard ? (
<PlayingCard key={trumpCard.id} card={trumpCard} size="md" />
) : (
<div className={styles.noTrump}>—</div>
)}
<span className={`${styles.trumpLabel} ${trumpSuit ? styles[trumpSuit] : ''}`}>{trumpText}</span>
</div>
<TrickArea observation={obs} players={players} completedTrick={completedTrick} />
</section>
@@ -96,9 +106,6 @@ export function GameScreen({ lobby, game, playerId, completedTrick, completedRou
<section className={styles.me}>
{myTurn && obs.phase === 'bidding' && <BidPanel observation={obs} onBid={(value) => actions.play({ type: 'bid', value })} />}
{myTurn && obs.phase === 'trump_selection' && (
<TrumpPicker onChoose={(suit) => actions.play({ type: 'trump', suit })} />
)}
<div className={styles.myRow}>
<PlayerSeat observation={obs} seat={you} player={players[you]} isYou showHand={false} />
<Hand observation={obs} onPlay={(card) => actions.play({ type: 'play', card })} />
@@ -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);
}
@@ -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 (
<div className={styles.panel}>
<p className={styles.question}>A wizard was turned up — choose the trump suit.</p>
<div className={styles.suits}>
{SUITS.map((suit) => (
<button key={suit} type="button" className={`${styles.suit} ${styles[suit]}`} onClick={() => onChoose(suit)}>
{SUIT_NAMES[suit]}
</button>
))}
</div>
</div>
)
}