Skip to content

Instantly share code, notes, and snippets.

@maksverver
Created October 20, 2024 20:06
Show Gist options
  • Save maksverver/e2de5d9b1f3cd32dbef9766f9dff1e7f to your computer and use it in GitHub Desktop.
Save maksverver/e2de5d9b1f3cd32dbef9766f9dff1e7f to your computer and use it in GitHub Desktop.
StackAndCoquer new AI players
/**
* \file MinimaxCPU.js
*
* \section LICENSE
*
* Copyright (C) 2024 Maks Verver
*
* This file is part of StackAndConquer.
*
* StackAndConquer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* StackAndConquer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with StackAndConquer. If not, see <https://www.gnu.org/licenses/>.
*
* \section DESCRIPTION
*
* CPU opponent using the Minimax algorithm.
*
* This file was auto-generated from multiple source files. The original source
* code is available here: https://github.com/maksverver/stackandconquer-ai/
*/
'use strict';
// src/minimax.js
var SEARCH_DEPTH = 4;
function findBestMoves(unusedCfg, state, moves) {
function search(depthLeft, alpha, beta) {
if (depthLeft === 0) {
return state.evaluate();
}
var moves2 = state.generateMoves();
if (moves2.length === 0) {
var value2 = state.evaluate();
if (value2 > 0) value2 += depthLeft;
if (value2 < 0) value2 -= depthLeft;
return value2;
}
var bestValue2 = -Infinity;
for (var i2 = 0; i2 < moves2.length; ++i2) {
var move2 = moves2[i2];
var undoState2 = state.doMove(move2);
var value2 = -search(depthLeft - 1, -beta, -alpha);
state.undoMove(move2, undoState2);
if (value2 > bestValue2) {
bestValue2 = value2;
if (value2 > alpha) alpha = value2;
if (value2 >= beta) break;
}
}
return bestValue2;
}
var bestMoves = [];
var bestValue = -Infinity;
for (var i = 0; i < moves.length; ++i) {
var move = moves[i];
var undoState = state.doMove(move);
var value = -search(SEARCH_DEPTH - 1, -Infinity, -bestValue + 1);
state.undoMove(move, undoState);
if (value > bestValue) {
bestValue = value;
bestMoves.length = 0;
}
if (value === bestValue) {
bestMoves.push(move);
}
}
return [bestMoves, bestValue];
}
// src/util.js
var log = typeof game === "object" ? game.log : console.log;
function createConfig(rows, cols, inputFields, outside, padding, paddingSize, winningHeight, playerCount) {
var paddedRows = rows + 2 * paddingSize;
var paddedCols = cols + 2 * paddingSize;
if (inputFields.length !== paddedRows * paddedCols) {
throw new Error("Invalid length of input fields");
}
var apiToFieldIndex = [];
var fieldIndexToApi = [];
var fieldCount = 0;
for (var r1 = 0; r1 < paddedRows; ++r1) {
for (var c1 = 0; c1 < paddedCols; ++c1) {
var i = paddedCols * r1 + c1;
if (inputFields[i] === outside || inputFields[i] === padding) {
apiToFieldIndex.push(-1);
} else {
apiToFieldIndex.push(fieldCount++);
fieldIndexToApi.push(i);
}
}
}
if (fieldCount > 30) {
throw new Error("Too many fields (maximum supported: 30)");
}
var moves = [];
var DR = [-1, -1, -1, 0, 0, 1, 1, 1];
var DC = [-1, 0, 1, -1, 1, -1, 0, 1];
for (var r2 = 0; r2 < paddedRows; ++r2) {
for (var c2 = 0; c2 < paddedCols; ++c2) {
var j = paddedCols * r2 + c2;
if (inputFields[j] === outside || inputFields[j] === padding) continue;
var dst = moves.length;
moves.push([]);
for (var height = 0; height < winningHeight; ++height) {
moves[dst].push([]);
}
for (var dir = 0; dir < 8; ++dir) {
var dr = DR[dir], dc = DC[dir];
var mask = 0;
for (var height = 1; height < winningHeight; ++height) {
var r1 = r2 - dr * height;
var c1 = c2 - dc * height;
if (r1 < 0 || r1 >= paddedRows || c1 < 0 || c1 >= paddedCols) break;
var i = paddedCols * r1 + c1;
if (inputFields[i] === outside || inputFields[i] === padding) break;
var src = apiToFieldIndex[i];
moves[dst][height].push([src, mask]);
mask |= 1 << src;
}
}
}
}
return Object.freeze({
apiToFieldIndex: Object.freeze(apiToFieldIndex),
fieldIndexToApi: Object.freeze(fieldIndexToApi),
fieldCount: fieldCount,
moves: Object.freeze(moves),
winningHeight: winningHeight,
playerCount: playerCount,
// These are only used for move parsing/formatting and debug printing:
rows: rows,
cols: cols,
paddingSize: paddingSize
});
}
function fieldIndexToRowCol(cfg2, index) {
var i = cfg2.fieldIndexToApi[index];
var pad = cfg2.paddingSize;
var rowStride = pad * 2 + cfg2.cols;
var col = i % rowStride;
var row = (i - col) / rowStride;
return [row - pad, col - pad];
}
function rowColToFieldIndex(cfg2, row, col) {
var pad = cfg2.paddingSize;
var rowStride = pad * 2 + cfg2.cols;
var res = cfg2.apiToFieldIndex[rowStride * (row + pad) + (col + pad)];
return res == null ? -1 : res;
}
function arrayOfValues(len, value) {
var res = [];
for (var i = 0; i < len; ++i) res.push(value);
return res;
}
function arrayEquals(a, b) {
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false;
return true;
}
function indexOfMove(moves, move) {
for (var i = 0; i < moves.length; ++i) {
if (arrayEquals(move, moves[i])) return i;
}
return -1;
}
function randomChoice(array) {
return array[Math.floor(Math.random() * array.length)];
}
// src/formatting.js
function formatRow(row) {
return String(row + 1);
}
function formatCol(col) {
return String.fromCharCode("a".charCodeAt(0) + col);
}
function formatField(cfg2, field) {
var coords = fieldIndexToRowCol(cfg2, field);
return formatCol(coords[1]) + formatRow(coords[0]);
}
function formatMove(cfg2, move) {
if (move.length === 0) return "pass";
var src = move[0];
var cnt = move[1];
var dst = move[2];
var res = "";
if (cnt !== 1) res += String(cnt);
if (src !== -1) res += formatField(cfg2, src);
res += formatField(cfg2, dst);
return res;
}
function formatMoves(cfg2, moves) {
var res = "";
for (var i = 0; i < moves.length; ++i) {
if (i > 0) res += " ";
res += formatMove(cfg2, moves[i]);
}
return res;
}
// src/State.js
var PASS = Object.freeze([]);
function cloneArray(arr) {
return arr.slice();
}
function cloneFields(arr) {
return arr.map(cloneArray);
}
function State(cfg2, fields, nextPlayer, lastMove, piecesLeft, scoresLeft, occupied) {
function getNextPlayer() {
return nextPlayer;
}
function getWinner() {
return scoresLeft.indexOf(0);
}
function doMoveInternal(move) {
var removed = null;
if (move.length !== 0) {
var src = move[0];
var cnt = move[1];
var dst = move[2];
var dstField = fields[dst];
if (src === -1) {
--piecesLeft[nextPlayer];
dstField.push(nextPlayer);
occupied ^= 1 << dst;
} else {
var srcField = fields[src];
dstField.push.apply(dstField, srcField.splice(srcField.length - cnt));
if (srcField.length === 0) {
occupied ^= 1 << src;
}
if (dstField.length >= cfg2.winningHeight) {
removed = dstField.splice(0);
var winner = removed[removed.length - 1];
scoresLeft[winner] -= 1;
occupied ^= 1 << dst;
for (var i = 0; i < removed.length; ++i) ++piecesLeft[removed[i]];
}
}
++nextPlayer;
if (nextPlayer === cfg2.playerCount) nextPlayer = 0;
lastMove = move;
return removed;
}
}
function doMove(move) {
return [lastMove, doMoveInternal(move)];
}
function undoMove(move, undoState) {
if (nextPlayer === 0) nextPlayer = cfg2.playerCount;
--nextPlayer;
lastMove = undoState[0];
if (move.length !== 0) {
var src = move[0];
var cnt = move[1];
var dst = move[2];
var dstField = fields[dst];
if (src === -1) {
++piecesLeft[nextPlayer];
dstField.pop();
occupied ^= 1 << dst;
} else {
var srcField = fields[src];
var removed = undoState[1];
if (removed != null) {
for (var i = 0; i < removed.length; ++i) --piecesLeft[removed[i]];
var winner = removed[removed.length - 1];
scoresLeft[winner] += 1;
dstField.push.apply(dstField, removed);
occupied ^= 1 << dst;
}
if (srcField.length === 0) {
occupied ^= 1 << src;
}
srcField.push.apply(srcField, dstField.splice(dstField.length - cnt));
}
}
}
function evaluate() {
var winner = getWinner();
if (winner !== -1) return winner === nextPlayer ? 1e9 : -1e9;
var score = 1e4 * (scoresLeft[1 - nextPlayer] - scoresLeft[nextPlayer]);
for (var dst = 0; dst < fields.length; ++dst) {
var dstField = fields[dst];
var dstHeight = dstField.length;
if (dstHeight > 0) {
var options = cfg2.moves[dst][dstHeight];
for (var i = 0; i < options.length; ++i) {
var src = options[i][0];
var srcField = fields[src];
var srcHeight = srcField.length;
if (srcHeight + dstHeight >= cfg2.winningHeight && (occupied & options[i][1]) === 0) {
if (srcField[srcHeight - 1] === nextPlayer) {
score += 1e3;
} else {
score -= 100;
}
}
}
if (dstField[dstHeight - 1] === nextPlayer) {
score += 10 * dstHeight;
} else {
score -= 10 * dstHeight;
}
for (var i = 0; i < dstHeight; ++i) {
if (dstField[i] === nextPlayer) {
score += 1 + i;
} else {
score -= 1 + i;
}
}
}
}
return score;
}
function generateMoves() {
if (getWinner() !== -1) return [];
var moveTemplates = cfg2.moves;
var moves = [];
var lastSrc = -1, lastCnt = 0, lastDst = -1;
if (lastMove != null && lastMove.length != 0) {
lastSrc = lastMove[0];
lastCnt = lastMove[1];
lastDst = lastMove[2];
}
for (var dst = 0; dst < fields.length; ++dst) {
var dstHeight = fields[dst].length;
if (dstHeight === 0) {
if (piecesLeft[nextPlayer]) {
moves.push([-1, 1, dst]);
}
} else {
var options = moveTemplates[dst][dstHeight];
for (var i = 0; i < options.length; ++i) {
var src = options[i][0];
var srcHeight = fields[src].length;
if (srcHeight !== 0 && (occupied & options[i][1]) === 0) {
for (var cnt = 1; cnt <= srcHeight; ++cnt) {
if (src == lastDst && cnt === lastCnt && dst == lastSrc) continue;
moves.push([src, cnt, dst]);
}
}
}
}
}
if (moves.length === 0) moves.push(PASS);
return moves;
}
function getMoveWinner(move) {
if (move.length !== 0 && fields[move[2]].length + move[1] >= cfg2.winningHeight) {
var srcField = fields[move[0]];
return srcField[srcField.length - 1];
}
return -1;
}
function triageMoves(moves) {
var winningMoves = [];
var neutralMoves = [];
var losingMoves = [];
for (var i = 0; i < moves.length; ++i) {
var move = moves[i];
var winner = getMoveWinner(move);
if (winner === -1) {
neutralMoves.push(move);
} else if (winner === nextPlayer) {
winningMoves.push(move);
} else {
losingMoves.push(move);
}
}
return [winningMoves, neutralMoves, losingMoves];
}
function playRandomMove() {
var triagedMoves = triageMoves(generateMoves());
for (var i = 0; i < triagedMoves.length; ++i) {
var moves = triagedMoves[i];
if (moves.length > 0) {
doMove(randomChoice(moves));
return;
}
}
throw new Error("No moves available!");
}
function randomPlayout(maxSteps) {
for (var step = 0; step < maxSteps; ++step) {
if (getWinner() !== -1) return step;
playRandomMove();
}
return maxSteps;
}
function debugPrint() {
log("Scores left: " + scoresLeft);
log("Pieces left: " + piecesLeft);
log("Player " + (nextPlayer + 1) + " to move.");
for (var r = 0; r < cfg2.rows; ++r) {
var line = formatRow(r) + " ";
for (var c = 0; c < cfg2.cols; ++c) {
var src = rowColToFieldIndex(cfg2, r, c);
var part = "";
if (src === -1) {
part = "#";
} else if (fields[src].length === 0) {
part = ".";
} else {
for (var i = 0; i < fields[src].length; ++i) {
part += String(fields[src][i] + 1);
}
}
while (part.length < cfg2.winningHeight) part += " ";
line += " " + part;
if (src !== -1 && (occupied & 1 << src) !== 0 != (fields[src].length !== 0)) {
log("INTERNAL ERROR: occupied does not match fields at " + src);
}
}
log(line);
}
var line = " ";
for (var c = 0; c < cfg2.cols; ++c) {
var part = formatCol(c);
while (part.length < cfg2.winningHeight) part += " ";
line += " " + part;
}
log(line);
log("last move: " + (lastMove ? formatMove(cfg2, lastMove) : "none"));
var moves = generateMoves();
log(moves.length + " possible moves: " + formatMoves(cfg2, moves));
}
function toJson() {
return {
fields: fields,
nextPlayer: nextPlayer,
lastMove: lastMove,
scoresLeft: scoresLeft,
piecesLeft: piecesLeft
};
}
function clone(winningScore) {
return State(
cfg2,
cloneFields(fields),
nextPlayer,
lastMove,
cloneArray(piecesLeft),
winningScore == null ? cloneArray(scoresLeft) : arrayOfValues(cfg2.playerCount, winningScore),
occupied
);
}
return {
getNextPlayer: getNextPlayer,
getWinner: getWinner,
generateMoves: generateMoves,
doMove: doMove,
undoMove: undoMove,
evaluate: evaluate,
triageMoves: triageMoves,
randomPlayout: randomPlayout,
debugPrint: debugPrint,
toJson: toJson,
clone: clone
};
}
function createStateFromJson(cfg2, inputJson) {
var fields = inputJson.fields;
var occupied = 0;
for (var i = 0; i < fields.length; ++i) {
if (fields[i].length > 0) occupied |= 1 << i;
}
return State(cfg2, fields, inputJson.nextPlayer, inputJson.lastMove, inputJson.piecesLeft, inputJson.scoresLeft, occupied);
}
// src/cpu-player.js
function convertMoveFromApi(cfg2, apiMove) {
if (apiMove == null) return null;
if (apiMove.length === 0) return apiMove;
var src = apiMove[0];
var cnt = apiMove[1];
var dst = apiMove[2];
if (src !== -1) src = cfg2.apiToFieldIndex[src];
dst = cfg2.apiToFieldIndex[dst];
return [src, cnt, dst];
}
function initCpuWrapper(jsonBoard) {
return createConfig(
game.getBoardDimensionY(),
game.getBoardDimensionX(),
JSON.parse(jsonBoard),
game.getOutside(),
game.getPadding(),
game.getHeightToWin(),
// padding size
game.getHeightToWin(),
game.getNumOfPlayers()
);
}
function callCpuWrapper(findBestMoves2, cfg2, jsonBoard) {
var board = JSON.parse(jsonBoard);
var nextPlayer = game.getID() - 1;
if (!Number.isInteger(nextPlayer) || nextPlayer < 0 || nextPlayer >= cfg2.playerCount) {
throw new Error("Invalid player ID");
}
var apiMoves = JSON.parse(game.getLegalMoves());
if (apiMoves.length === 0) throw new Error("No moves available");
if (apiMoves.length === 1) return apiMoves[0];
var moves = apiMoves.map(convertMoveFromApi.bind(null, cfg2));
var fields = [];
for (var i = 0; i < cfg2.fieldCount; ++i) {
var field = [];
var string = board[cfg2.fieldIndexToApi[i]];
for (var j = 0; j < string.length; ++j) {
var piece = string.charCodeAt(j) - "1".charCodeAt(0);
if (piece < 0 || piece >= cfg2.playerCount) throw new Error("Invalid piece");
field.push(piece);
}
fields.push(field);
}
if (fields.length !== cfg2.fieldCount) throw new Error("Invalid number of fields");
var state = createStateFromJson(cfg2, {
fields: fields,
nextPlayer: nextPlayer,
piecesLeft: game.getNumberOfStones(),
scoresLeft: game.getTowersNeededToWin(),
lastMove: convertMoveFromApi(game.getLastMove())
});
var result = findBestMoves2(cfg2, state, moves);
var bestMoves = result[0];
var bestValue = result[1];
var bestMove = randomChoice(bestMoves);
log(moves.length + " moves, " + bestMoves.length + " best with value " + bestValue + ": " + formatMoves(cfg2, bestMoves) + "; selected " + formatMove(cfg2, bestMove) + " at random");
var moveIndex = indexOfMove(moves, bestMove);
if (moveIndex < 0) throw new Error("Best move not found somehow?!");
return apiMoves[moveIndex];
}
// src/MinimaxCPU.js
var cfg = null;
function initCPU(jsonBoard) {
if (game.getNumOfPlayers() !== 2) {
throw new Error("Unsupported number of players!");
}
cfg = initCpuWrapper(jsonBoard);
}
function callCPU(jsonBoard) {
if (cfg == null) throw new Error("initCPU() has not been called!");
return callCpuWrapper(findBestMoves, cfg, jsonBoard);
}
/**
* \file MonteCarloCPU.js
*
* \section LICENSE
*
* Copyright (C) 2024 Maks Verver
*
* This file is part of StackAndConquer.
*
* StackAndConquer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* StackAndConquer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with StackAndConquer. If not, see <https://www.gnu.org/licenses/>.
*
* \section DESCRIPTION
*
* CPU opponent using Monte Carlo simulations.
*
* This file was auto-generated from multiple source files. The original source
* code is available here: https://github.com/maksverver/stackandconquer-ai/
*/
'use strict';
// src/util.js
var log = typeof game === "object" ? game.log : console.log;
function createConfig(rows, cols, inputFields, outside, padding, paddingSize, winningHeight, playerCount) {
var paddedRows = rows + 2 * paddingSize;
var paddedCols = cols + 2 * paddingSize;
if (inputFields.length !== paddedRows * paddedCols) {
throw new Error("Invalid length of input fields");
}
var apiToFieldIndex = [];
var fieldIndexToApi = [];
var fieldCount = 0;
for (var r1 = 0; r1 < paddedRows; ++r1) {
for (var c1 = 0; c1 < paddedCols; ++c1) {
var i = paddedCols * r1 + c1;
if (inputFields[i] === outside || inputFields[i] === padding) {
apiToFieldIndex.push(-1);
} else {
apiToFieldIndex.push(fieldCount++);
fieldIndexToApi.push(i);
}
}
}
if (fieldCount > 30) {
throw new Error("Too many fields (maximum supported: 30)");
}
var moves = [];
var DR = [-1, -1, -1, 0, 0, 1, 1, 1];
var DC = [-1, 0, 1, -1, 1, -1, 0, 1];
for (var r2 = 0; r2 < paddedRows; ++r2) {
for (var c2 = 0; c2 < paddedCols; ++c2) {
var j = paddedCols * r2 + c2;
if (inputFields[j] === outside || inputFields[j] === padding) continue;
var dst = moves.length;
moves.push([]);
for (var height = 0; height < winningHeight; ++height) {
moves[dst].push([]);
}
for (var dir = 0; dir < 8; ++dir) {
var dr = DR[dir], dc = DC[dir];
var mask = 0;
for (var height = 1; height < winningHeight; ++height) {
var r1 = r2 - dr * height;
var c1 = c2 - dc * height;
if (r1 < 0 || r1 >= paddedRows || c1 < 0 || c1 >= paddedCols) break;
var i = paddedCols * r1 + c1;
if (inputFields[i] === outside || inputFields[i] === padding) break;
var src = apiToFieldIndex[i];
moves[dst][height].push([src, mask]);
mask |= 1 << src;
}
}
}
}
return Object.freeze({
apiToFieldIndex: Object.freeze(apiToFieldIndex),
fieldIndexToApi: Object.freeze(fieldIndexToApi),
fieldCount: fieldCount,
moves: Object.freeze(moves),
winningHeight: winningHeight,
playerCount: playerCount,
// These are only used for move parsing/formatting and debug printing:
rows: rows,
cols: cols,
paddingSize: paddingSize
});
}
function fieldIndexToRowCol(cfg2, index) {
var i = cfg2.fieldIndexToApi[index];
var pad = cfg2.paddingSize;
var rowStride = pad * 2 + cfg2.cols;
var col = i % rowStride;
var row = (i - col) / rowStride;
return [row - pad, col - pad];
}
function rowColToFieldIndex(cfg2, row, col) {
var pad = cfg2.paddingSize;
var rowStride = pad * 2 + cfg2.cols;
var res = cfg2.apiToFieldIndex[rowStride * (row + pad) + (col + pad)];
return res == null ? -1 : res;
}
function arrayOfValues(len, value) {
var res = [];
for (var i = 0; i < len; ++i) res.push(value);
return res;
}
function arrayEquals(a, b) {
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false;
return true;
}
function indexOfMove(moves, move) {
for (var i = 0; i < moves.length; ++i) {
if (arrayEquals(move, moves[i])) return i;
}
return -1;
}
function randomChoice(array) {
return array[Math.floor(Math.random() * array.length)];
}
// src/montecarlo.js
var TOTAL_SIMULATIONS = 4e3;
var MIN_SIMULATIONS_PER_MOVE = 10;
var MAX_STEPS_TO_SIMULATE = 250;
function evaluateWithRandomPlayouts(player, initialState, simulations) {
var value = 0;
var errorPrinted = false;
for (var n = 0; n < simulations; ++n) {
var state = initialState.clone(1);
var steps = state.randomPlayout(MAX_STEPS_TO_SIMULATE);
if (steps >= MAX_STEPS_TO_SIMULATE && !errorPrinted) {
log("WARNING: maxSteps exceeded!");
errorPrinted = true;
}
var winner = state.getWinner();
value += winner === player ? 1 : winner === -1 ? 0.5 : 0;
}
return value / simulations;
}
function findBestMoves(cfg2, state, moves) {
var triagedMoves = state.triageMoves(moves);
if (triagedMoves[0].length > 0) {
return [triagedMoves[0], 1];
}
var neutralMoves = triagedMoves[1];
if (neutralMoves.length === 0) {
return [triagedMoves[2], 0];
}
var player = state.getNextPlayer();
var simulationsPerMove = Math.max(MIN_SIMULATIONS_PER_MOVE, Math.floor(TOTAL_SIMULATIONS / neutralMoves.length));
var bestMoves = [];
var bestValue = 0;
for (var i = 0; i < neutralMoves.length; ++i) {
var move = neutralMoves[i];
var undoState = state.doMove(move);
var value = evaluateWithRandomPlayouts(player, state, simulationsPerMove);
state.undoMove(move, undoState);
if (value > bestValue) {
bestValue = value;
bestMoves.length = 0;
}
if (value === bestValue) {
bestMoves.push(move);
}
}
return [bestMoves, bestValue];
}
// src/formatting.js
function formatRow(row) {
return String(row + 1);
}
function formatCol(col) {
return String.fromCharCode("a".charCodeAt(0) + col);
}
function formatField(cfg2, field) {
var coords = fieldIndexToRowCol(cfg2, field);
return formatCol(coords[1]) + formatRow(coords[0]);
}
function formatMove(cfg2, move) {
if (move.length === 0) return "pass";
var src = move[0];
var cnt = move[1];
var dst = move[2];
var res = "";
if (cnt !== 1) res += String(cnt);
if (src !== -1) res += formatField(cfg2, src);
res += formatField(cfg2, dst);
return res;
}
function formatMoves(cfg2, moves) {
var res = "";
for (var i = 0; i < moves.length; ++i) {
if (i > 0) res += " ";
res += formatMove(cfg2, moves[i]);
}
return res;
}
// src/State.js
var PASS = Object.freeze([]);
function cloneArray(arr) {
return arr.slice();
}
function cloneFields(arr) {
return arr.map(cloneArray);
}
function State(cfg2, fields, nextPlayer, lastMove, piecesLeft, scoresLeft, occupied) {
function getNextPlayer() {
return nextPlayer;
}
function getWinner() {
return scoresLeft.indexOf(0);
}
function doMoveInternal(move) {
var removed = null;
if (move.length !== 0) {
var src = move[0];
var cnt = move[1];
var dst = move[2];
var dstField = fields[dst];
if (src === -1) {
--piecesLeft[nextPlayer];
dstField.push(nextPlayer);
occupied ^= 1 << dst;
} else {
var srcField = fields[src];
dstField.push.apply(dstField, srcField.splice(srcField.length - cnt));
if (srcField.length === 0) {
occupied ^= 1 << src;
}
if (dstField.length >= cfg2.winningHeight) {
removed = dstField.splice(0);
var winner = removed[removed.length - 1];
scoresLeft[winner] -= 1;
occupied ^= 1 << dst;
for (var i = 0; i < removed.length; ++i) ++piecesLeft[removed[i]];
}
}
++nextPlayer;
if (nextPlayer === cfg2.playerCount) nextPlayer = 0;
lastMove = move;
return removed;
}
}
function doMove(move) {
return [lastMove, doMoveInternal(move)];
}
function undoMove(move, undoState) {
if (nextPlayer === 0) nextPlayer = cfg2.playerCount;
--nextPlayer;
lastMove = undoState[0];
if (move.length !== 0) {
var src = move[0];
var cnt = move[1];
var dst = move[2];
var dstField = fields[dst];
if (src === -1) {
++piecesLeft[nextPlayer];
dstField.pop();
occupied ^= 1 << dst;
} else {
var srcField = fields[src];
var removed = undoState[1];
if (removed != null) {
for (var i = 0; i < removed.length; ++i) --piecesLeft[removed[i]];
var winner = removed[removed.length - 1];
scoresLeft[winner] += 1;
dstField.push.apply(dstField, removed);
occupied ^= 1 << dst;
}
if (srcField.length === 0) {
occupied ^= 1 << src;
}
srcField.push.apply(srcField, dstField.splice(dstField.length - cnt));
}
}
}
function evaluate() {
var winner = getWinner();
if (winner !== -1) return winner === nextPlayer ? 1e9 : -1e9;
var score = 1e4 * (scoresLeft[1 - nextPlayer] - scoresLeft[nextPlayer]);
for (var dst = 0; dst < fields.length; ++dst) {
var dstField = fields[dst];
var dstHeight = dstField.length;
if (dstHeight > 0) {
var options = cfg2.moves[dst][dstHeight];
for (var i = 0; i < options.length; ++i) {
var src = options[i][0];
var srcField = fields[src];
var srcHeight = srcField.length;
if (srcHeight + dstHeight >= cfg2.winningHeight && (occupied & options[i][1]) === 0) {
if (srcField[srcHeight - 1] === nextPlayer) {
score += 1e3;
} else {
score -= 100;
}
}
}
if (dstField[dstHeight - 1] === nextPlayer) {
score += 10 * dstHeight;
} else {
score -= 10 * dstHeight;
}
for (var i = 0; i < dstHeight; ++i) {
if (dstField[i] === nextPlayer) {
score += 1 + i;
} else {
score -= 1 + i;
}
}
}
}
return score;
}
function generateMoves() {
if (getWinner() !== -1) return [];
var moveTemplates = cfg2.moves;
var moves = [];
var lastSrc = -1, lastCnt = 0, lastDst = -1;
if (lastMove != null && lastMove.length != 0) {
lastSrc = lastMove[0];
lastCnt = lastMove[1];
lastDst = lastMove[2];
}
for (var dst = 0; dst < fields.length; ++dst) {
var dstHeight = fields[dst].length;
if (dstHeight === 0) {
if (piecesLeft[nextPlayer]) {
moves.push([-1, 1, dst]);
}
} else {
var options = moveTemplates[dst][dstHeight];
for (var i = 0; i < options.length; ++i) {
var src = options[i][0];
var srcHeight = fields[src].length;
if (srcHeight !== 0 && (occupied & options[i][1]) === 0) {
for (var cnt = 1; cnt <= srcHeight; ++cnt) {
if (src == lastDst && cnt === lastCnt && dst == lastSrc) continue;
moves.push([src, cnt, dst]);
}
}
}
}
}
if (moves.length === 0) moves.push(PASS);
return moves;
}
function getMoveWinner(move) {
if (move.length !== 0 && fields[move[2]].length + move[1] >= cfg2.winningHeight) {
var srcField = fields[move[0]];
return srcField[srcField.length - 1];
}
return -1;
}
function triageMoves(moves) {
var winningMoves = [];
var neutralMoves = [];
var losingMoves = [];
for (var i = 0; i < moves.length; ++i) {
var move = moves[i];
var winner = getMoveWinner(move);
if (winner === -1) {
neutralMoves.push(move);
} else if (winner === nextPlayer) {
winningMoves.push(move);
} else {
losingMoves.push(move);
}
}
return [winningMoves, neutralMoves, losingMoves];
}
function playRandomMove() {
var triagedMoves = triageMoves(generateMoves());
for (var i = 0; i < triagedMoves.length; ++i) {
var moves = triagedMoves[i];
if (moves.length > 0) {
doMove(randomChoice(moves));
return;
}
}
throw new Error("No moves available!");
}
function randomPlayout(maxSteps) {
for (var step = 0; step < maxSteps; ++step) {
if (getWinner() !== -1) return step;
playRandomMove();
}
return maxSteps;
}
function debugPrint() {
log("Scores left: " + scoresLeft);
log("Pieces left: " + piecesLeft);
log("Player " + (nextPlayer + 1) + " to move.");
for (var r = 0; r < cfg2.rows; ++r) {
var line = formatRow(r) + " ";
for (var c = 0; c < cfg2.cols; ++c) {
var src = rowColToFieldIndex(cfg2, r, c);
var part = "";
if (src === -1) {
part = "#";
} else if (fields[src].length === 0) {
part = ".";
} else {
for (var i = 0; i < fields[src].length; ++i) {
part += String(fields[src][i] + 1);
}
}
while (part.length < cfg2.winningHeight) part += " ";
line += " " + part;
if (src !== -1 && (occupied & 1 << src) !== 0 != (fields[src].length !== 0)) {
log("INTERNAL ERROR: occupied does not match fields at " + src);
}
}
log(line);
}
var line = " ";
for (var c = 0; c < cfg2.cols; ++c) {
var part = formatCol(c);
while (part.length < cfg2.winningHeight) part += " ";
line += " " + part;
}
log(line);
log("last move: " + (lastMove ? formatMove(cfg2, lastMove) : "none"));
var moves = generateMoves();
log(moves.length + " possible moves: " + formatMoves(cfg2, moves));
}
function toJson() {
return {
fields: fields,
nextPlayer: nextPlayer,
lastMove: lastMove,
scoresLeft: scoresLeft,
piecesLeft: piecesLeft
};
}
function clone(winningScore) {
return State(
cfg2,
cloneFields(fields),
nextPlayer,
lastMove,
cloneArray(piecesLeft),
winningScore == null ? cloneArray(scoresLeft) : arrayOfValues(cfg2.playerCount, winningScore),
occupied
);
}
return {
getNextPlayer: getNextPlayer,
getWinner: getWinner,
generateMoves: generateMoves,
doMove: doMove,
undoMove: undoMove,
evaluate: evaluate,
triageMoves: triageMoves,
randomPlayout: randomPlayout,
debugPrint: debugPrint,
toJson: toJson,
clone: clone
};
}
function createStateFromJson(cfg2, inputJson) {
var fields = inputJson.fields;
var occupied = 0;
for (var i = 0; i < fields.length; ++i) {
if (fields[i].length > 0) occupied |= 1 << i;
}
return State(cfg2, fields, inputJson.nextPlayer, inputJson.lastMove, inputJson.piecesLeft, inputJson.scoresLeft, occupied);
}
// src/cpu-player.js
function convertMoveFromApi(cfg2, apiMove) {
if (apiMove == null) return null;
if (apiMove.length === 0) return apiMove;
var src = apiMove[0];
var cnt = apiMove[1];
var dst = apiMove[2];
if (src !== -1) src = cfg2.apiToFieldIndex[src];
dst = cfg2.apiToFieldIndex[dst];
return [src, cnt, dst];
}
function initCpuWrapper(jsonBoard) {
return createConfig(
game.getBoardDimensionY(),
game.getBoardDimensionX(),
JSON.parse(jsonBoard),
game.getOutside(),
game.getPadding(),
game.getHeightToWin(),
// padding size
game.getHeightToWin(),
game.getNumOfPlayers()
);
}
function callCpuWrapper(findBestMoves2, cfg2, jsonBoard) {
var board = JSON.parse(jsonBoard);
var nextPlayer = game.getID() - 1;
if (!Number.isInteger(nextPlayer) || nextPlayer < 0 || nextPlayer >= cfg2.playerCount) {
throw new Error("Invalid player ID");
}
var apiMoves = JSON.parse(game.getLegalMoves());
if (apiMoves.length === 0) throw new Error("No moves available");
if (apiMoves.length === 1) return apiMoves[0];
var moves = apiMoves.map(convertMoveFromApi.bind(null, cfg2));
var fields = [];
for (var i = 0; i < cfg2.fieldCount; ++i) {
var field = [];
var string = board[cfg2.fieldIndexToApi[i]];
for (var j = 0; j < string.length; ++j) {
var piece = string.charCodeAt(j) - "1".charCodeAt(0);
if (piece < 0 || piece >= cfg2.playerCount) throw new Error("Invalid piece");
field.push(piece);
}
fields.push(field);
}
if (fields.length !== cfg2.fieldCount) throw new Error("Invalid number of fields");
var state = createStateFromJson(cfg2, {
fields: fields,
nextPlayer: nextPlayer,
piecesLeft: game.getNumberOfStones(),
scoresLeft: game.getTowersNeededToWin(),
lastMove: convertMoveFromApi(game.getLastMove())
});
var result = findBestMoves2(cfg2, state, moves);
var bestMoves = result[0];
var bestValue = result[1];
var bestMove = randomChoice(bestMoves);
log(moves.length + " moves, " + bestMoves.length + " best with value " + bestValue + ": " + formatMoves(cfg2, bestMoves) + "; selected " + formatMove(cfg2, bestMove) + " at random");
var moveIndex = indexOfMove(moves, bestMove);
if (moveIndex < 0) throw new Error("Best move not found somehow?!");
return apiMoves[moveIndex];
}
// src/MonteCarloCPU.js
var cfg = null;
function initCPU(jsonBoard) {
cfg = initCpuWrapper(jsonBoard);
}
function callCPU(jsonBoard) {
if (cfg == null) throw new Error("initCPU() has not been called!");
return callCpuWrapper(findBestMoves, cfg, jsonBoard);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment