diff --git a/Makefile b/Makefile index 8881f59..84f3cc3 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ wasm: chess.wasm codegen: codegen.c $(CC) -o $@ $(CFLAGS) $^ -chess.wasm: wasm-compat-chatgpt.c mbb_rook.h mbb_bishop.h engine.h - $(CC) -DWASM -o $@ wasm-compat-chatgpt.c $(CFLAGS.$(CC)) $(CFLAGS.$(CC).wasm) +chess.wasm: wasm-compat.c mbb_rook.h mbb_bishop.h engine.h + $(CC) -DWASM -o $@ wasm-compat.c $(CFLAGS.$(CC)) $(CFLAGS.$(CC).wasm) mbb_rook.h: codegen ./codegen diff --git a/chess-worker.js b/chess-worker.js index 4f9859d..f8105b6 100644 --- a/chess-worker.js +++ b/chess-worker.js @@ -1,7 +1,7 @@ let exports; async function init() { - const resp = await fetch("./chess.wasm"); + const resp = await fetch("./chess.wasm?rand=" + crypto.randomUUID()); if (!resp.ok) throw new Error(`fetch wasm failed ${resp.status} ${resp.statusText}`); const { instance } = await WebAssembly.instantiateStreaming(resp, {}); diff --git a/chess.html b/chess.html index d4e4887..3a17924 100644 --- a/chess.html +++ b/chess.html @@ -36,6 +36,7 @@
-

+

+ diff --git a/chess.js b/chess.js index 4c96158..c40d06c 100644 --- a/chess.js +++ b/chess.js @@ -1,5 +1,7 @@ +/* this code is complete garbage but it's mostly for testing anyways */ const MoveResult = Object.freeze({ + ILLEGAL: -1, NORMAL: 0, CHECK: 1, REPEATS: 2, @@ -15,65 +17,44 @@ const _moveResultName = [ "Checkmate", ]; +const moveResultName = (mr) => { + if (mr === MoveResult.ILLEGAL) return "Illegal"; + return _moveResultName[mr] ?? `Unknown(${mr})`; +}; + +const isTerminal = (mr) => (mr === MoveResult.STALEMATE || mr === MoveResult.CHECKMATE); + const Player = Object.freeze({ White: 0, Black: 1, None: 2, }); -const _playerName = [ - "White", - "Black", - "None", -]; - const Piece = Object.freeze({ - Pawn: 0, - King: 1, - Queen: 2, + Empty: 0, + Pawn: 1, + Knight: 2, Bishop: 3, Rook: 4, - Knight: 5, - Empty: 6, + Queen: 5, + King: 6, }); -const _pieceName = [ - "Pawn", - "King", - "Queen", - "Bishop", - "Rook", - "Knight", - "Empty", -]; - -const _pieceChar = [ - 'P', - 'K', - 'Q', - 'B', - 'R', - 'N', - ' ', -]; - const _pieceUnicode = [ - ['♙', '♔', '♕', '♗', '♖', '♘',], // white - ['♟', '♚', '♛', '♝', '♜', '♞',], // black - ['' , '', '', '', '', '', ], // none + [' ', '♙', '♘', '♗', '♖', '♕', '♔',], // white + [' ', '♟', '♞', '♝', '♜', '♛', '♚',], // black + [' ', ' ', ' ', ' ', ' ', ' ', ' ',], // none ]; const indexDeserialize = (n) => ({ - rank: Math.floor(n / 8), // every day we stray further from God + rank: Math.floor(n / 8), file: (n % 8), fileChar() { return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'][this.file]; }, rankChar() { return ['1', '2', '3', '4', '5', '6', '7', '8'][this.rank]; }, }); - -const serializedIndexFromCoord = (rank, file) => (8*rank + file); - -const indexSerialize = (index) => (serializedIndexFromCoord(index.rank, index.file)); +const serializedIndexFromCoord = (rank, file) => (8 * rank + file); +const indexSerialize = (index) => serializedIndexFromCoord(index.rank, index.file); const moveDeserialize = (n) => ({ appeal: Number((n >> 24n) & 0xFFn), @@ -83,80 +64,50 @@ const moveDeserialize = (n) => ({ }); const moveSerialize = (move) => ( - (indexSerialize(move.from) & 0xFF) << 8) | ((indexSerialize(move.to) & 0xFF)); + ((indexSerialize(move.from) & 0xFF) << 8) | (indexSerialize(move.to) & 0xFF) +); -const squareDeserialize = (n) => ({ - player: n == -1 ? Player.None : (n >> 8) & 0xFF, - piece: n == -1 ? Piece.Empty : (n & 0xFF), - playerName() { - if (this.player === Player.None) { - return "Empty"; - } else { - return _playerName[this.player]; - } - }, - pieceName() { - if (this.player === Player.None) { - return ""; - } else { - return this.playerName() + _pieceName[this.piece]; - } - }, - pieceUnicode() { - return _pieceUnicode[this.player][this.piece]; - } -}); +const squareDeserialize = (n) => { + if (n === -1) { + return { + player: Player.None, + piece: Piece.Empty, + unicode: ' ', + }; + } -const WasmBoard = async (url) => { - /* - const res = await fetch(url); - if (!res.ok) { - throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); - } - const bytes = await res.arrayBuffer(); - const instantiated = await WebAssembly.instantiate(bytes, {}); - const instance = instantiated.instance ?? instantiated; - const exports = instance.exports; + const player = (n >> 8) & 0xFF; + const piece = n & 0xFF; + const unicode = _pieceUnicode[player][piece]; - const wb_init = exports.wb_init; - const wb_move = exports.wb_move; - const wb_search = exports.wb_search; - const wb_board_at = exports.wb_board_at; - */ - const worker = new Worker("./chess-worker.js", { type: "module" }); + return { + player: player, + piece: piece, + unicode: unicode, + }; +}; + +const WasmBoard = async () => { + const worker = new Worker("./chess-worker.js?rand=" + crypto.randomUUID(), { type: "module" }); await new Promise((resolve, reject) => { worker.addEventListener("message", (e) => { - if (e.data?.type === "ready") { - resolve(); - } + if (e.data?.type === "ready") resolve(); }, { once: true }); worker.addEventListener("error", reject, { once: true }); }); const wasmCall = (method, args = []) => new Promise((resolve, reject) => { const id = crypto.randomUUID(); + const onMessage = (e) => { - if (e.data?.id !== id) { - return; - } + if (e.data?.id !== id) return; cleanup(); - if (e.data.ok) { - resolve(e.data.value); - } else { - reject(new Error(e.data.error)); - } - }; - - const onError = (err) => { - cleanup(); - reject(err); - }; - - const onMessageError = (e) => { - cleanup(); - reject(new Error("Worker message deserialization failed (messageerror).")); + if (e.data.ok) resolve(e.data.value); + else reject(new Error(e.data.error)); }; + const onError = (err) => { cleanup(); reject(err); }; + const onMessageError = () => { cleanup(); reject(new Error("Worker messageerror")); }; const cleanup = () => { worker.removeEventListener("message", onMessage); @@ -174,96 +125,202 @@ const WasmBoard = async (url) => { await wasmCall("wb_init", []); return { - state: MoveResult.Normal, + state: MoveResult.NORMAL, - ongoing: function() /* nil -> bool */ { - return (this.state != MoveResult.STALEMATE - && this.state != MoveResult.CHECKMATE); + search: async function(depth, timeout) { + return wasmCall("wb_search", [depth]).then(moveDeserialize); }, - search: async function(depth) /* Int -> Move */ { - return wasmCall("wb_search", [depth]) - .then(moveDeserialize); - }, - - move: async function (move) /* (Move | SerializedMove) -> MoveResult */ { + move: async function(move) { const m = (typeof move === "number") ? move : moveSerialize(move); - return wasmCall("wb_move", [m]); + const mr = await wasmCall("wb_move", [m]); + + if (mr === MoveResult.NORMAL || mr === MoveResult.CHECK || mr === MoveResult.REPEATS || + mr === MoveResult.STALEMATE || mr === MoveResult.CHECKMATE) { + this.state = mr; + return mr; + } + return MoveResult.ILLEGAL; }, - at: async function (index) /* (Index | SerializedIndex) -> Square */ { + at: async function(index) { const i = (typeof index === "number") ? index : indexSerialize(index); return wasmCall("wb_board_at", [i]) - .then(squareDeserialize); + .then(squareDeserialize); + }, + + legalMoves: async function() { + const n = await wasmCall("wb_all_moves_len", []); + const out = new Uint16Array(n); + for (let i = 0; i < n; ++i) { + out[i] = await wasmCall("wb_all_moves_get", [i]); + } + return out; }, }; }; - const run = async () => { - const board = await WasmBoard("./chess-worker.js"); + const board = await WasmBoard(); - const nextFrame = () => new Promise(requestAnimationFrame); + const boardElem = document.getElementById("board"); + const resultEl = document.getElementById("result"); + const setStatus = (s) => { resultEl.textContent = s; }; - const createBoardUI = async (board) => { - const boardElem = document.getElementById("board"); + const squares = new Array(64); + + const idxToAlg = (idx) => { + const i = indexDeserialize(idx); + return `${i.fileChar()}${i.rankChar()}`; + }; + + const draw = async () => { + for (let rank = 7; rank >= 0; --rank) { + for (let file = 0; file < 8; ++file) { + const idx = 8 * rank + file; + const sq = await board.at(idx); + console.log(sq); + console.log(sq.unicode); + squares[idx].textContent = sq.unicode; + } + } + }; + + const createBoardUI = () => { boardElem.replaceChildren(); - const filesChars = ["a","b","c","d","e","f","g","h"]; - const ranksChars = ["8","7","6","5","4","3","2","1"]; - - const squares = new Array(64); - for (let rank = 7; rank >= 0; --rank) { for (let file = 0; file < 8; ++file) { + const idx = 8 * rank + file; const el = document.createElement("div"); - el.className = - "square " + ((file + rank) % 2 ? "dark" : "light"); - el.dataset.file = filesChars[file]; - el.dataset.rank = ranksChars[rank]; - - const sq = await board.at(serializedIndexFromCoord(rank, file)); - el.textContent = sq.pieceUnicode(); - + el.className = "square " + (((file + rank) % 2) ? "dark" : "light"); + el.dataset.idx = String(idx); boardElem.appendChild(el); - squares[8*rank+file] = el; - } - } - - return squares; - }; - - const draw = async (board, squareElems) => { - for (let rank = 7; rank >= 0; --rank) { - for (let file = 0; file < 8; ++file) { - const sq = await board.at(serializedIndexFromCoord(rank, file)); - squareElems[8*rank+file].textContent = sq.pieceUnicode(); + squares[idx] = el; } } }; - const sqElems = await createBoardUI(board); - for (let i = 0; i < 200; ++i) { - await draw(board, sqElems); - await nextFrame(); + createBoardUI(); + await draw(); - const move = await board.search(7); + let selected = null; // 0..63 or null + let inputEnabled = true; - const fromSq = await board.at(move.from); - const toSq = await board.at(move.to); + const clearSelection = () => { + if (selected !== null) squares[selected].classList.remove("blue"); + selected = null; + }; - const mr = board.move(move); - if (mr == MoveResult.STALEMATE || mr == MoveResult.CHECKMATE) { - const resultEl = document.getElementById("result"); - resultEl.textContent = _moveResultName[board.state]; - draw(board, sqElems); - await nextFrame(); - break; + const legalHasFrom = (moves, fromIdx) => { + for (const mv of moves) { + if (((mv >> 8) & 0xFF) === fromIdx) return true; } + return false; + }; + + const legalHasMove = (moves, fromIdx, toIdx) => { + const key = ((fromIdx & 0xFF) << 8) | (toIdx & 0xFF); + for (const mv of moves) { + if (mv === key) return true; + } + return false; + }; + + const engineReply = async (depth) => { + console.log("searching"); + setStatus("Black thinking..."); + const m = await board.search(depth, 2000); + console.log("found move", m, ); + let sooner = Date.now(); + const mr = await board.move(m); + await draw(); + let later = Date.now(); + let seconds = later - sooner; + setStatus(`Black played ${idxToAlg(indexSerialize(m.from))}-${idxToAlg(indexSerialize(m.to))} after ${seconds} seconds\nWhite to move...`); + return mr; + }; + + const onSquareClick = async (idx) => { + if (!inputEnabled) return; + if (isTerminal(board.state)) return; + + const legalMoves = await board.legalMoves(); + + if (selected === null) { + const sq = await board.at(idx); + if (sq.player !== Player.White) return; + + if (!legalHasFrom(legalMoves, idx)) { + clearSelection(); + setStatus(`No legal moves from ${idxToAlg(idx)}.`); + return; + } + + selected = idx; + squares[selected].classList.add("blue"); + setStatus(`Selected ${idxToAlg(idx)}.`); + return; + } + + if (idx === selected) { + clearSelection(); + setStatus(""); + return; + } + + const clickedSq = await board.at(idx); + + // user clicks another white piece: either switch selection (if it has moves) or deselect + if (clickedSq.player === Player.White) { + clearSelection(); + + if (!legalHasFrom(legalMoves, idx)) { + setStatus(`No legal moves from ${idxToAlg(idx)}.`); + return; + } + + selected = idx; + squares[selected].classList.add("blue"); + setStatus(`Selected ${idxToAlg(idx)}.`); + return; + } + + const from = selected; + const to = idx; + + if (!legalHasMove(legalMoves, from, to)) { + clearSelection(); + setStatus(`Illegal: ${idxToAlg(from)}-${idxToAlg(to)}.`); + return; + } + + clearSelection(); + inputEnabled = false; + + const mrWhite = await board.move({ from: indexDeserialize(from), to: indexDeserialize(to) }); + await draw(); + setStatus(`White played ${idxToAlg(from)}-${idxToAlg(to)}. ${moveResultName(mrWhite)}`); + + if (isTerminal(mrWhite)) { + inputEnabled = false; + return; + } + + const mrBlack = await engineReply(8); + if (isTerminal(mrBlack)) { + inputEnabled = false; + return; + } + + inputEnabled = true; + }; + + for (let i = 0; i < 64; ++i) { + squares[i].addEventListener("click", () => { onSquareClick(i).catch(console.error); }); } -} -run().catch((err) => { - console.error(err); -}); + setStatus("White to move."); +}; +run().catch((err) => console.error(err)); diff --git a/wasm-compat.c b/wasm-compat.c index ca2e5a7..92f7ff9 100644 --- a/wasm-compat.c +++ b/wasm-compat.c @@ -1,25 +1,37 @@ +/* The purpose of this file is to be a simple wasm helper layer, all state is global */ #include "engine.h" #include +#include static struct search_option g_tt_buf[TT_ENTRIES]; +static struct board g_board; +static uint16_t g_legal_moves_buf[MOVE_MAX]; +static uint32_t g_legal_moves_len; -static struct board g_board; +void wb_init(void) +{ + g_board = BOARD_INIT; + g_board.tt.entries = g_tt_buf; + g_board.tt.mask = TT_MASK; + board_init(&g_board); +} -static inline uint32_t move_serialize(struct move m) +static inline uint32_t move_serialize(struct move m) { _Static_assert(sizeof m.from * CHAR_BIT == 8, "this must be checked if struct move's `from` changes"); _Static_assert(sizeof m.to * CHAR_BIT == 8, "this must be checked if struct move's `to` changes"); - return ((uint32_t)m.appeal << 24ULL) - | ((uint32_t)m.attr << 16ULL) - | ((uint32_t)m.from << 8ULL) - | ((uint32_t)m.to); + + return ((uint32_t)m.appeal << 24U) + | ((uint32_t)m.attr << 16U) + | ((uint32_t)m.from << 8U) + | ((uint32_t)m.to << 0U); } -static inline struct move move_deserialize(uint64_t m) +static inline struct move move_deserialize(uint32_t m) { return (struct move) { /* appeal and attributes are ignored regardless */ @@ -34,14 +46,14 @@ static inline struct move move_deserialize(uint64_t m) uint64_t wb_search(int8_t max_depth) { - struct search_result const sr = search(&g_board, g_board.pos.player, max_depth); - return move_serialize(sr.move); + struct search_result const sr = search(&g_board, g_board.pos.moving_side, max_depth); + return (uint64_t)move_serialize(sr.move); } int32_t wb_move(uint32_t move) { struct move const m = move_deserialize(move); - enum move_result const mr = board_move_2(&g_board, m); + enum move_result const mr = board_move(&g_board, m); /* TODO: this checkmate/stalemate check needs to be abstracted better */ if (mr == MR_STALEMATE) { @@ -49,35 +61,56 @@ int32_t wb_move(uint32_t move) } struct move moves[MOVE_MAX]; size_t move_count = 0ULL; - all_moves(&g_board.pos, opposite_player(g_board.pos.player), &move_count, moves); + all_pseudolegal_moves(&g_board.pos, MG_ALL, other_side(g_board.pos.moving_side), &move_count, moves); if (move_count == 0ULL) { - return MR_CHECKMATE; + return (int32_t)MR_CHECKMATE; } return (int32_t)mr; } -void wb_init() -{ - g_board = BOARD_INIT; - g_board.tt.entries = g_tt_buf; -} -static int32_t player_piece_serialize(enum player c, enum piece pz) +static int32_t side_piece_serialize(Side8 c, Piece8 pz) { return ((c & 0xFF) << 8U) | (pz & 0xFF); } -int32_t wb_board_at(index at) +int32_t wb_board_at(uint8_t at) { - bitboard const m = SQ_MASK_FROM_INDEX(at); - for (enum player pl = PLAYER_BEGIN; pl < PLAYER_COUNT; ++pl) { - for (enum piece pz = PIECE_BEGIN; pz < PIECE_COUNT; ++pz) { - if (g_board.pos.pieces[pl][pz] & m) { - return player_piece_serialize(pl, pz); + Bb64 const m = MASK_FROM_SQ((Sq8)at); + for (Side8 side = SIDE_BEGIN; side < SIDE_COUNT; ++side) { + for (Piece8 pz = PIECE_BEGIN; pz < PIECE_COUNT; ++pz) { + if (g_board.pos.pieces[side][pz] & m) { + return side_piece_serialize(side, pz); } } } - return -1; + return -1; } + +uint32_t wb_all_moves_len(void) +{ + struct move moves[MOVE_MAX]; + size_t cnt = 0; + all_pseudolegal_moves(&g_board.pos, MG_ALL, g_board.pos.moving_side, &cnt, moves); + + if (cnt > MOVE_MAX) { + cnt = MOVE_MAX; + } + + g_legal_moves_len = (uint32_t)cnt; + for (size_t i = 0; i < cnt; ++i) { + g_legal_moves_buf[i] = (uint16_t)(((uint32_t)moves[i].from << 8U) + | (uint32_t)moves[i].to); + } + + return g_legal_moves_len; +} + +uint32_t wb_all_moves_get(uint32_t i) +{ + if (i >= g_legal_moves_len) return 0U; + return (uint32_t)g_legal_moves_buf[i]; +} +