Last active
January 11, 2020 21:02
-
-
Save primaryobjects/c46206ed4ba47902ca621b73ec2cf616 to your computer and use it in GitHub Desktop.
Isolation 3x3 with game tree generation and AI Minimax with Alpha-Beta Pruning algorithm. https://codepen.io/primaryobjects/pen/QWWGgmR
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Styles | |
https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css | |
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css | |
Libraries | |
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js | |
https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.min.js | |
https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js | |
https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class='container'> | |
<div id='root'></div> | |
</div> | |
<div class='container'> | |
<div id='graph'></div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Main React render hook. | |
$(function() { | |
const isolationCtrl = ReactDOM.render( | |
<div> | |
<IsolationContainer width="3" height="3"></IsolationContainer> | |
</div>, | |
document.getElementById('root') | |
); | |
}); | |
//import Tree from 'https://cdn.jsdelivr.net/npm/react-tree-graph@4.0.1/dist/index.min.js'; | |
window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 6000; | |
const IsolationManager = { | |
isValidMove: (x, y, playerIndex, players, values, width, height) => { | |
const activePlayer = players[playerIndex]; | |
const opponentPlayer = players[!playerIndex ? 1 : 0]; | |
let isValid = activePlayer.x === -1; // Initialize the first-move to valid. | |
if (x < 0 || x >= width || y < 0 || y >= height) { | |
isValid = false; | |
} | |
// Verify this cell is not already used (i.e., it's value is 0). | |
else if (values[y][x]) { | |
//console.log(`Cell ${x},${y} is already taken.`); | |
isValid = false; | |
} | |
else if (!isValid) { | |
// Verify this move is valid for the player and the path is not blocked. | |
let isBlocked; | |
// Verify path is valid. | |
if (x !== activePlayer.x && y !== activePlayer.y && (Math.abs(activePlayer.x - x) !== Math.abs(activePlayer.y - y))) { | |
// This is a diagonal move but not valid one for one. | |
isBlocked = true; | |
console.log(`Invalid move to ${x},${y}`); | |
} | |
// Verify path is not blocked. | |
else if (y < activePlayer.y && x < activePlayer.x) { | |
// Up-left. | |
let posy = activePlayer.y - 1 | |
for (let posx = activePlayer.x - 1; posx > x; posx--) { | |
if (values[posy][posx]) { | |
isBlocked = true; | |
break; | |
} | |
posy--; | |
} | |
} | |
else if (y < activePlayer.y && x > activePlayer.x) { | |
// Up-right. | |
let posy = activePlayer.y - 1 | |
for (let posx = activePlayer.x + 1; posx < x; posx++) { | |
if (values[posy][posx]) { | |
isBlocked = true; | |
break; | |
} | |
posy--; | |
} | |
} | |
else if (y > activePlayer.y && x < activePlayer.x) { | |
// Down-left. | |
let posy = activePlayer.y + 1; | |
for (let posx = activePlayer.x - 1; posx > x; posx--) { | |
if (values[posy][posx]) { | |
isBlocked = true; | |
break; | |
} | |
posy++; | |
} | |
} | |
else if (y > activePlayer.y && x > activePlayer.x) { | |
// Down-right. | |
let posy = activePlayer.y + 1; | |
for (let posx = activePlayer.x + 1; posx < x; posx++) { | |
if (values[posy][posx]) { | |
isBlocked = true; | |
break; | |
} | |
posy++; | |
} | |
} | |
else if (x > activePlayer.x) { | |
// Right. | |
for (let pos = activePlayer.x + 1; pos < x; pos++) { | |
if (values[y][pos]) { | |
isBlocked = true; | |
break; | |
} | |
} | |
} | |
else if (x < activePlayer.x) { | |
// Left. | |
for (let pos = activePlayer.x - 1; pos > x; pos--) { | |
if (values[y][pos]) { | |
isBlocked = true; | |
break; | |
} | |
} | |
} | |
else if (y > activePlayer.y) { | |
// Down. | |
for (let pos = activePlayer.y + 1; pos < y; pos++) { | |
if (values[pos][x]) { | |
isBlocked = true; | |
break; | |
} | |
} | |
} | |
else if (y < activePlayer.y) { | |
// Up. | |
for (let pos = activePlayer.y - 1; pos > y; pos--) { | |
if (values[pos][x]) { | |
isBlocked = true; | |
break; | |
} | |
} | |
} | |
isValid = !isBlocked; | |
} | |
return isValid; | |
}, | |
availableMoves: (playerIndex, players, values, width, height) => { | |
let moves = []; | |
const activePlayer = players[playerIndex]; | |
if (activePlayer.x !== -1) { | |
let x, y; | |
/*let blockUp = false; | |
let blockDown = false; | |
for (let step=1; step<height; step++) { | |
// Up. | |
y = activePlayer.y - step; | |
if (!blockUp && IsolationManager.isValidMove(activePlayer.x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x: activePlayer.x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockUp = true; | |
} | |
// Down. | |
y = activePlayer.y + step; | |
if (!blockDown && IsolationManager.isValidMove(activePlayer.x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x: activePlayer.x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockDown = true; | |
} | |
if (blockUp && blockDown) { | |
break; | |
} | |
}*/ | |
// Up. | |
for (y=activePlayer.y - 1; y>=0; y--) { | |
if (IsolationManager.isValidMove(activePlayer.x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x: activePlayer.x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
// Down. | |
for (y=activePlayer.y + 1; y<height; y++) { | |
if (IsolationManager.isValidMove(activePlayer.x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x: activePlayer.x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
/*let blockLeft = false; | |
let blockRight = false; | |
for (let step=1; step<width; step++) { | |
// Left. | |
x = activePlayer.x - step; | |
if (!blockLeft && IsolationManager.isValidMove(x, activePlayer.y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: activePlayer.y }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockLeft = true; | |
} | |
// Right. | |
x = activePlayer.x + step; | |
if (!blockRight && IsolationManager.isValidMove(x, activePlayer.y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: activePlayer.y }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockRight = true; | |
} | |
if (blockLeft && blockRight) { | |
break; | |
} | |
}*/ | |
// Left. | |
for (x=activePlayer.x - 1; x>=0; x--) { | |
if (IsolationManager.isValidMove(x, activePlayer.y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: activePlayer.y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
// Right. | |
for (x=activePlayer.x + 1; x<width; x++) { | |
if (IsolationManager.isValidMove(x, activePlayer.y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: activePlayer.y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
/*let blockUpLeft = false; | |
let blockUpRight = false; | |
let blockDownLeft = false; | |
let blockDownRight = false; | |
for (let step=1; step<height; step++) { | |
const yUp = activePlayer.y - step; | |
const yDown = activePlayer.y + step; | |
if (yUp < 0) { | |
blockUpLeft = true; | |
blockUpRight = true; | |
} | |
if (yDown >= height) { | |
blockDownLeft = true; | |
blockDownRight = true; | |
} | |
// Up-left. | |
x = activePlayer.x - step; | |
if (!blockUpLeft && IsolationManager.isValidMove(x, yUp, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: yUp }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockUpLeft = true; | |
} | |
// Down-left. | |
if (!blockDownLeft && IsolationManager.isValidMove(x, yDown, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: yDown }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockDownLeft = true; | |
} | |
// Up-right. | |
x = activePlayer.x + step; | |
if (!blockUpRight && IsolationManager.isValidMove(x, yUp, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: yUp }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockUpRight = true; | |
} | |
// Down-right. | |
if (!blockDownRight && IsolationManager.isValidMove(x, yDown, playerIndex, players, values, width, height)) { | |
moves.push({ x, y: yDown }); | |
} | |
else { | |
// Path is blocked from going further. | |
blockDownRight = true; | |
} | |
if (blockUpLeft && blockUpRight && blockDownLeft && blockDownRight) { | |
break; | |
} | |
}*/ | |
// Up-left. | |
x = activePlayer.x; | |
for (y=activePlayer.y - 1; y>=0; y--) { | |
x--; | |
if (x === -1) { | |
break; | |
} | |
if (IsolationManager.isValidMove(x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
// Up-right. | |
x = activePlayer.x; | |
for (y=activePlayer.y - 1; y>=0; y--) { | |
x++; | |
if (x >= width) { | |
break; | |
} | |
if (IsolationManager.isValidMove(x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
// Down-left. | |
x = activePlayer.x; | |
for (y=activePlayer.y + 1; y<height; y++) { | |
x--; | |
if (x === -1) { | |
break; | |
} | |
if (IsolationManager.isValidMove(x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
// Down-right. | |
x = activePlayer.x; | |
for (y=activePlayer.y + 1; y<height; y++) { | |
x++; | |
if (x >= width) { | |
break; | |
} | |
if (IsolationManager.isValidMove(x, y, playerIndex, players, values, width, height)) { | |
moves.push({ x, y }); | |
} | |
else { | |
// Path is blocked from going further. | |
break; | |
} | |
} | |
} | |
else { | |
moves = IsolationManager.allMoves(playerIndex, players, width, height, width, height); | |
} | |
return moves; | |
}, | |
allMoves: (playerIndex, players, width, height) => { | |
const moves = []; | |
// First move, all spaces are available. Second move, all spaces but 1 are available. | |
for (let y=0; y<height; y++) { | |
for (let x=0; x<width; x++) { | |
if (!playerIndex || x !== players[0].x || y !== players[0].y) { | |
moves.push({ x, y }); | |
} | |
} | |
} | |
return moves; | |
} | |
}; | |
const StrategyManager = { | |
none: function() { | |
return null; | |
}, | |
random: function(tree, playerIndex, players, values, width, height) { | |
let isValid = false; | |
let count = 0; | |
let x, y; | |
console.log('Using AI strategy random.'); | |
while (!isValid && count++ < 1000) { | |
x = Math.floor(Math.random() * width); | |
y = Math.floor(Math.random() * height); | |
isValid = IsolationManager.isValidMove(x, y, playerIndex, players, values, width, height); | |
} | |
if (count >= 1000) { | |
console.log('Random strategy failed to find a move.'); | |
} | |
return { x, y }; | |
}, | |
minimax: function(tree, playerIndex, players, values, width, height) { | |
// https://tonypoer.io/2016/10/28/implementing-minimax-and-alpha-beta-pruning-using-python/ | |
let bestState = null; | |
let bestVal = -9999; | |
let beta = 9999; | |
console.log('Using AI strategy minimax.'); | |
const getSuccessors = node => { | |
return node ? node.children : []; | |
}; | |
const isTerminal = node => { | |
return node ? node.children.length === 0 : true; | |
}; | |
const getUtility = node => { | |
return node ? node.score : -9999; | |
}; | |
const maxValue = (node, alpha, beta) => { | |
console.log(`AlphaBeta-->MAX: Visited Node: ${toString(node)}`); | |
if (isTerminal(node)) { | |
return getUtility(node); | |
} | |
else { | |
let value = -9999; | |
const successors = getSuccessors(node); | |
successors.forEach(state => { | |
value = Math.max(value, minValue(state, alpha, beta)); | |
if (value >= beta) { | |
return value; | |
} | |
else { | |
alpha = Math.max(alpha, value); | |
} | |
}); | |
return value; | |
} | |
}; | |
const minValue = (node, alpha, beta) => { | |
console.log(`AlphaBeta-->MIN: Visited Node: ${toString(node)}`); | |
if (isTerminal(node)) { | |
return getUtility(node); | |
} | |
else { | |
let value = 9999; | |
const successors = getSuccessors(node); | |
successors.forEach(state => { | |
value = Math.min(value, maxValue(state, alpha, beta)); | |
if (value <= alpha) { | |
return value; | |
} | |
else { | |
beta = Math.min(beta, value); | |
} | |
}); | |
return value; | |
} | |
}; | |
const toString = state => { | |
return `Depth ${state.depth}, Score ${state.score}, activePlayer ${state.activePlayer}, players: (${state.players[0].x}, ${state.players[0].y}), (${state.players[1].x}, ${state.players[1].y})`; | |
}; | |
const successors = getSuccessors(tree); | |
successors.forEach(state => { | |
const value = minValue(state, bestVal, beta); | |
if (value > bestVal) { | |
bestVal = value; | |
bestState = state; | |
} | |
}); | |
console.log(`MiniMax: Utility value of best node is ${bestVal}.`); | |
console.log(`MiniMax: Best state is: ${toString(bestState)}`) | |
return bestState ? { x: bestState.players[bestState.activePlayer].x, y: bestState.players[bestState.activePlayer].y } : { x: 1, y: 1 }; | |
}, | |
tree: function(playerIndex, players, values, width, height, maxDepth = 4) { | |
//playerIndex = 0; // Always use scores for human player. | |
const referencePlayerIndex = !playerIndex ? 1 : 0; // Point-of-view for the player that the tree is calculated for. The root node will be from the opposing player. | |
const heuristic = (playerMoves, opponentMoves) => { | |
// Use a herustic of (moves * 2) - opponent moves, thus maximizing the number of moves for the player while minimizing the number of moves for your opponent. | |
return (playerMoves * 2) - opponentMoves; | |
}; | |
let root = { depth: 0, player: playerIndex, activePlayer: playerIndex, baseScore: players[referencePlayerIndex].moves.length, score: heuristic(players[referencePlayerIndex].moves.length, players[!referencePlayerIndex ? 1 : 0].moves.length), moves: players[referencePlayerIndex].moves, players, values, children: [], width, height }; | |
let fringe = [ root ]; | |
let node = fringe.shift(); | |
while (node) { | |
if (node.depth <= maxDepth && node.moves.length) { | |
const newPlayerIndex = node.depth % 2 === 0 ? referencePlayerIndex : playerIndex;//0 : 1; | |
// Evaluate all possible moves from the current state. | |
node.moves.forEach(move => { | |
// Make a copy of the players. | |
let newPlayers = JSON.parse(JSON.stringify(node.players)); | |
// Move the player to a new position. | |
newPlayers[newPlayerIndex].x = move.x; | |
newPlayers[newPlayerIndex].y = move.y; | |
// Set the new position as used. | |
let newValues = JSON.parse(JSON.stringify(node.values)); | |
newValues[newPlayers[newPlayerIndex].y][newPlayers[newPlayerIndex].x] = !newPlayerIndex ? 'gray' : 'silver'; | |
// Get available moves at new position in relation to the reference player. | |
const movesReferencePlayer = IsolationManager.availableMoves(referencePlayerIndex, newPlayers, newValues, width, height); | |
// Get available moves with new game board for next player. | |
const moves = IsolationManager.availableMoves(!newPlayerIndex ? 1 : 0, newPlayers, newValues, width, height); | |
// Add the new node to our tree. | |
const child = { depth: node.depth + 1, player: playerIndex, activePlayer: newPlayerIndex, baseScore: movesReferencePlayer.length, score: heuristic(movesReferencePlayer.length, moves.length), moves, players: newPlayers, values: newValues, children: [] }; | |
node.children.push(child); | |
fringe.push(child); | |
}); | |
} | |
// Process next node. | |
node = fringe.shift(); | |
} | |
return root; | |
}, | |
renderTree: function(tree, maxNodes = 50) { | |
const graph = $('#graph'); | |
graph.html(''); | |
const fringe = [{ tree, depth: 0 }]; | |
let index = 0; | |
let count = 0; | |
let node = fringe.shift(); | |
while (node && count++ < maxNodes) { | |
const player1 = node.tree.players[0/*node.tree.activePlayer*/]; | |
const player2 = node.tree.players[1/*node.tree.activePlayer ? 0 : 1*/]; | |
const id = `node-${index++}-${node.depth}-${player1.x}-${player1.y}`; | |
// Append a new div line to the output. | |
graph.append(`<div id='xx'></div><div id='header-${id}' class='ml-${node.depth}'>Depth: ${node.depth}, Player 1: (${player1.x}, ${player1.y}), Player 2: (${player2.x}, ${player2.y}), Score: ${node.tree.score}, Active Player: ${ node.tree.activePlayer }, Moves: ${JSON.stringify(node.tree.moves)}<div id='${id}'</div></div>`); | |
// Render a mini version of the grid for this state. | |
ReactDOM.render( | |
<div> | |
<Isolation width={ tree.width } height={ tree.height } strategy={ StrategyManager.random } playerIndex = { node.tree.activePlayer } player1x={ player1.x } player1y={ player1.y } player2x={ player2.x } player2y={ player2.y } grid={ node.tree.values } moves={ node.tree.baseScore } cellStyle="small"></Isolation> | |
</div>, | |
document.getElementById(id) | |
); | |
// Add each child node from this state to the fringe. | |
node.tree.children.forEach(child => { | |
fringe.push({ tree: child, depth: node.depth + 1 }); | |
}); | |
node = fringe.pop(); | |
} | |
/* ReactDOM.render( | |
<div> | |
<Tree | |
data={tree} | |
height={400} | |
width={400}/> | |
</div>, | |
document.getElementById('xx') | |
);*/ | |
} | |
}; | |
class Cell extends React.Component { | |
constructor(props) { | |
super(props); | |
this.onClick = this.onClick.bind(this); | |
} | |
onClick(e) { | |
// Callback handler for cell click event. | |
this.props.onClick(this, this.props.x, this.props.y); | |
} | |
render() { | |
return ( | |
<div id={ `cell-${this.props.x}-${this.props.y}` } className={`cell ${this.props.cellStyle || ''}`} style={{width: this.props.width || '', height: this.props.height || '', backgroundColor: this.props.color }} onClick={ this.onClick }> | |
{ this.props.children } | |
</div> | |
); | |
}; | |
}; | |
class Grid extends React.Component { | |
constructor(props) { | |
super(props); | |
const values = props.grid || []; | |
if (!props.grid) { | |
// Populate the grid values with zeros. | |
for (let y=0; y <= props.height; y++) { | |
const row = []; | |
for (let x=0; x <= props.width; x++) { | |
row.push(0); | |
} | |
values.push(row); | |
} | |
} | |
this.state = { | |
values, | |
width: props.width, | |
height: props.height, | |
}; | |
this.onClick = this.onClick.bind(this); | |
this.setValue = this.setValue.bind(this); | |
} | |
componentDidUpdate(nextProps) { | |
const { width, height } = this.props; | |
// Reset the grid values when the width or height changes. | |
if ((width && nextProps.width !== width) || (height && nextProps.height !== height)) { | |
const values = []; | |
// Populate the grid values with zeros. | |
for (let y=0; y <= height; y++) { | |
const row = []; | |
for (let x=0; x <= width; x++) { | |
row.push(0); | |
} | |
values.push(row); | |
} | |
this.setState({ values, width, height }); | |
console.log(`Reset grid to ${width},${height}`); | |
} | |
} | |
onClick(cell, x, y) { | |
console.log(`${x},${y}`); | |
// Callback handler for cell click event. | |
this.props.onClick(x, y, this.state.values, cell); | |
} | |
setValue(x, y, value) { | |
// Set the cell value. | |
const values = this.state.values; | |
values[y][x] = value; | |
this.setState({ values }); | |
} | |
render() { | |
const rows = []; | |
for (let y=0; y<this.state.height; y++) { | |
const cols = [] | |
for (let x=0; x<this.state.width; x++) { | |
cols.push( | |
<td> | |
<Cell x={x} y={y} color={ this.state.values[y][x] } cellStyle={ this.props.cellStyle } onClick={ this.onClick }> | |
{ this.props.players.map((player, index) => { | |
return (x === player.x && y === player.y) ? this.props.children[index] : null | |
}) } | |
</Cell> | |
</td> | |
); | |
} | |
rows.push(<tr>{cols}</tr>); | |
} | |
return ( | |
<div class='grid'> | |
<table> | |
<tbody> | |
{rows} | |
</tbody> | |
</table> | |
</div> | |
) | |
} | |
} | |
class Player extends React.Component { | |
constructor(props) { | |
super(props); | |
} | |
render() { | |
// Adjust offset to position player icons on grid. | |
let leftOffset = 25; | |
let topOffset = 5; | |
const container = $('#app'); | |
if (container.length) { | |
const rect = container[0].getBoundingClientRect(); | |
leftOffset = rect.left + 15; | |
topOffset = rect.top + 5; | |
} | |
return ( | |
<i class={ `player ${this.props.cellStyle || ''} fas fa-female` } style={{ top: `${this.props.y * this.props.height + topOffset}px`, left: `${this.props.x * this.props.width + leftOffset}px`, color: this.props.color }}></i> | |
); | |
} | |
} | |
class Isolation extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
round: 1, | |
playerIndex: props.playerIndex || 0, | |
players: [ { x: props.player1x || -1, y: props.player1y || -1, moves: [{}] }, { x: props.player2x || -1, y: props.player2y || -1, moves: [{}] } ], | |
grid: props.grid, | |
strategy: props.strategy, | |
width: props.width, | |
height: props.height, | |
treeDepth: props.treeDepth, | |
}; | |
this.state.players[0].moves = IsolationManager.allMoves(0, this.state.players, props.width, props.height); | |
this.state.players[1].moves = IsolationManager.allMoves(1, this.state.players, props.width, props.height); | |
this.grid = React.createRef(); | |
this.onGrid = this.onGrid.bind(this); | |
} | |
componentDidUpdate(nextProps) { | |
const { strategy, width, height, treeDepth } = this.props; | |
if (strategy && nextProps.strategy !== strategy) { | |
this.setState({ strategy }); | |
} | |
if (width && nextProps.width !== width) { | |
this.setState({ width }); | |
} | |
if (height && nextProps.height !== height) { | |
this.setState({ height }); | |
} | |
if (treeDepth && nextProps.treeDepth !== treeDepth) { | |
this.setState({ treeDepth }); | |
} | |
} | |
onGrid(x, y, values) { | |
const playerIndex = this.state.playerIndex; | |
const players = this.state.players; | |
if (IsolationManager.isValidMove(x, y, playerIndex, players, values, this.grid.current.props.width, this.grid.current.props.height)) { | |
// Update player position. | |
players[playerIndex].x = x; | |
players[playerIndex].y = y; | |
// Update the grid local variable with the player move (so available moves will be accurate). | |
values[y][x] = playerIndex + 1; | |
// Update available moves for all players. | |
players[0].moves = IsolationManager.availableMoves(0, players, values, this.grid.current.props.width, this.grid.current.props.height); | |
players[1].moves = IsolationManager.availableMoves(1, players, values, this.grid.current.props.width, this.grid.current.props.height); | |
// Update cell value in the grid. | |
this.grid.current.setValue(x, y, !playerIndex ? 'gray' : 'silver'); | |
/*let tree = []; | |
if (playerIndex === 1) { | |
tree = StrategyManager.tree(!playerIndex ? 1 : 0, JSON.parse(JSON.stringify(players)), values, this.grid.current.props.width, this.grid.current.props.height); | |
StrategyManager.renderTree(tree); | |
}*/ | |
// Update state and play opponent's turn. | |
this.setState({ round: this.state.round + 1, playerIndex: !playerIndex ? 1 : 0, players }, () => { | |
if (this.state.playerIndex && this.state.players[this.state.playerIndex].moves.length > 0) { | |
if (this.state.strategy && this.state.strategy !== StrategyManager.none) { | |
// AI turn. | |
setTimeout(() => { | |
const tree = StrategyManager.tree(playerIndex, JSON.parse(JSON.stringify(players)), values, this.grid.current.props.width, this.grid.current.props.height); | |
StrategyManager.renderTree(tree, this.state.treeDepth); | |
// Get the AI's move. | |
({ x, y } = this.props.strategy(tree, this.state.playerIndex, this.state.players, values, this.grid.current.props.width, this.grid.current.props.height)); | |
console.log(`AI is moving to ${x},${y}.`) | |
// Move the AI player. | |
this.onGrid(x, y, values); | |
}, 1000); | |
} | |
} | |
}); | |
return true; | |
} | |
} | |
render() { | |
const moves = this.props.moves !== undefined ? this.props.moves : this.state.players[this.state.playerIndex].moves.length; | |
const winnerIndex = this.state.playerIndex ? 1 : 2; | |
return ( | |
<div id='app' ref={ this.container }> | |
<Grid width={ this.state.width } height={ this.state.height } grid={ this.props.grid } cellStyle={ this.props.cellStyle } players={ this.state.players } onClick={ this.onGrid } ref={ this.grid }> | |
<Player width="50" height="50" x={ this.state.players[0].x } y={ this.state.players[0].y } cellStyle={ this.props.cellStyle } color="blue"></Player> | |
<Player width="50" height="50" x={ this.state.players[1].x } y={ this.state.players[1].y } cellStyle={ this.props.cellStyle } color="orange"></Player> | |
</Grid> | |
<div class='row'> | |
<div class='col col-auto'> | |
<div class={ `badge ${!this.state.playerIndex ? 'badge-primary' : 'badge-warning'}` }>Player { this.state.playerIndex + 1 }'s Turn</div> | |
</div> | |
<div class='col col-auto'> | |
<div class='badge badge-light'>{ moves } Moves Available</div> | |
</div> | |
<div class='col col-auto'> | |
<div class={ `badge badge-success ${!moves ? '' : 'd-none'}` }>Player { winnerIndex } wins!</div> | |
</div> | |
</div> | |
<div class='row'> | |
<div class='col'> | |
<div class='badge badge-secondary'> | |
Move { Math.round(this.state.round / 2) } | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
} | |
} | |
class IsolationContainer extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
strategy: props.strategy || StrategyManager.minimax, | |
width: props.width || 3, | |
height: props.height || 3, | |
treeDepth: props.treeDepth || 25 | |
}; | |
this.onType = this.onType.bind(this); | |
this.onWidth = this.onWidth.bind(this); | |
this.onHeight = this.onHeight.bind(this); | |
this.onTreeDepth = this.onTreeDepth.bind(this); | |
} | |
onType(e) { | |
let strategy; | |
switch (e.currentTarget.value) { | |
case 'random': strategy = StrategyManager.random; break; | |
case 'minimax': strategy = StrategyManager.minimax; break; | |
case 'none': strategy = StrategyManager.none; break; | |
}; | |
this.setState({ strategy }); | |
console.log(`Strategy set to ${e.currentTarget.value}.`); | |
} | |
onWidth(e) { | |
this.setState({ width: e.currentTarget.value }); | |
} | |
onHeight(e) { | |
this.setState({ height: e.currentTarget.value }); | |
} | |
onTreeDepth(e) { | |
this.setState({ treeDepth: e.currentTarget.value }); | |
} | |
render() { | |
return ( | |
<div> | |
<Isolation width={ this.state.width } height={ this.state.height } treeDepth={ this.state.treeDepth } strategy={ this.state.strategy }></Isolation> | |
<div class="gamePlayOptions"> | |
<div> | |
<span>Game Play</span> | |
<input type="radio" name="type" value="minimax" checked={this.state.strategy === StrategyManager.minimax} onChange={ this.onType }/> <span>Minimax</span> | |
<input type="radio" name="type" value="random" checked={this.state.strategy === StrategyManager.random} onChange={ this.onType }/> <span>Random</span> | |
<input type="radio" name="type" value="none" checked={!this.state.strategy || this.state.strategy === StrategyManager.none} onChange={ this.onType }/> <span>2 Players</span> | |
</div> | |
<div> | |
<span>Grid Size</span> | |
<input type="number" id="width" name="width" value={this.state.width} onChange={ this.onWidth }/> | |
<input type="number" id="height" name="height" value={this.state.height} onChange={ this.onHeight }/> | |
</div> | |
<div> | |
<span>Tree Depth</span> | |
<input type="number" id="treeDepth" name="treeDepth" value={this.state.treeDepth} onChange={ this.onTreeDepth }/> | |
</div> | |
</div> | |
</div> | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.cell { | |
width: 50px; | |
height: 50px; | |
line-height: 0; | |
border: 1px solid black; | |
transition: background-color 0.25s; | |
&.small { | |
width: 10px; | |
height: 10px; | |
} | |
} | |
.player { | |
margin: 3px 0 0 12px; | |
font-size: 40px; | |
transition: all 0.25s; | |
&.small { | |
margin: 0; | |
font-size: 10px; | |
} | |
} | |
#graph { | |
min-height: 200px; | |
.ml-1 { | |
margin-left: 20px !important; | |
} | |
.ml-2 { | |
margin-left: 40px !important; | |
} | |
.ml-3 { | |
margin-left: 60px !important; | |
} | |
.ml-4 { | |
margin-left: 80px !important; | |
} | |
.ml-5 { | |
margin-left: 100px !important; | |
} | |
} | |
.gamePlayOptions { | |
span { | |
margin-right: 12px; | |
} | |
input[type='number'] { | |
width: 50px; | |
margin-right: 6px; | |
} | |
} |
Author
primaryobjects
commented
Nov 20, 2019
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment