Spaces:
Configuration error
Configuration error
| // Main file - Game initialization | |
| let catanBoard; | |
| let gameState = null; | |
| let eventSource = null; | |
| let pointMapping = null; // Point mapping - will be loaded from server | |
| let playerNames = {}; // Store player names from game | |
| window.replayServerEventsConnected = false; | |
| // Global functions for control buttons | |
| function zoomIn() { | |
| if (catanBoard) { | |
| catanBoard.zoomIn(); | |
| } | |
| } | |
| function zoomOut() { | |
| if (catanBoard) { | |
| catanBoard.zoomOut(); | |
| } | |
| } | |
| function resetZoom() { | |
| if (catanBoard) { | |
| catanBoard.resetZoom(); | |
| } | |
| } | |
| function toggleVertices() { | |
| if (catanBoard) { | |
| catanBoard.toggleVertices(); | |
| } | |
| } | |
| // Toggle building costs modal | |
| function toggleBuildingCosts() { | |
| const modal = document.getElementById('buildingCostsModal'); | |
| if (modal) { | |
| modal.classList.toggle('hidden'); | |
| } | |
| } | |
| // Clear action log | |
| function clearActionLog() { | |
| const logDiv = document.getElementById('action-log'); | |
| if (logDiv) { | |
| logDiv.innerHTML = '<div class="info">Log cleared โ</div>'; | |
| } | |
| } | |
| // ืืขืื ืช ืืืคืื ื ืงืืืืช ืืืฉืจืช | |
| function loadPointMapping() { | |
| return fetch('/api/point_mapping') | |
| .then(response => response.json()) | |
| .then(data => { | |
| pointMapping = data; | |
| // console.log('๐บ๏ธ Point mapping loaded:', `${data.total_points} points`); | |
| // console.log(' Example: point 1 at', data.point_to_coords[1]); | |
| // Make mapping global so board.js can access it | |
| window.pointMapping = pointMapping; | |
| return pointMapping; | |
| }) | |
| .catch(error => { | |
| console.error('โ Error loading point mapping:', error); | |
| // fallback - create basic mapping | |
| pointMapping = { | |
| point_to_coords: {}, | |
| coords_to_point: {}, | |
| total_points: 54, | |
| all_points: Array.from({length: 54}, (_, i) => i + 1) | |
| }; | |
| return pointMapping; | |
| }); | |
| } | |
| // ืืืืืจ ื-Flask server | |
| function connectToServer() { | |
| console.log('๐ Connecting to server...'); | |
| // First load point mapping | |
| loadPointMapping().then(() => { | |
| console.log('โ Point mapping loaded'); | |
| // Now load game state | |
| return Promise.all([ | |
| fetch('/api/game-state', {timeout: 5000}), | |
| fetch('/api/actions'), | |
| fetch('/api/chat') | |
| ]); | |
| }) | |
| .then(responses => { | |
| return Promise.all(responses.map(r => { | |
| if (!r.ok) throw new Error(`Server responded with ${r.status}`); | |
| return r.json(); | |
| })); | |
| }) | |
| .then(([gameStateData, actionsData, chatData]) => { | |
| console.log('๐ฅ Game state received from server:', gameStateData); | |
| // Check if state is empty (no hexes) | |
| if (!gameStateData.hexes || gameStateData.hexes.length === 0) { | |
| console.log('โ ๏ธ Server state is empty, using GAMEDATA as fallback'); | |
| updateGameState(GAMEDATA); | |
| } else { | |
| updateGameState(gameStateData); | |
| } | |
| // Store player names for later use | |
| if (gameStateData.players) { | |
| gameStateData.players.forEach((player, index) => { | |
| playerNames[index] = player.name || `Player ${index + 1}`; | |
| }); | |
| } | |
| // Load action history | |
| if (actionsData && Array.isArray(actionsData)) { | |
| console.log(`๐ฅ Loaded ${actionsData.length} previous actions`); | |
| actionsData.forEach(action => logAction(action)); | |
| } | |
| if (chatData && Array.isArray(chatData)) { | |
| console.log(`Loaded ${chatData.length} previous chat messages`); | |
| chatData.forEach(chat => handlePlayerChat(chat)); | |
| } | |
| console.log('โ Server connection established successfully'); | |
| // Connect to real-time updates | |
| connectToSSE(); | |
| }) | |
| .catch(error => { | |
| console.error('โ Error connecting to server:', error); | |
| console.log('๐ Using GAMEDATA as fallback'); | |
| updateGameState(GAMEDATA); | |
| }); | |
| } | |
| // Connect to Server-Sent Events for real-time updates | |
| function connectToSSE() { | |
| try { | |
| eventSource = new EventSource('/api/events'); | |
| eventSource.onopen = function() { | |
| window.replayServerEventsConnected = true; | |
| document.dispatchEvent(new Event('server-events-connected')); | |
| }; | |
| eventSource.onmessage = function(event) { | |
| const data = JSON.parse(event.data); | |
| // console.log('๐ก Update from server:', data); | |
| if (data.type === 'game_update' || data.type === 'state_updated') { | |
| // Update player names if we get new player data | |
| if (data.payload.players) { | |
| data.payload.players.forEach((player, index) => { | |
| playerNames[index] = player.name || `Player ${index + 1}`; | |
| }); | |
| } | |
| updateGameState(data.payload); | |
| } else if (data.type === 'action_executed') { | |
| logAction(data.payload); | |
| if (typeof handleBoardActionEvent === 'function') { | |
| handleBoardActionEvent(data.payload); | |
| } | |
| // Refresh game state to show any changes | |
| refreshGameState(); | |
| } else if (data.type === 'dice_roll') { | |
| logEvent(data.payload, 'log-dice'); | |
| if (typeof showBoardDiceRoll === 'function') { | |
| showBoardDiceRoll(data.payload.dice_values, data.payload.player_name); | |
| } | |
| } else if (data.type === 'resource_distribution') { | |
| logResourceDistribution(data.payload); | |
| if (typeof showBoardResourceDistribution === 'function') { | |
| showBoardResourceDistribution(data.payload.distributions); | |
| } | |
| // Refresh game state to show updated resources | |
| refreshGameState(); | |
| } else if (data.type === 'log_event') { | |
| logAction(data.payload); | |
| if (typeof handleBoardActionEvent === 'function') { | |
| handleBoardActionEvent(data.payload); | |
| } | |
| } else if (data.type === 'turn_start') { | |
| logEvent(data.payload, 'log-turn'); | |
| } else if (data.type === 'message') { | |
| logEvent(data.payload, 'info'); | |
| } else if (data.type === 'error') { | |
| logEvent(data.payload, 'error'); | |
| } else if (data.type === 'player_chat') { | |
| // Handle player chat message (say_outloud) | |
| handlePlayerChat(data.payload); | |
| } else if (data.type === 'ai_status') { | |
| // Handle AI thinking status update | |
| handleAIStatus(data.payload); | |
| } else if (data.type === 'replay_seek') { | |
| handleReplaySeek(data.payload); | |
| } | |
| }; | |
| eventSource.onerror = function(error) { | |
| console.error('โ Error in SSE connection:', error); | |
| }; | |
| console.log('โ Connected to real-time updates'); | |
| } catch (error) { | |
| console.warn('โ ๏ธ Unable to connect to SSE:', error); | |
| } | |
| } | |
| function handleReplaySeek(payload) { | |
| if (!payload) return; | |
| if (payload.game_state) { | |
| updateGameState(payload.game_state); | |
| } | |
| if (typeof handleBoardReplaySnapshot === 'function') { | |
| handleBoardReplaySnapshot(payload); | |
| } | |
| renderActionHistory(payload.action_history || []); | |
| renderChatHistory(payload.chat_history || []); | |
| if (payload.phase === 'speech' && Array.isArray(payload.chat_history) && payload.chat_history.length) { | |
| const latestChat = payload.chat_history[payload.chat_history.length - 1]; | |
| const playerName = latestChat.player_name || latestChat.from || latestChat.player; | |
| const message = latestChat.message || latestChat.text; | |
| if (playerName && message && typeof showPlayerChatBubble === 'function') { | |
| showPlayerChatBubble(playerName, message); | |
| } | |
| } | |
| if (window.replayControls && typeof window.replayControls.updateFromPayload === 'function') { | |
| window.replayControls.updateFromPayload(payload); | |
| } | |
| } | |
| function renderActionHistory(actions) { | |
| const logDiv = document.getElementById('action-log'); | |
| if (!logDiv) return; | |
| logDiv.innerHTML = ''; | |
| if (!actions.length) { | |
| logDiv.innerHTML = '<div class="info">Waiting for updates...</div>'; | |
| return; | |
| } | |
| actions.forEach(action => logAction(action)); | |
| } | |
| function renderChatHistory(messages) { | |
| const chatLog = document.getElementById('chat-log'); | |
| if (!chatLog) return; | |
| chatLog.innerHTML = ''; | |
| if (!messages.length) { | |
| chatLog.innerHTML = '<div class="info">No chat messages yet...</div>'; | |
| return; | |
| } | |
| messages.forEach(msg => { | |
| const playerName = msg.player_name || msg.from || msg.player || 'Unknown'; | |
| const message = msg.message || msg.text || ''; | |
| const timestamp = msg.timestamp || ''; | |
| const chatElement = document.createElement('div'); | |
| chatElement.className = 'chat-log-message'; | |
| chatElement.innerHTML = ` | |
| <div class="chat-log-header"> | |
| <span class="chat-log-player">ื ยโชโ ${escapeHtmlLocal(playerName)}</span> | |
| <span class="chat-log-time">${escapeHtmlLocal(timestamp)}</span> | |
| </div> | |
| <div class="chat-log-text">"${escapeHtmlLocal(message)}"</div> | |
| `; | |
| chatLog.appendChild(chatElement); | |
| }); | |
| scrollChatLogToBottom(); | |
| } | |
| function scrollChatLogToBottom() { | |
| const chatLog = document.getElementById('chat-log'); | |
| const chatPanel = document.getElementById('chat-log-panel'); | |
| const scrollTargets = [chatLog, chatPanel].filter(Boolean); | |
| if (!scrollTargets.length) return; | |
| const scrollToEnd = () => { | |
| scrollTargets.forEach(element => { | |
| element.scrollTop = element.scrollHeight; | |
| }); | |
| }; | |
| scrollToEnd(); | |
| requestAnimationFrame(scrollToEnd); | |
| } | |
| // Refresh game state from server | |
| async function refreshGameState() { | |
| try { | |
| const response = await fetch('/api/game-state'); | |
| if (response.ok) { | |
| const newState = await response.json(); | |
| updateGameState(newState); | |
| } | |
| } catch (error) { | |
| console.warn('Failed to refresh game state:', error); | |
| } | |
| } | |
| // Update game state | |
| function updateGameState(newState) { | |
| const previousState = gameState; | |
| gameState = newState; | |
| window.gameState = gameState; // Make globally accessible for unified UI | |
| if (catanBoard) { | |
| catanBoard.updateFromGameState(gameState); | |
| // Update vertex IDs if we have mapping | |
| if (pointMapping) { | |
| catanBoard.updateVertexIDsFromMapping(); | |
| } | |
| } | |
| updateGameInfo(gameState); | |
| if (typeof handleBoardStateUpdate === 'function') { | |
| handleBoardStateUpdate(gameState, previousState); | |
| } | |
| } | |
| // Update player information display | |
| function updateGameInfo(state) { | |
| const gameInfoDiv = document.getElementById('game-info'); | |
| if (!gameInfoDiv) return; | |
| // Preserve expanded state | |
| const expandedPlayers = new Set(); | |
| document.querySelectorAll('.player-info.expanded').forEach(el => { | |
| expandedPlayers.add(el.dataset.playerId); | |
| }); | |
| let html = '<h3>๐ Game Info</h3>'; | |
| if (state.players) { | |
| state.players.forEach((player, index) => { | |
| const activeClass = state.current_player === index ? 'active' : ''; | |
| const isExpanded = expandedPlayers.has(String(index)) ? 'expanded' : ''; | |
| const playerColors = ['#FF4444', '#4444FF', '#44FF44', '#FFAA00']; | |
| const playerColor = playerColors[index % 4]; | |
| // Get player name from stored names or use default | |
| const playerName = playerNames[index] || player.name || `Player ${index + 1}`; | |
| // Format cards lists | |
| let cardsHtml = ''; | |
| if (player.cards_list && player.cards_list.length > 0) { | |
| // Count cards by type | |
| const cardCounts = {}; | |
| player.cards_list.forEach(card => { | |
| cardCounts[card] = (cardCounts[card] || 0) + 1; | |
| }); | |
| cardsHtml += '<div><strong>Resources:</strong><ul class="card-list">'; | |
| for (const [card, count] of Object.entries(cardCounts)) { | |
| cardsHtml += `<li>${card}: ${count}</li>`; | |
| } | |
| cardsHtml += '</ul></div>'; | |
| } else { | |
| cardsHtml += '<div><em>No resource cards</em></div>'; | |
| } | |
| let devCardsHtml = ''; | |
| if (player.dev_cards_list && player.dev_cards_list.length > 0) { | |
| // Count dev cards by type | |
| const devCardCounts = {}; | |
| player.dev_cards_list.forEach(card => { | |
| devCardCounts[card] = (devCardCounts[card] || 0) + 1; | |
| }); | |
| devCardsHtml += '<div style="margin-top:5px;"><strong>Development:</strong><ul class="card-list">'; | |
| for (const [card, count] of Object.entries(devCardCounts)) { | |
| devCardsHtml += `<li>${card}: ${count}</li>`; | |
| } | |
| devCardsHtml += '</ul></div>'; | |
| } | |
| html += ` | |
| <div class="player-info ${activeClass} ${isExpanded}" data-player-id="${index}" onclick="togglePlayerInfo(this)" style="border-left-color: ${playerColor};"> | |
| <h4>๐ค ${playerName}</h4> | |
| <div class="player-resources"> | |
| <strong>๐ VP:</strong> ${player.victory_points || 0} | | |
| <strong>๐ด Cards:</strong> ${player.total_cards || 0} | |
| </div> | |
| <div class="player-resources"> | |
| <strong>๐๏ธ:</strong> ${player.settlements || 0} | | |
| <strong>๐๏ธ:</strong> ${player.cities || 0} | | |
| <strong>๐ฃ๏ธ:</strong> ${player.roads || 0} | |
| </div> | |
| <div class="player-cards"> | |
| ${cardsHtml} | |
| ${devCardsHtml} | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| if (state.current_phase) { | |
| html += `<div style="margin-top: 10px; padding: 10px; background: rgba(52, 152, 219, 0.1); border-radius: 6px;"><strong>๐ Current Phase:</strong> ${state.current_phase}</div>`; | |
| } | |
| gameInfoDiv.innerHTML = html; | |
| // Update unified UI if available (for unified view) | |
| if (window.unifiedUI) { | |
| window.unifiedUI.renderPlayerHub(state.players); | |
| window.unifiedUI.updateGameDetails(state); | |
| window.unifiedUI.updateGameStats(state); | |
| } | |
| } | |
| // Toggle player info visibility | |
| window.togglePlayerInfo = function(element) { | |
| element.classList.toggle('expanded'); | |
| } | |
| // Log action | |
| function logAction(actionData) { | |
| const logDiv = document.getElementById('action-log'); | |
| if (!logDiv) return; | |
| const actionElement = document.createElement('div'); | |
| // Determine class based on action type if needed, or just success/error | |
| let className = actionData.success ? 'success' : 'error'; | |
| let prefix = actionData.success ? 'โ' : 'โ'; | |
| // Add specific classes for certain actions | |
| if (actionData.success) { | |
| if (actionData.action_type && actionData.action_type.includes('BUILD')) { | |
| className = 'log-build'; | |
| prefix = '๐จ'; | |
| } | |
| } | |
| actionElement.className = className; | |
| actionElement.classList.add('event-log-entry'); | |
| actionElement.innerHTML = renderEventLogEntry(prefix, actionData); | |
| appendToLog(logDiv, actionElement); | |
| } | |
| function renderEventLogEntry(prefix, actionData) { | |
| const timestamp = actionData.timestamp || new Date().toLocaleTimeString('en-GB', { hour12: false }); | |
| const formatted = formatActionEventForDisplay(actionData); | |
| const detailHtml = formatted.details.length | |
| ? `<div class="event-log-details">${formatted.details.map(detail => `<span>${escapeHtmlLocal(detail)}</span>`).join('')}</div>` | |
| : ''; | |
| return ` | |
| <div class="event-log-main"> | |
| <span class="event-log-prefix">${escapeHtmlLocal(formatted.icon || prefix)}</span> | |
| <span class="event-log-message">${escapeHtmlLocal(formatted.message)}</span> | |
| <span class="event-log-time">${escapeHtmlLocal(timestamp)}</span> | |
| </div> | |
| ${detailHtml} | |
| `; | |
| } | |
| function formatActionEventForDisplay(actionData) { | |
| const data = actionData.data || {}; | |
| const eventType = String(actionData.event_type || actionData.action_type || '').toUpperCase(); | |
| const player = actionData.player_name || data.player_name || 'Player'; | |
| const fallback = actionData.message || `${player} performed ${eventType || 'action'}`; | |
| const details = []; | |
| if (eventType === 'TRADE_BANK') { | |
| const give = formatResourceBundleForLog(data.give || data.offer); | |
| const receive = formatResourceBundleForLog(data.receive || data.request); | |
| return { | |
| icon: '๐ฆ', | |
| message: `${player} traded with bank`, | |
| details: [`Gave: ${give}`, `Received: ${receive}`] | |
| }; | |
| } | |
| if (eventType === 'TRADE_EXECUTE') { | |
| const toPlayer = data.to_player || data.target_player || 'other player'; | |
| return { | |
| icon: 'โ ', | |
| message: `${player} traded with ${toPlayer}`, | |
| details: [ | |
| `${player} gave: ${formatResourceBundleForLog(data.offer || data.give)}`, | |
| `${player} received: ${formatResourceBundleForLog(data.request || data.receive)}` | |
| ] | |
| }; | |
| } | |
| if (eventType === 'TRADE_RESPONSE') { | |
| const response = data.response || data.trade_status || 'RESPONDED'; | |
| const toPlayer = data.to_player || data.target_player; | |
| return { | |
| icon: response === 'ACCEPT' ? 'โ ' : 'โ', | |
| message: toPlayer ? `${toPlayer} ${response.toLowerCase()} ${player}'s trade` : fallback, | |
| details | |
| }; | |
| } | |
| if (eventType === 'ROBBER_STEAL') { | |
| const victim = data.victim || data.victim_name || 'another player'; | |
| const card = formatCardNameForLog(data.card || data.stolen_card || 'card'); | |
| return { | |
| icon: '๐ฏ', | |
| message: `${player} stole ${card} from ${victim}`, | |
| details | |
| }; | |
| } | |
| if (eventType === 'ROBBER_MOVE' && data.victim) { | |
| const card = formatCardNameForLog(data.card || data.stolen_card || 'card'); | |
| return { | |
| icon: '๐ฆน', | |
| message: `${player} moved robber to tile ${data.tile || '?'} and stole ${card} from ${data.victim}`, | |
| details | |
| }; | |
| } | |
| if (eventType === 'DISCARD_CARDS') { | |
| return { | |
| icon: '๐๏ธ', | |
| message: `${player} discarded cards`, | |
| details: [formatResourceBundleForLog(data.discarded || data.cards)] | |
| }; | |
| } | |
| if (eventType === 'USE_DEV_CARD') { | |
| const card = formatCardNameForLog(data.card || data.card_type || 'development card'); | |
| if (String(card).toLowerCase().includes('monopoly') && data.total_stolen) { | |
| details.push(`Took ${data.total_stolen} ${formatCardNameForLog(data.resource || data.resource_type || '')}`); | |
| } | |
| if (String(card).toLowerCase().includes('year') && (data.gained || data.resources)) { | |
| details.push(`From bank: ${formatResourceBundleForLog(data.gained || data.resources)}`); | |
| } | |
| return { | |
| icon: 'โจ', | |
| message: `${player} used ${card}`, | |
| details | |
| }; | |
| } | |
| return { icon: '', message: fallback, details }; | |
| } | |
| function formatResourceBundleForLog(bundle) { | |
| const counts = normalizeResourceBundleForLog(bundle); | |
| const parts = Object.entries(counts) | |
| .filter(([, count]) => Number(count || 0) > 0) | |
| .map(([resource, count]) => `${count}x ${formatCardNameForLog(resource)}`); | |
| return parts.length ? parts.join(', ') : 'nothing'; | |
| } | |
| function normalizeResourceBundleForLog(bundle) { | |
| const counts = {}; | |
| if (!bundle) return counts; | |
| if (Array.isArray(bundle)) { | |
| bundle.forEach(card => { | |
| const key = normalizeCardKeyForLog(card); | |
| if (key) counts[key] = (counts[key] || 0) + 1; | |
| }); | |
| return counts; | |
| } | |
| if (typeof bundle === 'object') { | |
| Object.entries(bundle).forEach(([card, count]) => { | |
| const key = normalizeCardKeyForLog(card); | |
| const amount = Number(count || 0); | |
| if (key && amount > 0) counts[key] = (counts[key] || 0) + amount; | |
| }); | |
| return counts; | |
| } | |
| const key = normalizeCardKeyForLog(bundle); | |
| if (key) counts[key] = 1; | |
| return counts; | |
| } | |
| function normalizeCardKeyForLog(card) { | |
| return String(card || '').trim().toLowerCase().replace(/^(rescard|devcard)\./, '').replace(/[\s-]/g, '_'); | |
| } | |
| function formatCardNameForLog(card) { | |
| const key = normalizeCardKeyForLog(card); | |
| const labels = { | |
| wood: 'Wood', | |
| brick: 'Brick', | |
| sheep: 'Sheep', | |
| wool: 'Sheep', | |
| wheat: 'Wheat', | |
| grain: 'Wheat', | |
| ore: 'Ore', | |
| knight: 'Knight', | |
| road: 'Road Building', | |
| road_building: 'Road Building', | |
| monopoly: 'Monopoly', | |
| yearofplenty: 'Year of Plenty', | |
| year_of_plenty: 'Year of Plenty', | |
| victorypoint: 'Victory Point', | |
| victory_point: 'Victory Point' | |
| }; | |
| return labels[key] || String(card || 'Card').replace(/_/g, ' '); | |
| } | |
| // Log generic event | |
| function logEvent(data, className) { | |
| const logDiv = document.getElementById('action-log'); | |
| if (!logDiv) return; | |
| const element = document.createElement('div'); | |
| element.className = className; | |
| const timestamp = data.timestamp || new Date().toLocaleTimeString('en-GB', { hour12: false }); | |
| // Add emoji prefix based on type | |
| let prefix = ''; | |
| if (className === 'log-dice') prefix = '๐ฒ'; | |
| else if (className === 'log-turn') prefix = 'โค'; | |
| else if (className === 'info') prefix = 'โน๏ธ'; | |
| else if (className === 'error') prefix = 'โ ๏ธ'; | |
| element.textContent = `${prefix} ${data.message}`; | |
| appendToLog(logDiv, element); | |
| } | |
| // Log resource distribution specifically | |
| function logResourceDistribution(data) { | |
| const logDiv = document.getElementById('action-log'); | |
| if (!logDiv) return; | |
| const timestamp = data.timestamp || new Date().toLocaleTimeString('en-GB', { hour12: false }); | |
| // If we have detailed distributions, log them | |
| if (data.distributions) { | |
| for (const [player, resources] of Object.entries(data.distributions)) { | |
| if (resources && resources.length > 0) { | |
| const element = document.createElement('div'); | |
| element.className = 'log-resource'; | |
| // Count resources | |
| const counts = {}; | |
| resources.forEach(r => counts[r] = (counts[r] || 0) + 1); | |
| const resourceStr = Object.entries(counts) | |
| .map(([res, count]) => `${count}ร${res}`) | |
| .join(' '); | |
| element.textContent = `๐ฆ ${player}: ${resourceStr}`; | |
| appendToLog(logDiv, element); | |
| } | |
| } | |
| } else { | |
| // Fallback to generic message | |
| const element = document.createElement('div'); | |
| element.className = 'log-resource'; | |
| element.textContent = `๐ฆ ${data.message}`; | |
| appendToLog(logDiv, element); | |
| } | |
| } | |
| // Handle player chat message (say_outloud) | |
| function handlePlayerChat(data) { | |
| const playerName = data.player_name || data.from || 'Unknown'; | |
| const message = data.message || ''; | |
| const timestamp = data.timestamp || new Date().toLocaleTimeString(); | |
| // Store chat message for player | |
| if (!window.playerChatMessages) { | |
| window.playerChatMessages = {}; | |
| } | |
| window.playerChatMessages[playerName] = message; | |
| // Add to chat log panel only (not action log) | |
| const chatLog = document.getElementById('chat-log'); | |
| if (chatLog) { | |
| // Remove "no messages" placeholder if exists | |
| const placeholder = chatLog.querySelector('.info'); | |
| if (placeholder) { | |
| placeholder.remove(); | |
| } | |
| const chatElement = document.createElement('div'); | |
| chatElement.className = 'chat-log-message'; | |
| chatElement.innerHTML = ` | |
| <div class="chat-log-header"> | |
| <span class="chat-log-player">๐ค ${playerName}</span> | |
| <span class="chat-log-time">${timestamp}</span> | |
| </div> | |
| <div class="chat-log-text">"${message}"</div> | |
| `; | |
| chatLog.appendChild(chatElement); | |
| // Keep only last 50 messages | |
| while (chatLog.children.length > 50) { | |
| chatLog.removeChild(chatLog.firstChild); | |
| } | |
| scrollChatLogToBottom(); | |
| } | |
| // Update unified UI if available - show chat bubble | |
| if (window.showPlayerChatBubble) { | |
| window.showPlayerChatBubble(playerName, message); | |
| } | |
| } | |
| // Track current active player for thinking log | |
| let currentThinkingPlayer = null; | |
| // Handle AI thinking status update | |
| function handleAIStatus(data) { | |
| const playerName = data.player_name; | |
| const status = data.status; // 'thinking', 'tool_call', 'processing', 'done' | |
| const details = data.details || ''; | |
| console.log(`[AI_STATUS] ${playerName}: ${status} - ${details.substring(0, 30)}...`); | |
| // Find the thinking log element for this player | |
| const logElement = document.getElementById(`thinking-log-${playerName}`); | |
| if (!logElement) { | |
| console.warn(`[AI_STATUS] Could not find thinking-log-${playerName}`); | |
| return; | |
| } | |
| if (status === 'done' || status === 'idle') { | |
| // Don't hide immediately - let the reasoning stay visible | |
| // It will be cleared when a different player starts thinking | |
| return; | |
| } else if (status === 'thinking') { | |
| // Check if this is a NEW player starting to think | |
| if (currentThinkingPlayer !== playerName) { | |
| // Different player - clear ALL player logs first | |
| document.querySelectorAll('.player-thinking-log').forEach(log => { | |
| log.innerHTML = ''; | |
| log.style.display = 'none'; | |
| }); | |
| currentThinkingPlayer = playerName; | |
| } | |
| // Don't clear - just show and add to the log | |
| logElement.style.display = 'block'; | |
| } else { | |
| // Show the log container for other statuses | |
| logElement.style.display = 'block'; | |
| } | |
| // Create status entry | |
| let entry; | |
| let icon = ''; | |
| let text = ''; | |
| // Handle text_stream specially - replace content in a box | |
| if (status === 'text_stream') { | |
| // Find or create the streaming text box | |
| let textBox = logElement.querySelector('.streaming-text-box'); | |
| if (!textBox) { | |
| textBox = document.createElement('div'); | |
| textBox.className = 'thinking-entry streaming-text-box'; | |
| textBox.innerHTML = '<span class="thinking-icon">๐</span><span class="thinking-text streaming-text"></span>'; | |
| logElement.appendChild(textBox); | |
| } | |
| // Update the content (REPLACE, not append) | |
| const textSpan = textBox.querySelector('.streaming-text'); | |
| textSpan.textContent = details; | |
| // Scroll to show latest | |
| logElement.scrollTop = logElement.scrollHeight; | |
| return; // Don't create new entry | |
| } | |
| // Handle stream_done - remove the streaming box | |
| if (status === 'stream_done') { | |
| const textBox = logElement.querySelector('.streaming-text-box'); | |
| if (textBox) { | |
| textBox.remove(); | |
| } | |
| // Add completion icon | |
| entry = document.createElement('div'); | |
| entry.className = 'thinking-entry thinking-done'; | |
| entry.innerHTML = '<span class="thinking-icon">โ </span><span class="thinking-text">Ready</span>'; | |
| logElement.appendChild(entry); | |
| return; | |
| } | |
| // Regular status entries | |
| entry = document.createElement('div'); | |
| entry.className = `thinking-entry thinking-${status}`; | |
| if (status === 'thinking') { | |
| icon = '๐ญ'; | |
| text = details || 'Thinking...'; | |
| } else if (status === 'tool_call') { | |
| icon = '๐ง'; | |
| // Handle multiline tool calls (reasoning on separate line) | |
| text = (details || 'Using tools...').replace(/\n/g, '<br>'); | |
| } else if (status === 'executing_tools') { | |
| icon = 'โ๏ธ'; | |
| text = details || 'Executing tools...'; | |
| } else if (status === 'processing') { | |
| icon = 'โ๏ธ'; | |
| text = details || 'Processing...'; | |
| } else if (status === 'reasoning') { | |
| icon = '๐ก'; | |
| text = details; | |
| } else { | |
| icon = 'โข'; | |
| text = details || status; | |
| } | |
| entry.innerHTML = `<span class="thinking-icon">${icon}</span><span class="thinking-text">${text}</span>`; | |
| logElement.appendChild(entry); | |
| // Scroll to show latest | |
| logElement.scrollTop = logElement.scrollHeight; | |
| // Keep only last 15 entries to show full chain | |
| while (logElement.children.length > 15) { | |
| logElement.removeChild(logElement.firstChild); | |
| } | |
| } | |
| // Helper to append to log and scroll | |
| function appendToLog(container, element) { | |
| container.appendChild(element); | |
| // Keep only last 100 items | |
| while (container.children.length > 100) { | |
| container.removeChild(container.firstChild); | |
| } | |
| // Scroll to bottom | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| function escapeHtmlLocal(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text == null ? '' : String(text); | |
| return div.innerHTML; | |
| } | |
| // Game initialization | |
| document.addEventListener('DOMContentLoaded', async function() { | |
| console.log('๐ฒ Starting Catan board with server connection...'); | |
| try { | |
| // Create board instance (async) | |
| catanBoard = new CatanBoard(); | |
| window.catanBoard = catanBoard; // Expose to window for console access | |
| // Wait for board initialization | |
| if (catanBoard.init && typeof catanBoard.init === 'function') { | |
| console.log('โณ Waiting for board initialization...'); | |
| await catanBoard.init(); | |
| console.log('โ Board initialized successfully'); | |
| } | |
| // Connect to server | |
| connectToServer(); | |
| console.log('โ Catan board created successfully!'); | |
| console.log('๐ฎ Usage instructions:'); | |
| console.log(' - Click on hex to move robber'); | |
| console.log(' - Use mouse wheel to zoom'); | |
| console.log(' - Drag mouse to pan'); | |
| console.log(' - Click ๐ to see vertex numbers'); | |
| } catch (error) { | |
| console.error('โ Error initializing board:', error); | |
| // Try to create simple board anyway | |
| catanBoard = new CatanBoard(); | |
| connectToServer(); | |
| } | |
| }); | |
| // Cleanup on close | |
| window.addEventListener('beforeunload', function() { | |
| if (eventSource) { | |
| eventSource.close(); | |
| } | |
| }); | |