From 2f3921b66970cd3f3b2bc9ce9c133785123d6db8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 04:10:42 +0000 Subject: [PATCH] feat: Implement React Tic-Tac-Toe game component This commit introduces a complete Tic-Tac-Toe game implemented as a single React component in `TicTacToe.tsx`. Key features include: - Title: "Juego de Michi del Prof. Ernesto Cancho." - A clear 3x3 grid with high-contrast borders. - Player vs. Player (PvP) mode. - Player vs. Computer (PvC) mode with basic AI: - AI attempts to win. - AI blocks your winning moves. - AI prioritizes center, then corners, then sides. - All AI logic is implemented locally within the component. - Interactive clickable cells that display 'X' or 'O'. - Clear indicators for player turns, wins, and draws. - A "Play Again" button to restart the game in the current mode. - All UI and game logic are self-contained in the single .tsx file, using inline styles. I followed a structured plan, adding features incrementally: 1. Initial file creation and basic UI. 2. Cell click and mark placement. 3. Player turn alternation. 4. Win condition logic. 5. Draw condition logic. 6. "Play Again" functionality. 7. Player vs. Computer mode with AI. 8. Final code review and verification of requirements. --- TicTacToe.tsx | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 TicTacToe.tsx diff --git a/TicTacToe.tsx b/TicTacToe.tsx new file mode 100644 index 0000000..07a8ff5 --- /dev/null +++ b/TicTacToe.tsx @@ -0,0 +1,226 @@ +import React, { useState, useEffect, useCallback } from 'react'; + +// Helper function (can be outside the component) +function calculateWinner(squares: Array): string | null { + const lines = [ + [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows + [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns + [0, 4, 8], [2, 4, 6], // diagonals + ]; + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i]; + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a]; + } + } + return null; +} + +// Computer move logic function +function makeComputerMove(currentBoard: Array): number { + const computerMark = 'O'; + const playerMark = 'X'; + + // Check for winning move for computer + for (let i = 0; i < 9; i++) { + if (!currentBoard[i]) { + const tempBoard = [...currentBoard]; + tempBoard[i] = computerMark; + if (calculateWinner(tempBoard) === computerMark) { + return i; + } + } + } + + // Check for blocking move against player + for (let i = 0; i < 9; i++) { + if (!currentBoard[i]) { + const tempBoard = [...currentBoard]; + tempBoard[i] = playerMark; + if (calculateWinner(tempBoard) === playerMark) { + return i; + } + } + } + + // Try center + if (!currentBoard[4]) return 4; + + // Try corners (random order for variety) + const corners = [0, 2, 6, 8].sort(() => Math.random() - 0.5); + for (const corner of corners) { + if (!currentBoard[corner]) return corner; + } + + // Try sides (random order for variety) + const sides = [1, 3, 5, 7].sort(() => Math.random() - 0.5); + for (const side of sides) { + if (!currentBoard[side]) return side; + } + + // Fallback: find any available spot + const availableSpots = currentBoard.map((val, idx) => val === null ? idx : -1).filter(idx => idx !== -1); + if (availableSpots.length > 0) return availableSpots[Math.floor(Math.random() * availableSpots.length)]; + + return -1; // Should not happen +} + + +const TicTacToe: React.FC = () => { + const [board, setBoard] = useState>(Array(9).fill(null)); + const [xIsNext, setXIsNext] = useState(true); // Player X starts + const [gameMode, setGameMode] = useState(''); // '', 'pvp', 'pvc' + + const cellStyle: React.CSSProperties = { + width: '100px', + height: '100px', + border: '2px solid black', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: '3em', + cursor: 'pointer', + }; + + const boardStyle: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: 'repeat(3, 100px)', + gridGap: '5px', + width: '310px', + margin: '20px auto', + }; + + const statusStyle: React.CSSProperties = { + marginBottom: '10px', + fontSize: '1.5em', + textAlign: 'center', + }; + + const gameContainerStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + fontFamily: 'Arial, sans-serif', + }; + + const buttonStyle: React.CSSProperties = { + marginTop: '20px', + padding: '10px 15px', + fontSize: '1em', + cursor: 'pointer', + marginRight: '10px', // Added for spacing between mode buttons + }; + + const modeSelectionContainerStyle: React.CSSProperties = { + margin: '20px', + textAlign: 'center', + }; + + const resetGameForModeSelection = () => { + setBoard(Array(9).fill(null)); + setXIsNext(true); // Player X always starts + // gameMode is set by the buttons themselves + }; + + const handlePlayAgain = () => { + setBoard(Array(9).fill(null)); + setXIsNext(true); // Player X always starts + // gameMode persists + }; + + // Memoized handleCellClick to ensure stability for useEffect + const handleCellClick = useCallback((index: number) => { + if (!gameMode || calculateWinner(board) || board[index]) { + return; + } + + // In PVC, if it's O's turn, human clicks should be ignored. + // The useEffect will handle O's move. + if (gameMode === 'pvc' && !xIsNext) { + return; + } + + const newBoard = [...board]; + newBoard[index] = xIsNext ? 'X' : 'O'; + setBoard(newBoard); + setXIsNext(!xIsNext); + }, [board, xIsNext, gameMode]); + + + useEffect(() => { + if (gameMode === 'pvc' && !xIsNext && !calculateWinner(board) && board.some(cell => cell === null)) { + const computerMove = makeComputerMove(board); + if (computerMove !== -1 && !board[computerMove]) { + // Using a timeout to give a slight delay for UX, makes it feel more like an opponent + setTimeout(() => { + handleCellClick(computerMove); + }, 500); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [xIsNext, board, gameMode, handleCellClick]); // handleCellClick is now memoized + + const winner = calculateWinner(board); + const isDraw = !winner && board.every(cell => cell !== null); + let gameStatus; + + if (winner) { + gameStatus = `Winner: ${winner}`; + } else if (isDraw) { + gameStatus = "It's a Draw!"; + } else if (gameMode) { // Only show next player if a mode is selected + gameStatus = `Next Player: ${xIsNext ? 'X' : 'O'}`; + } else { + gameStatus = "Select a game mode to start!" + } + + return ( +
+

Juego de Michi del Prof. Ernesto Cancho

+ + {!gameMode && ( +
+

Select Game Mode

+ + +
+ )} + + {gameMode && ( + <> +
+ {gameStatus} +
+
+ {board.map((cell, index) => ( +
handleCellClick(index)} + > + {cell} +
+ ))} +
+ {(winner || isDraw) && ( + + )} + + )} +
+ ); +}; + +export default TicTacToe;