// Game state let gameActive = false; let stockCharts = {}; // Format large numbers: add commas for thousands function formatNumber(value) { const num = Number(value); if (isNaN(num)) return value; if (Number.isInteger(num)) return num.toLocaleString(); return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } async function startGame() { const daysInput = document.getElementById('daysInput'); const days = parseInt(daysInput.value); if (!days || days < 1) { alert('Please enter a valid number of days'); return; } try { const response = await fetch('/api/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ days: days }) }); if (!response.ok) throw new Error('Failed to start game'); gameActive = true; document.getElementById('startScreen').classList.add('hidden'); document.getElementById('gameScreen').classList.remove('hidden'); document.getElementById('totalDays').textContent = days; await updateGameDisplay(); populateStockSelect(); } catch (error) { alert('Error starting game: ' + error.message); } } async function updateGameDisplay() { try { const response = await fetch('/api/game-state'); if (!response.ok) throw new Error('Failed to fetch game state'); const data = await response.json(); document.getElementById('balance').textContent = formatNumber(data.balance); document.getElementById('day').textContent = data.day; document.getElementById('index').textContent = formatNumber(data.index); displayStocks(data.stocks); displayHoldings(data.holdings); displayMessage(data.message); updateCharts(data.priceHistory, data.stocks, data.day); } catch (error) { console.error('Error updating game display:', error); } } function displayStocks(stocks) { const stocksList = document.getElementById('stocksList'); stocksList.innerHTML = ''; stocks.forEach(stock => { const div = document.createElement('div'); div.className = 'stock-item'; div.innerHTML = `${stock.name}$${formatNumber(stock.price)}`; stocksList.appendChild(div); }); } function displayHoldings(holdings) { const holdingsList = document.getElementById('holdingsList'); holdingsList.innerHTML = ''; holdings.forEach(holding => { const div = document.createElement('div'); div.className = 'holding-item'; div.innerHTML = `${holding.name}${holding.amount} shares`; holdingsList.appendChild(div); }); } function displayMessage(message) { const messageDiv = document.getElementById('message'); if (message) { messageDiv.textContent = message; messageDiv.classList.add('show'); messageDiv.classList.remove('error'); } else { messageDiv.classList.remove('show'); } } function populateStockSelect() { // Fetch the current game state to get the actual stocks fetch('/api/game-state') .then(response => response.json()) .then(data => { const stocks = data.stocks; const select = document.getElementById('stockSelect'); select.innerHTML = ''; stocks.forEach(stock => { const option = document.createElement('option'); option.value = stock.id; option.textContent = `${stock.name}`; select.appendChild(option); }); }) .catch(error => console.error('Error populating stock select:', error)); } async function buyStock() { if (!gameActive) return; const stockId = parseInt(document.getElementById('stockSelect').value); const amount = parseInt(document.getElementById('amountInput').value); if (!amount || amount < 1) { showError('Please enter a valid amount'); return; } try { const response = await fetch('/api/buy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stock_id: stockId, amount: amount }) }); const data = await response.json(); if (!response.ok) { showError(data.error); return; } document.getElementById('amountInput').value = '1'; await updateGameDisplay(); } catch (error) { showError('Error buying stock: ' + error.message); } } async function sellStock() { if (!gameActive) return; const stockId = parseInt(document.getElementById('stockSelect').value); const amount = parseInt(document.getElementById('amountInput').value); if (!amount || amount < 1) { showError('Please enter a valid amount'); return; } try { const response = await fetch('/api/sell', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stock_id: stockId, amount: amount }) }); const data = await response.json(); if (!response.ok) { showError(data.error); return; } document.getElementById('amountInput').value = '1'; await updateGameDisplay(); } catch (error) { showError('Error selling stock: ' + error.message); } } async function buyAll() { if (!gameActive) return; const stockId = parseInt(document.getElementById('stockSelect').value); try { const gameStateResponse = await fetch('/api/game-state'); const gameStateData = await gameStateResponse.json(); const stocks = gameStateData.stocks; const balance = gameStateData.balance; const stockPrice = stocks[stockId - 1]['price']; // Calculate maximum amount we can buy const maxAmount = Math.min(Math.floor(balance / stockPrice), 999999999999999999); if (maxAmount <= 0) { showError('Insufficient funds to buy any shares of this stock'); return; } try { const response = await fetch('/api/buy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stock_id: stockId, amount: maxAmount }) }); const data = await response.json(); if (!response.ok) { showError(data.error); return; } document.getElementById('amountInput').value = '1'; await updateGameDisplay(); } catch (error) { showError('Error buying stock: ' + error.message); } } catch (error) { showError('Error fetching game state: ' + error.message); } } async function sellAll() { if (!gameActive) return; const stockId = parseInt(document.getElementById('stockSelect').value); try { const gameStateResponse = await fetch('/api/game-state'); const gameStateData = await gameStateResponse.json(); const holdings = gameStateData.holdings; const amountToSell = Math.min(holdings[stockId - 1]['amount'], 999999999999999999); if (amountToSell <= 0) { showError('You do not own any shares of this stock'); return; } try { const response = await fetch('/api/sell', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stock_id: stockId, amount: amountToSell }) }); const data = await response.json(); if (!response.ok) { showError(data.error); return; } document.getElementById('amountInput').value = '1'; await updateGameDisplay(); } catch (error) { showError('Error selling stock: ' + error.message); } } catch (error) { showError('Error fetching game state: ' + error.message); } } async function nextDay() { if (!gameActive) return; try { const response = await fetch('/api/next-day', { method: 'POST' }); const data = await response.json(); if (data.game_over) { await endGame(); return; } await updateGameDisplay(); } catch (error) { showError('Error advancing to next day: ' + error.message); } } async function endGame() { try { const response = await fetch('/api/end-game'); if (!response.ok) throw new Error('Failed to fetch end game stats'); const data = await response.json(); document.getElementById('startBalance').textContent = formatNumber(data.starting_balance); document.getElementById('endBalance').textContent = formatNumber(data.ending_balance); document.getElementById('netWorth').textContent = formatNumber(data.total_net_worth); document.getElementById('profit').textContent = formatNumber(data.profit); // Color profit red or green const profitText = document.getElementById('profitText'); if (data.profit >= 0) { profitText.style.color = '#4caf50'; } else { profitText.style.color = '#f44336'; } // Display final holdings const finalHoldings = document.getElementById('finalHoldings'); finalHoldings.innerHTML = ''; data.holdings.forEach((holding, index) => { if (holding.amount > 0) { const div = document.createElement('div'); div.className = 'holding-final'; const stockPrice = Number(data.stocks[index].price); const value = holding.amount * stockPrice; div.innerHTML = ` ${holding.name}: ${holding.amount} shares Value: $${formatNumber(value)} `; finalHoldings.appendChild(div); } }); // Fetch current game state to get price history for the chart const gameStateResponse = await fetch('/api/game-state'); const gameStateData = await gameStateResponse.json(); // Display the final price chart if (gameStateData.priceHistory) { createEndPriceChart(gameStateData.priceHistory); } gameActive = false; document.getElementById('gameScreen').classList.add('hidden'); document.getElementById('endScreen').classList.remove('hidden'); } catch (error) { showError('Error getting game stats: ' + error.message); } } function showError(message) { const messageDiv = document.getElementById('message'); messageDiv.textContent = message; messageDiv.classList.add('show', 'error'); } async function saveScore() { const playerName = document.getElementById('playerName').value.trim(); if (!playerName) { alert('Please enter your name'); return; } try { const response = await fetch('/api/save-score', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: playerName }) }); const data = await response.json(); if (data.success) { alert(`Score saved! You ranked #${data.rank} on the leaderboard!`); // Clear the name input document.getElementById('playerName').value = ''; } } catch (error) { showError('Error saving score: ' + error.message); } } async function viewLeaderboard() { try { const response = await fetch('/api/leaderboard'); const data = await response.json(); document.getElementById('startScreen').classList.add('hidden'); document.getElementById('leaderboardScreen').classList.remove('hidden'); displayLeaderboard(data.scores); } catch (error) { showError('Error loading leaderboard: ' + error.message); } } function displayLeaderboard(scores) { const leaderboardList = document.getElementById('leaderboardList'); const emptyMessage = document.getElementById('emptyLeaderboard'); if (scores.length === 0) { leaderboardList.innerHTML = ''; emptyMessage.style.display = 'block'; return; } emptyMessage.style.display = 'none'; leaderboardList.innerHTML = ''; scores.forEach((score, index) => { const rank = index + 1; const div = document.createElement('div'); div.className = `leaderboard-entry rank-${rank}`; const profitClass = score.profit >= 0 ? 'positive' : 'negative'; const profitSign = score.profit >= 0 ? '+' : ''; div.innerHTML = `
#${rank}
${score.name}
Days: ${score.leaderboardDays} | Balance: $${formatNumber(score.final_balance)} | Net Worth: $${formatNumber(score.net_worth)} | ${score.timestamp}
${profitSign}$${formatNumber(score.profit)}
`; leaderboardList.appendChild(div); }); } function backToStart() { document.getElementById('leaderboardScreen').classList.add('hidden'); document.getElementById('startScreen').classList.remove('hidden'); } function createEndPriceChart(priceHistory) { // Define colors for stocks const colors = ['#667eea', '#764ba2', '#4caf50', '#ff9800', '#f44336', '#e91e63', '#9c27b0']; // Get the history for all stocks let history = []; const stockKeys = Object.keys(priceHistory); if (stockKeys.length > 0) { history = priceHistory[stockKeys[0]] || []; } const days = Array.from({ length: history.length }, (_, i) => i + 1); // Create datasets for each stock dynamically const datasets = stockKeys.map((key, index) => ({ label: key.charAt(0).toUpperCase() + key.slice(1), data: priceHistory[key] || [], borderColor: colors[index % colors.length], backgroundColor: colors[index % colors.length] + '15', fill: false, tension: 0.4, borderWidth: 2, pointRadius: 3, pointHoverRadius: 5, pointBackgroundColor: colors[index % colors.length], pointBorderColor: '#fff', pointBorderWidth: 1 })); const ctx = document.getElementById('endPriceChart'); if (!ctx) return; new Chart(ctx, { type: 'line', data: { labels: days, datasets: datasets }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: true, position: 'top', labels: { font: { size: 12 }, padding: 15, usePointStyle: true } } }, scales: { y: { beginAtZero: false, ticks: { callback: function(value) { return '$' + formatNumber(value); } } }, x: { title: { display: true, text: 'Day' } } } } }); } function updateCharts(priceHistory, stocks, currentDay) { // Define colors for stocks const colors = ['#667eea', '#764ba2', '#4caf50', '#ff9800', '#f44336', '#e91e63', '#9c27b0']; // Get the history for all stocks - use first stock to get day count let history = []; const priceHistoryKeys = Object.keys(priceHistory); if (priceHistoryKeys.length > 0) { history = priceHistory[priceHistoryKeys[0]] || []; } const days = Array.from({ length: history.length }, (_, i) => i + 1); // Create datasets for each stock dynamically const datasets = priceHistoryKeys.map((key, index) => { // Find the stock name from the stocks array that matches this key const matchingStock = stocks.find(s => s.name.toLowerCase().replace(/\s+/g, '').replace(/[^a-z0-9]/g, '') === key.toLowerCase()); const label = matchingStock ? matchingStock.name : key.charAt(0).toUpperCase() + key.slice(1); return { label: label, data: priceHistory[key] || [], borderColor: colors[index % colors.length], backgroundColor: colors[index % colors.length] + '15', fill: false, tension: 0.4, borderWidth: 2, pointRadius: 3, pointHoverRadius: 5, pointBackgroundColor: colors[index % colors.length], pointBorderColor: '#fff', pointBorderWidth: 1 }; }); const ctx = document.getElementById('priceChart'); if (!ctx) return; if (stockCharts['combined']) { stockCharts['combined'].data.labels = days; stockCharts['combined'].data.datasets = datasets; stockCharts['combined'].update(); } else { stockCharts['combined'] = new Chart(ctx, { type: 'line', data: { labels: days, datasets: datasets }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: true, position: 'top', labels: { font: { size: 12 }, padding: 15, usePointStyle: true } } }, scales: { y: { beginAtZero: false, ticks: { callback: function(value) { return '$' + formatNumber(value); } } }, x: { title: { display: true, text: 'Day' } } } } }); } } // Initialize when page loads document.addEventListener('DOMContentLoaded', () => { populateStockSelect(); });