Skip to content

Game Calculator

Game calculator taken from Tomasz’s post

Multi-Player Game Calculator

Player Dashboard
Worktypes
Financial Center
Initial Purse Bank Investor

Deduct Fee

Architect’s initial design fee (10%) Architect’s initial design fee (11%) Architect’s initial design fee (12%) Architect additional design services fee (1%) Architect additional design services fee (2%) Engineer’s initial design fee (5%) Engineer’s initial design fee (6%) Engineer additional design services fee (1%) Engineer additional design services fee (2%) DOB initial fee (1%) DOB Post Approval amendment fee (1%) FDNY initial fee (1%) Contractor Fee Civil penalty fee Filing representative fee Bypass Skip Architect ($500 – 1 day) Bypass Skip Engineer ($1,000 – 2 days) Bypass Skip DOB ($1,500 – 3 days) Bypass Skip FDNY ($2,000 – 4 days) Bypass Caught, Low Consequences ($10,000 – 60 Days) Bypass Caught, High Fines ($100,000 – 365 Days)
Work Type Manager
Expeditor Card Effects
Apply Expeditor Card
Fixed Amount Percentage Self Other Player All Players
Game Controls
Game Controls
let players = []; let currentPlayerIndex = 0; const gamePhases = [ { name: ‘Initial Setup’, color: ‘blue’, message: “You Are In Initial Setup” }, { name: ‘Funding’, color: ‘green’, message: “You Are Figuring out if you need more $” }, { name: ‘Design’, color: ‘yellow’, message: “You and your team is figuring out the design details” }, { name: ‘Owner Review’, color: ‘orange’, message: “You Are checking your GUT to see if all Jives” }, { name: ‘Regulatory Review’, color: ‘red’, message: “Government is snooping in your biz” }, { name: ‘Construction’, color: ‘purple’, message: “You Are finally building” }, { name: ‘Completion’, color: ‘gold’, message: “You Are FINISHED!!!! Was it worth it?” } ]; function updateGamePhase(player, trigger) { let newPhaseIndex = player.currentPhaseIndex || 0; switch(trigger) { case ‘gameStart’: newPhaseIndex = 0; break; case ‘moneyTaken’: newPhaseIndex = Math.max(1, newPhaseIndex); break; case ‘architectFeePaid’: newPhaseIndex = Math.max(2, newPhaseIndex); break; case ‘dobFeePaid’: newPhaseIndex = Math.max(4, newPhaseIndex); break; case ‘constructionFeePaid’: newPhaseIndex = Math.max(5, newPhaseIndex); break; case ‘gameFinished’: newPhaseIndex = 6; break; } if (newPhaseIndex !== player.currentPhaseIndex) { player.currentPhaseIndex = newPhaseIndex; displayGamePhase(player); } } function displayGamePhase(player) { const phase = gamePhases[player.currentPhaseIndex || 0]; const gamePhaseDisplay = document.getElementById(‘gamePhaseDisplay’); gamePhaseDisplay.textContent = phase.message; gamePhaseDisplay.style.borderColor = phase.color; const projectTimelineDisplay = document.getElementById(‘projectTimelineDisplay’); projectTimelineDisplay.textContent = phase.message; projectTimelineDisplay.style.color = phase.color; } function initializeGamePhase(player) { player.currentPhaseIndex = 0; displayGamePhase(player); } function saveGameData() { localStorage.setItem(‘multiPlayerGameData’, JSON.stringify({ players: players, currentPlayerIndex: currentPlayerIndex })); console.log(‘Game data saved:’, players); } function loadGameData() { console.log(‘Attempting to load game data…’); const savedData = localStorage.getItem(‘multiPlayerGameData’); if (savedData) { console.log(‘Saved data found:’, savedData); const parsedData = JSON.parse(savedData); players = parsedData.players; currentPlayerIndex = parsedData.currentPlayerIndex || 0; updatePlayerSelect(); document.getElementById(‘playerSelect’).value = currentPlayerIndex; displayGamePhase(players[currentPlayerIndex]); updatePlayerDisplay(); console.log(‘Game data loaded and display updated’); } else { console.log(‘No saved data found’); setupGame(); } } function clearAllMemory() { localStorage.removeItem(‘multiPlayerGameData’); players = []; currentPlayerIndex = 0; setupGame(); console.log(‘Memory cleared, game reset’); } function setupGame() { let count = prompt(“Enter the number of players (1-10):”); count = parseInt(count); if (count 10 || isNaN(count)) { alert(“Please enter a number between 1 and 10.”); return; } for (let i = 1; i { let option = document.createElement(‘option’); option.value = index; option.textContent = player.name; select.appendChild(option); }); select.value = currentPlayerIndex; updatePlayerDisplay(); } function formatNumber(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, “,”); } function getColorClass(purse, estimatedCost) { let ratio = purse / estimatedCost; if (ratio > 1.1) return ‘green’; if (ratio >= 1) return ‘orange’; return ‘red’; } function updatePlayerDisplay() { let player = players[currentPlayerIndex]; let info = document.getElementById(‘playerInfo’); let estimatedCost = player.worktypes.reduce((sum, w) => sum + w.cost, 0); let colorClass = getColorClass(player.purse, estimatedCost); let designFeePercentage = estimatedCost > 0 ? (player.designFeesPaid / estimatedCost) * 100 : 0; let designFeeColorClass = designFeePercentage < 20 ? 'green' : 'red'; let regulatoryAgencyFees = calculateRegulatoryAgencyFees(player); let loanFees = calculateLoanFees(player); let totalProjectCost = estimatedCost + player.designFeesPaid + regulatoryAgencyFees + loanFees; info.innerHTML = `

Purse: $${formatNumber(player.purse)}

Estimated Project Cost: $${formatNumber(estimatedCost)}

Current Design Fee: $${formatNumber(player.designFeesPaid)} (${designFeePercentage.toFixed(2)}%)

Regulatory Agency Fees: $${formatNumber(regulatoryAgencyFees)}

Loan Fees: $${formatNumber(loanFees)}

Total Project Cost: $${formatNumber(totalProjectCost)}

Filing Status: ${player.filingStatus || ‘Not Set’}

Contractor Quality: ${player.contractorQuality || ‘Not Set’}

Construction Duration: ${player.constructionDays || ‘Not Set’} days

`; let worktypesList = document.getElementById(‘worktypesList’); worktypesList.innerHTML = `

Worktypes (${player.worktypes.length}/6):

    ${player.worktypes.slice(0, 6).map((w, i) => `
  • ${i + 1}. ${w.name}: $${formatNumber(w.cost)}
  • `).join(”)}
`; updateMoneySourceOptions(player); updateTransactionHistory(player); updateFeeAmount(); document.getElementById(‘filingStatus’).value = player.filingStatus; document.getElementById(‘contractorQuality’).value = player.contractorQuality; displayGamePhase(player); document.getElementById(‘turnCounter’).textContent = player.turnCount; if (designFeePercentage >= 20) { alert(`Game over for ${player.name}. Design fees have reached or exceeded 20% of the estimated project cost.`); endGameForPlayer(currentPlayerIndex); } } // … (to be continued in the next message) // … (continued from previous part) function calculateRegulatoryAgencyFees(player) { return player.transactions.reduce((sum, t) => { if (t.source.includes(‘DOB’) || t.source.includes(‘FDNY’)) { return sum + Math.abs(t.amount); } return sum; }, 0); } function calculateLoanFees(player) { return player.transactions.reduce((sum, t) => { if (t.source === ‘Bank Fee’ || t.source === ‘Investor Fee’) { return sum + Math.abs(t.amount); } return sum; }, 0); } function updateTransactionHistory(player) { let transactionList = document.getElementById(‘transactionList’); let transactions = []; transactions.push(`
Turn ${player.turnCount}: Current purse total: $${formatNumber(player.purse)}
`); transactions = transactions.concat(player.transactions.map(t => { let amount = t.amount >= 0 ? `+$${formatNumber(t.amount)}` : `-$${formatNumber(Math.abs(t.amount))}`; let source = t.source; let details = ”; if (source === “Investor”) { if (t.part) { details = ` (Part ${t.part})`; } } else if (source === “Investor Fee”) { let dayCount = t.part === 2 ? 30 : t.part === 3 ? 50 : 70; details = ` Fee 5% (${dayCount} days)`; } else if (source === “Bank”) { if (t.repaymentDays) { details = ` (${t.repaymentDays} days)`; } } else if (source === “Bank Fee”) { let feePercentage; if (Math.abs(t.amount) < 14000) { feePercentage = "1%"; } else if (Math.abs(t.amount) <= 55000) { feePercentage = "2%"; } else { feePercentage = "3%"; } details = ` Fee ${feePercentage}`; } else if (source === "Contractor Fee") { details = ` (${player.constructionDays} days)`; } return `
= 0 ? ‘positive’ : ‘negative’}”>Turn ${t.turn}: ${amount} ${source}${details}
`; })); transactionList.innerHTML = transactions.join(”); } function updateMoneySourceOptions(player) { let sourceSelect = document.getElementById(‘moneySource’); let initialOption = sourceSelect.querySelector(‘option[value=”Initial”]’); if (player.initialPurseUsed) { if (initialOption) initialOption.remove(); } else { if (!initialOption) { let newOption = document.createElement(‘option’); newOption.value = ‘Initial’; newOption.textContent = ‘Initial Purse’; sourceSelect.insertBefore(newOption, sourceSelect.firstChild); } } toggleMoneyAmountInput(); } function toggleMoneyAmountInput() { let source = document.getElementById(‘moneySource’).value; let amountInput = document.getElementById(‘moneyAmount’); if (source === “Investor”) { amountInput.classList.add(‘hidden’); } else { amountInput.classList.remove(‘hidden’); } } function addWorktype() { let player = players[currentPlayerIndex]; let name = document.getElementById(‘worktypeName’).value; let cost = Math.floor(Number(document.getElementById(‘worktypeCost’).value)); if (name && cost > 0 && player.worktypes.length `${i + 1}. ${w.name}`).join(‘\n’); let worktypeIndex = prompt(`Enter the number of the worktype to delete:\n${worktypeList}`) – 1; if (worktypeIndex >= 0 && worktypeIndex `${i + 1}. ${w.name}`).join(‘\n’); let worktypeIndex = prompt(`Enter the number of the worktype to replace:\n${worktypeList}`) – 1; if (worktypeIndex >= 0 && worktypeIndex 0) { player.worktypes[worktypeIndex] = {name, cost}; updatePlayerDisplay(); saveGameData(); } else { alert(“Invalid worktype name or cost.”); } } else { alert(“Invalid worktype number.”); } } function addMoney() { let player = players[currentPlayerIndex]; let source = document.getElementById(‘moneySource’).value; let amount = Math.floor(Number(document.getElementById(‘moneyAmount’).value)); if (source === “Investor”) { let amountCount = prompt(“How many amounts to add? (2-4)”); if (amountCount 4) { alert(“Invalid number of amounts. Please choose between 2 and 4.”); return; } let totalAmount = 0; let amounts = []; for (let i = 0; i 0) { amounts.push(partAmount); totalAmount += partAmount; } else { alert(‘Please enter a valid amount.’); return; } } if (totalAmount { player.transactions.push({amount: amount, source: “Investor”, part: index + 1, turn: player.turnCount}); }); player.transactions.push({amount: -fee, source: “Investor Fee”, part: amounts.length, turn: player.turnCount}); updateGamePhase(player, ‘moneyTaken’); } else if (source === “Bank”) { if (amount 4000000) { alert(‘Bank loans must be between $1 and $4,000,000.’); return; } let feeRate; if (amount < 1400000) { feeRate = 0.01; } else if (amount 0) { player.purse += amount; if (source === “Initial”) { player.initialPurseUsed = true; } player.transactions.push({amount, source, turn: player.turnCount}); } else { alert(‘Please enter a valid amount.’); return; } } updatePlayerDisplay(); document.getElementById(‘moneyAmount’).value = ”; saveGameData(); } // … (to be continued in the next message) // … (continued from previous parts) function updateFeeAmount() { let feeType = document.getElementById(‘feeType’).value; let feeAmountInput = document.getElementById(‘feeAmount’); let player = players[currentPlayerIndex]; let estimatedCost = player.worktypes.reduce((sum, w) => sum + w.cost, 0); if (feeType.includes(“initial design fee”) || feeType.includes(“additional design services fee”) || feeType.includes(“DOB initial fee”) || feeType.includes(“DOB Post Approval amendment fee”) || feeType.includes(“FDNY initial fee”)) { let percentage = parseInt(feeType.match(/\d+/)[0]); let calculatedFee = Math.floor(estimatedCost * (percentage / 100)); feeAmountInput.value = calculatedFee; feeAmountInput.disabled = true; } else if (feeType === “Contractor Fee”) { let feePercentage; switch (player.contractorQuality) { case ‘low’: feePercentage = 0.18; break; case ‘medium’: feePercentage = 0.19; break; case ‘high’: feePercentage = 0.20; break; default: alert(“Please select a contractor quality before calculating the fee.”); feeAmountInput.value = ”; feeAmountInput.disabled = false; return; } let calculatedFee = Math.floor(estimatedCost * feePercentage); feeAmountInput.value = calculatedFee; feeAmountInput.disabled = true; } else if (feeType.startsWith(“Bypass”)) { switch (feeType) { case “Bypass Skip Architect”: feeAmountInput.value = 500; break; case “Bypass Skip Engineer”: feeAmountInput.value = 1000; break; case “Bypass Skip DOB”: feeAmountInput.value = 1500; break; case “Bypass Skip FDNY”: feeAmountInput.value = 2000; break; case “Bypass Caught, Low Consequences”: feeAmountInput.value = 10000; break; case “Bypass Caught, High Fines”: feeAmountInput.value = 100000; break; } feeAmountInput.disabled = true; } else { feeAmountInput.value = ”; feeAmountInput.disabled = false; } } function deductFeeFromPlayer() { let player = players[currentPlayerIndex]; let feeType = document.getElementById(‘feeType’).value; let amount = Math.floor(Number(document.getElementById(‘feeAmount’).value)); if (amount player.purse) { alert(“Invalid fee amount or insufficient funds.”); return; } player.purse -= amount; player.transactions.push({amount: -amount, source: feeType, turn: player.turnCount}); if (feeType.includes(“Architect”)) { updateGamePhase(player, ‘architectFeePaid’); } else if (feeType.includes(“DOB”) || feeType.includes(“FDNY”)) { updateGamePhase(player, ‘dobFeePaid’); } else if (feeType === “Contractor Fee”) { updateGamePhase(player, ‘constructionFeePaid’); } if (feeType.includes(“design fee”) || feeType.includes(“design services fee”)) { player.designFeesPaid += amount; } if (feeType.startsWith(“Bypass”)) { console.log(`Player used ${feeType}`); } updatePlayerDisplay(); saveGameData(); } function updateContractorInfo() { let player = players[currentPlayerIndex]; let quality = document.getElementById(‘contractorQuality’).value; let dieRoll = parseInt(document.getElementById(‘dieRoll’).value); let contractorInfo = document.getElementById(‘contractorInfo’); if (!quality || isNaN(dieRoll) || dieRoll 6) { contractorInfo.innerHTML = ‘Please select a contractor quality and enter a valid die roll (1-6).’; return; } let estimatedCost = player.worktypes.reduce((sum, w) => sum + w.cost, 0); let contractorMultiplier; switch (quality) { case ‘low’: contractorMultiplier = 1.2; break; case ‘medium’: contractorMultiplier = 1; break; case ‘high’: contractorMultiplier = 0.8; break; } let constructionDays = Math.floor(0.4 * Math.sqrt(estimatedCost / 1000) * dieRoll * player.worktypes.length * contractorMultiplier); contractorInfo.innerHTML = `

