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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user