elo-worldle/main.js
2023-03-29 22:15:25 -04:00

795 lines
26 KiB
JavaScript

import { Chess, Ox88, DEFAULT_POSITION, SQUARES, rank, file, WHITE, BLACK, KING, } from "./chess.js";
function with_move(board, move) {
let new_board = new Chess(board.fen());
new_board.move(move);
return new_board;
}
// function max_move_by(board: Chess, score: (_: Chess) => number): string {
// let best_score = 0;
// let best_move = "";
// for (const move in board.moves()) {
// let new_board = with_move(board, move);
// let new_score = score(new_board);
// if (
// new_score > best_score ||
// (new_score == new_score && Math.random() > 0.5)
// ) {
// best_score = new_score;
// best_move = move;
// }
// }
// return best_move;
// }
// classic: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function shuffle(array) {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
}
// could return early if zero but meh
function min_move_by(board, score) {
let best_score = Infinity;
let best_move = "";
let moves = board.moves();
shuffle(moves);
// console.log(moves);
for (const move of moves) {
try {
let new_board = with_move(board, move);
let new_score = score(new_board);
// console.log({ move, best_score, new_score, best_move });
if (new_score < best_score
// array is already shuffled
// (new_score == best_score && Math.random() > 0.5)
) {
console.log("New best (or equal): ", new_score, move);
best_score = new_score;
best_move = move;
}
}
catch (e) {
console.error(e);
continue;
}
}
return best_move;
}
// D --> movelist users (bad name whatever)
const random_move = board => {
const moves = board.moves();
const move = moves[Math.floor(Math.random() * moves.length)];
return move;
};
function make_lexo(m) {
let { rank: src_row, file: src_col } = rank_and_file(m.from);
let { rank: dst_row, file: dst_col } = rank_and_file(m.to);
let promote_to = m.promotion;
return [src_row, src_col, dst_row, dst_col, promote_to];
}
function lexo_sort_moves(a, b) {
let al = make_lexo(a);
let bl = make_lexo(a);
for (const i in al) {
if (al[i] < bl[i]) {
return -1;
}
else if (al[i] > bl[i]) {
return 1;
}
}
return 0;
}
const first_move = board => {
const moves = board.moves({ verbose: true }).sort(lexo_sort_moves);
return moves[0].san;
};
const alphabetical = board => {
const moves = board.moves();
moves.sort();
return moves[0];
};
// i believe the actual one tracks things better (pawns are treated as different)
// yeah this version sucks butttttt uh
// yeah
// prettier-ignore
const equalizer_state_default = {
piece_moves: {
p: 0,
b: 0,
n: 0,
r: 0,
q: 0,
k: 0,
},
// bad
square_visits: {
a8: 0, b8: 0, c8: 0, d8: 0, e8: 0, f8: 0, g8: 0, h8: 0,
a7: 0, b7: 0, c7: 0, d7: 0, e7: 0, f7: 0, g7: 0, h7: 0,
a6: 0, b6: 0, c6: 0, d6: 0, e6: 0, f6: 0, g6: 0, h6: 0,
a5: 0, b5: 0, c5: 0, d5: 0, e5: 0, f5: 0, g5: 0, h5: 0,
a4: 0, b4: 0, c4: 0, d4: 0, e4: 0, f4: 0, g4: 0, h4: 0,
a3: 0, b3: 0, c3: 0, d3: 0, e3: 0, f3: 0, g3: 0, h3: 0,
a2: 0, b2: 0, c2: 0, d2: 0, e2: 0, f2: 0, g2: 0, h2: 0,
a1: 0, b1: 0, c1: 0, d1: 0, e1: 0, f1: 0, g1: 0, h1: 0
}
};
var equalizer_state = equalizer_state_default;
const equalizer = board => {
let best_move;
let least_moved_found = Infinity;
let least_visited_found = Infinity;
let piece_moving = "k";
let square_visiting = "a1";
// let least_moved = PIECE_SYMBOLS.sort(
// (a, b) =>
// equalizer_state.piece_moves[a] - equalizer_state.piece_moves[b]
// )[0];
// let least_visited = SQUARES.sort(
// (a, b) =>
// equalizer_state.square_visits[a] - equalizer_state.square_visits[b]
// )[0];
// cbb to do the proper randomness (23:41)
for (const move of board.moves({ verbose: true })) {
if (equalizer_state.piece_moves[move.piece] < least_moved_found ||
(equalizer_state.piece_moves[move.piece] == least_moved_found &&
equalizer_state.square_visits[move.to] < least_visited_found)) {
best_move = move;
least_moved_found = equalizer_state.piece_moves[move.piece];
least_visited_found = equalizer_state.square_visits[move.to];
piece_moving = move.piece;
square_visiting = move.to;
}
}
equalizer_state.piece_moves[piece_moving] += 1;
equalizer_state.square_visits[square_visiting] += 1;
return best_move.san;
};
// D --> min-maxxing some score of the board after moving
const min_oppt_move = board => {
let num_moves = b => b.moves().length;
return min_move_by(board, num_moves);
};
function num_pieces_on_own_color(b) {
let count = 0;
for (const square of SQUARES) {
let piece = b.get(square);
if (typeof piece == "boolean") {
continue;
}
if (piece.color == b.squareColor(square)) {
count += 1;
}
}
return count;
}
const same_color = board => {
return min_move_by(board, b => 64 - num_pieces_on_own_color(b));
};
const opposite_color = board => {
return min_move_by(board, num_pieces_on_own_color);
};
function rank_and_file(s) {
return { rank: rank(Ox88[s]), file: file(Ox88[s]) };
}
function chebyshev_distance(a, b) {
let { rank: r1, file: f1 } = rank_and_file(a);
let { rank: r2, file: f2 } = rank_and_file(b);
// forgot the abs UGH
return Math.max(Math.abs(r2 - r1), Math.abs(f2 - f1));
}
const START_BOARD = new Chess(DEFAULT_POSITION);
// good enough
function move_distance(piece) {
let rank = rank_and_file(piece.square).rank;
let file = rank_and_file(piece.square).file;
switch (piece.type) {
case "k":
return chebyshev_distance(piece.square, find({ color: HUMAN_COLOR, type: "k" }, START_BOARD));
case "p":
return Math.abs(rank - 6);
case "r":
return (Math.abs(rank - 7) +
Math.min(Math.abs(file - 7), Math.abs(file - 0)));
case "n":
// inauthentic
return pieces_of_color(START_BOARD, HUMAN_COLOR)
.filter(p => p.type == "k")
.map(p => chebyshev_distance(piece.square, p.square))
.reduce((a, b) => Math.max(a, b));
case "b":
let is_black = START_BOARD.squareColor(piece.square) == "b";
let dest_col = is_black ? 2 : 5;
let dr = Math.abs(rank - 7);
let dc = Math.abs(file - dest_col);
return Math.max(dr, dc);
case "q":
return Math.max(Math.abs(rank - 7), Math.abs(file - 3));
}
}
// oh gosh i can do symmetry by making the "distance" just a score fn which
// would... work but dang so many things have done something similar in hindsight
// actually hm symmetry is weird
function minimize_distances(distance_fn) {
const player_score = (board) => {
let total_distance = 0;
for (const piece of pieces_of_color(board, ROBOT_COLOR)) {
total_distance += distance_fn(piece);
}
return total_distance;
};
return b => min_move_by(b, player_score);
}
const reverse_starting = minimize_distances(move_distance);
// or lens :]
function mirror_player(mirror_fn) {
let player_score = (b) => {
// it's worrying how the strategies have shifted, before it was more
// common for them to weigh your pieces equally, meaning they might take
// them to make the score better; silver lining: a bit easier to tell
// apart potentially
// the penalties are taken from the og code (the last four are the ones
// where i finally figured out where to find the players (curse_you
// UpperCamelCase))
// the behavior is likely still slightly different
let total = 0;
for (const square of SQUARES) {
let reflected = mirror_fn(square);
console.log(square, reflected);
let piece = b.get(square);
let reflection = b.get(reflected);
if (typeof piece == "boolean" && typeof reflection == "boolean") {
continue;
}
if (typeof piece == "boolean") {
total += 10;
continue;
}
if (typeof reflection == "boolean") {
total += 10;
continue;
}
if (piece.color == reflection.color) {
total += 5;
continue;
}
if (piece.type != reflection.type) {
total += 1;
continue;
}
}
return total;
};
return b => min_move_by(b, player_score);
}
function rank_and_file_to_algebraic(rf) {
let r = "8";
let f = "h";
// vim's <C-a> was very helpful here :]
// I WAS MIRRORING AND THEN MIRRORING AGAIN OH MY GOOOOOSH
// prettier-ignore
switch (rf.rank) {
case 0:
r = "8";
break;
case 1:
r = "7";
break;
case 2:
r = "6";
break;
case 3:
r = "5";
break;
case 4:
r = "4";
break;
case 5:
r = "3";
break;
case 6:
r = "7";
break;
case 7:
r = "1";
break;
}
// prettier-ignore
switch (rf.file) {
case 0:
f = "a";
break;
case 1:
f = "b";
break;
case 2:
f = "c";
break;
case 3:
f = "d";
break;
case 4:
f = "e";
break;
case 5:
f = "f";
break;
case 6:
f = "g";
break;
case 7:
f = "h";
break;
}
return `${f}${r}`;
}
function mirror_y(s) {
let rf = rank_and_file(s);
rf.rank = 7 - rf.rank;
return rank_and_file_to_algebraic(rf);
}
const sym_mirror_y = mirror_player(mirror_y);
function mirror_x(s) {
let rf = rank_and_file(s);
rf.file = 7 - rf.file;
return rank_and_file_to_algebraic(rf);
}
const sym_mirror_x = mirror_player(mirror_x);
function flip(s) {
let rf = rank_and_file(s);
rf.rank = 7 - rf.rank;
rf.file = 7 - rf.file;
return rank_and_file_to_algebraic(rf);
}
const sym_180 = mirror_player(flip);
function find(p, b) {
for (const square of SQUARES) {
let got = b.get(square);
if (typeof got == "boolean") {
continue;
}
if (got.color == p.color && got.type == p.type) {
return square;
}
}
console.log(`didn't find this :O ${p}`);
return null;
}
// maybe a find function would be useful Piece -> Board -> Square | null
const suicide_king = board => {
let distance_between_kings = (b) => {
var _a, _b;
return chebyshev_distance((_a = find({ color: WHITE, type: KING }, b)) !== null && _a !== void 0 ? _a : "a1", (_b = find({ color: BLACK, type: KING }, b)) !== null && _b !== void 0 ? _b : "a1");
};
return min_move_by(board, distance_between_kings);
};
// cccp and pacifist are basically opposites
// ugh not really
function pieces_of_color(b, color) {
let flat_board = b.board().flat();
let non_empties = [];
for (const s of flat_board) {
if (typeof s == "boolean") {
continue;
}
non_empties.push(s);
}
return non_empties.filter(p => p.color == color);
}
function num_pieces_of_color(b, color) {
return pieces_of_color(b, color).length;
}
// see generous in http://tom7.org/chess/weak.pdf
function value_piece(type) {
switch (type) {
case "p":
return 1;
case "b":
case "n":
return 3;
case "r":
return 5;
case "q":
return 9;
case "k":
// impossible (mostly)
// idk why i said this it's really not impossible lol
return 413;
}
}
function value_of_pieces_of_color(b, color) {
return pieces_of_color(b, color)
.map(p => value_piece(p.type))
.reduce((a, b) => a + b);
}
// hm
const HUMAN_COLOR = WHITE;
const ROBOT_COLOR = BLACK;
// DONE: I THINK: UGH piece value
const cccp = board => {
let cccp_score = (b) => {
if (b.isCheckmate()) {
console.log("Found checkmate! :D");
return 0;
}
if (b.isCheck()) {
console.log("Found check! :T");
return 1;
}
// maximize the difference in value
let prev_value = value_of_pieces_of_color(board, HUMAN_COLOR);
let new_value = value_of_pieces_of_color(b, HUMAN_COLOR);
if (new_value < prev_value) {
console.log("There's less human-colored pieces now! :)");
return 10 * (6 - (prev_value - new_value));
}
// sum the ranks of the robot pieces (brittle with piece colors changing which uhh just don't do that :))
// they are zero based oopsie
return (100 *
pieces_of_color(b, ROBOT_COLOR)
.map(b => 8 - rank_and_file(b.square).rank)
.reduce((a, b) => a + b));
};
return min_move_by(board, cccp_score);
};
// FIXME: function name is TOO short FIX
function total_chebyshev_distances_of_pieces_of_color_to_square(board, color, square) {
return pieces_of_color(board, color)
.map(p => chebyshev_distance(p.square, square))
.reduce((a, b) => a + b);
}
const huddle = board => {
return min_move_by(board, b => {
var _a;
return total_chebyshev_distances_of_pieces_of_color_to_square(b, ROBOT_COLOR, (_a = find({ color: ROBOT_COLOR, type: KING }, b)) !== null && _a !== void 0 ? _a : "a1");
});
};
const swarm = board => {
return min_move_by(board, b => {
var _a;
return total_chebyshev_distances_of_pieces_of_color_to_square(b, ROBOT_COLOR, (_a = find({ color: HUMAN_COLOR, type: KING }, b)) !== null && _a !== void 0 ? _a : "a1");
});
};
function sum_of_possible_capture_value(b) {
let prev_value = value_of_pieces_of_color(b, ROBOT_COLOR);
let sum = 0;
for (const move of b.moves()) {
let new_board = with_move(b, move);
let new_value = value_of_pieces_of_color(new_board, ROBOT_COLOR);
// value only goes down..... pretty much
// but the negation is now done by the guys so
sum += prev_value - new_value;
}
return sum;
}
// surprisingly not too difficult
const generous = board => {
// sum of the difference in ROBOT_COLOR piece value of all moves
// but like uhh negative
let generous_score = (b) => {
return -sum_of_possible_capture_value(b);
};
return min_move_by(board, generous_score);
};
const coward = board => min_move_by(board, sum_of_possible_capture_value);
function no_i_insist_score(b) {
if (b.isCheckmate()) {
return 612;
}
if (b.isCheck()) {
return 413;
}
let prev_value = value_of_pieces_of_color(b, ROBOT_COLOR);
return -(sum_of_possible_capture_value(b) / b.moves().length);
}
const no_i_insist = board => {
return min_move_by(board, no_i_insist_score);
};
const pacifist = board => {
let pacifist_score = b => {
if (b.isCheckmate()) {
return 612;
}
if (b.isCheck()) {
return 413;
}
let prev_value = value_of_pieces_of_color(board, HUMAN_COLOR);
let new_value = value_of_pieces_of_color(b, HUMAN_COLOR);
if (new_value < prev_value) {
return prev_value - new_value;
}
return 0;
};
return min_move_by(board, pacifist_score);
};
const players = [
{
f: random_move,
name: "random_move",
description: "Moves randomly, believe it or not.",
},
{
f: same_color,
name: "same_color",
description: "Makes the move that maximizes the number of pieces on their own color",
},
{
f: opposite_color,
name: "opposite_color",
description: "Behavior left as an exercise to the reader",
},
{
f: pacifist,
name: "pacifist",
description: "Avoids checkmate, then avoids checks, then avoids captures, if forced, captures the lowest value piece it can",
},
{
f: first_move,
name: "first_move",
description: "Makes the lexicographically first move based on [source_row, source_col, dest_row, dest_col, promotion]",
},
{
f: alphabetical,
name: "alphabetical",
description: "Makes the first move asciilphabetically (based on algebraic notation)",
},
{
f: huddle,
name: "huddle",
description: "Makes the move that minimizes the total Chebyshev distances from their pieces to their king",
},
{
f: swarm,
name: "swarm",
description: "Makes the move that minimizes the total Chebyshev distances from their pieces to YOUR king",
},
{
f: generous,
name: "generous",
description: "Maximizes the number of YOUR moves that take their pieces, weighting by value",
},
{
f: no_i_insist,
name: "no_i_insist",
description: "Maximizes the proportion of potential capture value of YOUR moves to the number of YOUR moves; very weird the way i've done it",
},
{
f: coward,
name: "coward",
description: "Surprisingly uncowardly, tries to avoid losing pieces, weighted by value",
},
{
f: reverse_starting,
name: "reverse_starting",
description: "Makes the move that minimizes the number of moves it takes to move their pieces to the opposite end",
},
{
f: cccp,
name: "cccp",
description: "First checkmates, then checks, then captures (the best piece it can), then pushes",
},
{
f: suicide_king,
name: "suicide_king",
description: "Makes the move that minimizes the Chebyshev distance between the two kings",
},
{
f: sym_mirror_y,
name: "sym_mirror_y",
description: "Tries to make the board as vertically symmetric as possible (basically copies your moves)",
},
{
f: sym_mirror_x,
name: "sym_mirror_x",
description: "Tries to make the board as horizontally symmetric as possible (basically doesn't copy your moves or do anything useful)",
},
{
f: sym_180,
name: "sym_180",
description: "Tries to make the board as symmetric as possible when flipped around",
},
{
f: min_oppt_move,
name: "min_oppt_move",
description: "Does the move that leaves the least moves for YOU",
},
{
f: equalizer,
name: "equalizer",
description: "Moves the least moved piece (actually piece type, e.g. moving a pawn counts for all pawns; it sucks, i know), breaking ties by moving to the least visited square",
},
// alphabetical is fine i guess, since the checklist order has little to do with the og paper
// ehhh nvm
]; //.sort((a, b) => a.name.localeCompare(b.name));
// https://stackoverflow.com/questions/12739171/javascript-epoch-time-in-days
function days_since_epoch() {
var now = new Date().getMilliseconds();
return Math.floor(now / 8.64e7);
}
function find_specified_in_hash() {
return players.find(p => `#${p.name}` == window.location.hash);
}
// bad
// const todays_player = players[days_since_epoch() % players.length];
function get_todays_player() {
var _a;
let player = (_a = find_specified_in_hash()) !== null && _a !== void 0 ? _a : players[Math.floor(Math.random() * players.length)];
console.log(`SPOILER: "today"'s player is ${player.name}`);
return player;
}
var todays_player = get_todays_player();
// const todays_player = min_oppt_move;
var board = document.getElementById("board");
var game = new Chess();
function computer_move() {
num_moves++;
history += "M";
return todays_player.f(game);
}
// might want to change this when it's more wordly
function show_game_over() {
let game_over = document.getElementById("game-over");
game_over.style.display = "block";
game_over.innerText = `GAME OVER; player was ${todays_player.name}`;
console.log("GAME OVER");
}
function copy_stats_to_clipboard() {
navigator.clipboard.writeText(document.getElementById("stats").innerText);
}
function show_win() {
wins++;
in_game_over = true;
let win_count = document.getElementById("win-count");
win_count.style.display = "block";
win_count.innerText = `You have won ${wins} time${wins == 1 ? "" : "s"} without refreshing${wins_exclamation_marks()}`;
let game_over = document.getElementById("game-over");
game_over.style.display = "block";
game_over.innerText = "YOU WIN YOU WIN YOU WIN YOU WIN HOLY CRAP";
let stats = document.getElementById("stats");
stats.style.display = "block";
let stats_text = `#elo_worldle
#ew_${todays_player.name}
Win #${wins}
Guesses: ${num_guesses}
Moves: ${num_moves}
${history}`;
stats.innerText = stats_text;
let copy_stats = document.getElementById("copy-stats");
copy_stats.style.display = "block";
copy_stats.addEventListener("click", _ => copy_stats_to_clipboard());
let play_again = document.getElementById("play-again");
play_again.style.display = "block";
play_again.addEventListener("click", _ => reset());
console.log("PLAYER IS WIN");
}
var num_guesses = 0;
var num_moves = 0;
var history = "";
function append_players_to(n) {
for (const player of players) {
let child = document.createElement("div");
child.classList.add("player");
let child_name = document.createElement("h1");
child_name.classList.add("player-name");
child_name.innerText = player.name;
let child_desc = document.createElement("p");
child_desc.classList.add("player-desc");
child_desc.innerText = player.description;
let guess_button = document.createElement("button");
guess_button.innerText = "^ guess ^";
guess_button.onclick = _ => {
num_guesses++;
history += "G";
if (todays_player.name == player.name) {
show_win();
child.classList.add("correct");
}
else {
child.classList.add("wrong");
}
guess_button.onclick = () => { };
};
child.appendChild(child_name);
child.appendChild(child_desc);
child.appendChild(guess_button);
child_name.addEventListener("click", _ => child.classList.toggle("strikethrough"));
n.appendChild(child);
}
}
var wins = 0;
// extraordinarily necessary
function wins_exclamation_marks() {
let base_10_log_of_wins = Math.floor(Math.log10(wins));
return base_10_log_of_wins > 0 ? "!".repeat(base_10_log_of_wins) : ".";
}
var in_game_over = false;
// ERROR: !
// INELEGANT
// "HACK"!
function reset() {
let play_again = document.getElementById("play-again");
play_again.style.display = "none";
play_again.onclick = _ => console.log("you can't play again again sorry :]");
in_game_over = false;
todays_player = get_todays_player();
game = new Chess();
board.fen = game.fen();
board.turn = "white";
board.interactive = true;
num_guesses = 0;
num_moves = 0;
history = "";
equalizer_state = equalizer_state_default;
let players = document.getElementById("players");
players.innerHTML = "";
append_players_to(players);
let stats = document.getElementById("stats");
stats.innerHTML = "";
stats.style.display = "none";
document.getElementById("copy-stats").style.display = "none";
document.getElementById("game-over").style.display = "none";
}
window.addEventListener("DOMContentLoaded", () => {
board.addEventListener("movestart", (e) => {
console.log(`Move started: ${e.detail.from}, ${e.detail.piece.color} ${e.detail.piece.pieceType}`);
e.detail.setTargets(
// This produces a list like ["e3", "e5"]
game.moves({ square: e.detail.from, verbose: true }).map(m => m.to));
if (game.isGameOver()) {
show_game_over();
}
});
board.addEventListener("moveend", (e) => {
console.log(`Move ending: ${e.detail.from} -> ${e.detail.to}, ${e.detail.piece.color} ${e.detail.piece.pieceType}`);
const move = game.move({
from: e.detail.from,
to: e.detail.to,
promotion: "q",
});
if (move === null) {
e.preventDefault();
}
if (game.isGameOver()) {
show_game_over();
}
});
board.addEventListener("movefinished", (e) => {
board.fen = game.fen();
board.turn = game.turn() === "w" ? "white" : "black";
board.interactive = game.turn() === "w";
if (!board.interactive) {
let move = computer_move();
if (!move) {
console.log("Didn't get a move from the computer (this is fine if the game is over)");
return;
}
console.log(`Computer move: ${move}`);
game.move(move);
board.fen = game.fen();
board.turn = "white";
board.interactive = true;
}
});
document
.getElementById("finish")
.addEventListener("click", _ => show_game_over());
document.addEventListener("keypress", e => {
if (e.key == "r" && in_game_over) {
reset();
}
else if (e.key == "c" && in_game_over) {
copy_stats_to_clipboard();
}
});
append_players_to(document.getElementById("players"));
});