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 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", () => { document.getElementById("dummy-board").style.display = "none"; document.getElementById("players").classList.remove("much-tall"); 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")); });