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.


RedBlackGameview on GitHub ↗

Reveals the next card and compares its colour to your call; streak grows or resets, best persists to localStorage. Reshuffles when the deck empties.

'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';

const KEY = 'kaspirius:red-black:best';

interface State {
  deck: Card[];
  idx: number;
  streak: number;
  best: number;
  result: 'win' | 'lose' | null;
}

export function RedBlackGame() {
  const [game, setGame] = React.useState<State | null>(null);

  React.useEffect(() => {
    const best = Number(localStorage.getItem(KEY)) || 0;
    setGame({ deck: shuffledDeck(), idx: 0, streak: 0, best, result: null });
  }, []);

  React.useEffect(() => {
    if (game) localStorage.setItem(KEY, String(game.best));
  }, [game?.best]); // eslint-disable-line react-hooks/exhaustive-deps

  const guess = (red: boolean) => {
    setGame((prev) => {
      if (!prev) return prev;
      let { deck } = prev;
      const nextIdx = prev.idx + 1;
      if (nextIdx >= deck.length) deck = [...deck, ...shuffledDeck()];
      const next = deck[nextIdx];
      const correct = next.red === red;
      const streak = correct ? prev.streak + 1 : 0;
      return { deck, idx: nextIdx, streak, best: Math.max(prev.best, streak), result: correct ? 'win' : 'lose' };
    });
  };

  const current = game ? game.deck[game.idx] : null;
  const ring =
    game?.result === 'win' ? 'shadow-[0_0_0_3px_var(--accent-olive)]'
    : game?.result === 'lose' ? 'shadow-[0_0_0_3px_var(--accent-red)]' : '';

  return (
    <div className="flex flex-col items-center gap-7">
      <div className="flex gap-12 font-mono">
        <div className="text-center">
          <div className="text-5xl font-bold leading-none">{game?.streak ?? 0}</div>
          <div className="mt-2 text-[11px] uppercase tracking-[0.12em] opacity-55">streak</div>
        </div>
        <div className="text-center">
          <div className="text-5xl font-bold leading-none">{game?.best ?? 0}</div>
          <div className="mt-2 text-[11px] uppercase tracking-[0.12em] opacity-55">best</div>
        </div>
      </div>

      <div className={`w-56 rounded-[9px] ${ring}`}>
        <PlayingCard card={current} faceDown={!game} />
      </div>

      <div className="h-5 font-mono text-[13px] tracking-wide opacity-75">
        {game?.result === 'win' ? 'correct' : game?.result === 'lose' ? 'wrong — reset' : 'red or black?'}
      </div>

      <div className="flex gap-3">
        <Win98Button onClick={() => guess(true)} disabled={!game} className="h-9 min-w-24 px-5">
          ♥ Red
        </Win98Button>
        <Win98Button onClick={() => guess(false)} disabled={!game} className="h-9 min-w-24 px-5">
          ♠ Black
        </Win98Button>
      </div>
    </div>
  );
}
cards (deck + shuffle)view on GitHub ↗

Shared 52-card deck + Fisher–Yates shuffle; each card has a red/black flag.

/* ──────────────────────────────────────────────────────────────────────────
   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());
}
↖ kaspirius