/** * GameBoard component - main game interface. * * Orchestrates game play, navigation, and completion. */ function GameBoard() { const { sessionId, gameId, gameState, loading: gameLoading, error: gameError, startGame, getGameStatus, abandonGame, } = useGame(); const [pageContent, setPageContent] = React.useState(null); const [pageLoading, setPageLoading] = React.useState(false); const [pageError, setPageError] = React.useState(null); const [isCompleted, setIsCompleted] = React.useState(false); // Load page content const loadPage = React.useCallback((title) => { if (!gameId || !title) return; setPageLoading(true); setPageError(null); fetch(`${window.location.origin}/api/v1/pages?game_id=${gameId}&title=${encodeURIComponent(title)}`) .then((res) => { if (!res.ok) { return res.json().then((data) => { throw new Error(data.error?.message || 'Failed to load page'); }); } return res.json(); }) .then((data) => { setPageContent(data); setPageLoading(false); // Refresh game state from server after a short delay to ensure state is updated setTimeout(() => { getGameStatus(); }, 100); // Check if target reached if (gameState && data.title === gameState.targetPage) { setIsCompleted(true); } }) .catch((err) => { console.error('Failed to load page', err); setPageError(err.message); setPageLoading(false); }); }, [gameId, gameState, getGameStatus]); // Navigate to a new page const navigateToPage = React.useCallback((targetPage) => { if (!gameId || !gameState || isCompleted) return; // Get current page before navigation const currentPage = gameState?.currentPage || gameState?.startPage; // Call navigation step endpoint to record the step fetch(`${window.location.origin}/api/v1/navigation/step`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ game_id: gameId, source_page: currentPage, target_page: targetPage, }), }) .then((res) => { if (!res.ok) { return res.json().then((data) => { throw new Error(data.error?.message || 'Failed to navigate'); }); } return res.json(); }) .then((stepData) => { // Update game state from server getGameStatus(); // Check if completed if (stepData.is_complete) { setIsCompleted(true); } // Load page content loadPage(targetPage); }) .catch((err) => { console.error('Failed to navigate', err); setPageError(err.message); }); }, [gameId, gameState, loadPage, getGameStatus, isCompleted]); // Handle link click const handleLinkClick = React.useCallback((linkTitle) => { if (isCompleted) return; // Prevent navigation after completion navigateToPage(linkTitle); }, [isCompleted, navigateToPage]); // Track previous gameId to detect changes const prevGameIdRef = React.useRef(gameId); // Handler for Give Up button const handleGiveUp = React.useCallback(() => { if (window.confirm('Are you sure you want to give up?')) { setPageContent(null); setPageError(null); setIsCompleted(false); abandonGame(); } }, [abandonGame]); // Handler for New Game button const handleNewGame = React.useCallback(() => { if (window.confirm('Start a new game?')) { setIsCompleted(false); setPageContent(null); setPageError(null); // Clear current game from localStorage before starting new one try { localStorage.removeItem('wikigame_game_id'); } catch (e) { console.warn('Failed to clear game from localStorage:', e); } startGame('random'); // Mode is ignored, uses env var } }, [startGame]); // Clear page content when gameId changes (new game or abandonment) React.useEffect(() => { const prevGameId = prevGameIdRef.current; if (gameId !== prevGameId) { // gameId changed - clear all page-related state setPageContent(null); setPageError(null); setIsCompleted(false); prevGameIdRef.current = gameId; } }, [gameId]); // Load initial page when game starts React.useEffect(() => { if (gameState && gameState.currentPage && !pageContent && gameId) { loadPage(gameState.currentPage); } }, [gameState, pageContent, loadPage, gameId]); // Refresh game status periodically and check completion React.useEffect(() => { if (!gameId || !gameState) return; // Only check completion for games that are in progress // Don't mark as completed if we just reset isCompleted or if game is already abandoned if (gameState.status === 'in_progress') { // Check if game is completed if (gameState.currentPage === gameState.targetPage && gameState.currentPage !== gameState.startPage) { setIsCompleted(true); } const interval = setInterval(() => { getGameStatus(); }, 1000); // Update every second return () => clearInterval(interval); } else if (gameState.status === 'completed') { // If game is already marked as completed on server, set local state setIsCompleted(true); } }, [gameId, gameState, getGameStatus]); const [showInstructions, setShowInstructions] = React.useState(false); // Initial state: no game started if (!gameId) { return React.createElement( 'div', { className: 'game-board' }, React.createElement('h1', null, 'WikiGame Reloaded (Research Edition)'), // Instructions Modal showInstructions && React.createElement( 'div', { className: 'modal-overlay', style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }, onClick: () => setShowInstructions(false) }, React.createElement( 'div', { className: 'modal-content', style: { backgroundColor: 'white', padding: '2rem', borderRadius: '8px', maxWidth: '800px', width: '90%', maxHeight: '90vh', overflowY: 'auto', position: 'relative' }, onClick: e => e.stopPropagation() }, React.createElement('button', { onClick: () => setShowInstructions(false), style: { position: 'absolute', top: '10px', right: '10px', border: 'none', background: 'none', fontSize: '1.5rem', cursor: 'pointer', padding: '5px' } }, '×'), React.createElement('h3', null, 'How to Play'), React.createElement('p', null, 'Your goal is to navigate from the start page (in this case Berlin Wall) to the target page (Eiffel Tower) using only the links in the Wikipedia articles.'), React.createElement('img', { src: '/game_example.svg', alt: 'Game Example', style: { maxWidth: '100%', height: 'auto', margin: '1rem auto', display: 'block', border: '1px solid #ccc', borderRadius: '4px' } }), React.createElement('p', null, 'Within the game, you see at the top of the page the target page (Eiffel Tower) and the time. To stop the game, you need to reach the target page or press the "Give Up" or "New Game" buttons. "Give Up" will stop the game. "New Game" will start a new game with a new target page and a new start page.'), React.createElement('img', { src: '/example_run.png', alt: 'Example Run', style: { maxWidth: '90%', height: 'auto', margin: '1rem auto', display: 'block', border: '1px solid #ccc', borderRadius: '4px' } }), React.createElement( 'div', { style: { textAlign: 'center', marginTop: '1.5rem' } }, React.createElement('button', { className: 'btn btn-primary', onClick: () => setShowInstructions(false) }, 'Close') ) ) ), React.createElement( 'div', { className: 'info-box' }, React.createElement('strong', null, 'Research Information'), React.createElement('p', null, 'We are conducting research on link following tasks. Link following tasks are a common task that people perform on the web to navigate from one page to another. '), React.createElement('p', null, 'For this research, we collect anonymized data about your navigation patterns, including which links you click and how you navigate between Wikipedia articles.'), React.createElement('p', null, 'All data is anonymized and used solely for research purposes. No personally identifiable information is collected.') ), React.createElement( 'div', { style: { display: 'flex', gap: '1rem', justifyContent: 'center', marginTop: '1rem' } }, React.createElement( 'button', { type: 'button', className: 'btn btn-secondary', onClick: () => setShowInstructions(true), style: { backgroundColor: '#6c757d', color: 'white' } }, 'How to Play' ), React.createElement( 'button', { type: 'button', className: 'btn btn-primary', onClick: () => startGame('random'), // Mode is ignored, uses env var disabled: gameLoading, }, gameLoading ? 'Starting...' : 'Start Game' ) ), gameError && React.createElement( 'div', { className: 'error-message', role: 'alert' }, React.createElement('strong', null, 'Error: '), gameError ), pageError && React.createElement( 'div', { className: 'error-message', role: 'alert' }, React.createElement('strong', null, 'Error loading page: '), pageError ) ); } // Game completed state if (isCompleted && gameState) { return React.createElement( 'div', { className: 'game-board' }, React.createElement(GameCompletion, { clickCount: gameState.clickCount || 0, elapsedTime: gameState.startTime, path: gameState.path || [], onPlayAgain: (e) => { // Prevent any default behavior (page refresh, etc.) if (e && e.preventDefault) { e.preventDefault(); e.stopPropagation(); } // Clear all game-related state immediately setIsCompleted(false); setPageContent(null); setPageError(null); // For completed games, just clear local state without calling abandon endpoint // Completed games should remain "completed" in statistics, not "abandoned" if (gameId && gameState && gameState.status === 'completed') { // Clear localStorage and state immediately try { localStorage.removeItem('wikigame_game_id'); } catch (err) { console.warn('Failed to clear game from localStorage:', err); } // startGame() will clear gameId and gameState, so we can call it directly startGame('random'); // Mode is ignored, uses env var } else if (gameId) { // For in-progress games, properly abandon them before starting a new game abandonGame() .then(() => { // After abandoning is complete, start a new game startGame('random'); // Mode is ignored, uses env var }) .catch((err) => { console.warn('Failed to abandon game, starting new game anyway:', err); // Even if abandon fails, try to start a new game startGame('random'); }); } else { // No game to abandon, just start a new one startGame('random'); } }, }) ); } // If gameId exists but gameState is not yet loaded, show loading state // This happens when restoring game state after page reload if (gameId && !gameState) { return React.createElement( 'div', { className: 'game-board' }, React.createElement('h1', null, 'WikiGame Clone'), React.createElement('p', null, 'Restoring game...'), gameError && React.createElement( 'div', { className: 'error-message', role: 'alert' }, React.createElement('strong', null, 'Error: '), gameError ) ); } // If gameId exists but gameState is missing required fields, show error if (gameId && gameState && (!gameState.targetPage || !gameState.currentPage)) { return React.createElement( 'div', { className: 'game-board' }, React.createElement('h1', null, 'WikiGame Clone'), React.createElement( 'div', { className: 'error-message', role: 'alert' }, React.createElement('strong', null, 'Error: '), 'Game state is incomplete. Please try starting a new game.' ), React.createElement( 'button', { type: 'button', className: 'btn btn-primary', onClick: () => { setPageContent(null); setPageError(null); setIsCompleted(false); startGame('random'); // Mode is ignored, uses env var }, }, 'Start New Game' ) ); } // Active game state - ensure gameState exists and has required fields if (!gameId || !gameState) { return React.createElement( 'div', { className: 'game-board' }, React.createElement('h1', null, 'WikiGame Clone'), React.createElement('p', null, 'Loading game...') ); } if (!gameState.targetPage || !gameState.currentPage) { return React.createElement( 'div', { className: 'game-board' }, React.createElement('h1', null, 'WikiGame Clone'), React.createElement( 'div', { className: 'error-message', role: 'alert' }, React.createElement('strong', null, 'Error: '), 'Game state is incomplete. Please try starting a new game.' ), React.createElement( 'button', { type: 'button', className: 'btn btn-primary', onClick: () => { setPageContent(null); setPageError(null); setIsCompleted(false); startGame('random'); // Mode is ignored, uses env var }, }, 'Start New Game' ) ); } try { return React.createElement( 'div', { className: 'game-board' }, React.createElement(GameHUD, { targetPage: gameState.targetPage, startTime: gameState.startTime, isActive: !isCompleted, onGiveUp: handleGiveUp, onNewGame: handleNewGame, gameLoading: gameLoading, }), React.createElement( 'div', { className: 'content' }, gameState && gameState.currentPage && React.createElement(PageContent, { title: gameState.currentPage, content: pageContent?.content, links: pageContent?.links || [], loading: pageLoading, error: pageError, onLinkClick: handleLinkClick, disabled: isCompleted, }) ) ); } catch (renderError) { console.error('Error rendering game board:', renderError); return React.createElement( 'div', { className: 'game-board' }, React.createElement('h1', null, 'WikiGame Clone'), React.createElement( 'div', { className: 'error-message', role: 'alert' }, React.createElement('strong', null, 'Error: '), 'An error occurred while rendering the game. Please refresh the page.' ), React.createElement( 'button', { type: 'button', className: 'btn btn-primary', onClick: () => window.location.reload(), }, 'Refresh Page' ) ); } }