Skip to content

Instantly share code, notes, and snippets.

@ggorlen
Last active January 14, 2025 15:23
Show Gist options
  • Select an option

  • Save ggorlen/effe8d2878f0ab36e1dbabb542db66b2 to your computer and use it in GitHub Desktop.

Select an option

Save ggorlen/effe8d2878f0ab36e1dbabb542db66b2 to your computer and use it in GitHub Desktop.
tic tac toe with the monte carlo method in js
class TicTacToe {
constructor() {
this.x = 0; // 0b0xxx0xxx0xxx
this.o = 0; // 0b0ooo0ooo0ooo
this.str = new Array(9).fill(0).map((e, i) => i);
this.ply = 0;
}
copy() {
const ttt = new TicTacToe();
ttt.x = this.x;
ttt.o = this.o;
//ttt.str = this.str;
ttt.ply = this.ply;
return ttt;
}
randomPlayout() {
while (this.win() === 0 && !this.draw()) {
const moves = this.moves();
const idx = ~~(Math.random() * moves.length);
this.move(moves[idx]);
}
}
bestMoveMonteCarlo(simulations=1000) {
let bestScore = -Infinity;
const moves = this.moves();
if (moves.length === 0) {
throw Error("no moves to make");
}
let bestMove = moves[0];
for (const move of moves) {
let score = 0;
for (let i = 0; i < simulations; i++) {
const cpy = this.copy();
cpy.move(move);
cpy.randomPlayout();
if (cpy.win()) {
// scoring from beej: https://github.com/pberry/Monte-Carlo-Tic-Tac-Toe/blob/master/mcttt.c#L24
score += (this.ply & 1) === (cpy.ply & 1) ? -10 : 1;
}
}
//console.log(move, score)
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
return bestMove;
}
computerMove() {
this.move(this.bestMoveMonteCarlo());
}
draw() {
return this.ply > 8 && !this.win();
}
win() {
if (this.ply > 4 && this.testWin(this.x) ||
this.testWin(this.o)) {
return this.ply & 1 ? this.ply : -this.ply;
}
return 0;
}
testWin(board) {
return ( // any column
(board & (board >> 4) & (board >> 8) & 0x7 ||
((board + 0x111) & 0x888) || // any row
(board & 0x421) === 0x421 || // / diagonal
(board & 0x124) === 0x124) // \ diagonal
) > 0;
}
moves() {
return [...Array(9)].map((_, i) => i)
.filter(e => {
const dest = this.squareToBitLoc(e);
return this.legalMove(this.x, dest) &&
this.legalMove(this.o, dest)
;
})
;
}
squareToBitLoc(square) {
return ~~(square / 3) + square;
}
legalMove(board, square) {
return ((board >> square) & 1) !== 1;
}
move(square) {
const dest = this.squareToBitLoc(square);
if (!this.win() && !this.draw() &&
this.legalMove(this.x, dest) &&
this.legalMove(this.o, dest)) {
const mark = this.ply++ & 1 ? "o" : "x";
this.str[square] = mark;
this[mark] |= 1 << dest;
return true;
}
return false;
}
toString() {
return this.str.join("");
}
}
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const input = msg => new Promise(r => rl.question(msg, r));
(async () => {
const ttt = new TicTacToe();
while (ttt.win() === 0 && !ttt.draw()) {
console.log(ttt.toString().slice(0, 3));
console.log(ttt.toString().slice(3, 6));
console.log(ttt.toString().slice(6));
const move = +(await input("move? (0-9)"));
if (!ttt.move(move)) {
console.log("bad move");
continue;
}
else if (ttt.win() || ttt.draw()) {
break;
}
ttt.computerMove();
}
console.log(ttt.toString().slice(0, 3));
console.log(ttt.toString().slice(3, 6));
console.log(ttt.toString().slice(6));
rl.close();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment