<div id="tictactoe"></div>
<footer>
coded with
<span class="love">♥</span> by
<a
href="https://twitter.com/yagoestevez/"
target="_blank"
alt="about Yago Estévez"
title="about Yago Estévez">Yago Estévez
</a>
</footer>
body {
height : 100vh;
font-family : Merriweather, sans-serif;
background : #226293;
background : -webkit-radial-gradient(center, ellipse cover, #226293 0%,#1b4f79 100%);
background : -moz-radial-gradient(center, ellipse cover, #226293 0%, #1b4f79 100%);
background : radial-gradient(ellipse at center, #226293 0%,#1b4f79 100%);
filter : progid:DXImageTransform.Microsoft.gradient(
startColorstr='#226293',
endColorstr='#1b4f79',
GradientType=1
);
color : #eee;
display : flex;
justify-content : center;
align-items : center;
}
footer {
position : fixed;
bottom : 0;
background : #000;
color : #eee;
height : 1.4rem;
width : 100%;
padding : .2rem;
text-align : center;
font-size : .8rem;
z-index : 999;
}
footer a:link, footer a:visited {
text-decoration : none;
color : #eee;
transition : color 0.5s ease;
}
footer a:hover {
color : #fc0;
}
footer .love {
color : #b10;
font-size : 1rem;
vertical-align : baseline;
}
/* App.css */
.App {
text-align : center;
display : flex;
flex-direction : column;
opacity : 1;
-webkit-transition : opacity 500ms ease-in;
-moz-transition : opacity 500ms ease-in;
-o-transition : opacity 500ms ease-in;
transition : opacity 500ms ease-in;
}
.App-hidden {
text-align : center;
display : flex;
flex-direction : column;
opacity : 0;
}
.Game {
display : flex;
justify-content : center;
align-items : center;
}
#Menu.display {
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
background-color : #1b4f79;
background-color : #1b4f79;
-webkit-box-shadow : 0 0 2rem #00000080;
-moz-box-shadow : 0 0 2rem #00000080;
box-shadow : 0 0 2rem #00000080;
padding : 5rem;
width : 25vh;
height : 25vh;
border-radius : 50%;
border : 6px solid #fc0;
position : absolute;
text-align : center;
top : 50%;
left : 50%;
opacity : 1;
-webkit-transform : translate(-50%, -50%);
-ms-transform : translate(-50%, -50%);
transform : translate(-50%, -50%);
-webkit-transition : opacity 300ms ease-in 1.5s;
-moz-transition : opacity 300ms ease-in 1.5s;
-o-transition : opacity 300ms ease-in 1.5s;
transition : opacity 300ms ease-in 1.5s;
}
#Menu.hidden {
z-index : -100;
opacity : 0;
-webkit-transition : opacity 300ms ease-in;
-moz-transition : opacity 300ms ease-in;
-o-transition : opacity 300ms ease-in;
transition : opacity 300ms ease-in;
}
#Menu .title {
font-family : 'Indie Flower';
color : #fc0;
margin-bottom : 0;
}
#Menu hr {
color : #eee;
margin-bottom : 1rem;
width : 100%;
}
#Menu .menu-text {
font-size : 1rem;
font-weight : bold;
margin : .2rem 0rem;
}
#Menu .token {
color : #fc0;
margin : 1rem;
font-size : 1rem;
cursor : pointer;
}
#Menu button {
background-color : white;
color : #1b4f79;
border : none;
width : 3rem;
height : 3rem;
margin : 1rem .3rem;
border-radius : 50%;
font-size : 1.5rem;
cursor : pointer;
}
table {
width : 50vh;
height : 50vh;
border-collapse : collapse;
}
td {
text-align : center;
vertical-align : middle;
border : transparent;
}
@media (max-height: 600px) {
#Menu .title {
font-size : 1.5rem;
margin-bottom : 0;
}
#Menu .menu-text {
font-size : .8rem;
font-weight : bold;
margin : .2rem 0rem;
}
#Menu button {
width : 2rem;
height : 2rem;
margin : .3rem .3rem;
font-size : 1rem;
}
}
/* Grid.css */
#Grid {
position : absolute;
width : 50vh;
height : 50vh;
z-index : -500;
}
#Grid .line {
stroke : #fc0;
stroke-width : 3px;
stroke-dasharray : 180px;
stroke-dashoffset : -180px;
fill : transparent;
}
#Grid .animate1 {
stroke-dashoffset : 0px;
-webkit-transition : stroke-dashoffset 200ms ease-in;
-moz-transition : stroke-dashoffset 200ms ease-in;
-o-transition : stroke-dashoffset 200ms ease-in;
transition : stroke-dashoffset 200ms ease-in;
}
#Grid .line1-shown {
stroke-dashoffset : 0px;
-webkit-transition : stroke-dashoffset 200ms ease-in 200ms;
-moz-transition : stroke-dashoffset 200ms ease-in 200ms;
-o-transition : stroke-dashoffset 200ms ease-in 200ms;
transition : stroke-dashoffset 200ms ease-in 200ms;
}
#Grid .animate2 {
stroke-dashoffset : 0px;
-webkit-transition : stroke-dashoffset 200ms ease-in 200ms;
-moz-transition : stroke-dashoffset 200ms ease-in 200ms;
-o-transition : stroke-dashoffset 200ms ease-in 200ms;
transition : stroke-dashoffset 200ms ease-in 200ms;
}
#Grid .animate3 {
stroke-dashoffset : 0px;
-webkit-transition : stroke-dashoffset 200ms ease-in 400ms;
-moz-transition : stroke-dashoffset 200ms ease-in 400ms;
-o-transition : stroke-dashoffset 200ms ease-in 400ms;
transition : stroke-dashoffset 200ms ease-in 400ms;
}
#Grid .animate4 {
stroke-dashoffset : 0px;
-webkit-transition : stroke-dashoffset 200ms ease-in 600ms;
-moz-transition : stroke-dashoffset 200ms ease-in 600ms;
-o-transition : stroke-dashoffset 200ms ease-in 600ms;
transition : stroke-dashoffset 200ms ease-in 600ms;
}
/* Square.css */
.cross {
stroke : #fc0;
stroke-width : 6px;
stroke-dasharray : 226.274169921875px;
stroke-dashoffset : -226.274169921875px;
fill : transparent;
transition : stroke-dashoffset 0.3 linear;
}
.cross1-shown {
stroke : #fc0;
stroke-width : 6px;
stroke-dasharray : 226.274169921875px;
stroke-dashoffset : 0px;
fill : transparent;
-webkit-transition : stroke-dashoffset 200ms ease-in;
-moz-transition : stroke-dashoffset 200ms ease-in;
-o-transition : stroke-dashoffset 200ms ease-in;
transition : stroke-dashoffset 200ms ease-in;
}
.cross2-shown {
stroke : #fc0;
stroke-width : 6px;
stroke-dasharray : 226.274169921875px;
stroke-dashoffset : 0px;
fill : transparent;
-webkit-transition : stroke-dashoffset 200ms ease-in 200ms;
-moz-transition : stroke-dashoffset 200ms ease-in 200ms;
-o-transition : stroke-dashoffset 200ms ease-in 200ms;
transition : stroke-dashoffset 200ms ease-in 200ms;
}
.xWon1 {
stroke : #b10;
stroke-width : 6px;
stroke-dasharray : 226.274169921875px;
stroke-dashoffset : 0px;
fill : transparent;
-webkit-transition : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
-moz-transition : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
-o-transition : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
transition : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
}
.xWon2 {
stroke : #b10;
stroke-width : 6px;
stroke-dasharray : 226.274169921875px;
stroke-dashoffset : 0px;
fill : transparent;
-webkit-transition : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
-moz-transition : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
-o-transition : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
transition : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
}
.circle {
stroke : #fc0;
stroke-width : 6px;
stroke-dasharray : 263.93072509765625px;
stroke-dashoffset : -263.93072509765625px;
fill : transparent;
opacity : 0;
}
.circle-shown {
stroke : #fc0;
stroke-width : 6px;
stroke-dasharray : 263.93072509765625px;
stroke-dashoffset : 0px;
fill : transparent;
-webkit-transition : stroke-dashoffset 300ms ease-in 100ms;
-moz-transition : stroke-dashoffset 300ms ease-in 100ms;
-o-transition : stroke-dashoffset 300ms ease-in 100ms;
transition : stroke-dashoffset 300ms ease-in 100ms;
}
.oWon {
stroke : #b10;
stroke-width : 6px;
stroke-dasharray : 263.93072509765625px;
stroke-dashoffset : 0px;
fill : transparent;
-webkit-transition : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
-moz-transition : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
-o-transition : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
transition : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
}
.clickable {
cursor : pointer;
}
class App extends React.Component {
/***********************************************************
Initiates the state properties for the App component.
************************************************************/
constructor (props) {
super(props);
this.state = {
human: 'X',
computer: 'O',
gameStart: false,
winner: null,
winningSequence: [],
board: [0, 1, 2, 3, 4, 5, 6, 7, 8],
possibleWins: [
[0, 1, 2], // Horizontals
[0, 3, 6],
[3, 4, 5],
[1, 4, 7], // Verticals
[6, 7, 8],
[2, 5, 8],
[0, 4, 8], // Diagonals
[2, 4, 6]
],
showMenu: true
};
}
startGame = human => {
const computer = human === 'X' ? 'O' : 'X';
this.setState(
{
board: [0, 1, 2, 3, 4, 5, 6, 7, 8],
winner: null,
gameStart: true,
showMenu: false,
human: human,
computer: computer
},
() => {
if (human === 'O') this.humanMove(-1, computer);
}
);
};
/***********************************************************
Checks if there's a winner, it's a tie or still playing.
************************************************************/
gameOver = board => {
const possibleWins = this.state.possibleWins;
for (let i = 0; i < possibleWins.length; i++) {
const [a, b, c] = possibleWins[i];
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
// Returns the winner: X or O.
this.setState({ winningSequence: possibleWins[i] });
return board[a];
}
}
if (board.filter(sqr => sqr !== 'X' && sqr !== 'O').length) {
// Returns true when the game is not finished.
return false;
} else {
// Returns false when it's a tie.
return 'TIE';
}
};
/***********************************************************
Handles the click events from the squares of the board.
************************************************************/
play = index => {
if (this.state.winner || !this.state.gameStart) return false;
this.humanMove(index, this.state.computer);
};
/***********************************************************
Handles the player's turn. Then, launches the AI.
***********************************************************/
humanMove = (index, computerToken) => {
const newBoard = [...this.state.board];
if (index !== -1) {
if (isNaN(newBoard[index])) return;
newBoard[index] = this.state.human;
const computerThink = this.computerThink(newBoard, computerToken);
newBoard[computerThink.index] = computerToken;
} else {
// Faking the first computer's move as it slows the 'thinking' down
// quite much.
const randomFirstMove = Math.floor(Math.random() * 9);
newBoard[randomFirstMove] = computerToken;
}
const gameState = this.gameOver(newBoard);
if (gameState === 'X' || gameState === 'O') {
this.setState({
winner: gameState,
board: newBoard,
showMenu: true
});
} else if (gameState === 'TIE') {
this.setState({
winner: 'TIE',
board: newBoard,
showMenu: true
});
} else {
this.setState({
board: newBoard
});
}
};
/***********************************************************
Implementation of the MINIMAX algorithm.
[https://en.wikipedia.org/wiki/Minimax]
Simulates all possible movements against its opponent
and returns the score and index of the best moves.
************************************************************/
computerThink = (board, player) => {
// Has the available empty squares on the board.
const emptySquares = board.filter(sqr => sqr !== 'X' && sqr !== 'O');
// Checks if the game is over and returns its final state.
// Known as the leaves of the tree generated by the algorithm.
if (this.gameOver(board) === this.state.human) {
return { score: -10 };
} else if (this.gameOver(board) === this.state.computer) {
return { score: 10 };
} else if (emptySquares.length === 0) {
return { score: 0 };
}
// Holds each move with index and score from the empty squares.
// E.g.: { index: '' , score: '' }
let moves = [];
// Loops through the empty squares array
for (let i = 0; i < emptySquares.length; i++) {
let move = {}; // Holds each index/score.
move.index = board[emptySquares[i]]; // Holds the board's index.
board[emptySquares[i]] = player; // Simulates a player's move.
// Changes the player to continue the simulation and makes a recursive
// call to this method (the MiniMax Algorithm itself).
if (player === this.state.computer) {
let newMove = this.computerThink(board, this.state.human);
move.score = newMove.score;
} else if (player === this.state.human) {
var newMove = this.computerThink(board, this.state.computer);
move.score = newMove.score;
}
// Empties the board for the next iteration
board[emptySquares[i]] = move.index;
// Includes the simulated move into the moves array.
moves.push(move);
}
// Holds the bestMove, the one which scores the highest for the computer and
// the lowest for the human.
let bestMove;
// Returns the MiniMax scores: 'max' for the computer;'min' for the human.
if (player === this.state.computer) {
let bestScore = -5000; // Sets a small enough score to compare.
for (let i = 0; i < moves.length; i++) {
if (moves[i].score > bestScore) {
bestScore = moves[i].score;
bestMove = i;
}
}
} else if (player === this.state.human) {
let bestScore = 5000; // Sets a big enough score to compare.
for (let i = 0; i < moves.length; i++) {
if (moves[i].score < bestScore) {
bestScore = moves[i].score;
bestMove = i;
}
}
}
// Gives back the best possible moves as an array.
return moves[bestMove];
};
/***********************************************************
Renders the component and its children into the index.js
************************************************************/
render() {
return (
<React.Fragment>
<Menu
showMenu={this.state.showMenu}
startGame={this.startGame}
winner={this.state.winner}
players={[this.state.human,this.state.computer]}
/>
<div className="App">
{/* "App-hidden" */}
<section className="Game">
<Board
play={this.play}
gameBoard={this.state.board}
winner={this.state.winner}
winningSeq={this.state.winningSequence}
/>
<Grid didGameStart={this.state.gameStart} />
</section>
</div>
</React.Fragment>
);
}
}
const Board = props => {
const { play, gameBoard, winner, winningSeq } = props;
const buildSquares = i => {
return (
<td>
<Square
index={i}
play={play}
board={gameBoard}
winner={winner}
winningSeq={winningSeq}
/>
</td>
);
};
return (
<table>
<tbody>
<tr>
{buildSquares(0)}
{buildSquares(1)}
{buildSquares(2)}
</tr>
<tr>
{buildSquares(3)}
{buildSquares(4)}
{buildSquares(5)}
</tr>
<tr>
{buildSquares(6)}
{buildSquares(7)}
{buildSquares(8)}
</tr>
</tbody>
</table>
);
};
const Square = props => {
const { index, play, board, winner, winningSeq } = props;
const xClasses = i => {
return [
board[index] === 'X' ? `cross${i}-shown` : `cross`,
winner === 'X' && winningSeq.indexOf(index) !== -1 ? `xWon${i}` : '',
'clickable'
].join(' ');
}
const oClasses = () => {
return [
board[index] === 'O' ? 'circle-shown' : 'circle',
winner === 'O' && winningSeq.indexOf(index) !== -1 ? 'oWon' : '',
'clickable'
].join(' ');
}
return (
<svg viewBox="0 0 120 120" onClick={() => play(index)}>
{/* THE X TOKEN SPLIT INTO TWO PATHS */}
<g>
<path
id={`cross1${index}`}
className={xClasses(1)}
d="M 20,100 L 100,20"
/>
<path
id={`cross2${index}`}
className={xClasses(2)}
d="M 100,100 L 20,20"
/>
</g>
{/* THE O TOKEN */}
<circle
id={`circle${index}`}
className={oClasses()}
r={42}
cy={60}
cx={60}
fill="transparent"
strokeWidth={6}
/>
</svg>
);
};
const Grid = props => {
const animate= i => ( props.didGameStart ? `line animate${i}` : `line` );
return (
<svg id="Grid" viewBox="0 0 180 180">
<g>
<path className={animate(1)} d="M 0,60 H 180" />
<path className={animate(2)} d="M 180,120 H 0" />
<path className={animate(3)} d="M 60,180 V 0" />
<path className={animate(4)} d="M 120,0 V 180" />
</g>
</svg>
);
};
const Menu = props => {
const { showMenu, startGame, winner, players } = props;
const [ human, computer ] = players;
const buildMenu = () => {
if (winner === human) {
return (
<section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
<h1 className="title">Congrats! You made it!!</h1>
<hr />
<div>
<p className="menu-text">Wanna try again?</p>
<button onClick={token => startGame('X')}>X</button>
<button onClick={token => startGame('O')}>O</button>
</div>
</section>
);
} else if (winner === computer) {
return (
<section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
<h1 className="title">Oops...Sorry!!</h1>
<hr />
<div>
<p className="menu-text">Want another chance?</p>
<button onClick={token => startGame('X')}>X</button>
<button onClick={token => startGame('O')}>O</button>
</div>
</section>
);
} else if ( winner === 'TIE') {
return (
<section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
<h1 className="title">It was close!</h1>
<hr />
<div>
<p className="menu-text">Wanna do better?</p>
<button onClick={token => startGame('X')}>X</button>
<button onClick={token => startGame('O')}>O</button>
</div>
</section>
);
} else {
return (
<section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
<h1 className="title">Let's play!</h1>
<hr />
<div>
<button onClick={token => startGame('X')}>X</button>
<button onClick={token => startGame('O')}>O</button>
</div>
</section>
);
}
};
return buildMenu();
};
ReactDOM.render(<App />, document.getElementById('tictactoe'));
View Compiled
This Pen doesn't use any external CSS resources.