MyCityopoly — Realtime — Bank & Players
Status: connecting…

Game

Create a game (you become admin) or join with a 6‑digit code. Open this page on other devices to sync live.

No game connected.

#NameTokenBalance

Bank Dashboard

Transactions

MyCityopoly — Rules

  1. Each player starts with $1500. Highest roll begins.
  2. Roll two dice; move clockwise. Doubles = extra turn. Three doubles in a row = Go to Jury Room.
  3. Unowned properties may be bought; if you pass, they go to auction.
  4. Pay rent on owned properties. Houses increase rent.
  5. Follow Question Mark / Community Chest instructions.
  6. Game ends at time limit or when all but one are bankrupt. Highest net worth wins at time limit.

Create Game

Join Game

Player Setup

// ---- CONFIG ---- const firebaseConfig = window.FIREBASE_CONFIG || { apiKey: "", authDomain: "", projectId: "", storageBucket: "", messagingSenderId: "", appId: "" }; if (!firebaseConfig.apiKey) { alert("Firebase config missing. Paste your config into window.FIREBASE_CONFIG before ."); } // ---- Init ---- const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // Elements const $ = (id) => document.getElementById(id); const rulesDlg = $('rulesDlg'), createDlg = $('createDlg'), joinDlg = $('joinDlg'), setupDlg = $('setupDlg'); const statusEl = $('status'), gameInfoEl = $('gameInfo'), dashboard = $('dashboard'), logEl = $('log'); const playersTableWrap = $('playerTableWrap'), playersTbody = document.querySelector('#playersTable tbody'); const fromSel = $('fromPlayer'), toSel = $('toPlayer'), amtInput = $('amount'), roleLabel = $('roleLabel'); // Wire basics $('rulesBtn').addEventListener('click', () => rulesDlg.showModal()); $('rulesClose').addEventListener('click', () => rulesDlg.close()); $('rulesOk').addEventListener('click', () => rulesDlg.close()); $('createBtn').addEventListener('click', () => createDlg.showModal()); $('createClose').addEventListener('click', () => createDlg.close()); $('createCancel').addEventListener('click', () => createDlg.close()); $('joinBtn').addEventListener('click', () => joinDlg.showModal()); $('joinClose').addEventListener('click', () => joinDlg.close()); $('joinCancel').addEventListener('click', () => joinDlg.close()); $('setupBtn').addEventListener('click', () => { if (!window.role?.admin) { alert('Only admin can manage players.'); return; } setupDlg.showModal(); }); $('setupClose').addEventListener('click', () => setupDlg.close()); $('setupDone').addEventListener('click', () => setupDlg.close()); // App state let uid = null; let gameId = null; let unsub = null; // Auth signInAnonymously(auth).catch(console.error); onAuthStateChanged(auth, (user) => { if (!user) { statusEl.textContent = "Status: offline"; return; } uid = user.uid; statusEl.textContent = "Status: online"; }); // Create game (creator becomes admin). Also register a 6-digit code in /codes and in /games/{id}/codes/{code} $('createGo').addEventListener('click', async () => { const name = $('gameName').value.trim() || "MyCityopoly"; const code = String(Math.floor(100000 + Math.random()*900000)); // 6-digit const gamesCol = collection(db, "games"); const ref = doc(gamesCol); // auto-id await setDoc(ref, { name, code, createdAt: serverTimestamp(), tokens: {}, playersIndex: {}, log: [] }); await setDoc(doc(ref, "members", uid), { admin: true, joinedAt: serverTimestamp() }); // codes mapping await setDoc(doc(db, "codes", code), { gameId: ref.id, createdAt: serverTimestamp() }); await setDoc(doc(ref, "codes", code), { active: true, createdAt: serverTimestamp() }); connect(ref.id); createDlg.close(); alert(`Game created. Share code: ${code}`); }); // Join game by 6-digit code (no gameId needed) $('joinGo').addEventListener('click', async () => { const code = $('joinCode').value.trim(); if (!code || code.length !== 6) return alert('Enter a 6-digit code.'); const codeRef = doc(db, "codes", code); const codeSnap = await getDoc(codeRef); if (!codeSnap.exists()) return alert("Code not found."); const id = codeSnap.data().gameId; const ref = doc(db, "games", id); const gameSnap = await getDoc(ref); if (!gameSnap.exists()) return alert("Game not found."); // write membership with code for rule validation await setDoc(doc(ref, "members", uid), { admin: false, joinedAt: serverTimestamp(), codeEntered: code }, { merge: true }); connect(id); joinDlg.close(); }); // Connect to a game async function connect(id) { if (unsub) { unsub(); unsub = null; } gameId = id; const ref = doc(db, "games", id); const roleSnap = await getDoc(doc(ref, "members", uid)); window.role = roleSnap.exists() ? roleSnap.data() : { admin:false }; roleLabel.textContent = window.role.admin ? "(Admin)" : "(Player)"; const url = new URL(location.href); url.hash = id; history.replaceState(null, "", url.toString()); gameInfoEl.innerHTML = `Game: ${id} — Share the 6‑digit code to invite players.`; unsub = onSnapshot(ref, (snap) => { if (!snap.exists()) return; const data = snap.data(); renderPlayersSection(data.playersIndex || {}); renderLogSection(data.log || []); }); } function renderPlayersSection(index) { const entries = Object.values(index); entries.sort((a,b)=> a.name.localeCompare(b.name)); playersTbody.innerHTML = entries.map((p, i) => `${i+1}${escapeHtml(p.name)}${escapeHtml(p.token)}$${Number(p.balance).toLocaleString()}` ).join(''); playersTableWrap.style.display = entries.length ? 'block' : 'none'; dashboard.style.display = entries.length ? 'block' : 'none'; fromSel.innerHTML = `` + entries.map(p => ``).join(''); toSel.innerHTML = `` + entries.map(p => ``).join(''); document.querySelectorAll('#dashboard .btn[data-action]').forEach(b => b.disabled = entries.length === 0); } function renderLogSection(log) { logEl.innerHTML = (log||[]).slice(0,200).map(e => { const time = new Date(e.t || Date.now()).toLocaleTimeString(); return `
${time} — ${escapeHtml(e.msg)}
`; }).join(''); } // Player setup (admin only) $('setupAdd').addEventListener('click', async () => { if (!window.role?.admin) return alert('Admin only.'); if (!gameId) return alert('No game.'); const name = $('pName').value.trim(); const token = $('pToken').value; if (!name) return alert('Enter a name.'); const ref = doc(db, "games", gameId); await runTransaction(db, async (tx) => { const snap = await tx.get(ref); const data = snap.data() || {}; const idx = data.playersIndex || {}; const tokens = data.tokens || {}; if (Object.values(idx).some(p => p.name.toLowerCase() === name.toLowerCase())) { throw new Error('Name already taken.'); } if (tokens[token]) { throw new Error('Token already taken.'); } const id = crypto.randomUUID(); tokens[token] = id; idx[id] = { id, name, token, balance: 1500, mortgaged: 0 }; const log = [{ t: Date.now(), msg: `+ ${name} (${token})` }, ...(data.log||[])].slice(0,500); tx.set(ref, { tokens, playersIndex: idx, log }, { merge: true }); }).then(()=>{ $('pName').value=""; }).catch(e=>alert(e.message)); }); $('setupClear').addEventListener('click', async () => { if (!window.role?.admin) return alert('Admin only.'); if (!confirm('Remove all players?')) return; if (!gameId) return; const ref = doc(db, "games", gameId); await runTransaction(db, async (tx) => { const snap = await tx.get(ref); const data = snap.data() || {}; tx.set(ref, { tokens:{}, playersIndex:{}, log:[{t:Date.now(), msg:'Players cleared'}] }, { merge:true }); }); }); // Bank actions (admin only) document.querySelectorAll('#dashboard .btn[data-action]').forEach(btn => { btn.addEventListener('click', () => handleAction(btn.dataset.action)); }); async function handleAction(action) { if (!window.role?.admin) return alert('Admin only.'); if (!gameId) return alert('No game.'); const fromId = fromSel.value || null; const toId = toSel.value || null; const amt = Math.round(Number(amtInput.value || 0)); if (!amt || amt <= 0) return alert('Enter a positive amount.'); const ref = doc(db, "games", gameId); await runTransaction(db, async (tx) => { const snap = await tx.get(ref); const data = snap.data() || {}; const idx = data.playersIndex || {}; const get = (id) => idx[id]; const log = data.log || []; if (action === 'bankToPlayer') { if (!toId) throw new Error('Select recipient.'); get(toId).balance += amt; log.unshift({ t: Date.now(), msg: `Bank → ${get(toId).name}: $${amt}` }); } else if (action === 'playerToBank') { if (!fromId) throw new Error('Select payer.'); if ((get(fromId).balance||0) - amt < 0) throw new Error('Insufficient funds.'); get(fromId).balance -= amt; log.unshift({ t: Date.now(), msg: `${get(fromId).name} → Bank: $${amt}` }); } else if (action === 'playerToPlayer') { if (!fromId || !toId || fromId === toId) throw new Error('Pick two different players.'); if ((get(fromId).balance||0) - amt < 0) throw new Error('Insufficient funds.'); get(fromId).balance -= amt; get(toId).balance += amt; log.unshift({ t: Date.now(), msg: `${get(fromId).name} → ${get(toId).name}: $${amt}` }); } else if (action === 'mortgage') { if (!toId) throw new Error('Select player (use → select).'); get(toId).balance += amt; get(toId).mortgaged = (get(toId).mortgaged||0) + amt; log.unshift({ t: Date.now(), msg: `${get(toId).name} mortgaged, +$${amt}` }); } else if (action === 'unmortgage') { if (!fromId) throw new Error('Select player (use from ←).'); if ((get(fromId).mortgaged||0) - amt < 0) throw new Error('Owes less than that.'); if ((get(fromId).balance||0) - amt < 0) throw new Error('Insufficient funds.'); get(fromId).balance -= amt; get(fromId).mortgaged = (get(fromId).mortgaged||0) - amt; log.unshift({ t: Date.now(), msg: `${get(fromId).name} unmortgaged, -$${amt}` }); } else if (action === 'rent') { if (!fromId || !toId || fromId === toId) throw new Error('Pick payer and owner.'); if ((get(fromId).balance||0) - amt < 0) throw new Error('Insufficient funds.'); get(fromId).balance -= amt; get(toId).balance += amt; log.unshift({ t: Date.now(), msg: `${get(fromId).name} paid rent $${amt} → ${get(toId).name}` }); } tx.set(ref, { playersIndex: idx, log: log.slice(0,500) }, { merge: true }); }).then(()=>{ amtInput.value=""; }).catch(e=>alert(e.message)); } // Utils function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',\"'\":'''}[c])); } const firebaseConfig = { apiKey: "AIzaSyCniBqooNo6pGNMIxobgjqbFjVFiaLyuuI", authDomain: "mycityopoly-305c8.firebaseapp.com", projectId: "mycityopoly-305c8", storageBucket: "mycityopoly-305c8.firebasestorage.app", messagingSenderId: "184003631365", appId: "1:184003631365:web:1dc349340b950f19f9bf6c" };