I have been making a mancala bot using a minimax algorithm. It works for the first bit but it often just gives an output landing on a blank space. Anyone have any idea how to fix this so it plays well?
Here is my code:
let final;
function increment(board, ltif, player, re) {
let a = 0;
for(let i = 0; i < board[ltif]; i++) {
if((player && (ltif+i+a+1)%14 == 13) || (!player && (ltif+i+a+1)%14 == 6)) {
a += 1;
}
board[(ltif + i + a + 1)%14] += 1;
}
const bltif = board[ltif];
board[ltif] = 0;
let ans;
board[(bltif + ltif + a)%14] == 1 || (bltif + ltif + a)%14 == 6 || (bltif + ltif + a)%14 == 13 ? ans = board : ans = increment(board, (bltif + ltif + a)%14, player);
if(((bltif + ltif + a)%14 == 6 || (bltif + ltif + a)%14 == 13) && !re) {
ans = 2;;
}
if(board[(bltif + ltif + a)%14] == 1 && !re) {
ans = 3;
}
return ans;
}
function minimax(board, depth, player) {
if(board[6] > 24) {
return 15;
}else if(board[13] > 24) {
return -15;
}else if(board[6] == 24 && board[13] == 24) {
return 0;
}else if(depth === 0) {
return Math.floor((board[6]-board[13])/2);
}
let avail = board.map((element, index) => (element !== 0 && ((index < 6 && player)|| (index < 13 && index > 6 && !player)) ? index : -1)).filter(element => element !== -1);
if(player) {
let maxEval = [-Infinity];
for(let i = 0; i < avail.length; i++) {
let tboard = increment(board.slice(), avail[i], player, false);
let Eval;
if(tboard == 2) {
Eval = 13;
tboard = increment(board.slice(), avail[i], player, true);
}else if(tboard == 3) {
Eval = -13;
tboard = increment(board.slice(), avail[i], player, true);
}else{
Eval = minimax(tboard, depth - 1, false);
}
maxEval = [Math.max(Eval, maxEval[0]),avail[i],tboard];
}
final = [maxEval[1], maxEval[2]];
return maxEval[0];
}else{
let minEval = +Infinity;
for(let i = 0; i < avail.length; i++) {
let tboard = increment(board.slice(), avail[i], player, false);
let Eval;
if(tboard == 2) {
Eval = 13;
tboard = increment(board.slice(), avail[i], player, true);
}else if(tboard == 3) {
Eval = -13;
tboard = increment(board.slice(), avail[i], player, true);
}else{
Eval = minimax(tboard, depth - 1, false);
}
minEval = Math.min(Eval, minEval);
}
return minEval;
}
}
minimax([
5, 0, 5, 5, 5, 0,
3, 5, 5, 0, 5, 5,
5, 0
], 9, true);
console.log(final);
It runs out of a text based editor so that's why the output is into console and it only checks one board then you have to input another. Also just to clarify it is the avalanche version of mancala.
I do not have much experience using a minimax algorithm so if anyone has any insight into the problem that would be very helpful.
One example of this is that when given the board state:
[4, 4, 4, 4, 4, 4, 0, 4, 4, 4, 4, 4, 4, 0]
...it tells me to move the one on the 4th spot of the array so 5 from the right which gives this output:
[1, 6, 6, 6, 0, 1, 3, 6, 1, 6, 6, 0, 6, 0]
There are much better possible moves (like index 2) and I have no idea why the algorithm is selecting this. Also, the minimax algorithm I am using does not use alpha-beta pruning which is intended and not just a mistake in the code.
There are the following issues in your implementation:
minimaxthe first call ofincrementcan only return 2 or 3. This is becauseincrementwill keep making recursive calls as long as the stop condition is not true. And when the stop is true, it returns 2 or 3 (in casereis false). By consequence,minimaxwill never make a recursive call withdepth-1! I would suggest to solve this as follows. Don't stop the recursion whentboard == 3. This is the case when a turn ends "normally", i.e. without the right to get an extra turn. In that case there is no reason why the recursion should stop. So make the recursive call in that case, instead of settingEvalto -13.if(board[(bltif + ltif + a)%14] == 1 && !re)is a bug. This should be anelsebecause you don't want to overwriteanswith 3 when it was already set to 2, but the player's store happens to have 1 stone in it. And in theelsecase you can be sure that this condition is true, so it is not needed to evaluate it. Alternatively, you couldreturn ansin theifblock so there is no risk of overwriting thatanswith 3.maxEval = [Math.max(Eval, maxEval[0]),avail[i],tboard];is wrong, because it updates the second and third entry ofmaxEvaleven whenEvalis worse. This way you lose the move that was already associated with the best score. You should only updatemaxEval[1]andmaxEval[2]whenEvalis better thanmaxEval[0]. (I would also suggest using a plain object for this instead of an array. With a plain object we can give meaningful names to the three components).final = [maxEval[1], maxEval[2]];is wrong because it is also executed at deeper levels in the recursion tree, overwriting the result that was already registered at the top of the recursion tree, which is the result you are really interested in. The deeper problem here is thatfinalshould not be a global variable whichminimaxis altering as a "side-effect". This is bad practice. Instead, haveminimaxreturn that information.Other remarks:
numStonesinstead ofbltif.(bltif + ltif + a)%14. Instead have a variable dedicated to represent that value. You should only need one occurrence of the% 14operation in your code.incrementtwice on the same board only to get two different pieces of information from it, based onre. If you would just storeboard.slice()in a variable before callingincrement, you would already have the mutated board after theincrementcall. That way you don't need the call withreset to true, and can get rid of that parameter.ans = board) inside a larger expression. Convert such an expression to an assignment, where the right hand expression has the necessary logic.camelCasefor variables, so don't useEvalas a name. I would suggestscore(asevalis already used for a native function).avail, don't use.map, but use.filterimmediately. That way you don't need the virtual -1 value.Corrected code
Here is the code I ended up with while applying the above corrections and suggestions:
The algorithm now returns as best move, pit number 2. I didn't play this game enough to judge whether that is really the best move or not.