import { Chess, Move, Square, Ox88, DEFAULT_POSITION, SQUARES, rank, file, Piece, WHITE, BLACK, KING, PieceSymbol, Color, PIECE_SYMBOLS, } from "./chess.js"; type Player = (_: Chess) => string; function with_move(board: Chess, move: string): Chess { 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; // } // could return early if zero but meh function min_move_by(board: Chess, score: (_: Chess) => number): string { let best_score = Infinity; let best_move = ""; let moves = board.moves(); // console.log(moves); for (const i in moves) { let move = moves[i]; 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 || (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: Player = board => { const moves = board.moves(); const move = moves[Math.floor(Math.random() * moves.length)]; return move; }; function make_lexo(m: Move): [number, number, number, number, PieceSymbol] { 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: Move, b: Move): -1 | 0 | 1 { 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: Player = board => { const moves = board.moves({ verbose: true }).sort(lexo_sort_moves); return moves[0].san; }; const alphabetical: Player = 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 var equalizer_state: { piece_moves: Record; square_visits: Record; } = { 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 } }; const equalizer: Player = board => { let best_move: Move; let least_moved_found = Infinity; let least_visited_found = Infinity; let piece_moving: PieceSymbol = "k"; let square_visiting: Square = "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: Player = board => { let num_moves = b => b.moves().length; return min_move_by(board, num_moves); }; function num_pieces_on_own_color(b: Chess): number { 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: Player = board => { return min_move_by(board, b => 64 - num_pieces_on_own_color(b)); }; const opposite_color: Player = board => { return min_move_by(board, num_pieces_on_own_color); }; function rank_and_file(s: Square): { rank: number; file: number } { return { rank: rank(Ox88[s]), file: file(Ox88[s]) }; } function chebyshev_distance(a: Square, b: Square): number { 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: Chess = new Chess(DEFAULT_POSITION); // good enough function move_distance(piece: { square: Square; type: PieceSymbol }): number { 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: (piece: { square: Square; type: PieceSymbol }) => number ): Player { const player_score = (board: Chess) => { 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: Player = minimize_distances(move_distance); // or lens :] function mirror_player(mirror_fn: (_: Square) => Square): Player { let player_score = (b: Chess) => { // 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: { rank: number; file: number; }): Square { let r: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" = "8"; let f: "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" = "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: Square): Square { let rf = rank_and_file(s); rf.rank = 7 - rf.rank; return rank_and_file_to_algebraic(rf); } const sym_mirror_y: Player = mirror_player(mirror_y); function mirror_x(s: Square): Square { let rf = rank_and_file(s); rf.file = 7 - rf.file; return rank_and_file_to_algebraic(rf); } const sym_mirror_x: Player = mirror_player(mirror_x); function flip(s: Square): Square { 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: Player = mirror_player(flip); function find(p: Piece, b: Chess): Square | null { 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: Player = board => { let distance_between_kings = (b: Chess) => chebyshev_distance( find({ color: WHITE, type: KING }, b) ?? "a1", find({ color: BLACK, type: KING }, b) ?? "a1" ); return min_move_by(board, distance_between_kings); }; // cccp and pacifist are basically opposites // ugh not really function pieces_of_color( b: Chess, color: Color ): { square: Square; type: PieceSymbol; color: Color }[] { let flat_board = b.board().flat(); let non_empties: { square: Square; type: PieceSymbol; color: Color }[] = []; 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: Chess, color: Color): number { return pieces_of_color(b, color).length; } // see generous in http://tom7.org/chess/weak.pdf function value_piece(type: PieceSymbol): number { 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: Chess, color: Color): number { return pieces_of_color(b, color) .map(p => value_piece(p.type)) .reduce((a, b) => a + b); } // hm const HUMAN_COLOR: Color = WHITE; const ROBOT_COLOR: Color = BLACK; // DONE: I THINK: UGH piece value const cccp: Player = board => { let cccp_score = (b: Chess) => { 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: Chess, color: Color, square: Square ): number { return pieces_of_color(board, color) .map(p => chebyshev_distance(p.square, square)) .reduce((a, b) => a + b); } const huddle: Player = board => { return min_move_by(board, b => total_chebyshev_distances_of_pieces_of_color_to_square( b, ROBOT_COLOR, find({ color: ROBOT_COLOR, type: KING }, b) ?? "a1" ) ); }; const swarm: Player = board => { return min_move_by(board, b => total_chebyshev_distances_of_pieces_of_color_to_square( b, ROBOT_COLOR, find({ color: HUMAN_COLOR, type: KING }, b) ?? "a1" ) ); }; function sum_of_possible_capture_value(b: Chess): number { 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: Player = board => { // sum of the difference in ROBOT_COLOR piece value of all moves // but like uhh negative let generous_score = (b: Chess) => { return -sum_of_possible_capture_value(b); }; return min_move_by(board, generous_score); }; const no_i_insist: Player = board => { let no_i_insist_score = (b: Chess) => { 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); }; return min_move_by(board, no_i_insist_score); }; const pacifist: Player = 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: 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(): null | { f: Player; name: string; description: string; } { return players.find(p => `#${p.name}` == window.location.hash); } // bad // const todays_player = players[days_since_epoch() % players.length]; const todays_player = find_specified_in_hash() ?? players[Math.floor(Math.random() * players.length)]; console.log(`SPOILER: today's player is ${todays_player.name}`); // const todays_player = min_oppt_move; const board: any = document.getElementById("board"); const game = new Chess(); function computer_move() { num_moves++; 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 show_win() { 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\n#ew_${todays_player.name}\nGuesses: ${num_guesses}\nMoves: ${num_moves}`; stats.innerText = stats_text; let copy_stats = document.getElementById("copy-stats"); copy_stats.style.display = "block"; copy_stats.addEventListener("click", _ => navigator.clipboard.writeText(stats_text) ); console.log("PLAYER IS WIN"); } var num_guesses = 0; var num_moves = 0; function append_players_to(n: Node) { 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++; 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); } } window.addEventListener("DOMContentLoaded", () => { board.addEventListener("movestart", (e: any) => { 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: any) => { 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: any) => { 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()); append_players_to(document.getElementById("players")); });