Skip to content

Instantly share code, notes, and snippets.

@agronick
Created September 22, 2019 02:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agronick/4cbd662a1e03d35ac99ed8f375eb5431 to your computer and use it in GitHub Desktop.
Save agronick/4cbd662a1e03d35ac99ed8f375eb5431 to your computer and use it in GitHub Desktop.
import React from 'react';
import './App.css';
import { useState } from 'react';
import cloneDeep from "lodash-es/cloneDeep";
import shuffle from "lodash-es/shuffle";
class WinLocation {
constructor() {
this.posArray = [];
}
push(winLocation) {
this.posArray.push(winLocation);
}
posToArray(board) {
return this.posArray.reduce((arr, cur)=> {
return [...arr, board[cur[0]][cur[1]]];
}, [])
}
checkWin(board, who) {
return this.posToArray(board).every(i => i === who);
}
shouldMove(opponent, board) {
const asArray = this.posToArray(board);
if (asArray.filter(i => i === opponent).length === 2) {
for (const i in asArray) {
if (asArray[i] === null) {
return this.posArray[i];
}
}
}
return false;
}
}
function *makeWinLocations() {
for (const rowFirst of [true, false]) {
for (let i = 0; i < 3; i++) {
const loc = new WinLocation();
for (let j = 0; j < 3; j++) {
loc.push(rowFirst ? [i, j] : [j, i]);
}
yield loc;
}
}
const posListDag1 = new WinLocation();
const posListDag2= new WinLocation();
for (let i = 0; i < 3; i++) {
posListDag1.push([i, i]);
posListDag2.push([i, 2 - i]);
}
yield posListDag1;
yield posListDag2;
}
function alertAfterDraw(msg, cb) {
requestAnimationFrame(()=> {
requestAnimationFrame(()=> {
alert(msg);
if (cb) cb();
})
})
}
function App() {
const initialBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
const [board, setBoard] = useState(initialBoard);
const [readyReset, setReadyReset] = useState(false);
const reset = () => {
setBoard(cloneDeep(initialBoard));
setReadyReset(false);
};
const changeBoardValue = (x, y, value) => {
if (board[x][y] === null) {
board[x][y] = value;
setBoard(cloneDeep(board));
return true;
}
return false;
};
const checkWin = (who) => {
for (const loc of makeWinLocations()){
if (loc.checkWin(board, who)) {
alertAfterDraw(`${who} wins`);
setReadyReset(true);
return true;
}
}
};
const takeTurnComputer = () => {
const optimalSort = shuffle(Array.from(makeWinLocations())).map(loc => {
const asValues = loc.posToArray(board);
const unusable = asValues.some(j => j === 'x');
return {
loc,
asValues,
unusable,
selCount: asValues.filter(j => j === 'o').length
};
}).sort((a, b) => b.selCount - a.selCount).filter(i => !i.unusable);
// Check if no one can win
if (!optimalSort.length) {
return false;
}
// Check if we are 1 move away from winning or losing
for (const look of ['o', 'x']) {
for (const loc of makeWinLocations()) {
const result = loc.shouldMove(look, board);
if (result && changeBoardValue(...result, 'o')) {
return true;
}
}
}
// Set one of the best looking options
const selected = optimalSort[0];
for (const pos in shuffle([0, 1, 2])) {
if (selected.asValues[pos] === null) {
changeBoardValue(...selected.loc.posArray[pos], 'o');
return true;
}
}
return false;
};
const changeValue = (i, j) => {
if (readyReset) {
return reset();
}
if (changeBoardValue(i, j, 'x')) {
if (checkWin('x')) {
return;
}
if (!takeTurnComputer()) {
alertAfterDraw('No one wins');
setReadyReset(true);
return;
}
checkWin('o');
}
};
const table = board.reduce((all, row, i)=> {
return [...all, ...row.map((col, j)=> {
return (
<Cell changeValue={() => changeValue(i, j, 'x')} value={board[i][j]} key={`${i}|${j}`}/>
);
})];
}, []);
return (
<div className="App">
{table}
</div>
);
}
function Cell({changeValue, value}) {
return (
<div onClick={changeValue} className="cell">
<div className="cellVal">{value}</div>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment