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 dev
This 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/sync
for 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 aHEAD
request to/api/room
and automatically joins it. - If no room is joined, the
RoomPage
is displayed, offering to create or join a room. RoomCreator
andRoomPicker
components handle API calls to the backend (src/api/room.ts
), which usesFlashcore
to 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
players
array is the first piece of state synchronized by@robojs/sync
'suseSyncState
hook. Any player added or removed by one client is instantly reflected on all other clients in the same room. - Player Creation: The
PlayerCreationForm
allows users to add players.
- A user can create multiple AI players but only one human player (enforced by
validatePlayerAddition
inplayer/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 maingame
object, 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.tsx
component 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 localuseState
hook (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 anActionConfig
object that defines the move (e.g.,{ action: 'attack', params: { ... } }
).
- The Trigger: The
useEffect
hook inGamePage.tsx
listens for any changes to the synchronizedgame
object. - Turn Check: After each state update, this
useEffect
checks 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
Scenario
object (a complete, read-only snapshot of the game). - It passes this
Scenario
to the AI's selected strategy function (e.g.,randomAttackStrategy
). - The strategy function analyzes the
Scenario
and returns its desiredActionConfig
. executeAiStrategy
then calls the samegameContext.executeAction
function 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
getNextGameState
returns a new state,executeAction
commits it usingsetGame()
. @robojs/sync
detects this change and instantly broadcasts the newgame
state 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
turn
counter, skipping any eliminated players until it finds the next living player.
- The Loop Continues: This change to the
turn
number (orwinnerId
) is part of the new game state. TheuseEffect
inGamePage.tsx
detects 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
main
branch 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
Scenario
object and returns anActionConfig
. TheScenario
gives 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
strategiesList
insrc/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 dev
and 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.