A real-time multiplayer web implementation of the Boom card game, built with RoboJS and React.
Boom is a strategic card game where players eliminate opponents by destroying their life storage cards. The last player standing wins!
Contribute your AI! Anyone is more than welcome to develop new AI strategies and make a PR to add them. See the Developing AI Strategies section below for details.
Want to just run the game? Here's the fastest way:
- Install Node.js: Download from nodejs.org (choose the LTS version).
- Download this project: Click the green "Code" button above → "Download ZIP" → Extract it.
- Open a terminal/command prompt in the extracted folder.
- Run these commands:
npm install
npm run dev- Open your browser and go to
http://localhost:3000. - Share the tunnel URL (shown in your terminal) with friends to play together!
That's it! 🎉
Boom is a tactical card game for 2 or more players where the last one standing wins.
- Goal: Be the last player with life storage cards ("accumulators") remaining.
- Setup: Each player starts with 3 cards in hand and 3 accumulator cards on the board.
- Gameplay: On your turn, you can swap cards, attack opponents, use a powerful "Boom" ability, or discard.
- Cards: Numbered cards store life and deal damage, while special cards (value 0) enable the "Boom" action.
📖 Full Rules:
- Digital Game Help Page (in-game rules for the web version)
- Physical Version: English Rules
- Physical Version: Spanish Rules
- Node.js v22 or newer
- npm (comes with Node.js)
- A modern web browser
- (Optional) Google Cloud Platform account for deployment
- Clone the repository:
git clone https://github.com/guplem/boom.git
cd boom- Install dependencies:
npm install- Start the development server:
npm run devThis will start the server at http://localhost:3000/ and provide a public tunnel URL for multiplayer testing.
This project uses a modern web stack to create a real-time, interactive experience.
- Frontend: React, TypeScript, Vite
- Backend: RoboJS Server, Node.js
- Real-time State Sync:
@robojs/syncfor multiplayer state. - Client-side State: Zustand for persistent local state.
- Backend Key-Value Store: Flashcore for ephemeral server-side data.
- Styling: Plain CSS with modern features.
- Code Quality: ESLint, Prettier.
The application is structured around a clear separation of concerns: room management, lobby setup, and the core game loop. State management is key to its real-time functionality.
- Entry Point (
index.tsx->App.tsx): The app initializes, setting up React Router and theSyncContextProvider. - User Identification: On first load,
UserStore(a Zustand store) generates and persists a unique ID for the browser session inlocalStorage. This ID is used to determine ownership of players. - Room Handling (
App.tsx):
- The app checks the URL for a
?room=parameter. If present, it validates the room's existence via aHEADrequest to/api/roomand automatically joins it. - If no room is joined, the
RoomPageis displayed, offering to create or join a room. RoomCreatorandRoomPickercomponents handle API calls to the backend (src/api/room.ts), which usesFlashcoreto manage the list of available rooms.- Once a room is joined,
RoomStore(another Zustand store) saves the room name, and the app transitions to theGamePage.
- Synchronized State: In the lobby, the
playersarray is the first piece of state synchronized by@robojs/sync'suseSyncStatehook. Any player added or removed by one client is instantly reflected on all other clients in the same room. - Player Creation: The
PlayerCreationFormallows users to add players.
- A user can create multiple AI players but only one human player (enforced by
validatePlayerAdditioninplayer/manager.ts). - Each player is assigned an owner ID matching the user's session ID.
- Starting the Game: The "Start Game" button calls
gameContext.startGame. This function initializes the maingameobject, which is also a synchronized state viauseSyncState. The app then transitions from the lobby view to the game board.
The game does not run on a traditional timer-based loop. Instead, it's entirely event-driven, reacting to changes in the shared game state. The GamePage.tsx component orchestrates this flow.
- UI Enablement: The
GameBoardPage.tsxcomponent renders the main interface. It checks if the current player's ID matches the human player's ID and enables/disables controls accordingly. - Action Selection:
- The user clicks a card in their hand (
GameHandCard.tsx), which is stored in a localuseStatehook (handSelected). - The user then clicks a valid target (an opponent's accumulator for an attack, or their own for a swap).
- Executing the Action: This UI interaction calls
gameContext.executeAction, passing anActionConfigobject that defines the move (e.g.,{ action: 'attack', params: { ... } }).
- The Trigger: The
useEffecthook inGamePage.tsxlistens for any changes to the synchronizedgameobject. - Turn Check: After each state update, this
useEffectchecks if the game is running and if the current player (getCurrentPlayer(game)) is an AI owned by the current user. - Strategy Execution: If it's the AI's turn,
executeAiStrategy(ai/manager.ts) is called.
- It builds a
Scenarioobject (a complete, read-only snapshot of the game). - It passes this
Scenarioto the AI's selected strategy function (e.g.,randomAttackStrategy). - The strategy function analyzes the
Scenarioand returns its desiredActionConfig. executeAiStrategythen calls the samegameContext.executeActionfunction that a human player uses.
This is the heart of the game's rules engine, located in src/app/modules/game/manager.ts.
- Central Hub (
executeAction): All actions, whether from a human or AI, are processed here. - Validation (
getNextGameState):
- This pure function is the core of the game logic. It takes the current game state and an action.
- It performs all necessary validation (Is it the player's turn? Is the move legal? e.g., "Can't attack with a card value higher than the accumulator's HP?").
- If the action is valid, it returns a new, updated game state. If invalid, it returns
null.
- State Update:
- If
getNextGameStatereturns a new state,executeActioncommits it usingsetGame(). @robojs/syncdetects this change and instantly broadcasts the newgamestate to all connected clients.
- Turn Advancement (
advanceToNextTurn):
- After a valid action, this function is called.
- It checks for win/draw conditions by seeing how many players have > 0 HP. If the game ends, it sets the
winnerId. - Otherwise, it increments the
turncounter, skipping any eliminated players until it finds the next living player.
- The Loop Continues: This change to the
turnnumber (orwinnerId) is part of the new game state. TheuseEffectinGamePage.tsxdetects this change, and the cycle begins again.
This event-driven architecture ensures that the game state is always the single source of truth, and the UI and AI players simply react to its changes.
This project is pre-configured for deployment to Google Cloud Run.
- Update service name in
cloudbuild.yaml:substitutions: _SERVICE_NAME: boom-card-game # Or your preferred service name
- Follow the official RoboJS Cloud Run deployment guide.
- Pushes to the
mainbranch will trigger automatic deployments.
Contributions are welcome! This is an evolving project with many opportunities for improvement.
The game includes a simple, extensible AI system. You can easily create and add your own strategies.
-
Create a Strategy File: Add a new file in
src/app/modules/ai/strategies/. -
Define the Strategy Function: Export a function that accepts a
Scenarioobject and returns anActionConfig. TheScenariogives you a complete snapshot of the game state.// src/app/modules/ai/strategies/myCleverStrategy.ts import { Scenario } from '@/app/modules/ai/model'; import { ActionConfig, ActionTypes } from '@/app/modules/game/model'; export const myCleverStrategy = (gameScenario: Scenario): ActionConfig => { // TODO: Implement your brilliant AI logic here! // Analyze the board, hand, and history to make a decision. // Return the chosen action return { action: ActionTypes.Attack, params: { /* ... */ } }; };
-
Register Your Strategy: Import and add your strategy to the
strategiesListinsrc/app/modules/ai/strategies.ts.// src/app/modules/ai/strategies.ts import { myCleverStrategy } from './strategies/myCleverStrategy'; export const strategiesList = [ // ... existing strategies { name: 'My Clever Strategy', description: 'A brief description of what your AI does.', getActionFunction: myCleverStrategy, maxAttempts: 20 // Optional: Retries if the AI returns an invalid move. }, ];
-
Test It: Run
npm run devand select your new strategy from the dropdown in the game lobby.
Note on Robustness: The game manager will try to execute the action returned by your AI. If it's invalid, it will retry. The system also has a fallback mechanism (executeFallbackAction in ai/manager.ts) that will perform a safe "discard" action if the AI fails to produce a valid move after all attempts, ensuring the game never gets stuck.