A real-time multiplayer web app for the Wizard card game by Ken Fisher (1984). Players create or join rooms, bid on tricks, and play through a full game β with live score tracking and end-game stats.
Wizard is a trick-taking game for 3β6 players. The deck has 60 cards: the standard 52 plus 4 Wizards and 4 Jesters.
The number of rounds depends on the player count:
| Players | Rounds |
|---|---|
| 3 | 20 |
| 4 | 15 |
| 5 | 12 |
| 6 | 10 |
In round N, each player is dealt N cards. The dealer rotates each round.
After dealing, the top remaining card is flipped to determine trump:
- Standard card β that suit is trump
- Jester β no trump this round
- Wizard β the dealer chooses any suit as trump
- Last round β all cards are dealt; no trump card
Starting left of the dealer, each player bids how many tricks they expect to win. There is no restriction on the total bids β they may or may not equal the number of tricks available.
- The player left of the dealer leads the first trick
- Players must follow the led suit if possible
- Wizards and Jesters may be played at any time, even if you can follow suit
- If the led card is a Jester, the next non-Jester card sets the led suit
Trick winner (in priority order):
- The first Wizard played wins
- The highest trump card wins (if any trump was played)
- The highest card of the led suit wins
- If all cards are Jesters, the first Jester played wins
The winner of each trick leads the next one.
| Result | Points |
|---|---|
| Exact bid | +20 + (10 Γ tricks won) |
| Wrong bid | β10 Γ |tricks won β bid| |
Scores can go negative.
Examples:
- Bid 2, win 2 β +40
- Bid 3, win 1 β β20
- Bid 0, win 0 β +20
The player with the highest cumulative score after all rounds wins.
wizard/
βββ package.json # Root workspace (npm workspaces)
βββ server/ # Node.js + Express + Socket.io backend
β βββ index.js # Server entry point
β βββ src/
β βββ constants.js # GamePhase enum, error codes
β βββ deck.js # 60-card deck builder + Fisher-Yates shuffle
β βββ trickResolver.js # Trick winner resolution logic
β βββ scoring.js # Round scoring + bid validation
β βββ statsTracker.js # Per-player and global stat accumulation
β βββ GameEngine.js # Pure game state machine
β βββ RoomManager.js # In-memory room registry
β βββ GameRoom.js # Socket event handler per room
β βββ __tests__/ # Vitest unit tests
βββ client/ # React + Vite + Tailwind CSS frontend
β βββ src/
β βββ App.jsx # Phase-based screen router
β βββ socket.js # Singleton Socket.io client
β βββ store/ # Zustand global store
β βββ hooks/ # useSocket (eventβstore), useGameState
β βββ components/
β β βββ screens/ # Lobby, WaitingRoom, GameScreen, EndScreen
β β βββ game/ # PlayerHand, TrickArea, BidPanel, TrumpPicker, β¦
β β βββ ui/ # Button, Modal, RoomCodeDisplay
β βββ utils/ # cardHelpers (labels, colors, legal-play check)
β βββ __tests__/ # Vitest unit tests
βββ e2e/ # Playwright end-to-end tests
βββ playwright.config.ts
βββ tests/
βββ lobby.spec.ts # Room creation, joining, validation
βββ game-flow.spec.ts # 3-player game: hand display, bidding, trick play
βββ end-screen.spec.ts# Final scores, stats tab, round history
- Node.js v18 or later
- npm v8 or later
npm installnpm run devThis starts both servers concurrently:
- Server β
http://localhost:3001 - Client β
http://localhost:5173
Open http://localhost:5173 in multiple browser tabs or windows to play with multiple players locally.
- Open
http://localhost:5173in three browser tabs - In tab 1: enter a name β Create Room β note the 4-character room code
- In tabs 2 & 3: enter a name β Join Room β enter the room code
- Back in tab 1 (the host): click Start Game
- Each tab shows only that player's private hand β play proceeds in turn order
Unit tests cover all pure game logic on the server and client-side utilities.
Run all unit tests:
npm testThis runs vitest run in both the server and client workspaces.
Run server tests only:
npm test --workspace=serverRun client tests only:
npm test --workspace=clientServer (server/src/__tests__/)
| File | Coverage |
|---|---|
deck.test.js |
Deck composition (60 cards, 4W/4J/52 standard), suit counts, unique IDs, shuffle correctness, deal correctness |
scoring.test.js |
Exact bid scores, over/underbid penalties, negative scores, bid validation |
trickResolver.test.js |
All trick resolution rules: Wizard priority, first-of-multiple Wizards, highest trump, led suit, all-Jesters, Jester lead edge cases |
GameEngine.test.js |
Legal card determination (follow suit, void, specials always legal), round dealing, full 1-card round flow, turn enforcement |
statsTracker.test.js |
Exact bid counting, overbid/underbid tracking, exact-bid streak tracking, suit distribution, round history |
Client (client/src/__tests__/)
| File | Coverage |
|---|---|
cardHelpers.test.js |
Card labels, color classes, getLegalCards (client-side mirror of server logic) |
gameStore.test.js |
All store mutations: room events, game lifecycle (started β round β bids β tricks β end), error state |
Total: 99 unit tests
E2e tests spin up the full dev server and run multi-browser scenarios.
Install Playwright browsers (first time only):
npx playwright install --only-shell chromium
# or, from the e2e workspace:
npm run test:e2e --workspace=e2e -- --helpRun all e2e tests:
npm run test:e2eRun with a visible browser (headed mode):
cd e2e && npx playwright test --headedOpen the interactive Playwright UI:
cd e2e && npx playwright test --uilobby.spec.ts
- Creating a room shows a 4-character code
- Create button is disabled when no name is entered
- Invalid room code shows an error
- Join button disabled until both name and code are filled
- Two players see each other in the waiting room
- Host sees the Start Game button; non-host sees a waiting message
- Cannot start with fewer than 3 players (button disabled)
game-flow.spec.ts
- 3 players start a game and each see their private hand (1 card in round 1)
- Trump display is visible to all players after dealing
- Bid panel appears for the player whose turn it is to bid
- After all bids, the active player's cards are highlighted; playing a card updates the trick area
end-screen.spec.ts (plays a complete 20-round game)
- Final scoreboard lists all players with cumulative scores
- Winner is highlighted
- Stats tab shows per-player accuracy, wizard/jester counts, and suit distribution
- Round History tab shows all 20 rounds with bid/actual/delta per player
- "Play Again" resets to the lobby
Note: The end-screen tests run a full 20-round game and have a 120-second timeout. They may take 60β90 seconds to complete.
All game state lives on the server. Clients receive events and update a Zustand store. No client-to-client communication occurs directly.
Client A ββemit('playCard')βββΊ Server ββemit('CARD_PLAYED')βββΊ All clients
Private information (hands) is sent only to the owning socket. All other state is broadcast to the room.
WAITING β DEALING* β [TRUMP_CHOICE β] BIDDING
β TRICK_LEAD β TRICK_FOLLOW β TRICK_RESOLVE*
β (more tricks) β TRICK_LEAD
β (round done) β ROUND_SCORE* β DEALING* (next round)
β (game over) β GAME_OVER
* = server-internal transitions; clients see results via socket events.
| Direction | Event | Description |
|---|---|---|
| CβS | createRoom / joinRoom |
Lobby setup |
| CβS | startGame |
Host only |
| SβC | ROUND_STARTED |
Private hand delivery (per socket) |
| SβC | TRUMP_REVEALED |
Broadcast trump card/suit |
| CβS | chooseTrump |
Dealer picks suit after Wizard flip |
| CβS | placeBid |
Active bidder submits bid |
| CβS | playCard |
Active player plays a card |
| SβC | TRICK_WON |
Announces winner, updates trick counts |
| SβC | ROUND_ENDED |
Scores + stats broadcast |
| SβC | GAME_ENDED |
Final scores, stats, winner |