{ "version": 3, "sources": ["../../../static-src/chess/ts/chess-bot.ts", "../../../static-src/chess/ts/chess-main.ts"], "sourcesContent": ["import type { Square } from \"chess.js\"\n\n/**\n * @link https://github.com/official-stockfish/Stockfish/wiki/UCI-&-Commands\n */\n\nconst BEST_MOVE_UCI_ANSWER_PATTERN = /^bestmove ([a-h][1-8])([a-h][1-8])/\nconst SCORE_UCI_ANSWER_PATTERN = / score (cp|mate) (-?\\d+)/\n\nconst wasmSupported =\n typeof WebAssembly === \"object\" &&\n WebAssembly.validate(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00))\nlet botChessEngineWorker: Worker | null = null\n\ntype WorkerEvent = Event & { data: string }\n\nexport async function playFromFEN(\n fen: string,\n depth: number,\n botAssetsDataHolderElementId: string,\n engineWorker: Worker | null = null,\n): Promise<[Square, Square]> {\n if (!engineWorker) {\n if (!botChessEngineWorker) {\n const chessBotDataHolder = getBotAssetsDataHolderElementFromId(botAssetsDataHolderElementId)\n botChessEngineWorker = await getChessEngineWorker(chessBotDataHolder)\n }\n engineWorker = botChessEngineWorker\n }\n\n // Fo each move we reset the engine with `ucinewgame` and `isready` commands,\n // hoping to avoid any state from previous moves to affect the next move's result:\n // since we have a \"pre-calculated in the Django Admin\" solution machinery,\n // we choose to prioritise determinism over performance.\n // (we're talking about a few milliseconds here, not a few seconds)\n engineWorker.postMessage(\"ucinewgame\")\n engineWorker.postMessage(\"isready\")\n let startTime = 0\n\n return new Promise((resolve, reject) => {\n function onChessEngineMessage(e: WorkerEvent) {\n console.debug(\"onChessEngineMessage\", e.data)\n if (e.data.startsWith(\"readyok\")) {\n engineWorker!.postMessage(`position fen ${fen}`)\n engineWorker!.postMessage(`go depth ${depth}`)\n startTime = Date.now()\n return\n }\n\n const bestMoveAnswerMatch = BEST_MOVE_UCI_ANSWER_PATTERN.exec(e.data)\n if (!bestMoveAnswerMatch) {\n return\n }\n\n engineWorker!.removeEventListener(\"message\", onChessEngineMessage)\n console.log(\n `FEN ${fen}:`,\n \"bestMoveAnswerMatch: \",\n [bestMoveAnswerMatch[1], bestMoveAnswerMatch[2]],\n `(in ${Math.round(Date.now() - startTime)}ms, depth ${depth})`,\n )\n resolve([bestMoveAnswerMatch[1] as Square, bestMoveAnswerMatch[2] as Square])\n }\n\n engineWorker!.addEventListener(\"message\", onChessEngineMessage)\n })\n}\n\n// TODO: merge this function with the previous `playFromFEN` one\nexport async function getScoreFromFEN(\n fen: string,\n depth: number,\n botAssetsDataHolderElementId: string,\n engineWorker: Worker | null = null,\n): Promise<[\"cp\" | \"mate\", number]> {\n if (!engineWorker) {\n if (!botChessEngineWorker) {\n const chessBotDataHolder = getBotAssetsDataHolderElementFromId(botAssetsDataHolderElementId)\n botChessEngineWorker = await getChessEngineWorker(chessBotDataHolder)\n }\n engineWorker = botChessEngineWorker\n }\n\n engineWorker.postMessage(\"ucinewgame\")\n engineWorker.postMessage(\"isready\")\n let startTime = 0\n\n return new Promise((resolve, reject) => {\n function onChessEngineMessage(e: WorkerEvent) {\n console.debug(\"onChessEngineMessage\", e.data)\n if (e.data.startsWith(\"readyok\")) {\n engineWorker!.postMessage(`position fen ${fen}`)\n engineWorker!.postMessage(`go depth ${depth}`)\n startTime = Date.now()\n return\n }\n\n const scoreAnswerMatch = SCORE_UCI_ANSWER_PATTERN.exec(e.data)\n if (!scoreAnswerMatch) {\n return\n }\n botChessEngineWorker!.removeEventListener(\"message\", onChessEngineMessage)\n console.log(\n `FEN ${fen}:`,\n \"scoreAnswerMatch: \",\n scoreAnswerMatch[1],\n scoreAnswerMatch[2],\n `(in ${Math.round(Date.now() - startTime!)}ms)`,\n )\n resolve([scoreAnswerMatch[1] as \"cp\" | \"mate\", parseInt(scoreAnswerMatch[2], 10)])\n }\n\n engineWorker!.addEventListener(\"message\", onChessEngineMessage)\n })\n}\n\nexport async function getChessEngineWorker(botAssetsDataHolderElement: HTMLElement): Promise {\n const chessBotData = JSON.parse(botAssetsDataHolderElement.dataset.chessEngineUrls!)\n const engineWorkerScript = chessBotData[\"wasm\"] && wasmSupported ? chessBotData[\"wasm\"] : chessBotData[\"js\"]\n const engineWorker = new Worker(engineWorkerScript)\n\n return new Promise((resolve, reject) => {\n function onChessEngineMessage(e: WorkerEvent) {\n console.log(e.data)\n if (!e.data.startsWith(\"uciok\")) {\n return\n }\n engineWorker!.removeEventListener(\"message\", onChessEngineMessage)\n resolve(engineWorker!)\n }\n engineWorker.addEventListener(\"message\", onChessEngineMessage)\n engineWorker.postMessage(\"uci\")\n })\n}\n\nexport function resetChessEngineWorker(): void {\n if (!botChessEngineWorker) {\n return\n }\n console.log(\"terminating bot's chess engine Worker\")\n botChessEngineWorker.terminate()\n botChessEngineWorker = null\n}\n\nfunction getBotAssetsDataHolderElementFromId(botAssetsDataHolderElementId: string): HTMLElement {\n // See the `chess_bot_data` Python function for how it's generated\n const chessBotDataHolder = document.getElementById(botAssetsDataHolderElementId)\n if (!chessBotDataHolder) {\n throw `no #${botAssetsDataHolderElementId} element found to read bot assets data!`\n }\n return chessBotDataHolder\n}\n", "import { playFromFEN, getChessEngineWorker, getScoreFromFEN, resetChessEngineWorker } from \"./chess-bot\"\n\n// @ts-ignore\nwindow.cursorIsNotOnChessBoardInteractiveElement = cursorIsNotOnChessBoardInteractiveElement\n// @ts-ignore\nwindow.playBotMove = playBotMove\n// @ts-ignore\nwindow.computeScore = computeScore\n// @ts-ignore\nwindow.resetChessEngineWorker = resetChessEngineWorker\n// @ts-ignore\nwindow.closeSpeechBubble = closeSpeechBubble\n// @ts-ignore\nwindow.__admin__playFromFEN = playFromFEN\n// @ts-ignore\nwindow.__admin__getChessEngineWorker = getChessEngineWorker\n\nfunction cursorIsNotOnChessBoardInteractiveElement(boardId: string): boolean {\n // Must return `true` only if the user hasn't clicked on one of the game clickable elements.\n // @link https://htmx.org/attributes/hx-trigger/\n // (see our \"chess_arena\" Python component to see it used)\n\n const chessBoardContainer = document.getElementById(`chess-arena-${boardId}`)\n\n if (chessBoardContainer === null) {\n return false // not much we can do, as it seems that this chess board has mysteriously disappeared \uD83E\uDD37\n }\n\n const chessBoardState = (chessBoardContainer.querySelector(\"[data-board-state]\") as HTMLElement).dataset\n .boardState as string\n\n console.log(\"chessBoardState; \", chessBoardState)\n\n if (chessBoardState === \"waiting_for_player_selection\") {\n return false // there's no current selection, so we actually have nothing to do here\n }\n if (chessBoardState === \"waiting_for_bot_turn\") {\n // As we're 100% server-side-rendered in this 1st version, de-selecting a piece\n // triggers a HTMX refresh of the board, which may conflict with the wait for\n // the bot's move.\n // --> let's not allow de-selecting a piece when it's the bot's turn.\n return false\n }\n\n if (chessBoardState.includes(\"game_over\")) {\n return false // the UI stops being interactive when the game is over\n }\n\n const hoveredElements = Array.from(document.querySelectorAll(\":hover\"))\n\n const gamePiecesElements = chessBoardContainer.querySelectorAll(`#chess-board-pieces-${boardId} [data-piece-role]`)\n for (const pieceElement of gamePiecesElements) {\n if (hoveredElements.includes(pieceElement)) {\n return false // don't actually cancel the selection\n }\n }\n const selectedPieceTargetsElements = chessBoardContainer.querySelectorAll(\n `#chess-board-available-targets-${boardId} [data-square]`,\n )\n for (const targetElement of selectedPieceTargetsElements) {\n if (hoveredElements.includes(targetElement)) {\n return false // ditto\n }\n }\n\n const restartDailyChallengeButtonElement = chessBoardContainer.querySelector(\n `#chess-board-restart-daily-challenge-${boardId}`,\n )\n if (restartDailyChallengeButtonElement && hoveredElements.includes(restartDailyChallengeButtonElement)) {\n return false // don't actually cancel the selection when clicking on the \"restart daily challenge\" button\n }\n\n const speechBubble = getSpeechBubble()\n if (speechBubble && hoveredElements.includes(speechBubble)) {\n return false // don't actually cancel the selection when closing the speech bubble\n }\n\n const modalContainer = document.getElementById(\"modal-container\")\n if (modalContainer && hoveredElements.includes(modalContainer)) {\n return false // don't actually cancel the selection when interacting with a modal\n }\n\n console.log(\"no interactive UI element clicked: we reset the board state\")\n return true\n}\n\ntype BotMoveDescription = {\n fen: string\n htmxElementId: string\n botAssetsDataHolderElementId: string\n forcedMove: [string, string] | null\n depth: number\n}\n\nfunction playBotMove({\n fen,\n htmxElementId,\n botAssetsDataHolderElementId,\n forcedMove,\n depth,\n}: BotMoveDescription): void {\n const htmxElement = document.getElementById(htmxElementId)\n if (!htmxElement) {\n throw `no #${botAssetsDataHolderElementId} element found to play bot's move!`\n }\n\n const doPlayBotMove = (from: string, to: string) => {\n const targeUrlPattern = htmxElement.dataset.hxPost!\n const targeUrl = targeUrlPattern.replace(\"\", from).replace(\"\", to)\n htmxElement.dataset.hxPost = targeUrl\n window.htmx.process(htmxElement)\n window.htmx.trigger(htmxElement, \"playMove\", {})\n }\n\n if (forcedMove) {\n doPlayBotMove(forcedMove[0], forcedMove[1])\n return\n }\n\n playFromFEN(fen, depth, botAssetsDataHolderElementId).then((move) => {\n console.log(`bot wants to move from ${move[0]} to ${move[1]}`)\n doPlayBotMove(move[0], move[1])\n })\n}\n\nfunction computeScore(fen: string, botAssetsDataHolderElementId: string): Promise<[\"cp\" | \"mate\", number]> {\n return getScoreFromFEN(fen, 2, botAssetsDataHolderElementId).then(([type, score]) => {\n console.log(`Chess engine says score is ${score} (type: ${type})`)\n return [type, score]\n })\n}\n\nfunction closeSpeechBubble(speechBubbleId?: string): void {\n const speechBubble = getSpeechBubble(speechBubbleId)\n if (speechBubble?.parentNode) {\n speechBubble.remove()\n }\n}\n\nfunction getSpeechBubble(speechBubbleId?: string): Element | null {\n const speechBubble = document.querySelector(\"[data-speech-bubble]\")\n if (speechBubbleId && speechBubble?.dataset.speechBubbleId !== speechBubbleId) {\n return null // avoid closing a speech bubble that doesn't match the provided unique ID\n }\n return speechBubble\n}\n"], "mappings": "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", "names": ["BEST_MOVE_UCI_ANSWER_PATTERN", "SCORE_UCI_ANSWER_PATTERN", "wasmSupported", "botChessEngineWorker", "playFromFEN", "fen", "depth", "botAssetsDataHolderElementId", "engineWorker", "chessBotDataHolder", "getBotAssetsDataHolderElementFromId", "getChessEngineWorker", "startTime", "resolve", "reject", "onChessEngineMessage", "e", "bestMoveAnswerMatch", "getScoreFromFEN", "scoreAnswerMatch", "botAssetsDataHolderElement", "chessBotData", "engineWorkerScript", "resetChessEngineWorker", "cursorIsNotOnChessBoardInteractiveElement", "playBotMove", "computeScore", "resetChessEngineWorker", "closeSpeechBubble", "playFromFEN", "getChessEngineWorker", "boardId", "chessBoardContainer", "chessBoardState", "hoveredElements", "gamePiecesElements", "pieceElement", "selectedPieceTargetsElements", "targetElement", "restartDailyChallengeButtonElement", "speechBubble", "getSpeechBubble", "modalContainer", "fen", "htmxElementId", "botAssetsDataHolderElementId", "forcedMove", "depth", "htmxElement", "doPlayBotMove", "from", "to", "targeUrl", "move", "getScoreFromFEN", "type", "score", "speechBubbleId"] }