@@ -25,101 +25,6 @@ export default function TicTacToe() {
2525 const [ gameEnded , setGameEnded ] = useState ( false )
2626 const [ redirectCountdown , setRedirectCountdown ] = useState ( 10 )
2727
28- const updateGameState = useCallback ( ( data : GameState ) => {
29- console . log ( 'Updating game state:' , { turn : data . turn , winner : data . winner , won : data . won , board : data . totalBoard } )
30- // Always update gameData first to trigger re-renders
31- setGameData ( data )
32- if ( data . usernameToTacNumber ) {
33- const entries = Object . entries ( data . usernameToTacNumber )
34- // Find opponent from usernameToTacNumber (current player is username)
35- const myTacNumber = data . usernameToTacNumber [ username ] || 0
36- entries . forEach ( ( [ name , num ] ) => {
37- if ( num !== myTacNumber && num !== 0 ) {
38- setOpponent ( name )
39- }
40- } )
41- }
42- // Always update turn from gameData.turn
43- if ( data . turn !== undefined ) {
44- console . log ( 'Setting turn to:' , data . turn )
45- setTurn ( data . turn )
46- }
47- // winner is the integer (0 = ongoing, 1 or 2 = winner, -1 = cat's game/tie)
48- const winnerValue = data . winner ?? ( typeof data . won === 'number' ? data . won : 0 )
49- console . log ( 'Game state update:' , { winner : data . winner , won : data . won , winnerValue, turn : data . turn } )
50- setWinner ( winnerValue )
51-
52- // Check if game has ended (winner is not 0, including -1 for cat's game)
53- if ( winnerValue !== 0 && ! gameEnded ) {
54- setGameEnded ( true )
55- setRedirectCountdown ( 10 )
56- }
57- // Always update board state from backend, even if game ended
58- if ( data . totalBoard && Array . isArray ( data . totalBoard ) && data . totalBoard . length >= 3 ) {
59- const flatBoard : string [ ] = [ ]
60- for ( let row = 0 ; row < 3 ; row ++ ) {
61- if ( data . totalBoard [ row ] && Array . isArray ( data . totalBoard [ row ] ) ) {
62- for ( let col = 0 ; col < 3 ; col ++ ) {
63- const cellValue = data . totalBoard [ row ] [ col ]
64- const cellIndex = row * 3 + col
65- flatBoard [ cellIndex ] = cellValue === 1 ? "X" : cellValue === 2 ? "O" : ""
66- }
67- }
68- }
69- console . log ( 'Updating board state:' , flatBoard )
70- setBoard ( flatBoard )
71- }
72- } , [ username , gameEnded ] )
73-
74- const fetchGameState = useCallback ( async ( ) => {
75- if ( ! username ) return
76- try {
77- const res = await fetch ( `/api/gamestate/${ encodeURIComponent ( username ) } ` , { cache : 'no-store' } )
78- if ( ! res . ok ) {
79- // If game ended and both users acknowledged, backend returns null
80- if ( res . status === 404 || res . status === 200 ) {
81- const text = await res . text ( )
82- if ( ! text || text === "null" || text . trim ( ) === "" ) {
83- // Game has ended and been removed, redirect to homepage
84- console . log ( 'Game ended, redirecting to homepage' )
85- setTimeout ( ( ) => {
86- router . push ( '/?message=Game ended! Please queue for another match.' )
87- } , 2000 )
88- return
89- }
90- }
91- setGameData ( null )
92- return
93- }
94- const text = await res . text ( )
95- if ( ! text || text === "null" || text . trim ( ) === "" ) {
96- // Game has ended and been removed, but make sure we've shown the winner first
97- if ( gameEnded ) {
98- // Already showed winner, redirect after countdown
99- return
100- }
101- // Game was removed before we saw the winner, redirect immediately
102- console . log ( 'Game ended (null response), redirecting to homepage' )
103- setTimeout ( ( ) => {
104- router . push ( '/?message=Game ended! Please queue for another match.' )
105- } , 2000 )
106- return
107- }
108- try {
109- const data : GameState = JSON . parse ( text )
110- if ( data && ( data . type || data . usernameToTacNumber || data . turn !== undefined ) ) {
111- updateGameState ( data )
112- } else {
113- setGameData ( null )
114- }
115- } catch {
116- setGameData ( null )
117- }
118- } catch {
119- setGameData ( null )
120- }
121- } , [ username , router , gameEnded , updateGameState ] )
122-
12328 // Get username from localStorage on mount (stored during registration)
12429 useEffect ( ( ) => {
12530 if ( typeof window === 'undefined' ) return
@@ -133,249 +38,7 @@ export default function TicTacToe() {
13338 router . push ( '/' )
13439 }
13540 } , [ router ] )
136-
137- // Fetch game state immediately when username is available
138- useEffect ( ( ) => {
139- if ( username ) {
140- fetchGameState ( )
141- }
142- } , [ username , fetchGameState ] )
143-
144- // Countdown timer when game ends
145- useEffect ( ( ) => {
146- if ( ! gameEnded ) return
147-
148- setRedirectCountdown ( 10 )
149- const countdownInterval = setInterval ( ( ) => {
150- setRedirectCountdown ( ( prev ) => {
151- if ( prev <= 1 ) {
152- clearInterval ( countdownInterval )
153- router . push ( '/?message=Game ended! Please queue for another match.' )
154- return 0
155- }
156- return prev - 1
157- } )
158- } , 1000 )
159-
160- return ( ) => clearInterval ( countdownInterval )
161- } , [ gameEnded , router ] )
162-
163- // Poll game state continuously when it's not the player's turn
164- useEffect ( ( ) => {
165- if ( ! username || ! gameData ) return
166-
167- const myTacNumber = gameData . usernameToTacNumber ?. [ username ] || 0
168- const currentTurn = gameData . turn || 0
169- const isMyTurn = currentTurn === myTacNumber
170- const gameWinner = gameData . winner ?? ( typeof gameData . won === 'number' ? gameData . won : 0 )
171- const gameHasEnded = gameWinner !== 0
172-
173- console . log ( 'Polling check:' , { myTacNumber, currentTurn, isMyTurn, gameHasEnded, gameEndedState : gameEnded , gameData } )
174-
175- // Continue polling even after game ends to ensure both players see the result
176- // IMPORTANT: If game has ended, we MUST keep polling so both clients see the result
177- // This is especially important for the winner who just made the winning move
178-
179- // If game ended and we've already set gameEnded state, continue polling briefly to ensure both clients see the result
180- if ( gameHasEnded && gameEnded ) {
181- // Already showing winner, continue polling a few more times to ensure both clients sync, then stop
182- let pollCount = 0
183- const interval = setInterval ( ( ) => {
184- pollCount ++
185- // Poll for 3 more seconds to ensure both clients have time to see the countdown
186- if ( pollCount >= 3 ) {
187- clearInterval ( interval )
188- return
189- }
190- fetchGameState ( )
191- } , 1000 )
192- return ( ) => clearInterval ( interval )
193- }
194-
195- // If game has ended but we haven't set gameEnded state yet, keep polling to get the update
196- // This handles the case where the winner just made a move and the game ended
197- // We MUST continue polling here to ensure the winner sees their win screen
198- if ( gameHasEnded && ! gameEnded ) {
199- console . log ( 'Game ended detected during polling, continuing to poll to get final state' )
200- // Continue polling to get the final state - don't return early
201- fetchGameState ( )
202- const interval = setInterval ( ( ) => {
203- console . log ( 'Polling for final game state with winner...' )
204- fetchGameState ( )
205- // Stop after a few polls once we've gotten the state
206- setTimeout ( ( ) => clearInterval ( interval ) , 3000 )
207- } , 500 ) // Poll more frequently to catch the update
208- return ( ) => clearInterval ( interval )
209- }
210-
211- // Only stop polling if it's the player's turn and game hasn't ended
212- if ( isMyTurn && ! gameHasEnded ) {
213- console . log ( 'Stopping polling - my turn and game not ended' )
214- return
215- }
216-
217- console . log ( 'Starting polling - waiting for opponent or checking game end' )
218- // Fetch immediately, then poll every second
219- fetchGameState ( )
220- const interval = setInterval ( ( ) => {
221- console . log ( 'Polling for game state update...' )
222- fetchGameState ( )
223- } , 1000 )
224- return ( ) => {
225- console . log ( 'Clearing polling interval' )
226- clearInterval ( interval )
227- }
228- } , [ username , gameData , gameData ?. turn , gameData ?. winner , gameData ?. won , gameData ?. usernameToTacNumber , gameEnded , fetchGameState ] )
229-
230- const handleCellClick = async ( cellIndex : number ) => {
231- if ( ! username || ! gameData || isMakingMove ) {
232- console . log ( 'Early return:' , { username : ! ! username , gameData : ! ! gameData , isMakingMove } )
233- return
234- }
235- if ( board [ cellIndex ] !== "" ) {
236- console . log ( 'Cell already occupied:' , cellIndex )
237- return
238- }
239-
240- const myTacNumber = gameData . usernameToTacNumber ?. [ username ] || 0
241- if ( gameData . turn !== myTacNumber ) {
242- console . log ( 'Not your turn:' , { turn : gameData . turn , myTacNumber } )
243- return
244- }
245- // Check if game ended: winner should be 0 if game is ongoing, 1/2 if someone won, -1 if cat's game
246- const gameWinner = gameData . winner ?? ( typeof gameData . won === 'number' ? gameData . won : 0 )
247- if ( gameWinner !== 0 ) {
248- console . log ( 'Game already ended:' , { winner : gameData . winner , won : gameData . won , gameWinner } )
249- return
250- }
251-
252- const row = Math . floor ( cellIndex / 3 )
253- const col = cellIndex % 3
254-
255- // Optimistically update the board immediately
256- const newBoard = [ ...board ]
257- newBoard [ cellIndex ] = myTacNumber === 1 ? "X" : "O"
258- setBoard ( newBoard )
259- console . log ( 'Optimistically updated board:' , newBoard )
260-
261- setIsMakingMove ( true )
262- setError ( null )
263-
264- try {
265- console . log ( 'Making move:' , { username, location : [ row , col ] } )
266- const res = await fetch ( `/api/make_move/tictactoe/${ encodeURIComponent ( username ) } ` , {
267- method : 'POST' ,
268- headers : { 'Content-Type' : 'application/json' } ,
269- cache : 'no-store' ,
270- body : JSON . stringify ( { username, location : [ row , col ] } ) ,
271- } )
272-
273- if ( ! res . ok ) {
274- const text = await res . text ( )
275- console . error ( 'Move failed:' , text || res . status )
276- setError ( `Move failed: ${ text || res . status } ` )
277- // Revert optimistic update on failure
278- setBoard ( board )
279- } else {
280- console . log ( 'Move successful, fetching game state' )
281- // Small delay to ensure backend has processed the move
282- await new Promise ( resolve => setTimeout ( resolve , 150 ) )
283- // Fetch updated game state after making move - this will update board and winner
284- await fetchGameState ( )
285- // Fetch again after a short delay to ensure we get the final state with winner
286- // This is important because the backend might need a moment to process win detection
287- await new Promise ( resolve => setTimeout ( resolve , 200 ) )
288- await fetchGameState ( )
289- // One more fetch to be absolutely sure we have the final state
290- await new Promise ( resolve => setTimeout ( resolve , 200 ) )
291- await fetchGameState ( )
292- }
293- } catch ( err ) {
294- console . error ( 'Error making move:' , err )
295- setError ( `Error making move: ${ err instanceof Error ? err . message : 'Unknown error' } ` )
296- } finally {
297- setIsMakingMove ( false )
298- }
299- }
300-
301- const myTacNumber = username && gameData && gameData . usernameToTacNumber ? gameData . usernameToTacNumber [ username ] || 0 : 0
302- const currentTurn = gameData ?. turn || 0
303- const gameWinner = gameData ?. winner ?? ( typeof gameData ?. won === 'number' ? gameData . won : 0 )
304- const canMakeMove = ! isMakingMove && currentTurn === myTacNumber && gameWinner === 0 && gameData !== null
305-
306- console . log ( 'Render check:' , { myTacNumber, currentTurn, canMakeMove, turn, gameDataTurn : gameData ?. turn } )
307-
308- if ( ! username ) {
309- return (
310- < div className = "flex flex-col items-center justify-center min-h-screen bg-white text-black dark:bg-black dark:text-white p-6" >
311- < p > Loading...</ p >
312- </ div >
313- )
314- }
315-
31641 return (
317- < div className = "flex flex-col items-center justify-center min-h-screen bg-white text-black dark:bg-black dark:text-white p-4 sm:p-6" >
318- < h1 className = "text-2xl sm:text-4xl font-bold mb-4 text-center" > Tic - Tac - Toe</ h1 >
319-
320- { error && < p className = "mb-4 text-red-600" > { error } </ p > }
321-
322- { ! gameData && (
323- < p className = "mb-4 text-lg" > Loading game...</ p >
324- ) }
325-
326- { gameData && (
327- < div className = "rounded-2xl border-4 border-black dark:border-white p-4 sm:p-8 w-full max-w-[400px] flex flex-col items-center justify-between" >
328- < div className = "grid grid-cols-3 grid-rows-3 gap-2 w-full max-w-[192px] sm:w-48 sm:h-48 aspect-square" >
329- { board . map ( ( cell , i ) => (
330- < button
331- key = { i }
332- onClick = { ( ) => handleCellClick ( i ) }
333- disabled = { ! canMakeMove || cell !== "" }
334- className = { `border-2 border-black dark:border-white w-full h-full flex items-center justify-center text-3xl font-bold ${
335- canMakeMove && cell === "" ? "cursor-pointer hover:bg-gray-100 active:bg-gray-200" : "cursor-not-allowed opacity-60"
336- } `}
337- >
338- { cell }
339- </ button >
340- ) ) }
341- </ div >
342-
343- < div className = "flex justify-between w-full mt-4 sm:mt-6 text-sm sm:text-base" >
344- < div className = "text-left" >
345- < p className = "font-semibold underline" > You ({ myTacNumber === 1 ? "X" : "O" } )</ p >
346- < p className = "break-all" > { username } </ p >
347- </ div >
348- < div className = "text-right" >
349- < p className = "font-semibold underline" > Opponent ({ myTacNumber === 1 ? "O" : "X" } )</ p >
350- < p className = "break-all" > { opponent || "Waiting..." } </ p >
351- </ div >
352- </ div >
353-
354- < div className = "mt-4 text-center text-sm sm:text-base" >
355- { gameWinner === 0 ? (
356- < p > { currentTurn === myTacNumber ? "Your turn!" : "Opponent's turn" } </ p >
357- ) : gameWinner === - 1 ? (
358- < div >
359- < p className = "font-bold text-xl sm:text-2xl mb-2" > Cat's game! 🐱 It's a tie!</ p >
360- { gameEnded && (
361- < p className = "text-xs sm:text-sm text-gray-600 mt-2" >
362- Redirecting to homepage in { redirectCountdown } second{ redirectCountdown !== 1 ? "s" : "" } ...
363- </ p >
364- ) }
365- </ div >
366- ) : (
367- < div >
368- < p className = "font-bold text-xl sm:text-2xl mb-2" > { gameWinner === myTacNumber ? "You won! 🎉" : "You lost! 😔" } </ p >
369- { gameEnded && (
370- < p className = "text-xs sm:text-sm text-gray-600 mt-2" >
371- Redirecting to homepage in { redirectCountdown } second{ redirectCountdown !== 1 ? "s" : "" } ...
372- </ p >
373- ) }
374- </ div >
375- ) }
376- </ div >
377- </ div >
378- ) }
379- </ div >
42+ < > </ >
38043 )
38144}
0 commit comments