Code — how this page is built
Under the hood
The actual source of the components on this page, read straight from the repo. Copy it, read it, or open it on GitHub.
CardDealerGameview on GitHub ↗
Holds a shuffled deck in state, deals N cards off the top, reshuffles when short. Renders the shared brand PlayingCard.
'use client';
import * as React from 'react';
import { PlayingCard } from '@/components/games/PlayingCard';
import { Win98Button } from '@/components/ui/win-98-button';
import { shuffledDeck, type Card } from '@/lib/cards';
export function CardDealerGame() {
const [deck, setDeck] = React.useState<Card[]>([]);
const [hand, setHand] = React.useState<Card[]>([]);
const [handSize, setHandSize] = React.useState(5);
React.useEffect(() => {
setDeck(shuffledDeck());
}, []);
const deal = () => {
setDeck((d) => {
let working = d;
if (working.length < handSize) working = shuffledDeck();
setHand(working.slice(0, handSize));
return working.slice(handSize);
});
};
const reshuffle = () => {
setDeck(shuffledDeck());
setHand([]);
};
return (
<div className="flex w-full max-w-2xl flex-col items-center gap-7">
<div className="flex min-h-[10rem] flex-wrap items-center justify-center gap-4">
{hand.length === 0 ? (
<div className="font-mono text-sm opacity-50">deal a hand</div>
) : (
hand.map((c, i) => (
<div key={`${c.rank}-${c.suit}-${i}`} className="w-28">
<PlayingCard card={c} />
</div>
))
)}
</div>
<div className="flex items-center gap-2">
<Win98Button onClick={deal} className="h-10 min-w-28 px-6 text-sm">
Deal
</Win98Button>
<Win98Button onClick={() => setHandSize((n) => (n % 7) + 1)} aria-label="hand size">
{handSize} cards
</Win98Button>
<Win98Button onClick={reshuffle}>Reshuffle</Win98Button>
</div>
<div className="font-mono text-[11px] uppercase tracking-[0.12em] opacity-45">
{deck.length} cards left in the deck
</div>
</div>
);
}
cards (deck + shuffle)view on GitHub ↗
Standard 52-card deck + Fisher–Yates shuffle, shared across the card games.
/* ──────────────────────────────────────────────────────────────────────────
lib/cards.ts — a standard 52-card deck + shuffle.
Shared across the card games (Hi-Lo, Red-Black, Card Dealer…). Pure data +
functions, no React — importable from client or server. Ace is high (value
14) so Hi-Lo "higher/lower" reads naturally.
────────────────────────────────────────────────────────────────────────── */
export type Suit = 'spades' | 'hearts' | 'diamonds' | 'clubs';
export type Rank = 'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K';
export interface Card {
rank: Rank;
suit: Suit;
/** Comparable value, Ace high: 2–10, J=11, Q=12, K=13, A=14. */
value: number;
/** True for hearts/diamonds. */
red: boolean;
}
export const SUITS: Suit[] = ['spades', 'hearts', 'diamonds', 'clubs'];
export const RANKS: Rank[] = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
const RANK_VALUE: Record<Rank, number> = {
'2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10,
J: 11, Q: 12, K: 13, A: 14,
};
/** A fresh, ordered 52-card deck. */
export function buildDeck(): Card[] {
const deck: Card[] = [];
for (const suit of SUITS) {
for (const rank of RANKS) {
deck.push({
rank,
suit,
value: RANK_VALUE[rank],
red: suit === 'hearts' || suit === 'diamonds',
});
}
}
return deck;
}
/** Fisher–Yates shuffle into a new array (uses Math.random — client/runtime). */
export function shuffle<T>(input: T[]): T[] {
const arr = [...input];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
/** A freshly shuffled 52-card deck. */
export function shuffledDeck(): Card[] {
return shuffle(buildDeck());
}