Skip to content
 
 

Repository files navigation

Cards Against Humanity -- Web Game

A real-time multiplayer Cards Against Humanity web game built with Node.js, Express, Socket.IO, and MariaDB. Players create or join games via a shareable 6-character code, take turns as the Card Czar, and compete to reach the target score. Supports custom decks via CSV upload and handles player disconnection/reconnection gracefully.


Table of Contents

  1. Architecture Overview
  2. Project Structure
  3. Database Schema
  4. Game Flow
  5. Socket.IO Event Reference
  6. REST API Reference
  7. Reconnection Logic
  8. Deck Management
  9. Game Cleanup
  10. Setup & Running
  11. Configuration
  12. Known Issues & Future Work

Architecture Overview

The application follows a layered architecture:

  • Frontend -- Vanilla HTML, CSS, and JavaScript served as static files. Two pages: a landing page (index.html) for creating/joining games and a game page (game.html) that handles the lobby, active play, and game-over phases.
  • Transport -- Socket.IO provides real-time bidirectional communication for all in-game events. REST endpoints handle game creation, deck management, and state retrieval.
  • Server -- Express handles HTTP routing. Socket.IO event handlers are split into lobby and game modules. Business logic lives in service modules (cardService, gameService, csvService).
  • Database -- MariaDB accessed via the mysql2 connection pool. Schema comprises 8 tables covering decks, cards, games, players, hands, submissions, and the discard pile.

See docs/game-lifecycle.mmd and docs/socket-event-flow.mmd for visual diagrams.


Project Structure

cah-game/
├── server.js                  — Express + Socket.IO entry point
├── package.json
├── .env.example               — DB credentials template
├── db/
│   ├── init.sql               — Schema creation
│   ├── seed.sql               — Base deck (25 black, 60 white cards)
│   └── connection.js          — mysql2 pool setup from env vars
├── routes/
│   ├── decks.js               — Deck CRUD + CSV upload
│   └── games.js               — Game creation/state
├── sockets/
│   ├── index.js               — Socket.IO event handler registration + disconnect handling
│   ├── lobby.js               — Join/leave/settings/reconnect
│   └── game.js                — Round logic, card dealing, scoring
├── services/
│   ├── cardService.js         — Draw cards, manage hands, discard pile
│   ├── gameService.js         — Game state transitions, round management, cleanup
│   └── csvService.js          — CSV parsing + validation
├── public/
│   ├── index.html             — Landing page
│   ├── game.html              — Game page (lobby + playing + game over)
│   ├── css/
│   │   └── style.css          — Dark theme, responsive, card aesthetics
│   └── js/
│       ├── main.js            — Landing page logic
│       ├── game.js            — Socket.IO client + game UI
│       └── cards.js           — Card rendering helpers
├── scripts/
│   └── import-json.js         — Import JSON card datasets into DB
├── uploads/                   — Temp directory for CSV uploads
└── docs/                      — Mermaid architecture diagrams

Database Schema

The database uses 8 tables. See docs/database-er.mmd for the full entity-relationship diagram.

Table Purpose
decks Stores deck metadata (name, description, default flag)
cards Individual cards belonging to a deck (type, text, pick count)
games Active game instances (code, status, round, settings)
game_decks Many-to-many link between games and their selected decks
players Players in a game (nickname, score, connection state, czar order)
player_hand Cards currently in each player's hand
round_submissions Cards played by each player in the current round
discard_pile Used cards awaiting potential reshuffle

Key relationships:

  • A deck contains many cards.
  • A game uses many decks (via game_decks).
  • A game has many players.
  • A player has many cards in their hand (player_hand).
  • Each round, players create submissions (round_submissions).
  • Used cards move to the discard pile (discard_pile).

Game Flow

See docs/game-lifecycle.mmd for the state diagram.

  1. Create -- A player creates a game via the REST API, receiving a 6-character game code.
  2. Lobby -- Players join using the code. The host configures deck selection and the maximum points target (5, 7, 10, or 15).
  3. Start -- The host starts the game (minimum 3 players required).
  4. Round Begin -- A black card is drawn, the Card Czar rotates to the next player, and all players' hands are topped up to 10 cards.
  5. Submission -- Non-czar players select and submit white cards matching the black card's pick count. A 90-second timer enforces play; on timeout, random cards are auto-played from the player's hand.
  6. Reveal & Judging -- Submissions are shuffled and revealed to all players. The Card Czar picks the winning submission.
  7. Scoring -- The winner receives a point. If their score reaches the target, the game ends. Otherwise, a new round begins.
  8. Game Over -- Final scores are displayed. Players may vote to play again, returning to the lobby.

