“Chaseopoly” is a sophisticated multiplayer Monopoly-style board game implemented as a web application. The game features real-time multiplayer gameplay through Firebase, complete character customization, property management, financial transactions, and a comprehensive begging system for players facing bankruptcy. The application provides a modern, mobile-first interface for classic Monopoly mechanics.
chaseopoly/
└── index.html # Complete self-contained multiplayer game (26,875 lines)
// Firebase Realtime Database setup
const firebaseConfig = {
apiKey: "AIzaSyDcVXrIFKuY5Bf_7uOxEiPFFtQQSFoGfj8",
authDomain: "chaseopoly-ab4c0.firebaseapp.com",
databaseURL: "https://chaseopoly-ab4c0-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "chaseopoly-ab4c0",
storageBucket: "chaseopoly-ab4c0.appspot.com",
messagingSenderId: "527853504334",
appId: "1:527853504334:web:4b7e0d98bbac476ffae894"
};
const database = firebase.database();
// Comprehensive game state management
{
"games": {
"[gameId]": {
"config": {
"status": "waiting|active|finished", // Game lifecycle status
"maxPlayers": 4, // Player limit
"startingMoney": 1500, // Initial player money
"passGoAmount": 200, // Salary for passing GO
"createdAt": timestamp, // Game creation time
"createdBy": "userId" // Game creator
},
"players": {
"[playerId]": {
"name": "PlayerName", // Display name
"color": "#FF5722", // Player color
"money": 1500, // Current money
"position": 0, // Board position (0-39)
"properties": [], // Owned property indices
"inJail": false, // Jail status
"jailTurns": 0, // Turns remaining in jail
"isReady": false, // Ready to start flag
"bankrupt": false, // Bankruptcy status
"joinedAt": timestamp // Join timestamp
}
},
"gameState": {
"currentPlayerIndex": 0, // Current turn player
"playerOrder": ["uid1", "uid2"], // Turn order array
"turnStartedAt": timestamp, // Turn timing
"lastDiceRoll": { "die1": 3, "die2": 5 }, // Last dice result
"gameStartedAt": timestamp // Game start time
},
"beggingRequest": {
"playerId": "uid", // Player requesting help
"playerName": "Name", // Requesting player name
"amount": 500, // Amount needed
"reason": "rent to Player", // Reason for request
"status": "active|completed", // Request status
"donations": {
"[donorId]": {
"amount": 100, // Donation amount
"timestamp": timestamp // Donation time
}
}
},
"history": [
{
"message": "Player bought property", // Game event description
"timestamp": timestamp, // Event time
"playerId": "uid" // Associated player
}
]
}
}
}
// Sophisticated screen management system
const screens = {
splash: 'Initial loading screen',
character: 'Character creation and game setup',
waiting: 'Lobby for multiplayer coordination',
dashboard: 'Main game interface'
};
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(screen => {
screen.classList.remove('active');
});
document.getElementById(screenId).classList.add('active');
}
// Comprehensive Firebase listeners for real-time gameplay
async function setupGameListeners() {
// Player list updates
database.ref(`games/${gameId}/players`).on('value', updatePlayerList);
// Game status changes
database.ref(`games/${gameId}/config/status`).on('value', handleStatusChange);
// Game state synchronization
database.ref(`games/${gameId}/gameState`).on('value', handleGameStateChange);
// Begging request system
database.ref(`games/${gameId}/beggingRequest`).on('value', beggingRequestListener);
// Individual player money tracking
database.ref(`games/${gameId}/players/${currentUser.uid}/money`).on('value', (snapshot) => {
updateMoneyDisplay(snapshot.val());
});
// Property ownership updates
database.ref(`games/${gameId}/players/${currentUser.uid}/properties`).on('value', (snapshot) => {
updatePropertiesDisplay(snapshot.val());
});
}
// Comprehensive character creation
const playerColors = [
'#FF5722', '#2196F3', '#4CAF50', '#FF9800',
'#9C27B0', '#607D8B', '#795548', '#E91E63'
];
async function createCharacter() {
const name = document.getElementById('playerName').value.trim();
const selectedColor = document.querySelector('.color-option.selected').dataset.color;
// Validate character name
if (name.length < 1 || name.length > 20) {
showError('Name must be 1-20 characters');
return;
}
// Check for existing players and color conflicts
const playersSnapshot = await database.ref(`games/${gameId}/players`).once('value');
const players = playersSnapshot.val() || {};
// Ensure unique colors per game
const colorTaken = Object.values(players).some(player =>
player.color === selectedColor && player.id !== currentUser.uid
);
if (colorTaken) {
showError('Color already taken by another player');
return;
}
// Create player record
await database.ref(`games/${gameId}/players/${currentUser.uid}`).set({
name: name,
color: selectedColor,
money: gameConfig.startingMoney || 1500,
position: 0,
properties: [],
inJail: false,
jailTurns: 0,
isReady: false,
bankrupt: false,
joinedAt: firebase.database.ServerValue.TIMESTAMP
});
}
// Intelligent game matching and creation
async function findOrCreateGame() {
try {
// Check for saved game first
const savedGameId = localStorage.getItem('currentGameId');
if (savedGameId) {
const gameSnapshot = await database.ref(`games/${savedGameId}`).once('value');
if (gameSnapshot.exists()) {
const playerSnapshot = await database.ref(`games/${savedGameId}/players/${currentUser.uid}`).once('value');
if (playerSnapshot.exists()) {
gameId = savedGameId;
return gameId;
}
}
}
// Search for available games
const activeGamesSnapshot = await database.ref('games')
.orderByChild('config/status')
.equalTo('waiting')
.limitToFirst(10)
.once('value');
const activeGames = activeGamesSnapshot.val() || {};
// Find joinable game
for (const [id, game] of Object.entries(activeGames)) {
const players = game.players || {};
const playerCount = Object.keys(players).length;
const maxPlayers = game.config?.maxPlayers || 4;
if (playerCount < maxPlayers) {
gameId = id;
return gameId;
}
}
// Create new game if none available
gameId = database.ref('games').push().key;
const initialGameState = {
config: {
status: 'waiting',
maxPlayers: 4,
startingMoney: 1500,
passGoAmount: 200,
createdAt: firebase.database.ServerValue.TIMESTAMP,
createdBy: currentUser.uid
},
players: {},
gameState: {
currentPlayerIndex: 0,
playerOrder: []
},
history: []
};
await database.ref(`games/${gameId}`).set(initialGameState);
return gameId;
} catch (error) {
showError('Failed to find or create game');
throw error;
}
}
// Complete property definitions with accurate pricing
const properties = [
{ name: "GO", type: "special", action: "collect" },
{ name: "Mediterranean Avenue", type: "property", price: 60, rent: [2, 10, 30, 90, 160, 250], color: "brown" },
{ name: "Community Chest", type: "card", deck: "community" },
{ name: "Baltic Avenue", type: "property", price: 60, rent: [4, 20, 60, 180, 320, 450], color: "brown" },
{ name: "Income Tax", type: "tax", amount: 200 },
{ name: "Reading Railroad", type: "railroad", price: 200, rent: [25, 50, 100, 200] },
{ name: "Oriental Avenue", type: "property", price: 100, rent: [6, 30, 90, 270, 400, 550], color: "lightblue" },
{ name: "Chance", type: "card", deck: "chance" },
{ name: "Vermont Avenue", type: "property", price: 100, rent: [6, 30, 90, 270, 400, 550], color: "lightblue" },
{ name: "Connecticut Avenue", type: "property", price: 120, rent: [8, 40, 100, 300, 450, 600], color: "lightblue" },
{ name: "Jail", type: "special", action: "jail" },
// ... continues for all 40 board spaces
];
// Dynamic rent calculation based on ownership
function calculateRent(propertyIndex, ownerId) {
const property = properties[propertyIndex];
if (!property || property.type !== 'property') return 0;
const owner = playerData[ownerId];
if (!owner) return 0;
// Check for monopoly
const sameColorProperties = properties.filter(p =>
p.type === 'property' && p.color === property.color
);
const ownedSameColor = sameColorProperties.filter(p =>
owner.properties.includes(properties.indexOf(p))
);
const hasMonopoly = ownedSameColor.length === sameColorProperties.length;
let rentIndex = 0; // Base rent
if (hasMonopoly) {
rentIndex = 1; // Double rent for monopoly
}
return property.rent[rentIndex];
}
// Sophisticated dice mechanics with jail handling
async function rollDice() {
if (isRollingDice) return;
isRollingDice = true;
try {
// Animate dice rolling
showDiceAnimation();
await new Promise(resolve => setTimeout(resolve, 1000));
// Generate random dice values
const die1 = Math.floor(Math.random() * 6) + 1;
const die2 = Math.floor(Math.random() * 6) + 1;
const total = die1 + die2;
const isDoubles = die1 === die2;
// Update database with dice result
await database.ref(`games/${gameId}/gameState/lastDiceRoll`).set({ die1, die2 });
const playerSnapshot = await database.ref(`games/${gameId}/players/${currentUser.uid}`).once('value');
const player = playerSnapshot.val();
// Handle jail mechanics
if (player.inJail) {
if (isDoubles) {
// Doubles gets you out of jail
await database.ref(`games/${gameId}/players/${currentUser.uid}`).update({
inJail: false,
jailTurns: 0
});
await addToHistory(`${player.name} rolled doubles and escaped jail!`);
} else {
const newJailTurns = (player.jailTurns || 0) + 1;
if (newJailTurns >= 3) {
// Forced to pay after 3 turns
await database.ref(`games/${gameId}/players/${currentUser.uid}`).update({
money: player.money - 50,
inJail: false,
jailTurns: 0
});
await addToHistory(`${player.name} paid $50 to leave jail`);
} else {
await database.ref(`games/${gameId}/players/${currentUser.uid}/jailTurns`).set(newJailTurns);
await addToHistory(`${player.name} is still in jail`);
await endTurn();
return;
}
}
}
// Calculate new position
const currentPosition = player.position || 0;
let newPosition = (currentPosition + total) % 40;
// Handle passing GO
if (newPosition < currentPosition || (currentPosition + total >= 40)) {
await database.ref(`games/${gameId}/players/${currentUser.uid}/money`)
.set(player.money + 200);
await addToHistory(`${player.name} passed GO and collected $200`);
}
// Update player position
await database.ref(`games/${gameId}/players/${currentUser.uid}/position`).set(newPosition);
// Handle landing on space
await handleLandOnSpace(newPosition);
// End turn (unless doubles were rolled and not in jail)
if (!isDoubles || player.inJail) {
await endTurn();
}
} catch (error) {
showError('Failed to roll dice');
} finally {
isRollingDice = false;
hideDiceAnimation();
}
}
async function handleLandOnSpace(position) {
const property = properties[position];
const playerSnapshot = await database.ref(`games/${gameId}/players/${currentUser.uid}`).once('value');
const player = playerSnapshot.val();
switch (property.type) {
case 'property':
case 'railroad':
case 'utility':
await handlePropertySpace(position, property, player);
break;
case 'tax':
await database.ref(`games/${gameId}/players/${currentUser.uid}/money`)
.set(player.money - property.amount);
await addToHistory(`${player.name} paid $${property.amount} in taxes`);
break;
case 'card':
await handleCardSpace(property.deck, player);
break;
case 'special':
await handleSpecialSpace(property.action, player);
break;
}
}
async function handlePropertySpace(position, property, player) {
// Check ownership
const ownersSnapshot = await database.ref(`games/${gameId}/players`).once('value');
const players = ownersSnapshot.val();
let owner = null;
for (const [playerId, playerData] of Object.entries(players)) {
if (playerData.properties && playerData.properties.includes(position)) {
owner = { id: playerId, ...playerData };
break;
}
}
if (owner && owner.id !== currentUser.uid) {
// Pay rent to owner
const rent = calculateRent(position, owner.id);
const currentPlayerSnapshot = await database.ref(`games/${gameId}/players/${currentUser.uid}`).once('value');
const currentPlayer = currentPlayerSnapshot.val();
if (currentPlayer.money >= rent) {
await payRent(owner, rent);
} else {
// Insufficient funds - trigger begging system
await triggerBeggingRequest(rent, `rent to ${owner.name}`);
}
} else if (!owner) {
// Property available for purchase
showPropertyPurchaseModal(position, property);
}
}
// Chance and Community Chest cards
const chanceCards = [
{ type: 'move', text: 'Advance to GO', action: 'moveToGo' },
{ type: 'move', text: 'Go to Jail', action: 'goToJail' },
{ type: 'money', text: 'Bank pays you $50', amount: 50 },
{ type: 'money', text: 'Pay poor tax of $15', amount: -15 },
{ type: 'move', text: 'Take a trip to Reading Railroad', position: 5 },
{ type: 'relative', text: 'Go back 3 spaces', spaces: -3 },
// ... more cards
];
const communityChestCards = [
{ type: 'money', text: 'You inherit $100', amount: 100 },
{ type: 'money', text: 'Doctor fee - Pay $50', amount: -50 },
{ type: 'move', text: 'Go to Jail', action: 'goToJail' },
{ type: 'money', text: 'Income tax refund - Collect $20', amount: 20 },
// ... more cards
];
async function handleCardSpace(deckType, player) {
const deck = deckType === 'chance' ? chanceCards : communityChestCards;
const randomCard = deck[Math.floor(Math.random() * deck.length)];
showCardModal(randomCard.text);
switch (randomCard.type) {
case 'money':
if (randomCard.amount > 0) {
await database.ref(`games/${gameId}/players/${currentUser.uid}/money`)
.set(player.money + randomCard.amount);
} else {
await database.ref(`games/${gameId}/players/${currentUser.uid}/money`)
.set(player.money + randomCard.amount);
}
break;
case 'move':
if (randomCard.action === 'goToJail') {
await sendToJail();
} else if (randomCard.action === 'moveToGo') {
await database.ref(`games/${gameId}/players/${currentUser.uid}`).update({
position: 0,
money: player.money + 200
});
} else if (randomCard.position !== undefined) {
await database.ref(`games/${gameId}/players/${currentUser.uid}/position`)
.set(randomCard.position);
}
break;
case 'relative':
const currentPos = player.position || 0;
const newPos = Math.max(0, currentPos + randomCard.spaces);
await database.ref(`games/${gameId}/players/${currentUser.uid}/position`)
.set(newPos);
break;
}
await addToHistory(`${player.name}: ${randomCard.text}`);
}
// Advanced begging system for financial assistance
async function triggerBeggingRequest(amount, reason) {
const playerSnapshot = await database.ref(`games/${gameId}/players/${currentUser.uid}`).once('value');
const player = playerSnapshot.val();
// Remove any existing begging request
await database.ref(`games/${gameId}/beggingRequest`).remove();
// Create new begging request
await database.ref(`games/${gameId}/beggingRequest`).set({
playerId: currentUser.uid,
playerName: player.name,
amount: amount,
reason: reason,
status: 'active',
createdAt: firebase.database.ServerValue.TIMESTAMP,
donations: {}
});
await addToHistory(`${player.name} is asking for $${amount} for ${reason}`);
showBeggingModal(amount, reason);
}
// Real-time donation tracking
async function makeDonation(amount) {
const myMoneySnapshot = await database.ref(`games/${gameId}/players/${currentUser.uid}/money`).once('value');
const myMoney = myMoneySnapshot.val() || 0;
if (myMoney < amount) {
showError('Insufficient funds to donate');
return;
}
// Record donation
await database.ref(`games/${gameId}/beggingRequest/donations/${currentUser.uid}`).set({
amount: amount,
timestamp: firebase.database.ServerValue.TIMESTAMP
});
// Update donor's money
await database.ref(`games/${gameId}/players/${currentUser.uid}/money`).set(myMoney - amount);
const playerSnapshot = await database.ref(`games/${gameId}/players/${currentUser.uid}`).once('value');
const player = playerSnapshot.val();
await addToHistory(`${player.name} donated $${amount}`);
}
// Process completed donations
async function processBeggingRequest() {
const beggingSnapshot = await database.ref(`games/${gameId}/beggingRequest`).once('value');
const beggingData = beggingSnapshot.val();
if (!beggingData || beggingData.status !== 'active') return;
// Calculate total donations
const donations = beggingData.donations || {};
let totalDonated = 0;
for (const donation of Object.values(donations)) {
totalDonated += donation.amount || 0;
}
// Give money to begging player
const playerSnapshot = await database.ref(`games/${gameId}/players/${beggingData.playerId}/money`).once('value');
const currentMoney = playerSnapshot.val() || 0;
const newMoney = currentMoney + totalDonated;
await database.ref(`games/${gameId}/players/${beggingData.playerId}/money`).set(newMoney);
// Mark request as completed
await database.ref(`games/${gameId}/beggingRequest/status`).set('completed');
await addToHistory(`${beggingData.playerName} received $${totalDonated} in donations`);
}
async function declareBankruptcy() {
const playerSnapshot = await database.ref(`games/${gameId}/players/${currentUser.uid}`).once('value');
const player = playerSnapshot.val();
// Mark player as bankrupt
await database.ref(`games/${gameId}/players/${currentUser.uid}/bankrupt`).set(true);
await addToHistory(`${player.name} declared bankruptcy`);
// Remove any pending begging requests
await database.ref(`games/${gameId}/beggingRequest`).remove();
// Check if game should end
const playersSnapshot = await database.ref(`games/${gameId}/players`).once('value');
const allPlayers = playersSnapshot.val();
const activePlayers = Object.values(allPlayers).filter(p => !p.bankrupt);
if (activePlayers.length <= 1) {
await database.ref(`games/${gameId}/config/status`).set('finished');
}
}
async function endTurn() {
try {
// Get current game state
const currentGameState = await database.ref(`games/${gameId}/gameState`).once('value');
const gameState = currentGameState.val();
if (!gameState || !gameState.playerOrder) {
console.error('Invalid game state');
return;
}
let nextPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.playerOrder.length;
// Skip bankrupt players
const playersSnapshot = await database.ref(`games/${gameId}/players`).once('value');
const allPlayers = playersSnapshot.val();
let attempts = 0;
while (attempts < gameState.playerOrder.length) {
const nextPlayerId = gameState.playerOrder[nextPlayerIndex];
const nextPlayer = allPlayers[nextPlayerId];
if (nextPlayer && !nextPlayer.bankrupt) {
break; // Found valid next player
}
nextPlayerIndex = (nextPlayerIndex + 1) % gameState.playerOrder.length;
attempts++;
}
// Update game state
await database.ref(`games/${gameId}/gameState`).update({
currentPlayerIndex: nextPlayerIndex,
turnStartedAt: firebase.database.ServerValue.TIMESTAMP
});
} catch (error) {
console.error('Error ending turn:', error);
}
}
/* Sophisticated CSS custom property system */
:root {
--white: #FFFFFD;
--black: #383838;
--blue: #007AFF;
--yellow: #FFCC02;
--shadow: 0 2px 8px rgba(56, 56, 56, 0.1);
--radius: 12px;
}
/* Apple-inspired typography and spacing */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--white);
color: var(--black);
min-height: 100vh;
overflow-x: hidden;
}
/* Screen transition system */
.screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.screen.active {
opacity: 1;
pointer-events: all;
}
/* Sophisticated keyframe animations */
@keyframes pulse {
0% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.05); opacity: 1; }
100% { transform: scale(1); opacity: 0.8; }
}
@keyframes slideDown {
from { transform: translateX(-50%) translateY(-100%); }
to { transform: translateX(-50%) translateY(0); }
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Mobile optimization */
@media (max-height: 700px) {
.setup-title { font-size: 24px; margin-bottom: 20px; }
.number-input { font-size: 36px; }
.color-option { width: 50px; height: 50px; }
.die { width: 60px; height: 60px; font-size: 28px; }
}
/* Touch-friendly interface */
* {
-webkit-tap-highlight-color: transparent;
box-sizing: border-box;
}
.btn {
padding: 16px 32px;
border-radius: var(--radius);
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--shadow);
width: 100%;
max-width: 320px;
margin: 10px auto;
}
.btn:active {
transform: scale(0.98);
}
async function addToHistory(message, playerId = null) {
const historyEntry = {
message: message,
timestamp: firebase.database.ServerValue.TIMESTAMP,
playerId: playerId
};
await database.ref(`games/${gameId}/history`).push(historyEntry);
}
// Real-time history updates
function updateGameHistory(snapshot) {
const history = snapshot.val() || {};
const historyContainer = document.getElementById('gameHistory');
if (!historyContainer) return;
historyContainer.innerHTML = '';
// Sort history by timestamp
const sortedHistory = Object.values(history)
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
.slice(0, 50); // Show last 50 entries
sortedHistory.forEach(entry => {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
const time = new Date(entry.timestamp);
historyItem.innerHTML = `
<div class="history-message">${entry.message}</div>
<div class="history-time">${time.toLocaleTimeString()}</div>
`;
historyContainer.appendChild(historyItem);
});
}
function showError(message) {
const errorEl = document.getElementById('errorMessage') || createErrorElement();
errorEl.textContent = message;
errorEl.style.display = 'block';
setTimeout(() => {
errorEl.style.display = 'none';
}, 3000);
}
// Robust Firebase error handling
async function safeFirebaseOperation(operation, errorMessage = 'Operation failed') {
try {
return await operation();
} catch (error) {
console.error('Firebase operation failed:', error);
showError(errorMessage);
throw error;
}
}
// Data validation
function validatePlayerName(name) {
if (!name || typeof name !== 'string') return false;
if (name.length < 1 || name.length > 20) return false;
return true;
}
function validateMoneyAmount(amount) {
return typeof amount === 'number' && amount >= 0 && isFinite(amount);
}
// Batched updates for performance
async function updatePlayerState(playerId, updates) {
const updateObject = {};
for (const [key, value] of Object.entries(updates)) {
updateObject[`games/${gameId}/players/${playerId}/${key}`] = value;
}
await database.ref().update(updateObject);
}
// Connection state monitoring
database.ref('.info/connected').on('value', (snapshot) => {
if (snapshot.val() === true) {
hideConnectionError();
} else {
showConnectionError();
}
});
// Memory management
function cleanupListeners() {
database.ref(`games/${gameId}`).off();
}
window.addEventListener('beforeunload', cleanupListeners);
Chaseopoly represents an exceptionally sophisticated implementation of multiplayer Monopoly that successfully translates the classic board game experience to a modern web application. The technical implementation demonstrates mastery of real-time web applications, Firebase integration, and complex game state management.
Technical Rating: 9.2/10
The application successfully creates an engaging multiplayer experience that captures the strategic depth and social dynamics of Monopoly while leveraging modern web technologies for seamless real-time gameplay across multiple devices.