Build Influence: Multiplayer Card Game
Build and deploy a realtime three-player card game with Firebase.
Introduction
30 Second Summary
A shared game night falls apart when one screen knows something the others do not. Hidden information only feels fair when every player reaches the reveal together.
In this project, you will build Influence, a three-round browser card game that keeps exactly three players synchronized. Each card displays its information type while concealing its strategic category until the round reveal.
What You'll Build
Three players open your public game on separate devices and finish round three with the same winner on every screen.
By the end of this project, you'll have:
- A shareable three-player game that opens on separate devices through one public link.
- Hidden-information cards that show Misinformation, Disinformation, or Malinformation before a choice while concealing Chaos, Money, or Influence until the reveal.
- A synchronized three-round scoreboard that applies the 15/10/5 scoring matrix and names the same winner on every screen.
- Secret Mission: Test whether the visible information type helps players while the hidden strategic category remains difficult to predict.
What do I need before starting?
You need a Google account plus your existing Windows computer with Visual Studio Code and Google Chrome.
The project walks you through Node.js, the Firebase CLI, and every cloud setup task from scratch.
Before We Start
Before the hands-on work begins, commit to the principle behind Influence: every card shows its information type while its strategic category stays hidden until scoring.
Prepare Your Windows Development Tools
Influence needs a dependable local server before its browser sessions can meet in one room. It also needs a deployment command for the public game.
You will use Windows PowerShell to prepare the toolchain. Node.js v26.13.0 LTS provides the JavaScript runtime.
The Firebase CLI connects your computer to Firebase. You will pin it to version 15.34.2 so every later command follows the same tool version.
In this step, get ready to:
- Prepare Node.js v26.13.0 LTS with npm.
- Authenticate your pinned Firebase CLI.
- Create the game folders in Visual Studio Code.
Install Node.js and verify npm
Node.js runs the local development tools used throughout this project. Its installer also provides npm for installing the Firebase CLI.
- Press the Windows key to open the Windows search bar.
- Type PowerShell into the search bar.
- Select Windows PowerShell from the search results.
- Check your current Node.js version and npm installation by running these commands:
node --version
npm --version
What Do These Checks Show?
- The first command prints the installed Node.js version.
- The second command prints the installed npm CLI version.
✔️ I see v26.13.0
Your Node.js runtime is ready. A version number from the second command confirms that npm is ready too.
ⓧ I see an older version
An older Node.js release does not match the runtime used in this project. Install the pinned LTS release before continuing.
- Visit the official Node.js download page.
- Download Node.js v26.13.0 LTS for Windows.
- Run the downloaded installer.
- Complete the installer using its default choices.
- Approve the Windows permission request if one appears.
- Close the current PowerShell window.
- Press the Windows key to open the Windows search bar.
- Type PowerShell into the search bar.
- Select Windows PowerShell from the search results.
- Confirm the installation by running these checks:
node --version
npm --version
What Confirms the Installation?
The first line should now report Node.js v26.13.0. The second line should report an npm version number.
ⓧ Command not found
A missing command means Node.js is not available in PowerShell. The official Windows installer adds Node.js plus npm to your machine.
- Visit the official Node.js download page.
- Download Node.js v26.13.0 LTS for Windows.
- Run the downloaded installer.
- Complete the installer using its default choices.
- Approve the Windows permission request if one appears.
- Close the current PowerShell window.
- Press the Windows key to open the Windows search bar.
- Type PowerShell into the search bar.
- Select Windows PowerShell from the search results.
- Confirm the installation by running these checks:
node --version
npm --version
What Confirms the Installation?
The first line should report Node.js v26.13.0. The second line should report an npm version number.
Still Seeing the Wrong Node.js Version?
- Close every PowerShell window so Windows can refresh the available commands.
- Open a new Windows PowerShell window through the Windows search bar.
- Run the version checks again.
Help me diagnose my Node.js version on Windows.
Install the Firebase CLI and sign in
The Firebase CLI handles project selection plus deployment from PowerShell. A global installation makes the firebase command available from your project folder.
- Check whether the Firebase CLI is already available by running this command:
firebase --version
What Does This Check Show?
The command prints the installed Firebase CLI version. This project uses version 15.34.2.
✔️ I see version 15.34.2
Your Firebase CLI matches the project version. You can continue to account authentication.
ⓧ I see an older version
The global package needs an update to the pinned release. npm replaces the older global package during installation.
- Install Firebase CLI version 15.34.2 globally by running this command:
npm install -g firebase-tools@15.34.2
What Does This Command Do?
npm installs the pinned Firebase CLI package for your Windows user. The global installation makes its command available outside a single project.
- Confirm the installed Firebase CLI version by running:
firebase --version
What Confirms the Update?
The output should now show 15.34.2.
ⓧ Command not found
The Firebase CLI has not been installed globally. npm can add the pinned package now that Node.js is ready.
- Install Firebase CLI version 15.34.2 globally by running this command:
npm install -g firebase-tools@15.34.2
What Does This Command Do?
npm downloads the pinned Firebase CLI package. The global installation makes its command available from PowerShell.
- Confirm the installed Firebase CLI version by running:
firebase --version
What Confirms the Installation?
The output should show 15.34.2.
Firebase CLI Installation Failing?
- Confirm that the Node.js checks from the previous substep still print version numbers.
- Restart PowerShell after the global installation completes.
- Run the Firebase CLI version check again.
Help me fix my Firebase CLI installation on Windows.
Firebase authentication uses your browser to connect the CLI with a Google account. The browser flow keeps your account credentials out of PowerShell.
- Start the Firebase authentication flow by running this command:
firebase login
What Does This Command Do?
The command connects your local machine to Firebase through a Google account. PowerShell waits for the browser authentication flow to finish.
- Complete the Google account sign-in in the browser.
- Return to PowerShell after the authentication flow confirms completion.
- Check your Firebase project access by running this command:
firebase projects:list
What Does This Check Prove?
The command lists the Firebase projects available to your authenticated account. A successful response confirms that the CLI can access Firebase.
That clears the account hurdle. Your computer can now use authenticated Firebase commands from the terminal.
Sign-In Not Completing?
- Confirm that your browser is signed into the intended Google account.
- Return to PowerShell to check whether the authentication command is still waiting.
- Run the authentication command again after closing an incomplete browser flow.
Help me complete Firebase CLI authentication on Windows.
Create the project folders in VS Code
The influence-game folder keeps every local project file together. Its empty public directory is reserved for the Firebase Hosting files created later.
- Move PowerShell to your Desktop by running this command:
Set-Location -Path "$HOME\Desktop"
Why Start on the Desktop?
PowerShell creates relative paths inside its current location. Starting on the Desktop makes the project folder easy to find in Windows.
- Create the influence-game folder and enter it by running these commands:
New-Item -ItemType Directory -Path ".\influence-game"
Set-Location -Path ".\influence-game"
What Do These Commands Do?
- The first command creates the local project folder on your Desktop.
- The second command changes PowerShell's current location to that folder.
- Create the empty Firebase Hosting directory by running this command:
New-Item -ItemType Directory -Path ".\public"
What Does This Command Create?
The command creates an empty public directory inside influence-game. Later steps place the browser game files here.
The next command opens the current folder in VS Code. It starts a desktop window while PowerShell remains available for later commands.
- Open the current project folder in VS Code by running this command:
code .
What Does This Command Do?
The command asks VS Code to open PowerShell's current folder. You should see influence-game as the open project.
VS Code Did Not Open?
- Restart PowerShell so it reloads the Windows command path.
- Run the VS Code folder command again from influence-game.
- Use Windows search to open VS Code if the command remains unavailable.
- Select the influence-game folder from your Desktop when VS Code asks for a folder.
Help me open my PowerShell project folder in VS Code.
✔️ Awesome, I've got everything!
Your empty project structure is ready for the Firebase configuration files in the next step.
ⓧ I'd like to double check the full code
No application code exists yet. Your project structure should contain exactly these folders:
- influence-game/: Created local project folder.
- public/: Created empty Firebase Hosting public directory.
Before you run the final checks, which two pinned version numbers do you expect PowerShell to print?
- Switch back to the PowerShell window from earlier.
- Verify the complete toolchain by running these commands:
node --version
npm --version
firebase --version
What Do the Final Checks Cover?
- The first command reports the Node.js runtime version.
- The second command reports the npm CLI version.
- The third command reports the Firebase CLI version.
You should see Node.js v26.13.0. You should also see an npm version plus Firebase CLI 15.34.2.
- Return to VS Code after the version checks pass.
- Select the Explorer icon in the left sidebar.
- Confirm that the empty public folder appears inside influence-game.
Your Windows toolchain is ready. Next up, you will create the shared Firebase backend that connects all three players.
Create Your Firebase Backend
Your Windows tools are ready. That setup now gives you a dependable path from local development to a deployed game.
Each browser keeps its own memory. Your game needs a shared source of truth, so this step creates a Firebase backend for player identities plus room state.
In this step, get ready to:
- Register the Influence Web app inside a new Spark-plan Firebase project.
- Enable anonymous players through Firebase Authentication.
- Connect the influence-game folder to Firebase Realtime Database plus Firebase Hosting.
Create the Firebase project and register the Web app
The Firebase project holds every managed service used by the game. Its registered Web app provides the configuration values that your browser code uses later.
Choosing a cloud plan can feel risky. The Spark plan needs no payment information.
- In the Firebase console, start a new project.
- Enter Influence Game as the project name.
- Keep the project on the Spark pricing plan.
- Skip Google Analytics for this prototype.
- Use the final project-creation control to create the project.
Good progress. Your Spark project is now the shared home for the game's backend services.
- Select the Web icon from the Firebase project overview.
- Enter Influence Game Web as the app nickname.
- Select Register app.
- Leave the displayed Firebase configuration object available in its current browser tab.
Why keep the configuration available?
Registering the Web app produces the configuration object for that app. You use those exact values when the browser connects to Firebase later.
The configuration must include databaseURL because this game uses Realtime Database.
Enable anonymous sign-in and create the database
Anonymous Authentication gives each browser session a temporary player identity. Realtime Database stores one room state that all three identities can observe.
- Select Security in the Firebase console sidebar.
- Select Authentication.
- Open Sign-in method.
- Select Anonymous as the sign-in provider.
- Enable the Anonymous provider.
The provider list now shows Anonymous as enabled. Each browser can receive an identity without asking a player to create an account.
- Select Databases & Storage in the Firebase console sidebar.
- Select Realtime Database.
- Select Create database.
- Choose an available database location near your players.
- Select Locked mode.
- Select Done.
Your Realtime Database is live in Locked mode. The local rules file you add next grants access only after Firebase has authenticated a player.
Add local Firebase configuration
The Firebase console now has the services your game needs. Local configuration tells the CLI which files to host plus which database rules to deploy.
- In the VS Code Explorer sidebar from earlier, create firebase.json inside the influence-game folder.
- Add the Hosting plus database configuration by pasting this code:
{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
},
"database": {
"rules": "database.rules.json"
}
}
What does this configuration do?
- The hosting.public setting tells Firebase Hosting to serve files from public.
- The ignore list keeps local configuration plus dependencies out of the deployment.
- The database.rules setting points Firebase CLI to database.rules.json.
- Save firebase.json.
- Confirm the VS Code Explorer sidebar lists firebase.json beside the public directory.
Does VS Code mark the JSON as invalid?
Check that every opening brace has a matching closing brace. Remove any Markdown fence lines that were copied around the JSON.
Help me check my Firebase configuration JSON
Firebase Realtime Database Security Rules control access to stored room data. These rules require a Firebase identity before a browser can read or write a room.
- In the VS Code Explorer sidebar, create database.rules.json inside the influence-game folder.
- Add the authenticated room rules by pasting this code:
{
"rules": {
"rooms": {
"$room_id": {
".read": "auth !== null",
".write": "auth !== null"
}
}
}
}
What do these rules allow?
- The rooms path holds every multiplayer room.
- The $room_id rule applies the same access check to each room code.
- The auth !== null condition permits access after Firebase Authentication identifies the browser.
- Save database.rules.json.
- Confirm the VS Code Explorer sidebar now lists both JSON configuration files.
Are the rules showing a syntax problem?
Check the quotation marks around .read plus .write. Confirm that the file name is exactly database.rules.json.
Help me check my Realtime Database rules
A Firebase project alias connects this local folder to the correct cloud project. The default alias lets later deployment commands use that project automatically.
- Switch back to the Windows PowerShell terminal from earlier.
- When the project list appears, select the Firebase project you just created.
- When the alias prompt appears, enter default.
- Start the interactive project selector by running this command:
firebase use --add
What does this command create?
The command assigns your chosen Firebase project to the default alias. Firebase CLI stores that mapping in a generated .firebaserc file.
Your local folder is now linked to the cloud project. The VS Code Explorer sidebar should list .firebaserc with the two JSON files you created.
Can't find your new Firebase project?
Confirm that PowerShell still uses the same Google account that created the project. Return to the Firebase console to confirm that the project finished creating.
Help me find my Firebase project in the CLI
✔️ Awesome, I've got everything!
Your local Firebase configuration now matches the backend services you created.
ⓧ I'd like to double check the full code
Your generated .firebaserc contains your real Firebase project ID where this reference shows your-firebase-project-id.
{
"projects": {
"default": "your-firebase-project-id"
}
}
This file connects the default alias to the Firebase project selected through the CLI.
Your firebase.json should match this complete Hosting plus database configuration.
{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
},
"database": {
"rules": "database.rules.json"
}
}
This configuration serves the public directory. It also connects database deployments to the rules file.
Your database.rules.json should match these authenticated room rules.
{
"rules": {
"rooms": {
"$room_id": {
".read": "auth !== null",
".write": "auth !== null"
}
}
}
}
These rules grant authenticated access beneath each rooms/$room_id path.
Before you run the final check, which Firebase project do you expect this folder to report as active?
- Confirm the active Firebase project by running this command:
firebase use
What should I see?
PowerShell lists the Firebase project you selected as the active project for the influence-game folder.
That is the backend foundation in place. Your local folder now knows exactly which Firebase project receives its future deployments.
Your shared backend is ready. Next, you will build the local card experience that exposes why separate browser memory cannot support three players.
Build the Local Card Demo
Your Firebase Hosting configuration is ready to serve a browser game. The next goal is a playable card interaction that works before the online services are connected.
A local card choice gives you an early visible win. Comparing that choice across two browser sessions tests whether local state can meet the three-device requirement.
In this step, get ready to:
- Build the local Influence interface inside the public folder.
- Serve the game through Firebase Hosting on a local URL.
- Test a card reveal across two independent browser sessions.
Create the local demo files
The page structure lives in index.html. Its elements give the game script places to display the room controls, cards, scores, and reveals.
- Return to Visual Studio Code from earlier.
- Expand the public folder in the Explorer sidebar.
- Create index.html inside the public folder.
- Select the double-check tab below to add the complete page structure.
✔️ My index.html file matches
Your page contains the setup form, room view, scoreboard, and game area required by the script.
ⓧ I'd like to double check the full code
- Replace the contents of public/index.html with this complete reference:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Influence</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="shell">
<header class="hero">
<p class="eyebrow">Three-player hidden-information game</p>
<h1>Influence</h1>
<p>Read the information type. Choose a card. Reveal its hidden strategic category.</p>
<p id="auth-status" class="status">Starting...</p>
</header>
<section id="message" class="message hidden" aria-live="polite"></section>
<section id="setup-card" class="panel">
<h2>Create or join a room</h2>
<div class="form-grid">
<label>
Display name
<input id="name-input" maxlength="20" placeholder="Alex">
</label>
<label>
Role
<select id="role-select">
<option value="red">Red: Chaos</option>
<option value="blue">Blue: Money</option>
<option value="yellow">Yellow: Influence</option>
</select>
</label>
<label>
Room code
<input id="room-input" maxlength="6" placeholder="ABC123">
</label>
</div>
<div class="button-row">
<button id="create-button">Create room</button>
<button id="join-button" class="secondary">Join room</button>
</div>
</section>
<section id="room-view" class="hidden">
<div class="panel room-bar">
<div>
<span class="label">Room</span>
<strong id="room-code">------</strong>
</div>
<button id="start-button" class="hidden">Start game</button>
</div>
<section id="scoreboard" class="scoreboard"></section>
<section id="game-area" class="panel"></section>
</section>
</main>
<script type="module" src="app.js"></script>
</body>
</html>
What does this page provide?
- The setup form collects a display name, role, and room code.
- The room view provides containers for the scoreboard and current game state.
- The module script loads the game logic from app.js.
- Save public/index.html.
- Confirm that index.html is listed under public in the Explorer sidebar.
The page needs a visual system that keeps the three roles distinct. The stylesheet also makes the card grid usable on narrow screens.
- Create style.css inside the public folder.
- Select the double-check tab below to add the complete interface styling.
✔️ My style.css file matches
Your stylesheet covers the page layout, controls, cards, badges, messages, and mobile scoreboard.
ⓧ I'd like to double check the full code
- Replace the contents of public/style.css with this complete reference:
:root {
color-scheme: dark;
font-family: Inter, system-ui, sans-serif;
background: #090b10;
color: #f6f7fb;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(120, 61, 255, 0.2), transparent 32rem),
#090b10;
}
button,
input,
select {
font: inherit;
}
button {
border: 0;
border-radius: 0.75rem;
padding: 0.8rem 1rem;
background: #7c5cff;
color: white;
cursor: pointer;
font-weight: 700;
}
button:hover:not(:disabled) {
filter: brightness(1.12);
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
button.secondary {
background: #282d3a;
}
.shell {
width: min(1100px, calc(100% - 2rem));
margin: 0 auto;
padding: 2rem 0 4rem;
}
.hero {
margin-bottom: 1.25rem;
}
.hero h1 {
margin: 0;
font-size: clamp(2.6rem, 8vw, 5rem);
}
.eyebrow,
.label {
color: #a9b0c3;
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.status {
color: #b9c2db;
}
.panel,
.score-card,
.play-card,
.reveal-card {
border: 1px solid #2b3140;
border-radius: 1rem;
background: rgba(20, 24, 33, 0.94);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.22);
}
.panel {
padding: 1.25rem;
margin-bottom: 1rem;
}
.form-grid,
.card-grid,
.scoreboard,
.reveal-grid {
display: grid;
gap: 1rem;
}
.form-grid {
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
label {
display: grid;
gap: 0.45rem;
color: #cdd3e1;
font-weight: 700;
}
input,
select {
width: 100%;
border: 1px solid #3a4254;
border-radius: 0.7rem;
padding: 0.75rem;
background: #10131a;
color: white;
}
.button-row,
.room-bar,
.round-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.button-row {
justify-content: flex-start;
margin-top: 1rem;
}
.scoreboard {
grid-template-columns: repeat(3, 1fr);
margin-bottom: 1rem;
}
.score-card {
padding: 1rem;
border-top-width: 0.45rem;
}
.score-card.red {
border-top-color: #ff5c6c;
}
.score-card.blue {
border-top-color: #4f8cff;
}
.score-card.yellow {
border-top-color: #ffd84d;
}
.score-card strong {
display: block;
font-size: 1.8rem;
}
.card-grid,
.reveal-grid {
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.play-card,
.reveal-card {
padding: 1rem;
}
.play-card {
width: 100%;
min-height: 220px;
text-align: left;
background: linear-gradient(145deg, #1c2230, #121620);
}
.play-card h3,
.reveal-card h3 {
margin-top: 0.4rem;
}
.country {
color: #9ca6bc;
font-weight: 700;
}
.badge-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin: 0.75rem 0;
}
.badge {
border-radius: 999px;
padding: 0.3rem 0.65rem;
background: #30384a;
font-size: 0.82rem;
font-weight: 800;
}
.badge.info {
background: #2d4f69;
}
.badge.strategy {
background: #563d75;
}
.message {
margin-bottom: 1rem;
border-radius: 0.8rem;
padding: 0.85rem 1rem;
background: #24314a;
}
.message.error {
background: #5b2730;
}
.player-list,
.points-list {
display: grid;
gap: 0.65rem;
padding: 0;
list-style: none;
}
.player-list li,
.points-list li {
display: flex;
justify-content: space-between;
gap: 1rem;
border-bottom: 1px solid #2b3140;
padding-bottom: 0.55rem;
}
.hidden {
display: none !important;
}
.muted {
color: #a9b0c3;
}
@media (max-width: 700px) {
.scoreboard {
grid-template-columns: 1fr;
}
}
What does this stylesheet control?
- The layout rules arrange forms, cards, reveals, and score panels into responsive grids.
- The role classes give Red, Blue, and Yellow distinct score-card borders.
- The badge classes distinguish visible information types from revealed strategic categories.
- Save public/style.css.
- Confirm that style.css is listed under public in the Explorer sidebar.
The configuration file separates project-specific Firebase values from the game logic. Keeping its supplied placeholders unchanged makes isConfigured select local demo mode.
- Create firebase-config.js inside the public folder.
- Select the double-check tab below to add the placeholder configuration.
✔️ My firebase-config.js file matches
Your configuration object still contains every supplied placeholder required for local demo mode.
ⓧ I'd like to double check the full code
- Replace the contents of public/firebase-config.js with this complete reference:
export const firebaseConfig = {
apiKey: "your-api-key-here",
authDomain: "your-project-id.firebaseapp.com",
databaseURL: "https://your-database-name.firebasedatabase.app",
projectId: "your-project-id",
appId: "your-app-id-here"
};
Why keep these placeholders?
The game checks these values before starting its online services. The supplied placeholders send this version directly into startDemoMode().
- Save public/firebase-config.js.
- Confirm that firebase-config.js is listed under public in the Explorer sidebar.
The JavaScript file contains both the local demo path and the complete multiplayer game engine. In this step, the unchanged configuration keeps the online path dormant while the local card interaction runs.
- Create app.js inside the public folder.
- Select the double-check tab below to add the complete game logic.
✔️ My app.js file matches
Your game logic contains the nine-card deck, local demo renderer, score matrix, and multiplayer functions.
ⓧ I'd like to double check the full code
- Replace the contents of public/app.js with this complete reference:
import { initializeApp } from "https://www.gstatic.com/firebasejs/13.1.0/firebase-app.js";
import {
getAuth,
onAuthStateChanged,
signInAnonymously
} from "https://www.gstatic.com/firebasejs/13.1.0/firebase-auth.js";
import {
get,
getDatabase,
onValue,
ref,
runTransaction,
set,
update
} from "https://www.gstatic.com/firebasejs/13.1.0/firebase-database.js";
import { firebaseConfig } from "./firebase-config.js";
const ROLE_DETAILS = {
red: { label: "Red", objective: "Chaos" },
blue: { label: "Blue", objective: "Money" },
yellow: { label: "Yellow", objective: "Influence" }
};
const SCORE_MATRIX = {
Chaos: { red: 15, yellow: 10, blue: 5 },
Influence: { yellow: 15, blue: 10, red: 5 },
Money: { blue: 15, red: 10, yellow: 5 }
};
const CARDS = [
{
id: "c1",
title: "Yesterday's Storm Map",
country: "Fictional Coastland",
event: "A resident accidentally reposts a two-year-old evacuation map as if it applies to today's storm.",
category: "Chaos",
infoType: "Misinformation",
rationale: "The claim is false in the current situation, but the resident did not intend to cause harm."
},
{
id: "c2",
title: "The Missing Decimal",
country: "Fictional Ledgeria",
event: "A volunteer misreads a budget spreadsheet and shares an incorrect claim that public spending increased tenfold.",
category: "Money",
infoType: "Misinformation",
rationale: "The financial claim is false and spread by mistake rather than as a deliberate deception."
},
{
id: "c3",
title: "A Rushed Translation",
country: "Fictional Oratoria",
event: "An unofficial translation changes the meaning of a minister's speech and is shared before anyone checks it.",
category: "Influence",
infoType: "Misinformation",
rationale: "The altered meaning is false, but the scenario gives no intention to mislead or harm."
},
{
id: "c4",
title: "The Forged Bank Notice",
country: "Fictional Ledgeria",
event: "A rival company creates a fake notice claiming a local bank will close and circulates it to move customers elsewhere.",
category: "Money",
infoType: "Disinformation",
rationale: "The notice is fabricated and deliberately distributed to manipulate financial behavior."
},
{
id: "c5",
title: "The Invented Endorsement",
country: "Fictional Oratoria",
event: "A campaign group creates a fake image showing a respected scientist endorsing its policy and knowingly promotes it.",
category: "Influence",
infoType: "Disinformation",
rationale: "The endorsement is false and deliberately created to influence an audience."
},
{
id: "c6",
title: "The Fake Contamination Alert",
country: "Fictional Coastland",
event: "An anonymous account invents a water contamination warning to trigger panic during a crowded festival.",
category: "Chaos",
infoType: "Disinformation",
rationale: "The warning is false and intentionally created to cause confusion and harm."
},
{
id: "c7",
title: "The Leaked Address List",
country: "Fictional Coastland",
event: "A real private list of emergency workers' home addresses is published to intimidate them after a disputed response.",
category: "Chaos",
infoType: "Malinformation",
rationale: "The information is factual, but private facts are weaponized to cause harm."
},
{
id: "c8",
title: "The Cropped Pricing Email",
country: "Fictional Ledgeria",
event: "A genuine internal pricing email is cropped to remove its discount conditions and released to manipulate negotiations.",
category: "Money",
infoType: "Malinformation",
rationale: "The source is genuine, but removing context changes how the factual information is understood."
},
{
id: "c9",
title: "The Selective Quote",
country: "Fictional Oratoria",
event: "A real private comment is published without the surrounding discussion to damage a public figure's credibility.",
category: "Influence",
infoType: "Malinformation",
rationale: "The quote is based on fact, but it is used out of context to mislead and harm."
}
];
const appState = {
auth: null,
db: null,
user: null,
roomCode: "",
room: null,
unsubscribe: null,
resolving: false
};
const elements = {
authStatus: document.querySelector("#auth-status"),
message: document.querySelector("#message"),
setupCard: document.querySelector("#setup-card"),
nameInput: document.querySelector("#name-input"),
roleSelect: document.querySelector("#role-select"),
roomInput: document.querySelector("#room-input"),
createButton: document.querySelector("#create-button"),
joinButton: document.querySelector("#join-button"),
roomView: document.querySelector("#room-view"),
roomCode: document.querySelector("#room-code"),
startButton: document.querySelector("#start-button"),
scoreboard: document.querySelector("#scoreboard"),
gameArea: document.querySelector("#game-area")
};
const isConfigured =
!firebaseConfig.apiKey.startsWith("your-") &&
!firebaseConfig.projectId.startsWith("your-") &&
firebaseConfig.databaseURL.startsWith("https://");
elements.createButton.addEventListener("click", createRoom);
elements.joinButton.addEventListener("click", joinRoom);
elements.startButton.addEventListener("click", startGame);
elements.roomInput.addEventListener("input", () => {
elements.roomInput.value = elements.roomInput.value.toUpperCase();
});
if (isConfigured) {
startOnlineMode();
} else {
startDemoMode();
}
function startDemoMode() {
elements.authStatus.textContent = "Local demo mode: Firebase is not configured.";
elements.setupCard.classList.add("hidden");
elements.roomView.classList.remove("hidden");
elements.roomCode.textContent = "DEMO";
elements.startButton.classList.add("hidden");
renderDemo();
}
function renderDemo(selectedId = "") {
const selected = CARDS.find((card) => card.id === selectedId);
const scores = selected ? SCORE_MATRIX[selected.category] : { red: 0, blue: 0, yellow: 0 };
renderDemoScoreboard(scores);
elements.gameArea.innerHTML = `
<div class="round-heading">
<div>
<p class="eyebrow">Early visible win</p>
<h2>Choose a fictional event card</h2>
</div>
<span class="badge">Local only</span>
</div>
<p class="muted">The information type is visible. Only the strategic category stays hidden until you choose.</p>
<div class="card-grid">
${CARDS.slice(0, 3).map((card) => `
<button class="play-card" data-demo-card="${card.id}">
<span class="badge info">${escapeHtml(card.infoType)}</span>
<h3>${escapeHtml(card.title)}</h3>
<p class="country">${escapeHtml(card.country)}</p>
<p>${escapeHtml(card.event)}</p>
</button>
`).join("")}
</div>
${selected ? `
<article class="reveal-card">
<p class="eyebrow">Strategic category revealed</p>
<h3>${escapeHtml(selected.title)}</h3>
<div class="badge-row">
<span class="badge info">${escapeHtml(selected.infoType)}</span>
<span class="badge strategy">${escapeHtml(selected.category)}</span>
</div>
<p>${escapeHtml(selected.rationale)}</p>
</article>
` : ""}
`;
document.querySelectorAll("[data-demo-card]").forEach((button) => {
button.addEventListener("click", () => renderDemo(button.dataset.demoCard));
});
}
function renderDemoScoreboard(scores) {
elements.scoreboard.innerHTML = ["red", "blue", "yellow"].map((role) => `
<article class="score-card ${role}">
<span class="label">${ROLE_DETAILS[role].label}: ${ROLE_DETAILS[role].objective}</span>
<strong>${scores[role]}</strong>
<span>demo points</span>
</article>
`).join("");
}
function startOnlineMode() {
const app = initializeApp(firebaseConfig);
appState.auth = getAuth(app);
appState.db = getDatabase(app);
onAuthStateChanged(appState.auth, async (user) => {
if (user) {
appState.user = user;
elements.authStatus.textContent = `Anonymous player ready: ${user.uid.slice(0, 8)}`;
const savedRoom = localStorage.getItem("influence-room");
if (savedRoom) {
listenToRoom(savedRoom);
}
return;
}
try {
await signInAnonymously(appState.auth);
} catch (error) {
showMessage(error.message, true);
}
});
}
async function createRoom() {
if (!requireOnlinePlayer()) return;
const profile = readProfile();
if (!profile) return;
try {
let code = createRoomCode();
let roomReference = ref(appState.db, `rooms/${code}`);
let snapshot = await get(roomReference);
while (snapshot.exists()) {
code = createRoomCode();
roomReference = ref(appState.db, `rooms/${code}`);
snapshot = await get(roomReference);
}
await set(roomReference, {
hostUid: appState.user.uid,
status: "lobby",
round: 1,
maxRounds: 3,
players: {
[appState.user.uid]: {
name: profile.name,
role: profile.role,
score: 0
}
}
});
localStorage.setItem("influence-room", code);
listenToRoom(code);
showMessage(`Room ${code} created.`);
} catch (error) {
showMessage(error.message, true);
}
}
async function joinRoom() {
if (!requireOnlinePlayer()) return;
const profile = readProfile();
if (!profile) return;
const code = elements.roomInput.value.trim().toUpperCase();
if (code.length !== 6) {
showMessage("Enter the six-character room code.", true);
return;
}
const roomReference = ref(appState.db, `rooms/${code}`);
try {
const snapshot = await get(roomReference);
if (!snapshot.exists()) {
throw new Error("That room does not exist.");
}
let rejection = "";
await runTransaction(roomReference, (room) => {
if (!room) return room;
const players = room.players || {};
const existingPlayer = players[appState.user.uid];
if (room.status !== "lobby" && !existingPlayer) {
rejection = "That game has already started.";
return;
}
if (Object.keys(players).length >= 3 && !existingPlayer) {
rejection = "That room already has three players.";
return;
}
const roleTaken = Object.entries(players).some(
([uid, player]) => uid !== appState.user.uid && player.role === profile.role
);
if (roleTaken) {
rejection = "That role is already taken.";
return;
}
players[appState.user.uid] = {
name: profile.name,
role: profile.role,
score: existingPlayer?.score || 0
};
room.players = players;
return room;
});
if (rejection) throw new Error(rejection);
localStorage.setItem("influence-room", code);
listenToRoom(code);
showMessage(`Joined room ${code}.`);
} catch (error) {
showMessage(error.message, true);
}
}
function listenToRoom(code) {
if (!appState.db || !appState.user) return;
if (appState.unsubscribe) appState.unsubscribe();
appState.roomCode = code;
elements.roomCode.textContent = code;
appState.unsubscribe = onValue(
ref(appState.db, `rooms/${code}`),
(snapshot) => {
if (!snapshot.exists()) {
localStorage.removeItem("influence-room");
appState.room = null;
elements.roomView.classList.add("hidden");
elements.setupCard.classList.remove("hidden");
showMessage("The saved room no longer exists.", true);
return;
}
appState.room = snapshot.val();
renderRoom();
},
(error) => showMessage(error.message, true)
);
}
function renderRoom() {
const room = appState.room;
const players = room.players || {};
const me = players[appState.user.uid];
if (!me) {
localStorage.removeItem("influence-room");
elements.setupCard.classList.remove("hidden");
elements.roomView.classList.add("hidden");
return;
}
elements.setupCard.classList.add("hidden");
elements.roomView.classList.remove("hidden");
renderScoreboard(players);
if (room.status === "lobby") renderLobby(room);
if (room.status === "playing") renderPlaying(room);
if (room.status === "reveal") renderReveal(room);
if (room.status === "finished") renderFinished(room);
}
function renderScoreboard(players) {
const byRole = Object.values(players).reduce((result, player) => {
result[player.role] = player;
return result;
}, {});
elements.scoreboard.innerHTML = ["red", "blue", "yellow"].map((role) => {
const player = byRole[role];
return `
<article class="score-card ${role}">
<span class="label">${ROLE_DETAILS[role].label}: ${ROLE_DETAILS[role].objective}</span>
<strong>${player?.score || 0}</strong>
<span>${player ? escapeHtml(player.name) : "Open seat"}</span>
</article>
`;
}).join("");
}
function renderLobby(room) {
const playerEntries = Object.values(room.players || {});
const isHost = room.hostUid === appState.user.uid;
elements.startButton.classList.toggle("hidden", !isHost);
elements.startButton.disabled = playerEntries.length !== 3;
elements.gameArea.innerHTML = `
<p class="eyebrow">Lobby</p>
<h2>${playerEntries.length}/3 players connected</h2>
<ul class="player-list">
${playerEntries.map((player) => `
<li>
<span>${escapeHtml(player.name)}</span>
<strong>${ROLE_DETAILS[player.role].label}: ${ROLE_DETAILS[player.role].objective}</strong>
</li>
`).join("")}
</ul>
<p class="muted">${isHost ? "Start unlocks when all three roles are claimed." : "Waiting for the host to start."}</p>
`;
}
async function startGame() {
const room = appState.room;
if (!room || room.hostUid !== appState.user.uid) return;
if (Object.keys(room.players || {}).length !== 3) return;
try {
await update(ref(appState.db, `rooms/${appState.roomCode}`), {
status: "playing",
round: 1
});
} catch (error) {
showMessage(error.message, true);
}
}
function renderPlaying(room) {
elements.startButton.classList.add("hidden");
const round = room.round;
const submissions = room.submissions?.[round] || {};
const mySubmission = submissions[appState.user.uid];
const hand = getHand(appState.user.uid, round);
const chosenCard = CARDS.find((card) => card.id === mySubmission);
elements.gameArea.innerHTML = `
<div class="round-heading">
<div>
<p class="eyebrow">Round ${round} of ${room.maxRounds}</p>
<h2>${mySubmission ? "Card submitted" : "Choose one event"}</h2>
</div>
<span class="badge">${Object.keys(submissions).length}/3 submitted</span>
</div>
${mySubmission ? `
<article class="play-card">
<span class="badge info">${escapeHtml(chosenCard.infoType)}</span>
<span class="badge">Strategic category hidden</span>
<h3>${escapeHtml(chosenCard.title)}</h3>
<p class="country">${escapeHtml(chosenCard.country)}</p>
<p>${escapeHtml(chosenCard.event)}</p>
</article>
<p class="muted">Waiting for the other players.</p>
` : `
<div class="card-grid">
${hand.map((card) => `
<button class="play-card" data-card-id="${card.id}">
<span class="badge info">${escapeHtml(card.infoType)}</span>
<h3>${escapeHtml(card.title)}</h3>
<p class="country">${escapeHtml(card.country)}</p>
<p>${escapeHtml(card.event)}</p>
</button>
`).join("")}
</div>
`}
`;
document.querySelectorAll("[data-card-id]").forEach((button) => {
button.addEventListener("click", () => playCard(button.dataset.cardId));
});
const playerCount = Object.keys(room.players || {}).length;
if (
room.hostUid === appState.user.uid &&
Object.keys(submissions).length === playerCount &&
!room.reveals?.[round]
) {
void resolveRound(room);
}
}
async function playCard(cardId) {
if (!appState.room || appState.room.status !== "playing") return;
try {
await set(
ref(appState.db, `rooms/${appState.roomCode}/submissions/${appState.room.round}/${appState.user.uid}`),
cardId
);
} catch (error) {
showMessage(error.message, true);
}
}
async function resolveRound(room) {
if (appState.resolving) return;
appState.resolving = true;
try {
const round = room.round;
const submissions = room.submissions?.[round] || {};
const playerEntries = Object.entries(room.players || {});
const roundPoints = Object.fromEntries(playerEntries.map(([uid]) => [uid, 0]));
const revealedCards = {};
Object.entries(submissions).forEach(([submitterUid, cardId]) => {
const card = CARDS.find((candidate) => candidate.id === cardId);
if (!card) return;
playerEntries.forEach(([uid, player]) => {
roundPoints[uid] += SCORE_MATRIX[card.category][player.role];
});
revealedCards[submitterUid] = {
playerName: room.players[submitterUid].name,
title: card.title,
country: card.country,
event: card.event,
category: card.category,
infoType: card.infoType,
rationale: card.rationale
};
});
const changes = {
[`reveals/${round}`]: {
cards: revealedCards,
points: roundPoints
},
status: "reveal"
};
playerEntries.forEach(([uid, player]) => {
changes[`players/${uid}/score`] = (player.score || 0) + roundPoints[uid];
});
await update(ref(appState.db, `rooms/${appState.roomCode}`), changes);
} catch (error) {
showMessage(error.message, true);
} finally {
appState.resolving = false;
}
}
function renderReveal(room) {
elements.startButton.classList.add("hidden");
const reveal = room.reveals?.[room.round];
if (!reveal) {
elements.gameArea.innerHTML = "<h2>Preparing the reveal...</h2>";
return;
}
const isHost = room.hostUid === appState.user.uid;
const isLastRound = room.round >= room.maxRounds;
elements.gameArea.innerHTML = `
<div class="round-heading">
<div>
<p class="eyebrow">Round ${room.round} reveal</p>
<h2>Hidden strategic categories revealed</h2>
</div>
${isHost ? `<button data-next-round>${isLastRound ? "Finish game" : "Next round"}</button>` : ""}
</div>
<p class="muted">Information type was visible on every card. Strategic category now determines points.</p>
<div class="reveal-grid">
${Object.values(reveal.cards).map((card) => `
<article class="reveal-card">
<p class="eyebrow">Played by ${escapeHtml(card.playerName)}</p>
<h3>${escapeHtml(card.title)}</h3>
<p class="country">${escapeHtml(card.country)}</p>
<p>${escapeHtml(card.event)}</p>
<div class="badge-row">
<span class="badge info">${escapeHtml(card.infoType)}</span>
<span class="badge strategy">${escapeHtml(card.category)}</span>
</div>
<p>${escapeHtml(card.rationale)}</p>
</article>
`).join("")}
</div>
<h3>Points earned this round</h3>
<ul class="points-list">
${Object.entries(room.players).map(([uid, player]) => `
<li><span>${escapeHtml(player.name)}</span><strong>+${reveal.points[uid]}</strong></li>
`).join("")}
</ul>
${!isHost ? "<p class=\"muted\">Waiting for the host to continue.</p>" : ""}
`;
const nextButton = document.querySelector("[data-next-round]");
if (nextButton) nextButton.addEventListener("click", nextRound);
}
async function nextRound() {
const room = appState.room;
if (!room || room.hostUid !== appState.user.uid) return;
const changes = room.round >= room.maxRounds
? { status: "finished" }
: { status: "playing", round: room.round + 1 };
try {
await update(ref(appState.db, `rooms/${appState.roomCode}`), changes);
} catch (error) {
showMessage(error.message, true);
}
}
function renderFinished(room) {
elements.startButton.classList.add("hidden");
const players = Object.values(room.players || {});
const topScore = Math.max(...players.map((player) => player.score || 0));
const winners = players.filter((player) => player.score === topScore);
const winnerText = winners.length === 1
? `${winners[0].name} wins for ${ROLE_DETAILS[winners[0].role].objective}`
: `Tie: ${winners.map((player) => player.name).join(" and ")}`;
elements.gameArea.innerHTML = `
<p class="eyebrow">Game complete</p>
<h2>${escapeHtml(winnerText)}</h2>
<p>Every device should show this same result.</p>
`;
}
function readProfile() {
const name = elements.nameInput.value.trim();
const role = elements.roleSelect.value;
if (!name) {
showMessage("Enter a display name.", true);
return null;
}
return { name, role };
}
function requireOnlinePlayer() {
if (!isConfigured || !appState.user) {
showMessage("Wait for anonymous sign-in to finish.", true);
return false;
}
return true;
}
function createRoomCode() {
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
return Array.from({ length: 6 }, () =>
alphabet[Math.floor(Math.random() * alphabet.length)]
).join("");
}
function getHand(uid, round) {
const random = seededRandom(hashString(`${appState.roomCode}:${uid}:${round}`));
const shuffled = [...CARDS];
for (let index = shuffled.length - 1; index > 0; index -= 1) {
const target = Math.floor(random() * (index + 1));
[shuffled[index], shuffled[target]] = [shuffled[target], shuffled[index]];
}
return shuffled.slice(0, 3);
}
function hashString(value) {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return hash >>> 0;
}
function seededRandom(seed) {
return function random() {
seed += 0x6d2b79f5;
let value = seed;
value = Math.imul(value ^ (value >>> 15), value | 1);
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
};
}
function showMessage(text, isError = false) {
elements.message.textContent = text;
elements.message.classList.remove("hidden", "error");
if (isError) elements.message.classList.add("error");
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (character) => ({
"&": "&",
"<": "<",
">": ">",
"\"": """,
"'": "'"
})[character]);
}
How does the demo path work?
- The CARDS array stores nine fictional events with visible infoType values and hidden category values.
- The isConfigured check detects the supplied placeholders and calls startDemoMode().
- The renderDemo() function shows cards c1 through c3 before revealing the selected card.
- The SCORE_MATRIX and renderDemoScoreboard() function calculate the Red, Blue, and Yellow demo scores.
- Save public/app.js.
- Confirm that app.js is listed under public in the Explorer sidebar.
Seeing a copied-code problem?
Confirm that all four files sit directly inside public. Check that their names use the same lowercase spelling shown above.
Compare the affected file with its full-code tab to find missing braces, quotes, or closing tags.
Help me compare my Influence demo files with the supplied code.
Start Firebase Hosting locally
A local Hosting server serves the files in public through a browser URL. The server also loads the JavaScript modules in the environment they expect.
- Switch back to the PowerShell session for influence-game from earlier.
- Start the local Hosting server by running this command:
firebase serve --only hosting
What does this command do?
The command serves the static Hosting content configured in firebase.json. PowerShell prints a local URL when the server is ready.
This process keeps the PowerShell session busy while the game is available. Leave it running throughout the browser tests.
- Record the printed local URL here: your local Hosting URL.
- Paste your local Hosting URL into the Chrome address bar.
- Press Enter to load the demo.
You should see the Influence heading above three zero-point score cards. You should also see three fictional event cards beneath the local demo message.
Local page not loading?
Confirm that PowerShell is still running the Hosting process from the influence-game folder. Check that firebase.json still serves the public directory.
Help me diagnose my local Firebase Hosting page.
Prove the browser sessions stay isolated
Each card face shows its information type before you make a choice. Its strategic category controls the score reveal and stays hidden until selection.
- Inspect the information-type badge on each of the three visible cards.
You should see Misinformation on all three cards. No card face should show Chaos, Money, or Influence as its strategic category.
Before you select a card, which hidden details do you expect the demo to reveal?
- Select any one of the three event cards.
You should see the selected card's strategic category and rationale beneath the card grid. The Red, Blue, and Yellow score cards should update to the corresponding 15, 10, and 5 values.
That is the first playable slice working. Your card now protects the strategic category until the exact moment the choice is revealed.
Before you test a second session, do you expect your selected card to remain revealed there?
- Open an Incognito window in Chrome.
- Paste your local Hosting URL into the address bar.
- Press Enter to load the same demo.
- Inspect the three event cards in the Incognito window.
You should see three unselected cards with zero-point score cards. The choice from your first Chrome session is absent.
This shortfall is intentional
Each browser session holds its selected card in its own local page state. The two sessions share a URL without sharing the choice.
This proves that local state cannot synchronize a three-player game across separate devices.
Your local card demo now makes the synchronization gap visible. Next, you will connect anonymous players to a shared room so every browser sees the same round.
Synchronize the Three-Player Game
The local demo proved the card logic works. Each browser still keeps its own selection, which blocks a shared round.
Firebase Authentication gives every browser an anonymous identity. Firebase Realtime Database stores the room as one shared source of truth.
Realtime listeners push every room update to all three sessions. This closes the isolation you saw in the last step.
In this step, get ready to:
- Connect the browser game to your Firebase Web app.
- Deploy authenticated room access rules.
- Play one synchronized round across three Chrome sessions.
Connect the game to Firebase
The Firebase configuration object directs the browser to your registered Web app. Replacing the supplied placeholders makes isConfigured true, which starts online mode when the page loads.
Match each config field
- The apiKey field receives the exact apiKey value from your registered Web app.
- The authDomain field receives the exact authDomain value.
- The databaseURL field receives the exact Realtime Database URL.
- The projectId field receives the exact projectId value.
- The appId field receives the exact appId value.
- Switch back to the Firebase Web app configuration object you kept in Step 2.
- Return to public/firebase-config.js in Visual Studio Code.
- Replace your-api-key-here with the matching value from the Firebase configuration object.
- Replace your-project-id.firebaseapp.com with the matching value from the Firebase configuration object.
- Replace https://your-database-name.firebasedatabase.app with the exact Realtime Database URL.
- Replace your-project-id with the matching value from the Firebase configuration object.
- Replace your-app-id-here with the matching value from the Firebase configuration object.
- Save public/firebase-config.js.
Before you refresh, do you expect the page to return to demo mode or create an anonymous player?
- Refresh the local Hosting URL in your normal Chrome session.
You should see Anonymous player ready: followed by a short player ID. That status proves startOnlineMode() authenticated this browser.
Still seeing local demo mode?
- Check public/firebase-config.js for any supplied placeholder that remains.
- Confirm that the databaseURL value starts with https://.
- Save the file before refreshing Chrome again.
Ask for help with the configuration values:
Deploy authenticated room rules
Realtime Database started in Locked mode, so the game cannot access room data yet. Your existing rules grant room access after Firebase Authentication has populated auth.
- Return to the PowerShell terminal running local Hosting.
- Press Ctrl+C to stop the local server.
- Deploy database.rules.json to the selected Firebase project by running:
firebase deploy --only database
What does this deployment do?
The --only database selection deploys the Realtime Database rules referenced by firebase.json. The rules require an authenticated user before a browser can read or write a room.
These broad authenticated-user rules suit this trusted three-player prototype. A public competitive game needs narrower write permissions plus server-authoritative scoring.
- Confirm that PowerShell reports a completed database rules deployment for your selected Firebase project.
Database rules deployment failed?
- Read the project name shown in the deployment output.
- Compare that name with the Firebase project containing your Realtime Database.
- Check that firebase.json still points to database.rules.json.
Ask for help with the failed deployment:
Play a synchronized round
Your configuration now targets the shared backend. The deployed rules let each anonymously authenticated session use the same room document.
- Restart the local Hosting server by running:
firebase serve --only hosting
What does this command do?
This command serves the files inside public from a local URL. Keep this PowerShell process running while all three Chrome sessions play.
- Return to the printed local URL in your normal Chrome session.
- Refresh the page.
You should see the room setup form beneath the anonymous player status. This browser becomes the host for your shared game.
Local URL missing?
- Check that PowerShell is still running inside the influence-game folder.
- Stop any earlier Hosting process before starting the server again.
Ask for help starting the local server:
- Enter Alex in the Display name field.
- Select Red: Chaos from the Role menu.
- Click Create room.
- Record the generated room code here: your six-character room code.
The host session should show 1/3 players connected. Red appears on the scoreboard with a score of zero.
- Switch back to the second Chrome session from the previous step.
- Refresh the local Hosting URL.
- Enter Blair in the Display name field.
- Select Blue: Money from the Role menu.
- Enter your six-character room code in the Room code field.
- Click Join room.
Both sessions should now show 2/3 players connected. Blue occupies the Money seat on both scoreboards.
- Open a third Chrome profile or Incognito session.
- Load the printed local Hosting URL in that session.
- Enter Casey in the Display name field.
- Select Yellow: Influence from the Role menu.
- Enter your six-character room code in the Room code field.
- Click Join room.
All three sessions should show Red, Blue, and Yellow with zero points. The host's Start game button is now enabled.
How does the room stay synchronized?
Each anonymous browser writes to the same room in Realtime Database. onValue updates every session whenever that room changes.
The room transaction also protects the three unique role seats. Every browser therefore renders the same players from the same stored state.
- Return to the Red host session.
- Click Start game.
Every session should move to round one. Each player sees a deterministic three-card hand based on their anonymous player ID.
Before anyone chooses, each card shows its title, fictional country, event, and infoType badge. The category and rationale remain hidden.
- Choose one card in the Red session.
Red should show Strategic category hidden while the other two players continue choosing.
- Choose one card in the Blue session.
Before the final player chooses, do you expect the three browsers to stay in their current states or reveal the round together?
- Choose one card in the Yellow session.
That is the synchronization gap closed: all three sessions now reveal the same played cards. Each reveal shows the public information type plus the previously hidden strategic category and rationale.
How does the matrix score the reveal?
Every revealed card awards points to all three roles according to its strategic category. Each player's round score is the sum of the points from all three cards.
- A Chaos card gives Red 15 points, Yellow 10 points, and Blue 5 points.
- An Influence card gives Yellow 15 points, Blue 10 points, and Red 5 points.
- A Money card gives Blue 15 points, Red 10 points, and Yellow 5 points.
- Compare the three revealed categories across every Chrome session.
- Calculate each role's round total from the three revealed categories.
- Compare your calculated totals with the synchronized score cards.
You should see identical categories, rationales, and updated scores in all three sessions. The host should also see the control for advancing to round two.
Your three-player room now shares identities, turns, reveals, and scores in real time. Next, you will deploy the complete game so three separate devices can play from one public URL.
Deploy and Play on Separate Devices
Your local browser sessions now share the complete game state. That proves the multiplayer lifecycle works on one computer.
Separate devices need an address they can all reach. Publishing to Firebase Hosting gives the game a public HTTPS address.
In this step, get ready to:
- Publish the complete project with one combined deployment.
- Connect three separate devices to a fresh shared room.
- Complete three rounds with synchronized reveals and scores.
Deploy the game and rules
A combined deployment keeps the hosted game aligned with its access rules. The selected Firebase project receives both configured targets from the same folder.
- Return to the PowerShell window running the local Hosting server.
- Stop the local Hosting server by pressing Ctrl+C.
Before you deploy, what two resources do you expect Firebase to publish? Hold that prediction as you run the command.
- Deploy the hosted game and database rules by running this command:
firebase deploy --only hosting,database
What does this deployment publish?
- The Hosting target uploads the finished files from public.
- The Realtime Database target publishes database.rules.json to the selected Firebase project.
- The selected targets leave the rest of the Firebase project unchanged.
- Record the printed Hosting URL here: https://your-project-id.web.app.
- Open https://your-project-id.web.app in a browser.
That is the deployment milestone complete: Influence now loads over HTTPS from Firebase Hosting.
The status line begins with Anonymous player ready:. This confirms that the deployed app loaded your configured Firebase Web app.
Deployment did not load the game?
- Confirm PowerShell is inside the influence-game folder containing firebase.json.
- Confirm the selected project matches the default project recorded in .firebaserc.
- Confirm the browser address matches https://your-project-id.web.app.
Help me diagnose why my Firebase deployment did not publish the Influence game.
Create a fresh room across three devices
Each device receives a distinct anonymous identity from Firebase Authentication. The room code connects those identities to one shared database path.
- Open https://your-project-id.web.app in a browser on the first device.
The first device shows the create-or-join form with an anonymous player status.
- Enter Host in the Display name field.
- Select Red: Chaos from the Role menu.
The first device is now prepared to claim the Red role.
- Click Create room.
You will see a six-character room code with 1/3 players connected.
- Record the displayed room code here: ABC234.
How do the devices meet?
The Hosting URL opens the same deployed application on every device. The room code points every client at the same rooms/{roomCode} path.
- Open https://your-project-id.web.app in a browser on the second device.
The second device authenticates as a different anonymous player.
- Enter Player Blue in the Display name field.
- Select Blue: Money from the Role menu.
The second device is now prepared to claim the Blue role.
- Enter ABC234 in the Room code field.
- Click Join room.
Both connected devices now show 2/3 players connected.
- Open https://your-project-id.web.app in a browser on the third device.
The third device receives its own anonymous identity before joining the room.
- Enter Player Yellow in the Display name field.
- Select Yellow: Influence from the Role menu.
The third device is now prepared to claim the Yellow role.
- Enter ABC234 in the Room code field.
- Click Join room.
Every device now shows all three role cards. The host also sees 3/3 players connected with the start control available.
Cannot connect all three devices?
- Confirm every device opened https://your-project-id.web.app instead of the earlier local URL.
- Choose the unclaimed role if the selected color already belongs to another player.
- Re-enter ABC234 from the host device if a player cannot find the room.
Help me diagnose why one device cannot join my deployed Influence room.
Complete three synchronized rounds
Every card shows one visible information type: Misinformation, Disinformation, or Malinformation.
Chaos, Money, or Influence stays hidden until all three submissions exist. The reveal then applies the shared scoring matrix.
- Click Start game on the host device.
All three devices show Round 1 with a deterministic three-card hand for that player.
Before you inspect the cards, can any player see a strategic category early? Keep your prediction in mind as you check each device.
- Inspect one unselected card on each device.
Each card face shows its title, fictional country, event, and information type. No Chaos, Money, or Influence category appears yet.
- Select one event card on the Red device.
The Red device shows its submitted card with the strategic category still hidden. Every session reports 1/3 submitted.
- Select one event card on the Blue device.
Every session now reports 2/3 submitted.
Before Yellow submits, do you expect the reveal to appear on one device or all three? Keep that prediction as you make the final choice.
- Select one event card on the Yellow device.
All three devices switch to the same Round 1 reveal. Every reveal card keeps its information type visible while adding its strategic category and rationale.
The scoreboards update together. Each device shows the same points earned during the round.
How are reveal points assigned?
- A Chaos card gives Red 15 points. It gives Yellow 10 points. It gives Blue 5 points.
- An Influence card gives Yellow 15 points. It gives Blue 10 points. It gives Red 5 points.
- A Money card gives Blue 15 points. It gives Red 10 points. It gives Yellow 5 points.
- Click Next round on the host device.
- Select one Round 2 event card on the Red device.
Every session reports the first Round 2 submission.
- Select one Round 2 event card on the Blue device.
Every session reports two Round 2 submissions.
- Select one Round 2 event card on the Yellow device.
The Round 2 reveal appears everywhere. Each scoreboard now shows synchronized cumulative scores.
- Click Next round on the host device.
- Select one Round 3 event card on the Red device.
Every session reports the first Round 3 submission.
- Select one Round 3 event card on the Blue device.
Every session reports two Round 3 submissions.
- Select one Round 3 event card on the Yellow device.
The final reveal appears on all three devices. The information type remains beside each newly revealed strategic category.
Before the host finishes the game, do you expect every device to name the same winner? Keep that prediction as you complete the final check.
- Click Finish game on the host device.
All three devices show the same winner sentence. Their scoreboards also show identical final totals.
Across all three rounds, the information type stayed visible before submission. The strategic category appeared only during each synchronized reveal.
Devices show different game states?
- Confirm every device still displays ABC234 as its room code.
- Confirm all three players submitted a card if the host cannot advance.
- Compare each revealed category with the scoring matrix above if a score looks unexpected.
Help me diagnose why my three deployed Influence clients show different rounds or scores.
You did it. Influence now runs from a public URL with one synchronized winner across three separate devices.
Secret mission
Test Hidden-Category Balance
A strong hidden-information game gives players clues without turning every choice into a certainty. Run one controlled round. Capture nine private predictions before using the synchronized reveal to decide whether one card's wording should stay or change.
Clean Up Your Resources
Clean Up Your Resources
Your game remains live on Firebase's Spark plan with no payment information required. Decide whether to keep the game available, pause public access, or delete its cloud resources.
Cost warning
The Spark plan needs no payment information. Each product stays available while usage remains inside its no-cost quota.
- Realtime Database includes 1 GB of storage plus 10 GB of monthly downloads.
- Firebase Hosting includes 10 GB of storage plus 10 GB of monthly data transfer.
- Realtime Database supports up to 100 simultaneous connections on Spark.
If a product exceeds its no-cost quota, Firebase shuts that product off for the rest of the billing period. This prevents an overage charge.
- Keep a Cloud Billing account unlinked if this project must remain strictly $0.
Resources you used:
- One Spark-plan Firebase project containing the registered Web app.
- Anonymous users created through Firebase Authentication during your game sessions.
- A Firebase Realtime Database containing room data plus authenticated prototype rules.
- A Firebase Hosting deployment serving the game from its HTTPS URL.
Keep everything running
No action is needed. Choose this if you want people to keep playing Influence from its deployed URL.
- Leave the Firebase project active on the Spark plan.
- Check the Firebase usage dashboards to confirm the project remains inside its no-cost quotas.
- Keep your local influence-game source folder for future card revisions.
Pause - I'll come back to this later
This option stops the public game while preserving its backend resources. Your room data plus project configuration remain available for later.
- Switch back to the PowerShell window from earlier.
- Pause public Hosting by running this command:
firebase hosting:disable
What Does This Command Do?
This command stops Firebase Hosting from serving the deployed game. The Firebase project plus Realtime Database continue to exist.
- Refresh the deployed web.app URL.
You should see Site Not Found. This confirms that public Hosting access is paused.
Still Seeing the Game?
- Confirm that PowerShell still has the Firebase project from earlier selected.
- Run the disable command again from the influence-game folder.
Help me work out why my Firebase Hosting site remains available after I disabled it.
Delete - I don't want to use this again
This option removes every cloud resource for Influence. Your local influence-game source folder remains on your computer as an offline copy.
Project Deletion Takes 30 Days
Deleting the Firebase project is a broad cleanup because it also deletes the corresponding project across Firebase plus Google Cloud. Complete deletion takes 30 days.
- Open the Firebase console in your browser.
- Select the Firebase project that hosts Influence.
- Open the project's Project settings area.
- Select the project deletion control.
- Read the console warning before confirming.
- Confirm the project deletion when you are certain that you no longer need the deployed game.
- Return to the Firebase project list after confirming deletion.
- Confirm that the project no longer opens as an active Firebase project.
What Does Project Deletion Remove?
Project deletion covers every cloud resource listed above.
- The registered Firebase Web app is removed.
- The anonymous Authentication users are removed.
- The Realtime Database room data plus its deployed rules are removed.
- The Firebase Hosting deployment is removed.
Nice Work!
Nice Work!
You did it! You brought Influence from a local prototype to a deployed three-player browser game that stays synchronized across separate devices.
You've learned how to:
- Create hidden-information cards that show Misinformation, Disinformation, or Malinformation before revealing Chaos, Money, or Influence.
- Synchronize three anonymous players through Firebase Authentication plus Firebase Realtime Database. Each finished round applies the Red, Blue, and Yellow 15/10/5 scoring matrix to the shared scoreboard.
- Deploy the complete game through Firebase Hosting for three-device play over a public HTTPS URL.
- Secret Mission: Playtested hidden-category balance by comparing private predictions with synchronized reveals. You also recorded a retain-or-revise decision for card wording.
Ready to quiz yourself?