Socket.IO Event Reference

Client to Server

Event Payload Description
join_game { gameCode, playerName } Join a game lobby (also re-emitted on reconnect)
start_game -- Host starts the game (min 3 players)
play_cards { cardIds } Submit white cards for the round
czar_pick_winner { playerId } Card Czar selects the winning submission
play_again -- Vote to return to lobby for another game
update_settings { gameCode, maxPoints } Host updates game settings
toggle_deck { gameCode, deckId, enabled } Host toggles a deck on or off

Server to Client

Event Payload Description
joined { playerId, gameCode, playerName } Confirmation of successful join
lobby_update { players, settings, decks } Lobby state broadcast
game_started { game } Game has begun
new_round { blackCard, czar, hand, round } New round details
player_played { playerId } A player has submitted cards
all_played { submissions } All submissions in (shuffled)
round_winner { winnerName, winnerPlayerId, winningCards, allSubmissions, scoreboard } Czar's pick result
game_over { scoreboard } Final game results
return_to_lobby { game } Returning to lobby for new game
player_joined { player } New player entered lobby
player_disconnected { playerName } Player lost connection
player_reconnected { playerName } Player regained connection
reconnect_state { phase, hand, ... } Full state restore for reconnecting player
error { message } Error notification

See docs/socket-event-flow.mmd for a sequence diagram of a complete round.


REST API Reference

All endpoints are prefixed with /api.

Decks

Method Endpoint Description
GET /api/decks List all decks with card counts. Sorted by: default decks first, then by popularity (number of games using the deck), then alphabetically by name.
POST /api/decks/upload Upload a new deck via CSV. Multipart form data with fields: name, description, file.
DELETE /api/decks/:id Delete a custom deck. Default decks cannot be deleted.

Games

Method Endpoint Description
POST /api/games Create a new game with selected deck IDs. Returns { code }.
GET /api/games/:code Retrieve full game state (used for reconnection).

Reconnection Logic

See docs/reconnection-flow.mmd for the flowchart.

Socket.IO may silently reconnect at the transport level (e.g. due to ping timeouts), assigning the client a new socket.id. To handle this, the client re-emits join_game on every socket.on('connect') event, ensuring the server always has the correct socket ID for each player. This prevents stale socket IDs from causing silent handler failures.

When a player disconnects:

  1. The server marks is_connected = FALSE in the players table.
  2. A player_disconnected event is broadcast to remaining players.
  3. If fewer than 3 players remain connected, a warning is emitted.
  4. If the disconnected player is the Card Czar, a 120-second reconnection timer starts.

When a player reconnects within the window:

  1. The server restores is_connected = TRUE and rebinds the new socket ID.
  2. Any pending Card Czar disconnect timer for the player is cancelled.
  3. A reconnect_state event is sent to the returning player with full game state: current phase, their hand, whether they have already played, whether they are the Czar, and (during the reveal phase) the shuffled submissions.
  4. A player_reconnected event is broadcast to other players.

If the reconnection window expires:

  • Regular player: The player is removed from the game. Their hand cards are discarded.
  • Card Czar: A random winner is automatically selected from the current submissions after the 120-second timeout.

If a non-czar player fails to submit cards within the 90-second round timer, random cards are auto-played from their hand.


Deck Management

Default Deck

The base deck is seeded via db/seed.sql and contains 25 black cards and 60 white cards. Default decks cannot be deleted.

Custom Decks via CSV

Upload custom decks through POST /api/decks/upload. The CSV format:

card_type,text,pick
white,"A windmill full of corpses",1
black,"Why can't I sleep? _.",1
black,"_ + _ = _.",3
Column Values Description
card_type white or black The card colour
text Any string Card text. Underscores (_) denote blanks on black cards.
pick Integer (1--3) Number of white cards to play against this black card

The CSV is validated by csvService.js before insertion.

JSON Import

The scripts/import-json.js utility imports card datasets in JSON format directly into the database.

