Skip to content

Commit 064135f

Browse files
robertspurlint2013anurag
authored andcommitted
Create tic-tac-toe.js in React (#997)
1 parent ef30621 commit 064135f

File tree

1 file changed

+348
-0
lines changed

1 file changed

+348
-0
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import './index.css';
4+
5+
6+
// Initialization of variables.
7+
let human, computer;
8+
9+
// Dummy components. Only returns JSX to render when called by Board.
10+
function Square(props) {
11+
return (
12+
<button className="square" onClick={props.onClick} value={props.number}>
13+
{props.value}
14+
</button>
15+
);
16+
}
17+
18+
function Button(props) {
19+
return (
20+
<button className="reset" onClick={props.onClick}>
21+
Reset?
22+
</button>
23+
);
24+
}
25+
26+
27+
// The actual board. Where all of the rendering of the actual game is, with the algorithim.
28+
29+
class Board extends React.Component {
30+
constructor(props) {
31+
super(props);
32+
this.state = {
33+
squares: Array(9).fill(null), // Board definition.
34+
xIsNext: props.isX, // Recieving from Game based on user input. Returns true or false
35+
}
36+
}
37+
38+
39+
// Calls the square and adds clicking functions to it.
40+
renderSquare(i) {
41+
return (
42+
<Square
43+
value={this.state.squares[i]}
44+
number={i} // testing purposes
45+
onClick={() => this.handleClick(i)}
46+
/>
47+
);
48+
}
49+
50+
51+
// changes the state everytime a button is clicked, but tests if the game isn't over before
52+
handleClick(i) {
53+
54+
computer = this.state.xIsNext ? 'O' : 'X';
55+
human = this.state.xIsNext ? 'X' : 'O';
56+
57+
const squares = this.state.squares.slice();
58+
if (calculateWinner(squares) || squares[i] || isItATie(squares)) {
59+
return;
60+
}
61+
squares[i] = this.state.xIsNext ? 'X' : 'O';
62+
this.setState({
63+
squares: squares,
64+
xIsNext: !this.state.xIsNext,
65+
},
66+
this.AIDecider
67+
);
68+
}
69+
70+
71+
// resets the board when the button is clicked.
72+
resetClick() {
73+
this.setState({
74+
squares: Array(9).fill(null), // Clears the board.
75+
xIsNext: this.props.isX, // if true, returns 'X'. else, returns 'O'.
76+
});
77+
}
78+
79+
80+
// Easier to just define the function and keep calling it with this
81+
copyBoard(board) {
82+
return board.slice();
83+
}
84+
85+
/*
86+
Checks to make sure if the move that AIDecider, minScore, and maxScore is valid.
87+
If not, doesn't return anything.
88+
If it is valid, it returns every possible combination in arrays.
89+
*/
90+
91+
validMove(index, player, board) {
92+
let thisCopy = this.copyBoard(board);
93+
if (thisCopy[index] === null) {
94+
thisCopy[index] = player;
95+
return thisCopy;
96+
} else {
97+
return null;
98+
}
99+
}
100+
101+
/*
102+
The actual decider, and beginning of the MiniMax algorithim.
103+
Calls maxScore, which calls minScore until it finds an index.
104+
Then, copies the board and gives back the board with the decision.
105+
*/
106+
107+
AIDecider() {
108+
let boardCopy = this.copyBoard(this.state.squares);
109+
let move = null;
110+
let bestMoveScore = -100;
111+
let newBoard = null;
112+
let publishedBoard = null;
113+
// doesn't do anything if the game is over
114+
if (calculateWinner(boardCopy) === computer || calculateWinner(boardCopy) === human || isItATie(boardCopy)) {
115+
return null;
116+
}
117+
118+
/*
119+
The initial loop that suggests any and all moves possible. Calls maxScore which
120+
has its own loop which adds a move and calls validMove
121+
(therefore putting it a move ahead), and maxScore calls minScore that has its own loop
122+
(therefore putting it two moves ahead). Then, decides by point incentive whether
123+
it is a good move or not.
124+
*/
125+
126+
for (let i = 0; i < boardCopy.length; i++) {
127+
newBoard = this.validMove(i, computer, boardCopy);
128+
if (newBoard) {
129+
let moveScore = this.maxScore(newBoard);
130+
if (moveScore > bestMoveScore) {
131+
bestMoveScore = moveScore;
132+
move = i;
133+
}
134+
}
135+
}
136+
137+
/*
138+
When the loop is over, it will assign the index that is the best move to play.
139+
Assigns the new index with whatever the computer is playing (X or O), then adds it onto
140+
a copy of the board (publishedBoard) where it will be change the state of the board.
141+
setTimeout given for user experience (giving the illusion that the computer is "thinking").
142+
*/
143+
144+
publishedBoard = boardCopy;
145+
publishedBoard[move] = computer;
146+
setTimeout(() => {
147+
this.setState({
148+
squares: publishedBoard,
149+
xIsNext: !this.state.xIsNext
150+
});
151+
}, 300);
152+
}
153+
154+
/*
155+
minScore and maxScore call on each other where they add more possible moves onto
156+
the copied board until it is either a tie, or someone has won with the theoretical
157+
boards. Then, returns a number value where the loop in AIDecider will start again
158+
until the loop runs out.
159+
*/
160+
161+
minScore(board) {
162+
if (calculateWinner(board) === human) {
163+
return -10;
164+
} else if (calculateWinner(board) === computer) {
165+
return 10;
166+
} else if (isItATie(board)) {
167+
return 0;
168+
} else {
169+
let bestMoveValue = -100;
170+
let move = 0;
171+
for (let i = 0; i < board.length; i++) {
172+
let newBoard = this.validMove(i, computer, board);
173+
if (newBoard) {
174+
let predictedMoveValue = this.maxScore(newBoard);
175+
if (predictedMoveValue > bestMoveValue) {
176+
bestMoveValue = predictedMoveValue;
177+
move = i;
178+
}
179+
}
180+
}
181+
return bestMoveValue;
182+
}
183+
}
184+
185+
// Called first by AIDecider, then calls minScore
186+
187+
maxScore(board) {
188+
if (calculateWinner(board) === human) {
189+
return -10;
190+
} else if (calculateWinner(board) === computer) {
191+
return 10;
192+
} else if (isItATie(board)) {
193+
return 0;
194+
} else {
195+
let bestMoveValue = 100;
196+
let move = 0;
197+
for (let i = 0; i < board.length; i++) {
198+
let newBoard = this.validMove(i, human, board);
199+
if (newBoard) {
200+
let predictedMoveValue = this.minScore(newBoard);
201+
if (predictedMoveValue < bestMoveValue) {
202+
bestMoveValue = predictedMoveValue;
203+
move = i;
204+
}
205+
}
206+
}
207+
return bestMoveValue;
208+
}
209+
}
210+
211+
render() {
212+
const winner = calculateWinner(this.state.squares);
213+
const tie = isItATie(this.state.squares);
214+
let status, button = null;
215+
216+
if (winner) {
217+
status = "Winner: " + winner;
218+
button = <Button onClick={() => this.resetClick()}/>
219+
} else if (tie) {
220+
status = "It's a tie!";
221+
button = <Button onClick={() => this.resetClick()}/>
222+
} else {
223+
status = "Your turn, " + (this.state.xIsNext ? 'X' : 'O');
224+
}
225+
226+
return (
227+
<div>
228+
<div className="status">{status}</div>
229+
<div style={{textAlign: 'center'}}>{button}</div>
230+
<div className="board-row">
231+
{this.renderSquare(0)}
232+
{this.renderSquare(1)}
233+
{this.renderSquare(2)}
234+
</div>
235+
<div className="board-row">
236+
{this.renderSquare(3)}
237+
{this.renderSquare(4)}
238+
{this.renderSquare(5)}
239+
</div>
240+
<div className="board-row">
241+
{this.renderSquare(6)}
242+
{this.renderSquare(7)}
243+
{this.renderSquare(8)}
244+
</div>
245+
</div>
246+
);
247+
}
248+
}
249+
250+
251+
// Initialization of game. Passes the choice of the user to Board, where the actual game will render.
252+
class Game extends React.Component {
253+
constructor() {
254+
super();
255+
this.state = {
256+
hasChosen: false,
257+
isX: true,
258+
}
259+
}
260+
261+
question() {
262+
return (
263+
<div className='questionRow'>
264+
Tic Tac Toe
265+
<br />
266+
<br />
267+
X or O?
268+
<br />
269+
<button className='button' onClick={() => this.test(true)}>X</button>
270+
<button className='button' onClick={() => this.test(false)}>O</button>
271+
</div>
272+
);
273+
}
274+
275+
test(bool) {
276+
if (bool) {
277+
this.setState({
278+
hasChosen: true,
279+
isX: true,
280+
});
281+
} else {
282+
this.setState({
283+
hasChosen: true,
284+
isX: false,
285+
});
286+
}
287+
}
288+
289+
render() {
290+
let board = null;
291+
292+
if (!this.state.hasChosen) {
293+
board = this.question();
294+
} else {
295+
board = <Board isX={this.state.isX} />
296+
}
297+
298+
return (
299+
<div>
300+
<div className="game">
301+
<div className="game-board">
302+
{board}
303+
</div>
304+
</div>
305+
</div>
306+
);
307+
}
308+
}
309+
310+
/*
311+
Where it calculates if there is a possible winner with the MiniMax loops, or ties.
312+
Note: status does not render until there is an actual winner (not theoretical winners
313+
made up by MiniMax), tested by the actual state of the board.
314+
*/
315+
316+
function calculateWinner(squares) {
317+
const lines = [
318+
[0, 1, 2],
319+
[3, 4, 5],
320+
[6, 7, 8],
321+
[0, 3, 6],
322+
[1, 4, 7],
323+
[2, 5, 8],
324+
[0, 4, 8],
325+
[2, 4, 6],
326+
];
327+
for (let i = 0; i < lines.length; i++) {
328+
const [a, b, c] = lines[i];
329+
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
330+
return squares[a];
331+
}
332+
}
333+
return null;
334+
}
335+
336+
function isItATie(squares) {
337+
let copy = squares.slice();
338+
if (copy.some(element => !element)) {
339+
return false;
340+
} else {
341+
return true;
342+
}
343+
}
344+
345+
ReactDOM.render(
346+
<Game />,
347+
document.getElementById('root')
348+
);

0 commit comments

Comments
 (0)