This project implements Exploding Kittens using React (frontend) and boardgame.io (game engine).
src/common/: Shared game logic, state definitions, and move implementations. Start here to understand game mechanics.src/client/: React frontend.src/server/:boardgame.iogame server.
The project uses a custom wrapper pattern over standard boardgame.io state (G and ctx):
TheGameClass (src/common/entities/game.ts): Wraps the raw context. Always instantiate this to interact with game state.IGameState(src/common/models/game-state.model.ts): Defines the shape ofG(piles, deck type, etc.).- Moves with
inGameHOC: All game moves are wrapped with theinGamehigher-order function (src/common/moves/in-game.ts).- Pattern: Define moves as
(game: TheGame, ...args) => void. The HOC handles context injection. - Example: See
src/common/moves/draw-move.ts.
- Pattern: Define moves as
The React app accesses game state via GameContext (src/client/context/GameContext.tsx).
TheGameClient: ExtendsTheGamewith client-specific features (match ID, chat, multiplayer flags).- Use
useGame()hook to access the current game instance in components.
The project requires two concurrent processes:
- Game Server:
npm run server:dev(Runs onhttp://localhost:51399usually, configurable) - Client:
npm run dev(Runs onhttp://localhost:5173)
- Client:
npm run build - Server:
npm run build:server
- Currently, there is no automated test suite. Verification is manual via playing the game in browsers.
The codebase strictly separates raw state from game logic using an OOP wrapper pattern:
- Models (
src/common/models/): Pure TypeScriptinterfaces prefix withI(e.g.,ICard,IGameState,IPlayer). These represent the plain JSON-serializable state strictly managed byboardgame.io. Never add methods here. - Entities (
src/common/entities/): Object-Oriented wrapper classes (e.g.,Card,TheGame,Player). They take the raw state interface in their constructor and provide getter/setter and mutation logic.- Rule: When mutating state in moves, always instantiate or access the Entity wrapper, execute methods on it, and let it safely update the underlying raw Model state. Do not mutate the
IPlayerorICardinterfaces directly.
- Rule: When mutating state in moves, always instantiate or access the Entity wrapper, execute methods on it, and let it safely update the underlying raw Model state. Do not mutate the
When implementing game moves (actions):
- Create a function in
src/common/moves/. - Function signature must be
(game: TheGame, ...args). - Mutate state via
game.piles,game.players, orgame.gameState. - Register the move in
src/common/exploding-kittens.tsusinginGame(yourMove). - If the move transitions the game state, use constants from
src/common/constants/phases.tsorsrc/common/constants/stages.ts.
Example Move:
import { TheGame } from "../entities/game";
export const myMove = (game: TheGame, argument: string) => {
// Use entity helpers
if (game.players.actingPlayer.hasCard(argument)) {
game.players.actingPlayer.discard(argument);
}
};- Game View:
src/client/components/game-view/handles the main game screen. - Board:
src/client/components/board/handles the visual representation of the table.
docker-compose.ymlorchestrates both client and server containers.- Environment variables are managed in
stack.envand passed to containers.