I have implement JavaScript applying alpha-beta minimax. It works well with 3x3 board, but when I change the board to 4x4 or higher, the program seems to hang.
UPDATE: The program is not efficient when available moves are more than 10
Here is the alpha-beta minimax function:
function AlphaBetaMinMax(game_board, depth, alpha, beta) {
if (CheckForWinner(game_board) === 1 || CheckForWinner(game_board) === 2 ||
CheckForWinner(game_board) === 3)
return GameScore(game_board, depth);
depth += 1;
var availableMoves = GetAvailableMoves(game_board);
var move, result, possible_game;
if (active_turn === "COM") {
for (var i = 0; i < availableMoves.length; i++) {
move = availableMoves[i];
possible_game = GetNewState(move, game_board);
result = AlphaBetaMinMax(possible_game, depth, alpha, beta);
game_board = UndoMove(game_board, move);
if (result > alpha) {
alpha = result;
if (depth == 1)
choice = move;
} else if (alpha >= beta) {
return alpha;
}
}
return alpha;
} else {
for (var i = 0; i < availableMoves.length; i++) {
move = availableMoves[i];
possible_game = GetNewState(move, game_board);
result = AlphaBetaMinMax(possible_game, depth, alpha, beta);
game_board = UndoMove(game_board, move);
if (result < beta) {
beta = result;
if (depth == 1)
choice = move;
} else if (beta <= alpha) {
return beta;
}
}
return beta;
}
}
function GameScore(game_board, depth) {
var score = CheckForWinner(game_board);
var t = (board_size + 1);
if (score === 1)
return 0;
else if (score === 2)
return depth - t;
else if (score === 3)
return t - depth;
}
function UndoMove(game_board, move) {
game_board[move] = UNOCCUPIED;
ChangeTurn();
return game_board;
}
function GetNewState(move, game_board) {
var piece = ChangeTurn();
game_board[move] = piece;
return game_board;
}
function ChangeTurn() {
var piece;
if (active_turn === "COM") {
piece = 'X';
active_turn = "PLAYER";
} else {
piece = 'O';
active_turn = "COM";
}
return piece;
}
function GetAvailableMoves(game_board) {
var AvailableMoves = new Array();
for (var i = 0; i < board_size; i++) {
if (game_board[i] === UNOCCUPIED) {
AvailableMoves.push(i);
}
}
return AvailableMoves;
}
CheckForWinner() returns:
0 for not a tie or not a win
1 for a tie
2 for Player win
3 for Computer win
Thanks for your help
I have successful built a game named Othello (It's almost like GO games) and it same what you are doing but my main language is Java (and you are using JavaScript). So, I will show my pseudocode for you (because I don't know JavaScript). I hope it can help you in this case:
function minimax(turn, max_depth ,depth, alpha, beta):
if isFinishState() or depth==max_depth :
return value of the node
if turn == "computer" :
best = -INFINITY
turn = "Human"
for each move in available moves :
possible_game = GetNewState(move, game_board);
value = minimax(turn, depth+1,max_depth, alpha, beta)
//insert some code to undo if you have changed the game board
best = max( best, value)
alpha = max( alpha, best)
if beta <= alpha:
break
return best
else : //human turn
best = +INFINITY
turn = "Computer"
for each move in available_moves:
possible_game = GetNewState(move, game_board);
value = minimax(turn, depth+1, max_depth, alpha, beta)
//insert some code to undo if you have changed the game board
best = min( best, value)
beta = min( beta, best)
if beta <= alpha:
break
return best
Then, you need a function like findBestMove() to return the best next position for computer:
int findBestMove(int max_depth) :
alpha = -9999999; //-INFINITY
beta = 9999999; //+INFINITY
choice = null;
best =-9999999;//-INFINITY
for each move in available_moves:
possible_game = GetNewState(move, game_board);
moveVal = minimax("computer", 0, max_depth, alpha, beta);
if (best < moveVal):
best = moveVal;
choice = move;
//insert code here to undo game_board
return choice;
Related
I've almost completed the logic for my tic tac toe minimax and its working quite well. But i'm still finding a few diagonal moves where the AI is seemingly getting it wrong. For example if you play square 3,6,5 and 7 you will win. It doesn't block the bottom corner for some reason.
Here's the minimax call recursively getting the best score that our computer can make:
let compMove;
let bestScore = -Infinity;
for (let i = 0; i < 9; i++) {
if (gameBoard.array[i] == '') {
gameBoard.array[i] = 'X';
let score = minimax(gameBoard.array, 0, true);
gameBoard.array[i] = '';
if (score > bestScore) {
bestScore = score;
compMove = i;
}
}
}
Here is the actual minimax function itself:
let scores = {
X: 10,
O: -10,
tie: 0
}
const minimax = (board, depth, maximizingPlayer) => {
let result = checkWinner(gameBoard.currentPlayer);
if (result !== null) {
return scores[result];
}
if (maximizingPlayer) {
let bestScore = -Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] == '') {
board[i] = 'O';
let score = minimax(board, depth + 1, true);
board[i] = '';
if (score > bestScore) {
bestScore = score;
}
}
}
return bestScore;
} else {
let bestScore = Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] == '') {
board[i] = 'X';
let score = minimax(board, depth + 1, false);
board[i] = '';
if (score < bestScore) {
bestScore = score;
}
}
}
return bestScore;
}
}
If anything sticks out to anyone I'd greatly appreciate it. I'm stumped!
If you'd like to test the current state of the game here's the codepen:
https://codepen.io/itswakana/pen/gOeMrym?editors=1111
There are these issues:
As the "computer" plays with "O" the code where you set compMove should not move with "X", but move with "O". So the first code block you have in your post should be:
let compMove;
let bestScore = Infinity; // <-- to minimize (positive)
for (let i = 0; i < 9; i++) {
if (gameBoard.array[i] == '') {
gameBoard.array[i] = 'O'; // <-- not X
let score = minimax(gameBoard.array, 0, true);
gameBoard.array[i] = '';
if (score < bestScore) { // <-- minimize
bestScore = score;
compMove = i;
}
}
}
checkWinner(gameBoard.currentPlayer) is always checking a win for the same player, as gameBoard.currentPlayer is not altered during the minimax search. So this will miss wins for the opponent. I would in fact leave gameBoard.currentPlayer alone, and pass an argument that is based on the maximizingPlayer argument:
let result = checkWinner("XO"[+maximizingPlayer]); // dynamically determine player
You always pass the current value of maximizingPlayer to the recursive call. When it is false, you pass false, and when it is true, you pass true. This means it never toggles (see next point).
When maximizing the score, your code lets "O" make moves, but that is the minimizing player. So that needs to be switched.
const minimax = (board, depth, maximizingPlayer) => {
let result = checkWinner("XO"[+maximizingPlayer]); // <--
if (result !== null) {
return scores[result];
}
let bestMove;
if (maximizingPlayer) {
let bestScore = -Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] == '') {
board[i] = 'X'; // <--
let score = minimax(board, depth + 1, false); // <--
board[i] = '';
if (score > bestScore) {
bestMove = i;
bestScore = score;
}
}
}
return bestScore;
} else {
let bestScore = Infinity;
for (let i = 0; i < 9; i++) {
if (board[i] == '') {
board[i] = 'O'; // <--
let score = minimax(board, depth + 1, true); // <--
board[i] = '';
if (score < bestScore) {
bestMove = i;
bestScore = score;
}
}
}
return bestScore;
}
}
With those changes it will work.
Final remark: it is a pity that in your code you have at least three concepts that correspond to the notion of "player": maximizingPlayer is a boolean, Player is an object with a team member, currentPlayer is a character property, and the player argument is the character too. This should be harmonised. Certainly the currentPlayer property serves hardly any purpose. It is just set here and there to a hard-coded "X" or "O" and then immediately passed as argument.
I am currently in the process of making a simple chess engine, the issue is that even with a simple evaluation function and looking at a depth of 2, the engine takes about 15 seconds to make a move. I want to improve the engine's performance by using a transposition table, so I tried implementing a transposition table myself, but there were no gains in performance. What may be wrong with my implementation, and how can I fix it?
Here is the code:
function alphaBeta(node, depth, alpha, beta, maximisingPlayer){
if(depth === 0 || isTerminalNode(node)){
if(isTerminalNode(node)){
if(isCheckmate(blackPieces, node)){
return 900;
} else {
return -900;
}
} else {
let zorbistKeyValue = evaluateBoardZorbistKeyCode(node);
let storedMoveItem = storedMoves[zorbistKeyValue];
if(storedMoveItem != undefined){
return storedMoveItem;
} else {
let evaluation = evaluateBoard(node);
storedMoves[zorbistKeyValue] = evaluation;
return evaluation;
}
}
}
maximisingPlayer ? player = whitePieces : player = blackPieces;
availableSpots = availableValidSpots(node, player);
if(maximisingPlayer){
value = -Infinity;
for(availableSpot = 0; availableSpot < availableSpots.length; availableSpot++){
child = availableSpots[availableSpot].node;
value = Math.max(value, alphaBeta(child, depth - 1, alpha, beta, false));
alpha = Math.max(alpha, value);
if(alpha >= beta){
break;
}
}
return value;
} else {
value = Infinity;
for(availableSpot = 0; availableSpot < availableSpots.length; availableSpot++){
child = availableSpots[availableSpot].node;
value = Math.min(value, alphaBeta(child, depth - 1, alpha, beta, true));
beta = Math.min(beta, value);
if(alpha >= beta){
break;
}
}
return value;
}
}
function findBestMove(board, player){
player == blackPieces ? isMaximisingPlayer = false : isMaximisingPlayer = true;
let availSpots = availableValidSpots(board, player);
if(isMaximisingPlayer){
let bestScore = -Infinity;
for(availSpot = 0; availSpot < availSpots.length; availSpot++){
let value = alphaBeta(availSpots[availSpot].node, 2, -Infinity, Infinity, true);
if(value > bestScore){
bestScore = value;
bestMove = availSpots[availSpot];
}
}
} else {
let bestScore = Infinity;
for(availSpot = 0; availSpot < availSpots.length; availSpot++){
let value = alphaBeta(availSpots[availSpot].node, 2, -Infinity, Infinity, false);
if(value < bestScore){
bestScore = value;
bestMove = availSpots[availSpot];
}
}
}
return bestMove;
}
Im working on a sorting visualizer using p5.js, and I need to know if its possible to slow down merge sort so it can be drawn slower. Im currently trying to use the sleep function below to slow down they merge function, but I get
Uncaught TypeError: a.slice is not a function.
Am I just making a silly mistake, or am I approaching the problem incorrectly?
let rectWidth;
let depth = 0;
function setup() {
let numOfRects = document.getElementById('numOfRects').value;
let width = document.getElementById('canvas').offsetWidth;
let height = document.getElementById('canvas').offsetHeight;
let canvas = createCanvas(width, height);
rectWidth = floor(width / numOfRects);
canvas.parent('canvas');
values = new Array(floor(width / rectWidth));
for (let i = 0; i < values.length; i++) {
values[i] = random(height);
}
frameRate(1);
}
function draw() {
background(23);
values = mergeSort(values, depth);
depth++;
for (let i = 0; i < values.length; i++) {
stroke(0);
fill(255);
rect(i * rectWidth, height - values[i], rectWidth, values[i]);
}
}
function mergeSort(a, d) {
if (a.length <= 1) {
return a;
}
d--;
if (d < 1) {
return (a);
}
var mid = Math.round((a.length / 2));
var left = a.slice(0, mid);
var right = a.slice(mid);
let leArr = mergeSort(left, d);
let riArr = mergeSort(right, d);
return merge(leArr, riArr);
}
async function merge(left, right) {
sorted = [];
while (left && left.length > 0 && right && right.length > 0) {
if (left[0] <= right[0]) {
sorted.push(left.shift());
} else {
sorted.push(right.shift());
}
}
await sleep(50);
return sorted.concat(left, right);
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
A sleep inside your merge algorithm makes no sense at all. Your draw function can just draw the final array which is returned by mergeSort. So even if you make the algorithm slower, you won't see any intermediate till mergeSort has not finished for the specified depth.
A possibility would be to do the Merge sort in a separate thread and to visualize in draw the current state of the array. But in this case you've to apply the sorting on the "original" array, rather than to create copies of the array (by slice) and to write them back to the original array at the end.
See the example:
let values = [];
let startSort = true;
function mergeSort(a) {
// create copy of the array
copy = a.slice()
// asynchronous sort the copy
mergeSortSlice(copy, 0, copy.length);
return;
}
async function mergeSortSlice(a, start, end) {
if (end-start <= 1)
return;
var mid = Math.round((end+start) / 2);
// wait till divides are sort
await mergeSortSlice(a, start, mid);
await mergeSortSlice(a, mid, end);
// merge divides
let i = start, j = mid;
while (i < end && j < end) {
if (a[i] > a[j]) {
let t = a[j]; a.splice(j, 1); a.splice(i, 0, t);
j ++;
}
i ++;
if (i==j) j ++;
// copy back the current state of the sorting
values = a.slice();
// slow down
await sleep(100);
}
// restart
if (start == 0 && end == a.length) {
await sleep(2000);
startSort = true;
}
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function setup() {
createCanvas(600, 190);
frameRate(60);
}
let numOfRects = 15;
let rectWidth;
function draw() {
if (startSort) {
startSort = false;
rectWidth = floor(width / numOfRects);
values = new Array(floor(width / rectWidth));
for (let i = 0; i < values.length; i++) {
values[i] = random(height);
}
mergeSort(values);
}
background(23);
stroke(0);
fill(255);
for (let i = 0; i < values.length; i++) {
rect(i * rectWidth, height - values[i], rectWidth, values[i]);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.js"></script>
Note: I also posted this question on p5.js' forum: https://discourse.processing.org/t/p5-js-prims-algorithm-maze-generation-stuck-in-infinite-loop/8277
I'm trying to implement a randomized Prim's algorithm to generate a maze. However, the program keeps getting stuck in an infinite loop as the length of the list of walls (wallList) is always in the thousands. Currently I am using an if statement that stops the maze generation after 11500 iterations to prevent an infinite loop.
My pseudocode is based on Wikipedia's description of the algorithm:
Start with a grid full of walls.
Pick a cell, mark it as part of the
maze. Add the walls of the cell to the wall list.
While there are
walls in the list:
Pick a random wall from the list. If only one of
the two cells that the wall divides is visited, then:
Make the wall a
passage and mark the unvisited cell as part of the maze.
Add the
neighboring walls of the cell to the wall list.
Remove the wall from
the list.
HTML:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.0/p5.js"></script>
</script>
</head>
<body>
<div id="game-wrapper">
<div id="canvas-wrapper">
</div>
</div>
</div>
<script type="text/javascript" src="maze.js"></script>
</body>
</html>
JavaScript (maze.js):
// Note to self: JavaScript variables have function scope, not block scope
var numIterations = 0; // Just for debugging purposes (stop at 1000 iterations otherwise the program goes into
// an infinite loop
// The directions and vectors arrays correspond to each other
// For example, the first element of directions is "N" and the first element of vectors also represents a
// north vector
var directions = ["N", "E", "S", "W"]
var vectors = [
[-1, 0],
[0, 1],
[1, 0],
[0, -1]
];
var wallList = {}; // Structure of a wall [rol (num), col (num), direction (string)]
function Maze(numRows, numColumns) {
/*
Defines a maze given the number of rows and the number of columns in the maze
*/
this.numColumns = numColumns;
this.numRows = numRows;
this.numCells = numRows * numColumns;
this.cellGraph = [];
for (var i = 0; i < numRows; i++) { // For every single row
this.cellGraph.push([]); // Start out with an empty row
}
}
Maze.prototype.computeFrontierWalls = function (cellRow, cellColumn) {
/*
The frontier walls of a cell is defined as all the walls of the adjacent cells
*/
/*
Coordinates of adjacent cells:
Up [cellRow - 1, cellColumn]
Down [cellRow + 1, cellColumn]
Right [cellRow, cellColumn + 1]
Left [cellRow, cellColumn - 1]
*/
var coordinates = [
[cellRow - 1, cellColumn],
[cellRow + 1, cellColumn],
[cellRow, cellColumn + 1],
[cellRow, cellColumn - 1]
];
var computedFrontier = []; // List of frontier cells
var originalCell = this.cellGraph[cellRow][cellColumn]; // We want to calculate the frontier of the original cell
for (var i = 0; i < coordinates.length; i++) {
// Get the coordinates of the adjacent cell
var coordinate = coordinates[i];
var row = coordinate[0];
var col = coordinate[1];
// See if a cell exists at that area
// If there is a cell that exists, add all of the walls of the cell to the computedFrontier array
if (row >= 0 && row < this.cellGraph.length && col >= 0 && col < this.cellGraph[0].length) {
var cell = this.cellGraph[parseInt(row)][parseInt(col)];
for (var j = 0; j < directions.length; j++) {
computedFrontier.push([cell.row, cell.column, directions[j]]);
}
}
}
return computedFrontier;
}
function Cell(cellSize, row, column) {
this.cellSize = cellSize; // The width and height of the cell
this.column = column;
this.row = row;
this.xPos = column * cellSize;
this.yPos = row * cellSize;
this.walls = [true, true, true, true]; // 0 = top, 1 = right, 2 = bottom, 3 = left
this.visited = false; // Whether the cell has been traversed or not
}
function getRandomPos(widthCells, heightCells) {
var row = Math.floor(Math.random() * heightCells); // Generate a random row
var column = Math.floor(Math.random() * widthCells); // Generate a random column
return [row, column];
}
var mazeIntro = function (p) {
var maze = new Maze(20, 35); // Generate a new maze with 20 rows and 35 columns
Maze.prototype.createMaze = function () { // Build an empty maze
for (var i = 0; i < this.numRows; i++) { // Iterate through every row
for (var j = 0; j < this.numColumns; j++) { // Iterate through every column
var cell = new Cell(20, i, j); // Create a new size at row i and column j with size 20
maze.cellGraph[i].push(cell); // Add the cell to the row
}
}
}
maze.createMaze(); // Build the maze
p.setup = function () {
var canvas = p.createCanvas(700, 400);
p.background(255, 255, 255);
p.smooth();
p.noLoop();
// Pick a cell, mark it as part of the maze. Add the walls of the cell to the wall list
var pos = getRandomPos(maze.cellGraph[0].length, maze.cellGraph.length);
;
var row = pos[0];
var column = pos[1];
maze.cellGraph[row][column].visited = true;
for (var k = 0; k < directions.length; k++) {
var key = row.toString() + column.toString() + directions[k].toString();
if (!wallList[key]) {
wallList[key] = [row, column, directions[k]];
}
}
}
Cell.prototype.display = function () {
/*
For each wall:
1. Check if it is on the border of the maze:
2. If it is on the border: don't draw the wall
3. If it isn't on the border: draw the wall
*/
p.stroke(0, 0, 0);
if (this.walls[0] && this.row != 0) { // Top
p.line(this.xPos, this.yPos, this.xPos + this.cellSize, this.yPos);
}
if (this.walls[1] && this.column != maze.widthCells - 1) { // Right
p.line(this.xPos + this.cellSize, this.yPos, this.xPos + this.cellSize, this.yPos + this.cellSize);
}
if (this.walls[2] && this.row != maze.heightCells - 1) { // Bottom
p.line(this.xPos + this.cellSize, this.yPos + this.cellSize, this.xPos, this.yPos + this.cellSize);
}
if (this.walls[3] && this.column != 0) { // Left
p.line(this.xPos, this.yPos + this.cellSize, this.xPos, this.yPos);
}
p.noStroke();
}
Cell.prototype.toString = function () {
/*
Mainly used for debugging purposes, converts the object into a string containing the row and the column of the cell
*/
return this.row + "|" + this.column;
}
function deleteWall(current, neighbor) {
/*
Deletes two walls separating two cells: current and neighbor
Calculates if neighbor is to the left, right, top, or bottom of cell
Removes the current's wall and the corresponding neighbor's wall
*/
var deltaX = current.column - neighbor.column;
var deltaY = current.row - neighbor.row;
if (deltaX == 1) { // Current is to the right of the neighbor
current.walls[3] = false;
neighbor.walls[1] = false;
}
if (deltaX == -1) { // Current is to the left of the neighbor
current.walls[1] = false;
neighbor.walls[3] = false;
}
if (deltaY == 1) { // Current is to the bottom of the neighbor
current.walls[0] = false;
neighbor.walls[2] = false;
}
if (deltaY == -1) { // Current is to the top of the neighbor
current.walls[2] = false;
neighbor.walls[0] = false;
}
}
function isWall(cellA, cellB) {
// Whether there's a wall or not depends on the orientation of the blocks
// If it's vertical, it has to be false between even numbers
// If it's horizontal, it has to be false between odd numbers
for (var j = 0; j < cellA.walls.length; j++) {
for (var k = 0; k < cellB.walls.length; k++) {
if (Math.abs(j - k) == 2 && !cellA.walls[j] && !cellB.walls[k]) {
var rA = cellA.row;
var cA = cellA.column;
var rB = cellB.row;
var cB = cellB.column
if ((rA - rB) == 1 && j == 0 || (rA - rB) == -1 && j == 2 || (cA - cB) == 1 && j == 3 || (cA - cB) == -1 && j == 1) {
return false;
}
}
}
}
return true;
}
function calculateCellDivision(wall) {
// Calculate the two cells that the wall divides
// For example:
// If the wall is [10, 11, "N"]
// The two cells that the wall divides are (10, 11) and (9, 11)
var row = wall[0];
var col = wall[1];
var cell1 = maze.cellGraph[row][col]; // Get the cell of the wall
// Get the corresponding vector based upon the direction of the wall
var vectorIndex = directions.indexOf(wall[2]);
// Add the vector to the position of cell1
var cell2Row = parseInt(cell1.row) + vectors[vectorIndex][0];
var cell2Column = parseInt(cell1.column) + vectors[vectorIndex][1];
if (cell2Row < 0 || cell2Row >= maze.cellGraph.length || cell2Column < 0 || cell2 >= maze.cellGraph[0].length) {
return -1;
}
var cell2 = maze.cellGraph[cell2Row][cell2Column]; // Get the corresponding cell
var cellsVisited = 0;
var unvisitedCell;
if (cell1.visited) {
cellsVisited += 1;
unvisitedCell = cell2;
}
if (!cell2) { // This means that the wall is a border wall
return -1;
}
if (cell2.visited) {
cellsVisited += 1;
unvisitedCell = cell1;
}
if (cellsVisited == 1) {
return [cell1, cell2, cellsVisited, unvisitedCell];
}
return -1;
}
function getCellWalls(row, col) {
// Gets a cell's walls
var cellWalls = [];
for (var i = 0; i < directions.length; i++) {
cellWalls.push(row + col + directions[i]);
}
return cellWalls;
}
p.draw = function () {
while (Object.keys(wallList).length > 0) { // While there are still walls in the list
console.log("Object.keys(wallList).length = " +
// Pick a random wall of the list
var wallListKeys = $.map(wallList, function (value, key) {
return key;
});
var randomKey = wallListKeys[Math.floor(Math.random() * wallListKeys.length)];
var randomWall = wallList[randomKey];
var components = calculateCellDivision(randomWall);
if (components != -1) {
var numVisited = components[2];
var cell1 = components[0];
var cell2 = components[1];
// If only one of the two cells that the wall divides is visited, then:
// 1. Make the wall a passage and mark the unvisited cell as part of the maze.
// 2. Add the neighboring walls of the cell to the wall list.
// Remove the wall from the list.
if (numVisited == 1) {
deleteWall(cell1, cell2);
var unvisitedCell = maze.cellGraph[components[3].row][components[3].column];
unvisitedCell.visited = true;
var unvisitedString = unvisitedCell.row + "|" + unvisitedCell.column;
// Add the neighboring walls of the cell to the wall list
// Format of the walls (by index):
// 0 = top, 1 = right, 2 = bottom, 3 = left
var computedFrontierWalls = maze.computeFrontierWalls(unvisitedCell.row, unvisitedCell.column);
for (var k = 0; k < computedFrontierWalls.length; k++) {
var computedWall = computedFrontierWalls[k];
var keyString = computedWall[0].toString() + computedWall[1].toString() + computedWall[2];
if (!wallList[keyString]) {
wallList[keyString] = computedWall;
}
}
// Delete randomKey from the list of walls, and then delete the same wall from the corresponding cell
delete wallList[randomKey];
// Calculate the corresponding cell
var direction = randomWall[2];
var directionIndex = directions.indexOf(direction);
var oppositeDirectionIndex = -1;
if (directionIndex == 0) {
oppositeDirectionIndex = 2;
}
if (directionIndex == 2) {
oppositeDirectionIndex = 0;
}
if (directionIndex == 1) {
oppositeDirectionIndex = 3;
}
if (directionIndex == 3) {
oppositeDirectionIndex = 1;
}
var vector = vectors[directionIndex];
var correspondingString = (randomWall[0] + vector[0]).toString() + (randomWall[1] + vector[1]).toString() + directions[oppositeDirectionIndex];
delete wallList[correspondingString];
}
}
numIterations += 1;
if (numIterations == 11500) { // Prevents infinite loop
break;
}
}
p.clear();
// Draw the maze
for (var i = 0; i < maze.cellGraph.length; i++) { // Iterate through row
for (var j = 0; j < maze.cellGraph[i].length; j++) { // Iterate through every column
maze.cellGraph[i][j].display(); // Display the cell
}
}
p.line(0, 400, 400, 400);
}
};
var myp5 = new p5(mazeIntro, "canvas-wrapper"); // Initialize the graphics engine for the canvas
The generation does work, as shown in the screenshot below. But I'm pretty sure my implementation is not correct as the wall list shouldn't contain thousands of walls.
According to the Wikipedia article, a wall must be removed once is picked randomly and analysed, regardless of the results of said analysis. In your code, the lines delete wallList[randomKey]; and delete wallList[correspondingString]; must be outside of the conditional for the wall to be eliminated from the list.
After deleting said lines, replace this:
numIterations += 1;
if (numIterations == 11500) { // Prevents infinite loop
break;
}
with this:
delete wallList[randomKey];
delete wallList[correspondingString];
and you're good to go. (Disclaimer: I tested it and it worked; but that's a hefty amount of code you have there, so I'm not sure if anything else breaks).
I wanted to try and make it more readable so I could understand better and came up with this. Hope it helps.
// 1. Start with a grid full of walls.
const _WALL = '█';
const _PATH = ' ';
const _COLS = 60;
const _ROWS = 60;
let maze = [];
for(let i = 0; i < _COLS; i++){
maze.push([]);
for(let j = 0; j < _ROWS; j++)
maze[i][j] = _WALL;
}
// 2. Pick a cell, mark it as part of the maze.
let cell = {x:Math.floor(Math.random()*_COLS), y:Math.floor(Math.random()*_ROWS)};
maze[cell.x][cell.y] = _PATH;
// 2.1 Add the walls of the cell to the wall list.
let walls = [];
if(cell.x+1 < _COLS) walls.push({x:cell.x+1, y:cell.y});
if(cell.x-1 >= 0) walls.push({x:cell.x-1, y:cell.y});
if(cell.y+1 < _ROWS) walls.push({x:cell.x, y:cell.y+1});
if(cell.y-1 >= 0) walls.push({x:cell.x, y:cell.y-1});
// 3. While there are walls in the list:
while(walls.length > 0){
// 3.1 Pick a random wall from the list.
let wallIndex = Math.floor(Math.random() * walls.length);
let wall = walls[wallIndex];
// 3.2 If only one of the two cells that the wall divides is visited, then:
let uc = []; // uc will be short for 'unvisited cell'
if(wall.x+1 < _COLS && maze[wall.x+1][wall.y] === _PATH) uc.push({x:wall.x-1, y:wall.y});
if(wall.x-1 >= 0 && maze[wall.x-1][wall.y] === _PATH) uc.push({x:wall.x+1, y:wall.y});
if(wall.y+1 < _ROWS && maze[wall.x][wall.y+1] === _PATH) uc.push({x:wall.x, y:wall.y-1});
if(wall.y-1 >= 0 && maze[wall.x][wall.y-1] === _PATH) uc.push({x:wall.x, y:wall.y+1});
if(uc.length === 1){
// 3.2.1 Make the wall a passage and mark the unvisited cell as part of the maze.
maze[wall.x][wall.y] = _PATH;
if(uc[0].x >=0 && uc[0].x <_COLS && uc[0].y >=0 && uc[0].y<_ROWS){
maze[uc[0].x][uc[0].y] = _PATH;
// 3.2.2 Add the neighboring walls of the cell to the wall list.
if(uc[0].x+1 < _COLS && maze[uc[0].x+1][uc[0].y] === _WALL) walls.push({x:uc[0].x+1, y:uc[0].y});
if(uc[0].x-1 >= 0 && maze[uc[0].x-1][uc[0].y] === _WALL) walls.push({x:uc[0].x-1, y:uc[0].y});
if(uc[0].y+1 < _ROWS && maze[uc[0].x][uc[0].y+1] === _WALL) walls.push({x:uc[0].x, y:uc[0].y+1});
if(uc[0].y-1 >= 0 && maze[uc[0].x][uc[0].y-1] === _WALL) walls.push({x:uc[0].x, y:uc[0].y-1});
}
}
// 3.3 Remove the wall from the list.
walls.splice(wallIndex, 1);
}
console.table(maze);
function setup(){
createCanvas(400, 400);
fill(0);
let widthUnit = width / _COLS;
let heightUnit = height / _ROWS;
for(let i = 0; i < _COLS; i++)
for(let j = 0; j < _ROWS; j++)
if(maze[i][j] === _WALL){
//rect(i*widthUnit, j*heightUnit, widthUnit, heightUnit);
if(i-1 >= 0 && i+1 < _COLS){
if(maze[i-1][j] === _WALL) line((i+0.5)*widthUnit, (j+0.5)*heightUnit, i*widthUnit, (j+0.5)*heightUnit);
if(maze[i+1][j] === _WALL) line((i+0.5)*widthUnit, (j+0.5)*heightUnit, (i+1)*widthUnit, (j+0.5)*heightUnit);
}
if(j-1 >= 0 && j+1 < _ROWS){
if(maze[i][j-1] === _WALL) line((i+0.5)*widthUnit, (j+0.5)*heightUnit, (i+0.5)*widthUnit, j*heightUnit);
if(maze[i][j+1] === _WALL) line((i+0.5)*widthUnit, (j+0.5)*heightUnit, (i+0.5)*widthUnit, (j+1)*heightUnit);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
I'm trying to write a Tic-Tac-Toe game and decided to use the MiniMax algorithm, but I'm having trouble implementing it. For example:
on a
board = [
"E", "E", "X",
"E", "E", "X",
"E", "O", "O"
];
It's the AI's turn and the function returns AiMove { score: -10, coordinates: 0 } as the best move. I've been trying to debug for quite some time now, but the recursive nature of the function and the amount of possible game trees, early game states in particular, are quite hard to follow and debug.
Could someone lend a hand?
https://jsfiddle.net/LdLqk1z8/4/
var factions = {
AIplayer: "X",
humanPlayer: "O"
};
var gameResults = {
winner: ""
};
var emptyCells = function(board) { //check for empty cells and return an array with the number of empty cells
var indices = [];
for (var itr = 0; itr < 9; itr++) {
if (board[itr] === "E") {
indices.push(itr);
}
}
return indices;
};
var isGameOver = function(board) {
var tile = board;
//check for victory conditions
for (var i = 0; i <= 6; i = i + 3) {
if (tile[i] !== "E" && tile[i] === tile[i + 1] && tile[i + 1] === tile[i + 2]) {
if (factions.AIplayer === tile[i]) {
gameResults.winner = "AIplayer";
} else if (tile[i] === factions.humanPlayer) {
gameResults.winner = "humanPlayer";
}
return true;
}
}
for (var i = 0; i <= 2; i++) {
if (tile[i] !== "E" && tile[i] === tile[i + 3] && tile[i + 3] === tile[i + 6]) {
if (factions.AIplayer === tile[i]) {
gameResults.winner = "AIplayer";
} else if (tile[i] === factions.humanPlayer) {
gameResults.winner = "humanPlayer";
}
return true;
}
}
for (var i = 0, j = 4; i <= 2; i = i + 2, j = j - 2) {
if (tile[i] !== "E" && tile[i] === tile[i + j] && tile[i + j] === tile[i + 2 * j]) {
if (factions.AIplayer === tile[i]) {
gameResults.winner = "AIplayer";
} else if (tile[i] === factions.humanPlayer) {
gameResults.winner = "humanPlayer";
}
return true;
}
}
var check = emptyCells(board); //check if the game ended with a draw
if (check.length === 0) {
gameResults.winner = "draw";
return true;
} else {
return false; //if no condition is matched the game has not concluded
}
};
var getBestMove = function(board, player) {
// return an AiMove object initialized to 10 if the AI player wins, -10 if the human player wins and 0 if the game is a draw
if (isGameOver(board)) {
if (gameResults.winner === "AIplayer") {
return new AiMove(10);
} else if (gameResults.winner === "humanPlayer") {
return new AiMove(-10);
} else if (gameResults.winner === "draw") {
return new AiMove(0);
}
}
var moves = []; //array to store all moves
var currentPlayer = player;
for (var i = 0, l = board.length; i < l; i++) { //iterate over the board
if (board[i] == "E") { //if the tile is empty
var move = new AiMove; //create new AiMove object and assign a coordinate
move.coordinates = i;
board[i] = currentPlayer; //update board
//call getBestMove recursively with the next player
if (currentPlayer === factions.AIplayer) {
move.score = getBestMove(board, factions.humanPlayer).score;
} else if (currentPlayer === factions.humanPlayer) {
move.score = getBestMove(board, factions.AIplayer).score;
}
moves.push(move);
board[i] = "E"; //clear tile after move is pushed in to the moves array
}
}
//if it's the AI player's turn select biggest value from the moves array, if it's the human player's turn select the smallest value
if (currentPlayer === factions.AIplayer) {
var bestMove = 0;
var bestScore = -10000;
for (var i = 0; i < moves.length; i++) {
if (moves[i].score > bestScore) {
bestScore = moves[i].score;
bestMove = i;
}
}
} else if (currentPlayer === factions.humanPlayer) {
var bestMove = 0;
var bestScore = 10000;
for (var i = 0; i < moves.length; i++) {
if (moves[i].score < bestScore) {
bestMove = i;
bestScore = moves[i].score;
}
}
}
return moves[bestMove]; //return best move
};
var board = [
"E", "E", "X",
"E", "E", "X",
"E", "O", "O"
];
function AiMove(score) {
this.coordinates,
this.score = score;
}
console.log(getBestMove(board, factions.AIplayer))
EDIT: Could it be that, since the board set up is unwinnable, and the AI is fatalistic, it "gives-up"? Would implementing the concept of "depth" solve this?
The idea of introducing depth in the equation is indeed the right one.
Before doing moves.push(move) add the following line:
move.score = Math.sign(move.score) * (Math.abs(move.score) - 1);
This will make the score 1 point less grave (-10 becomes -9, 10 becomes 9, ...etc). It expresses the idea that the further away a loss is, the less grave it is, and the closer a win is, the better it is.
In the sample board set up you provided, this will return 6 as the best move, which circumvents the immediate win of the human player. Of course, the AI will still lose against best play, as the game will continue like this:
. . X . . X . . X X . X X O X
. . X → . . X → . O X → . O X → . O X
. O O X O O X O O X O O X O O
For better debugging possibilities, I would write a function that prints the board to the console. For instance:
function printBoard(board) {
console.log(board.slice(0, 3).join (' ').replace(/E/g, '.'));
console.log(board.slice(3, 6).join (' ').replace(/E/g, '.'));
console.log(board.slice(6, 9).join (' ').replace(/E/g, '.'));
console.log('');
}
You could also look into making the code more object oriented, writing methods on a Game object, ...etc.