PyCatan-AI / pycatan /static /js /main.js
shon
1
74221d3
// 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();
}
});