In-Lobby Deck Selection

During the lobby phase, the host can toggle decks on or off using the toggle_deck socket event. At least one deck must be selected before starting.


Game Cleanup

See docs/cleanup-flow.mmd for the flowchart.

An automated cleanup routine runs every 5 minutes and also on server startup. It removes abandoned or stale games based on three rules:

Game State Condition Timeout
Playing No connected players 30 minutes
Lobby Idle (no activity) 60 minutes
Finished Idle after game over 10 minutes

Cleanup deletes the game record and all associated data (players, hands, submissions, discard pile) via cascading deletes or explicit cleanup queries.


Setup & Running

Prerequisites

  • Node.js (v16 or later)
  • MariaDB (or MySQL-compatible server)

Installation

# Clone the repository
git clone <repository-url>
cd cah-game

# Install dependencies
npm install

# Configure environment
cp .env.example .env
# Edit .env with your database credentials

Database Setup

# Create the database and tables
mysql -u root -p < db/init.sql

# Seed the default deck
mysql -u root -p your_database < db/seed.sql

Running

# Start the server
node server.js

The server starts on the port specified by the PORT environment variable (default behaviour depends on your .env). Open http://localhost:<PORT> in a browser to play.

Importing Additional Card Sets

node scripts/import-json.js path/to/cards.json

Configuration

Environment Variables

Set these in your .env file (see .env.example):

Variable Description
DB_HOST Database host address
DB_PORT Database port
DB_USER Database username
DB_PASSWORD Database password
DB_NAME Database name
PORT HTTP server port
HOST HTTP server host/bind address

In-Code Configurable Values

Setting Default Location
Hand size 10 cards services/cardService.js (fillPlayerHand)
Round timer 90 seconds sockets/game.js
Reconnect timeout 120 seconds sockets/index.js
Cleanup interval 5 minutes server.js
Max points options 5, 7, 10, 15 sockets/lobby.js
Game code length 6 characters services/gameService.js

Error Handling & Debug Logging

Server-Side Logging

All socket event handlers use structured logging with a [handler_name] prefix that includes the game code, socket ID, and player identity where available. Example:

[czar_pick_winner] game=ABC123 socket=xyz123 winnerId=42
[disconnect] socket=xyz123
[startNewRound] game=ABC123

Every catch block both logs the error to the server console and emits an error event to the relevant client(s) so failures are never silent. Room-wide errors (e.g. startNewRound failure) are broadcast to all players in the game.

Client-Side Debug Logging

The game client includes a debug logging system gated by a DEBUG flag at the top of public/js/game.js. When enabled, all major socket events are logged to the browser console with a [CaH] prefix for easy filtering:

[CaH] connected — socket.id: abc123
[CaH] new round: 3, czar: Player1, black card: "Why can't I sleep? _."
[CaH] round winner: Player2

Socket lifecycle events (disconnect, transport errors, reconnect attempts) are also logged and surfaced to the player via toast notifications.

Error Visibility

All error events from the server are displayed to the player as toast notifications. This includes session errors ("Session not recognised — please refresh the page"), game state errors ("Game is not in progress"), and infrastructure failures ("Failed to start next round — please refresh the page").


Known Issues & Future Work

Known Issues

  • No authentication or user accounts -- players are identified solely by nickname within a game session.
  • No rate limiting on the REST API or Socket.IO events.
  • The CSV upload stores files temporarily in uploads/ without automatic cleanup of failed uploads.
  • No HTTPS configuration out of the box -- a reverse proxy (e.g. Nginx) is recommended for production.

Future Work

  • Spectator mode -- Allow users to watch a game without participating.
  • Persistent player accounts -- Optional sign-in with game history and statistics.
  • Card favourites -- Let players mark favourite submissions during a game.
  • Kick/ban players -- Host moderation tools for the lobby.
  • Additional card types -- Support for draw-2 and other special card mechanics.
  • Mobile-optimised UI -- Further responsive improvements for small screens.
  • Automated testing -- Unit and integration tests for services and socket handlers.
  • Configurable timers -- Allow the host to adjust round and reconnection timeouts from the lobby.
  • Server-side session tokens -- Replace name-based reconnection with a session token to prevent impersonation.

About

Cards against Humanity for freinds

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages