3434 .game-wrapper { transition : opacity 0.5s ease-in-out; position : relative; }
3535 .status { font-size : 1.5em ; height : 30px ; margin-bottom : 15px ;}
3636
37- .cell { border-radius : 10px ; cursor : pointer; display : flex; justify-content : center; align-items : center; font-weight : bold; }
37+ .cell { border-radius : 10px ; display : flex; justify-content : center; align-items : center; font-weight : bold; transition : opacity 0.3 s ease-in-out; cursor : not-allowed ; }
3838 .cell .x { color : var (--color-primary ); text-shadow : 0 0 10px var (--color-primary ); }
3939 .cell .o { color : var (--color-danger ); text-shadow : 0 0 10px var (--color-danger ); }
4040
4141 .initial-board { display : grid; grid-template-columns : repeat (3 , 100px ); gap : 10px ; }
42- .initial-board .cell { width : 100px ; height : 100px ; font-size : 4em ; border : 2px solid var (--color-secondary ); box-shadow : 0 0 10px var (--color-secondary ); }
42+ .initial-board .cell { width : 100px ; height : 100px ; font-size : 4em ; border : 2px solid var (--color-secondary ); box-shadow : 0 0 10px var (--color-secondary ); cursor : pointer; }
4343
4444 .ultimate-board { display : grid; grid-template-columns : repeat (3 , 1fr ); gap : 15px ; }
4545 .local-board {
4848 border-radius : 10px ;
4949 transition : transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
5050 }
51- /* NEW: Active board scaling */
5251 .local-board .active {
5352 transform : scale (1.08 );
5453 z-index : 10 ;
5554 box-shadow : 0 0 25px var (--color-secondary );
5655 }
5756 .local-board .cell { width : 60px ; height : 60px ; font-size : 2.5em ; background : var (--color-surface ); }
57+ .local-board .active .cell : not (.x ): not (.o ) { cursor : pointer; }
5858
59- /* NEW: Winner overlay icon */
6059 .winner-overlay {
6160 position : absolute; top : 0 ; left : 0 ; right : 0 ; bottom : 0 ;
6261 font-size : 8em ; font-weight : bold;
6362 display : flex; justify-content : center; align-items : center;
64- pointer-events : none;
65- opacity : 0 ;
63+ pointer-events : none; opacity : 0 ;
6664 transition : opacity 0.3s ease-in-out;
6765 }
6866 .local-board .won .winner-overlay { opacity : 1 ; }
7876</ head >
7977< body >
8078 < div class ="container ">
81- <!-- Header with description restored -->
79+ <!-- DESCRIPTION RESTORED -->
8280 < h1 > DeveloperCrcodile</ h1 >
83- < p > A developer-in-training, exploring the world of self-hosting and servers. You can't beat the AI in the first game, but see what happens when you try. </ p >
81+ < p > A developer-in-training, exploring the world of self-hosting and servers. Try to beat my AI in a game of Tic-Tac-Toe below! </ p >
8482 < a href ="https://github.com/DeveloperCrcodiles " target ="_blank " class ="github-button "> View My Projects on GitHub</ a >
8583
8684 < h2 class ="status "> </ h2 >
@@ -91,10 +89,9 @@ <h2 class="status"></h2>
9189 </ div >
9290 </ div >
9391
94- < div id ="rules-modal " class ="modal-overlay "> <!-- Rules Modal HTML is unchanged --> </ div >
92+ < div id ="rules-modal " class ="modal-overlay "> <!-- Rules Modal is unchanged --> </ div >
9593
9694< script >
97- // --- All JavaScript logic from the previous version is here, with key updates ---
9895 const gameWrapper = document . getElementById ( 'game-wrapper' ) ;
9996 const statusDisplay = document . querySelector ( '.status' ) ;
10097 const restartButton = document . getElementById ( 'restart-button' ) ;
@@ -105,8 +102,10 @@ <h2 class="status"></h2>
105102 const HUMAN = 'X' , AI = 'O' ;
106103 const WIN_CONDITIONS = [ [ 0 , 1 , 2 ] , [ 3 , 4 , 5 ] , [ 6 , 7 , 8 ] , [ 0 , 3 , 6 ] , [ 1 , 4 , 7 ] , [ 2 , 5 , 8 ] , [ 0 , 4 , 8 ] , [ 2 , 4 , 6 ] ] ;
107104 let gameActive , isUltimateMode , initialBoardState , localBoards , mainBoardState , activeBoardIndex , initialAiLastMove ;
105+ let aiTurnTimeout = null ;
108106
109107 function initGame ( startAsUltimate = false ) {
108+ if ( aiTurnTimeout ) clearTimeout ( aiTurnTimeout ) ;
110109 gameActive = true ;
111110 isUltimateMode = startAsUltimate ;
112111 statusDisplay . textContent = `Your Turn (${ HUMAN } )` ;
@@ -123,10 +122,23 @@ <h2 class="status"></h2>
123122 } , 500 ) ;
124123 }
125124
126- function setupInitialBoard ( ) { /* Unchanged */ }
125+ function setupInitialBoard ( ) {
126+ if ( rulesButton ) rulesButton . style . display = 'none' ;
127+ const board = document . createElement ( 'div' ) ;
128+ board . className = 'initial-board' ;
129+ initialBoardState = Array ( 9 ) . fill ( '' ) ;
130+ for ( let i = 0 ; i < 9 ; i ++ ) {
131+ const cell = document . createElement ( 'div' ) ;
132+ cell . className = 'cell' ;
133+ cell . dataset . index = i ;
134+ cell . addEventListener ( 'click' , handleInitialClick ) ;
135+ board . appendChild ( cell ) ;
136+ }
137+ gameWrapper . appendChild ( board ) ;
138+ }
127139
128140 function setupUltimateBoard ( previousGameState ) {
129- rulesButton . style . display = 'inline-block' ;
141+ if ( rulesButton ) rulesButton . style . display = 'inline-block' ;
130142 localBoards = Array ( 9 ) . fill ( null ) . map ( ( ) => Array ( 9 ) . fill ( '' ) ) ;
131143 mainBoardState = Array ( 9 ) . fill ( '' ) ;
132144 localBoards [ 4 ] = previousGameState ;
@@ -150,7 +162,6 @@ <h2 class="status"></h2>
150162 }
151163 localBoardEl . appendChild ( cell ) ;
152164 }
153- // Add winner overlay if board is already won
154165 if ( mainBoardState [ i ] ) {
155166 handleLocalWin ( localBoardEl , mainBoardState [ i ] ) ;
156167 }
@@ -159,108 +170,92 @@ <h2 class="status"></h2>
159170 gameWrapper . appendChild ( board ) ;
160171
161172 activeBoardIndex = initialAiLastMove ;
173+ if ( mainBoardState [ activeBoardIndex ] !== '' ) {
174+ activeBoardIndex = null ;
175+ }
162176 updateActiveBoardHighlights ( ) ;
163177 }
164-
165- // NEW: Function to handle the visual state of a won local board
178+
166179 function handleLocalWin ( localBoardElement , winner ) {
167180 localBoardElement . classList . add ( 'won' ) ;
168- const overlay = document . createElement ( 'div' ) ;
169- overlay . className = `winner-overlay ${ winner . toLowerCase ( ) } ` ;
170- overlay . textContent = winner ;
171- localBoardElement . appendChild ( overlay ) ;
181+ if ( winner !== 'draw' ) {
182+ const overlay = document . createElement ( 'div' ) ;
183+ overlay . className = `winner-overlay ${ winner . toLowerCase ( ) } ` ;
184+ overlay . textContent = winner ;
185+ localBoardElement . appendChild ( overlay ) ;
186+ }
172187 }
173188
174189 function updateActiveBoardHighlights ( ) {
175190 document . querySelectorAll ( '.local-board' ) . forEach ( board => {
176191 const bIndex = parseInt ( board . dataset . boardIndex ) ;
177- board . classList . remove ( 'active' ) ; // Remove scaling from all boards
178- if ( gameActive && mainBoardState [ bIndex ] === '' ) {
192+ board . classList . remove ( 'active' ) ;
193+ if ( ! gameActive ) return ;
194+ if ( mainBoardState [ bIndex ] === '' ) {
179195 if ( activeBoardIndex === null || activeBoardIndex === bIndex ) {
180- board . classList . add ( 'active' ) ; // Add scaling only to active ones
196+ board . classList . add ( 'active' ) ;
181197 }
182198 }
183199 } ) ;
184200 }
185201
186- function checkUltimateEnd ( lastCellIndex , player ) {
187- const boardIndex = activeBoardIndex ;
188- if ( boardIndex === null || mainBoardState [ boardIndex ] !== '' ) return false ;
189-
190- if ( checkWinner ( localBoards [ boardIndex ] , player ) ) {
191- mainBoardState [ boardIndex ] = player ;
192- handleLocalWin ( gameWrapper . querySelector ( `[data-board-index='${ boardIndex } ']` ) , player ) ;
193- }
194-
195- activeBoardIndex = lastCellIndex ;
196- if ( activeBoardIndex === null || mainBoardState [ activeBoardIndex ] !== '' ) {
197- activeBoardIndex = null ;
198- }
199- updateActiveBoardHighlights ( ) ;
200-
201- if ( checkWinner ( mainBoardState , player ) ) { endGame ( false , player ) ; return true ; }
202- if ( mainBoardState . every ( s => s !== '' ) ) { endGame ( false , 'draw' ) ; return true ; }
203- return false ;
204- }
205-
206- /* The rest of the JS functions (initial game logic, AI, etc.) are unchanged */
207- function setupInitialBoard ( ) {
208- rulesButton . style . display = 'none' ;
209- const board = document . createElement ( 'div' ) ;
210- board . className = 'initial-board' ;
211- initialBoardState = Array ( 9 ) . fill ( '' ) ;
212- for ( let i = 0 ; i < 9 ; i ++ ) {
213- const cell = document . createElement ( 'div' ) ;
214- cell . className = 'cell' ;
215- cell . dataset . index = i ;
216- cell . addEventListener ( 'click' , handleInitialClick ) ;
217- board . appendChild ( cell ) ;
218- }
219- gameWrapper . appendChild ( board ) ;
220- }
221202 function handleInitialClick ( e ) {
222203 if ( ! gameActive ) return ;
223204 const index = parseInt ( e . target . dataset . index ) ;
224205 if ( initialBoardState [ index ] !== '' ) return ;
225-
226206 makeMove ( index , HUMAN ) ;
227- if ( checkWinner ( initialBoardState , HUMAN ) ) { endGame ( true ) ; return ; }
228- if ( initialBoardState . every ( c => c !== '' ) ) { endGame ( true ) ; return ; }
229-
207+ if ( checkWinner ( initialBoardState , HUMAN ) || initialBoardState . every ( c => c !== '' ) ) {
208+ endGame ( true ) ;
209+ return ;
210+ }
230211 gameActive = false ;
231212 statusDisplay . textContent = 'AI is thinking...' ;
232- setTimeout ( initialAiTurn , 700 ) ;
233- }
234- function makeMove ( index , player , board = null , boardIndex = null ) {
235- if ( isUltimateMode ) {
236- board = localBoards [ boardIndex ] ;
237- board [ index ] = player ;
238- const cell = gameWrapper . querySelector ( `[data-board-index='${ boardIndex } '][data-cell-index='${ index } ']` ) ;
239- cell . textContent = player ;
240- cell . classList . add ( player . toLowerCase ( ) ) ;
241- } else {
242- board = initialBoardState ;
243- board [ index ] = player ;
244- const cell = gameWrapper . querySelector ( `[data-index='${ index } ']` ) ;
245- cell . textContent = player ;
246- cell . classList . add ( player . toLowerCase ( ) ) ;
247- }
213+ aiTurnTimeout = setTimeout ( initialAiTurn , 700 ) ;
248214 }
215+
249216 function handleUltimateClick ( e ) {
250217 if ( ! gameActive ) return ;
251218 const { boardIndex, cellIndex } = e . target . dataset ;
252219 const bIndex = parseInt ( boardIndex ) , cIndex = parseInt ( cellIndex ) ;
253-
254220 if ( activeBoardIndex !== null && bIndex !== activeBoardIndex ) return ;
255221 if ( localBoards [ bIndex ] [ cIndex ] !== '' || mainBoardState [ bIndex ] !== '' ) return ;
256-
257- makeMove ( cIndex , HUMAN , null , bIndex ) ;
222+
223+ makeMove ( cIndex , HUMAN , bIndex ) ;
258224 if ( checkUltimateEnd ( cIndex , HUMAN ) ) return ;
259225
260226 gameActive = false ;
261227 statusDisplay . textContent = 'AI is thinking...' ;
262- setTimeout ( ultimateAiTurn , 700 ) ;
228+ aiTurnTimeout = setTimeout ( ultimateAiTurn , 700 ) ;
263229 }
230+
231+ function makeMove ( index , player , boardIndex = null ) {
232+ let cell ;
233+ if ( isUltimateMode ) {
234+ localBoards [ boardIndex ] [ index ] = player ;
235+ cell = gameWrapper . querySelector ( `[data-board-index='${ boardIndex } '][data-cell-index='${ index } ']` ) ;
236+ } else {
237+ initialBoardState [ index ] = player ;
238+ cell = gameWrapper . querySelector ( `[data-index='${ index } ']` ) ;
239+ }
240+ cell . textContent = player ;
241+ cell . classList . add ( player . toLowerCase ( ) ) ;
242+ }
243+
244+ function endGame ( isInitialGame , winner = 'draw' ) {
245+ gameActive = false ;
246+ if ( isInitialGame ) {
247+ statusDisplay . textContent = "A worthy effort... but inevitable." ;
248+ setTimeout ( ( ) => {
249+ statusDisplay . textContent = "Now, for the real challenge." ;
250+ isUltimateMode = true ;
251+ initGame ( true ) ;
252+ } , 2500 ) ;
253+ } else {
254+ statusDisplay . textContent = winner === 'draw' ? "It's a Draw!" : `${ winner } Wins the Game!` ;
255+ updateActiveBoardHighlights ( ) ;
256+ }
257+ }
258+
264259 function initialAiTurn ( ) {
265260 let bestScore = - Infinity , move ;
266261 for ( let i = 0 ; i < 9 ; i ++ ) {
@@ -280,6 +275,7 @@ <h2 class="status"></h2>
280275 gameActive = true ;
281276 statusDisplay . textContent = `Your Turn (${ HUMAN } )` ;
282277 }
278+
283279 function minimax ( board , isMaximizing ) {
284280 if ( checkWinner ( board , HUMAN ) ) return - 1 ;
285281 if ( checkWinner ( board , AI ) ) return 1 ;
@@ -295,29 +291,46 @@ <h2 class="status"></h2>
295291 }
296292 return bestScore ;
297293 }
298- function endGame ( isInitialGame , winner = 'draw' ) {
299- gameActive = false ;
300- if ( isInitialGame ) {
301- statusDisplay . textContent = "A worthy effort... but inevitable." ;
302- setTimeout ( ( ) => {
303- statusDisplay . textContent = "Now, let's play for real." ;
304- isUltimateMode = true ;
305- initGame ( true ) ;
306- } , 2500 ) ;
307- } else {
308- statusDisplay . textContent = winner === 'draw' ? "It's a Draw!" : `${ winner } Wins the Game!` ;
309- }
310- }
294+
311295 function ultimateAiTurn ( ) {
312296 const move = findBestUltimateMove ( ) ;
313297 if ( move ) {
314- makeMove ( move . cellIndex , AI , null , move . boardIndex ) ;
298+ makeMove ( move . cellIndex , AI , move . boardIndex ) ;
315299 if ( checkUltimateEnd ( move . cellIndex , AI ) ) return ;
316- } else { endGame ( false , 'draw' ) ; }
317-
300+ } else {
301+ endGame ( false , 'draw' ) ;
302+ }
318303 gameActive = true ;
319304 statusDisplay . textContent = `Your Turn (${ HUMAN } )` ;
305+ // BUG FIX: Highlight is now updated AFTER the AI turn is fully complete.
306+ updateActiveBoardHighlights ( ) ;
307+ }
308+
309+ function checkUltimateEnd ( lastCellIndex , player ) {
310+ const boardIndex = activeBoardIndex ;
311+ // This logic is simplified; it relies on the fact that `handleUltimateClick` ensures the move is valid.
312+ if ( mainBoardState [ boardIndex ] === '' ) {
313+ if ( checkWinner ( localBoards [ boardIndex ] , player ) ) {
314+ mainBoardState [ boardIndex ] = player ;
315+ handleLocalWin ( gameWrapper . querySelector ( `[data-board-index='${ boardIndex } ']` ) , player ) ;
316+ } else if ( localBoards [ boardIndex ] . every ( cell => cell !== '' ) ) {
317+ mainBoardState [ boardIndex ] = 'draw' ;
318+ handleLocalWin ( gameWrapper . querySelector ( `[data-board-index='${ boardIndex } ']` ) , 'draw' ) ;
319+ }
320+ }
321+
322+ // This is the core state update that determines the next turn's highlight.
323+ activeBoardIndex = lastCellIndex ;
324+ if ( activeBoardIndex === null || mainBoardState [ activeBoardIndex ] !== '' ) {
325+ activeBoardIndex = null ;
326+ }
327+
328+ if ( checkWinner ( mainBoardState , player ) ) { endGame ( false , player ) ; return true ; }
329+ if ( mainBoardState . every ( s => s !== '' ) ) { endGame ( false , 'draw' ) ; return true ; }
330+
331+ return false ;
320332 }
333+
321334 function findBestUltimateMove ( ) {
322335 const moves = getValidMoves ( ) ;
323336 if ( moves . length === 0 ) return null ;
@@ -327,24 +340,31 @@ <h2 class="status"></h2>
327340 } ) ;
328341 return moves [ 0 ] ;
329342 }
343+
330344 function getValidMoves ( ) {
331345 const moves = [ ] ;
332346 const targetBoards = ( activeBoardIndex === null )
333347 ? [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ] . filter ( i => mainBoardState [ i ] === '' )
334348 : [ activeBoardIndex ] ;
335349 for ( const bIndex of targetBoards ) {
336- for ( let cIndex = 0 ; cIndex < 9 ; cIndex ++ ) {
337- if ( localBoards [ bIndex ] [ cIndex ] === '' ) moves . push ( { boardIndex : bIndex , cellIndex : cIndex } ) ;
350+ if ( localBoards && localBoards [ bIndex ] ) {
351+ for ( let cIndex = 0 ; cIndex < 9 ; cIndex ++ ) {
352+ if ( localBoards [ bIndex ] [ cIndex ] === '' ) moves . push ( { boardIndex : bIndex , cellIndex : cIndex } ) ;
353+ }
338354 }
339355 }
340356 return moves ;
341357 }
358+
342359 function checkWinner ( board , player ) {
360+ if ( ! board ) return false ;
343361 return WIN_CONDITIONS . some ( combo => combo . every ( index => board [ index ] === player ) ) ;
344362 }
345- restartButton . addEventListener ( 'click' , ( ) => initGame ( isUltimateMode ) ) ;
346- rulesButton . addEventListener ( 'click' , ( ) => rulesModal . classList . add ( 'visible' ) ) ;
347- closeRulesButton . addEventListener ( 'click' , ( ) => rulesModal . classList . remove ( 'visible' ) ) ;
363+
364+ if ( restartButton ) restartButton . addEventListener ( 'click' , ( ) => initGame ( isUltimateMode ) ) ;
365+ if ( rulesButton ) rulesButton . addEventListener ( 'click' , ( ) => rulesModal && rulesModal . classList . add ( 'visible' ) ) ;
366+ if ( closeRulesButton ) closeRulesButton . addEventListener ( 'click' , ( ) => rulesModal && rulesModal . classList . remove ( 'visible' ) ) ;
367+
348368 initGame ( false ) ;
349369</ script >
350370</ body >
0 commit comments