Construction Duration: ${constructionDays} days

Die Roll: ${dieRoll}

`; player.contractorQuality = quality; player.constructionDays = constructionDays; saveGameData(); updatePlayerDisplay(); } function updateFilingStatus() { let player = players[currentPlayerIndex]; let status = document.getElementById(‘filingStatus’).value; player.filingStatus = status; updatePlayerDisplay(); saveGameData(); } function undoLastTransaction() { let player = players[currentPlayerIndex]; if (player.transactions.length > 0) { let lastTransaction = player.transactions.pop(); player.purse -= lastTransaction.amount; if (lastTransaction.source.includes(‘design fee’) || lastTransaction.source.includes(‘design services fee’)) { player.designFeesPaid += lastTransaction.amount; } updatePlayerDisplay(); saveGameData(); } else { alert(“No transactions to undo.”); } } function undoLastTwoTransactions() { let player = players[currentPlayerIndex]; if (player.transactions.length >= 2) { for (let i = 0; i player.worktypes.length === 0)) { updateGamePhase(players[currentPlayerIndex], ‘gameFinished’); alert(“All players have completed their projects. Game Over!”); } } function endGameForPlayer(playerIndex) { players[playerIndex].worktypes = []; checkGameEnd(); } function confirmAction(action) { let message; switch(action) { case ‘previousPlayer’: message = “Are you sure you want to go to the previous player?”; break; case ‘nextPlayer’: message = “Are you sure you want to go to the next player?”; break; case ‘clearAllMemory’: message = “Are you sure you want to clear all memory and start a new game?”; break; case ‘undoLastTransaction’: message = “Are you sure you want to undo the last transaction?”; break; case ‘undoLastTwoTransactions’: message = “Are you sure you want to undo the last two transactions?”; break; } if (confirm(message)) { switch(action) { case ‘previousPlayer’: previousPlayer(); break; case ‘nextPlayer’: nextPlayer(); break; case ‘clearAllMemory’: clearAllMemory(); break; case ‘undoLastTransaction’: undoLastTransaction(); break; case ‘undoLastTwoTransactions’: undoLastTwoTransactions(); break; } } } function previousPlayer() { currentPlayerIndex = (currentPlayerIndex – 1 + players.length) % players.length; updatePlayerSelect(); updatePlayerDisplay(); } function nextPlayer() { currentPlayerIndex = (currentPlayerIndex + 1) % players.length; updatePlayerSelect(); updatePlayerDisplay(); } function endTurn() { players[currentPlayerIndex].turnCount++; currentPlayerIndex = (currentPlayerIndex + 1) % players.length; updatePlayerSelect(); updatePlayerDisplay(); saveGameData(); } function initExpeditorCards() { document.getElementById(‘expeditorCardTarget’).addEventListener(‘change’, function() { const targetPlayerSelect = document.getElementById(‘expeditorCardTargetPlayer’); targetPlayerSelect.classList.toggle(‘hidden’, this.value !== ‘other’); }); const targetPlayerSelect = document.getElementById(‘expeditorCardTargetPlayer’); players.forEach((player, index) => { let option = document.createElement(‘option’); option.value = index; option.textContent = player.name; targetPlayerSelect.appendChild(option); }); } function applyExpeditorCard() { const cardType = document.getElementById(‘expeditorCardType’).value; const cardValue = parseFloat(document.getElementById(‘expeditorCardValue’).value); const cardTarget = document.getElementById(‘expeditorCardTarget’).value; const cardDescription = document.getElementById(‘expeditorCardDescription’).value; if (isNaN(cardValue) || cardValue applyCardToPlayer(player, cardType, cardValue, cardDescription)); } updatePlayerDisplay(); saveGameData(); } function applyCardToPlayer(player, cardType, cardValue, description) { if (cardType === ‘fixed’) { player.purse -= cardValue; player.transactions.push({ amount: -cardValue, source: `Expeditor Card: ${description}`, turn: player.turnCount }); } else if (cardType === ‘percentage’) { const estimatedCost = player.worktypes.reduce((sum, w) => sum + w.cost, 0); const effect = Math.floor(estimatedCost * (cardValue / 100)); player.purse -= effect; player.transactions.push({ amount: -effect, source: `Expeditor Card: ${description} (${cardValue}% of ${estimatedCost})`, turn: player.turnCount }); } } function toggleGameControls() { const gameControls = document.querySelector(‘.game-controls’); gameControls.classList.toggle(‘visible’); } // Initialize the game window.addEventListener(‘load’, () => { loadGameData(); initExpeditorCards(); });