|
import {toSAN} from "./dummyette/notation.js" |
|
import {standardBoard, types} from "./dummyette/chess.js" |
|
|
|
let format = n => |
|
{ |
|
let mate |
|
if (n.startsWith("#")) |
|
{ |
|
mate = true |
|
n = n.slice(1) |
|
} |
|
|
|
n = Number(n) |
|
let sign = "+" |
|
if (n < 0) |
|
{ |
|
n *= -1 |
|
sign = "\u2212" |
|
} |
|
|
|
if (mate) return "#" + sign + n |
|
|
|
if (n === 0) return sign + "0.0" |
|
if (n < 10) return sign + n.toFixed(2) |
|
return sign + n.toFixed(1) |
|
} |
|
|
|
export let toElement = (game, {document = globalThis.document, prefix = "dummyette"} = {}) => |
|
{ |
|
prefix = String(prefix) |
|
if (prefix) prefix += "-" |
|
|
|
let widget = document.createElement("div") |
|
widget.classList.add(prefix + "widget") |
|
widget.tabIndex = -1 |
|
|
|
let whitePlayer = document.createElement("div") |
|
let blackPlayer = document.createElement("div") |
|
whitePlayer.classList.add(prefix + "player", prefix + "white-player") |
|
blackPlayer.classList.add(prefix + "player", prefix + "black-player") |
|
|
|
widget.append(whitePlayer, blackPlayer) |
|
|
|
if (game.info.WhiteTitle) |
|
{ |
|
let title = document.createElement("span") |
|
title.classList.add(prefix + "title") |
|
title.append(game.info.WhiteTitle) |
|
whitePlayer.append(title) |
|
} |
|
if (game.info.BlackTitle) |
|
{ |
|
let title = document.createElement("span") |
|
title.classList.add(prefix + "title") |
|
title.append(game.info.BlackTitle) |
|
blackPlayer.append(title) |
|
} |
|
|
|
let white = document.createElement("span") |
|
white.classList.add(prefix + "name") |
|
white.append(game.info.White ?? "???") |
|
whitePlayer.append(white) |
|
let black = document.createElement("span") |
|
black.classList.add(prefix + "name") |
|
black.append(game.info.Black ?? "???") |
|
blackPlayer.append(black) |
|
|
|
if (game.info.WhiteElo) |
|
{ |
|
let rating = document.createElement("span") |
|
rating.classList.add(prefix + "rating") |
|
rating.append(game.info.WhiteElo) |
|
whitePlayer.append(rating) |
|
} |
|
if (game.info.BlackElo) |
|
{ |
|
let rating = document.createElement("span") |
|
rating.classList.add(prefix + "rating") |
|
rating.append(game.info.BlackElo) |
|
blackPlayer.append(rating) |
|
} |
|
|
|
let controls = document.createElement("div") |
|
controls.classList.add(prefix + "controls") |
|
widget.append(controls) |
|
|
|
let gotoStart = document.createElement("button") |
|
gotoStart.append("\u21C7") |
|
let gotoEnd = document.createElement("button") |
|
gotoEnd.append("\u21C9") |
|
let previous = document.createElement("button") |
|
previous.append("\u2190") |
|
let next = document.createElement("button") |
|
next.append("\u2192") |
|
let flip = document.createElement("button") |
|
flip.append("\u27F3") |
|
|
|
controls.append(gotoStart, previous, flip, next, gotoEnd) |
|
|
|
gotoStart.addEventListener("click", () => { update(-3) }) |
|
gotoEnd.addEventListener("click", () => { update(3) }) |
|
previous.addEventListener("click", () => update(-1)) |
|
next.addEventListener("click", () => update(1)) |
|
flip.addEventListener("click", () => { flipped = !flipped ; update() }) |
|
|
|
let board = document.createElement("div") |
|
board.classList.add(prefix + "board") |
|
widget.append(board) |
|
|
|
let marksElement = document.createElement("div") |
|
marksElement.classList.add(prefix + "marks") |
|
board.append(marksElement) |
|
|
|
let moveList = document.createElement("div") |
|
moveList.classList.add(prefix + "moves") |
|
widget.append(moveList) |
|
|
|
let pieces = [] |
|
|
|
let chessBoard = game.deltas[0]?.before ?? standardBoard |
|
|
|
for (let x = 0 ; x < 8 ; x++) |
|
for (let y = 0 ; y < 8 ; y++) |
|
{ |
|
let piece = chessBoard.at(x, y) |
|
if (!piece) continue |
|
|
|
let element = document.createElement("div") |
|
element.classList.add(prefix + "piece", prefix + piece.color, prefix + piece.color + "-" + piece.type) |
|
|
|
pieces.push({x, y, element, type: piece.type}) |
|
|
|
marksElement.before(element) |
|
} |
|
|
|
let stops = [] |
|
let nodes = [{children: [], pieces}] |
|
|
|
let handleVariations = (deltas, parentElement, pieces0, parent) => |
|
{ |
|
let moveElement = document.createElement("div") |
|
moveElement.classList.add(prefix + "move") |
|
|
|
for (let {move, comments, before, variations, annotation} of deltas) |
|
{ |
|
parentElement.append(moveElement) |
|
|
|
let j = nodes.length |
|
|
|
let pieces = pieces0.slice() |
|
|
|
let children = [] |
|
let node = {parent, children, pieces} |
|
nodes.push(node) |
|
nodes[parent].children.push(j) |
|
|
|
let i = pieces.findIndex(({x, y, captured}) => !captured && x === move.from.x && y === move.from.y) |
|
let moved = [pieces[i].element] |
|
node.moved = moved |
|
|
|
if (move.captured) |
|
{ |
|
let i = pieces.findIndex(({x, y, captured}) => !captured && x === move.captured.position.x && y === move.captured.position.y) |
|
moved.push(pieces[i].element) |
|
pieces[i] = {...pieces[i], captured: true} |
|
} |
|
|
|
pieces[i] = {...pieces[i], x: move.to.x, y: move.to.y} |
|
if (move.promotion) pieces[i].type = move.promotion.type |
|
|
|
if (move.rook) |
|
{ |
|
let i = pieces.findIndex(({x, y, captured}) => !captured && x === move.rook.from.x && y === move.rook.from.y) |
|
moved.push(pieces[i].element) |
|
pieces[i] = {...pieces[i], x: move.rook.to.x, y: move.rook.to.y} |
|
} |
|
|
|
for (let comment of comments) |
|
{ |
|
let time = comment.match(/\[%clk ([0-9:]+)\]/) |
|
if (time) |
|
{ |
|
node.time = time[1].slice(time[1].match(/^[0:]*/)[0].length).padStart(4, "0:00") |
|
break |
|
} |
|
} |
|
|
|
if(!node.time) node.time = nodes[parent].time |
|
|
|
let scoreElement = "" |
|
|
|
for (let comment of comments) |
|
{ |
|
let scoreText = comment.match(/\[%eval (#?[-+]?[0-9\.]+)\]/) |
|
if (!scoreText) continue |
|
let score = format(scoreText[1]) |
|
scoreElement = document.createElement("span") |
|
scoreElement.classList.add(prefix + "score") |
|
scoreElement.append(score) |
|
break |
|
} |
|
|
|
let annotationText = "" |
|
if (annotation === 1) annotationText = "!" |
|
if (annotation === 2) annotationText = "?" |
|
if (annotation === 3) annotationText = "!!" |
|
if (annotation === 4) annotationText = "??" |
|
if (annotation === 5) annotationText = "!?" |
|
if (annotation === 6) annotationText = "?!" |
|
|
|
let button = document.createElement("button") |
|
button.classList.add(prefix + before.turn + "-move") |
|
button.append(toSAN(move) + annotationText, " ", scoreElement) |
|
moveElement.append(button) |
|
|
|
node.button = button |
|
|
|
button.addEventListener("click", () => |
|
{ |
|
current = j |
|
update() |
|
}) |
|
|
|
for (let comment of comments) |
|
{ |
|
if (comment[0] !== "{") continue |
|
if (comment.startsWith("{ [%")) continue |
|
|
|
let commentElement = document.createElement("span") |
|
commentElement.classList.add(prefix + "comment") |
|
commentElement.append(comment.slice(1, -1)) |
|
moveElement.append(commentElement) |
|
} |
|
|
|
for (let deltas of variations) |
|
{ |
|
let moveList = document.createElement("div") |
|
moveList.classList.add(prefix + "variation") |
|
moveElement.append(moveList) |
|
stops.push(nodes.length) |
|
handleVariations(deltas, moveList, pieces0, parent) |
|
} |
|
|
|
parent = j |
|
pieces0 = pieces |
|
|
|
if (before.turn === "black") |
|
{ |
|
moveElement = document.createElement("div") |
|
moveElement.classList.add(prefix + "move") |
|
} |
|
} |
|
} |
|
|
|
handleVariations(game.deltas, moveList, pieces, 0) |
|
|
|
let whiteTime |
|
let blackTime |
|
|
|
if (nodes.some(({time}) => time)) |
|
{ |
|
whiteTime = document.createElement("div") |
|
blackTime = document.createElement("div") |
|
whiteTime.classList.add(prefix + "time", prefix + "white-time") |
|
blackTime.classList.add(prefix + "time", prefix + "black-time") |
|
widget.append(whiteTime, blackTime) |
|
} |
|
|
|
let getPosition = event => |
|
{ |
|
let rect = board.getBoundingClientRect() |
|
let x = Math.floor((event.x - rect.x - board.clientLeft) * 8 / board.scrollWidth) |
|
let y = Math.floor((event.y - rect.y - board.clientTop) * 8 / board.scrollHeight) |
|
if (x < 0) x = 0 |
|
if (y < 0) y = 0 |
|
if (x > 7) x = 7 |
|
if (y > 7) y = 7 |
|
return {x, y} |
|
} |
|
|
|
let marks = new Set() |
|
|
|
board.addEventListener("contextmenu", event => event.preventDefault()) |
|
|
|
let currentMark |
|
|
|
board.addEventListener("pointerdown", event => |
|
{ |
|
if (event.button !== 2) |
|
{ |
|
for (let mark of marks) |
|
mark.element.remove() |
|
marks.clear() |
|
return |
|
} |
|
|
|
let {x, y} = getPosition(event) |
|
|
|
let element = document.createElement("div") |
|
element.classList.add(prefix + "mark", prefix + "circle") |
|
board.append(element) |
|
|
|
element.style.setProperty("--" + prefix + "x0", x) |
|
element.style.setProperty("--" + prefix + "y0", y) |
|
element.style.setProperty("--" + prefix + "x1", x) |
|
element.style.setProperty("--" + prefix + "y1", y) |
|
|
|
currentMark = {element, x0: x, y0: y, x1: x, y1: y} |
|
marks.add(currentMark) |
|
|
|
marksElement.append(element) |
|
}) |
|
|
|
board.addEventListener("pointermove", event => |
|
{ |
|
if (!currentMark) return |
|
let {x, y} = getPosition(event) |
|
|
|
currentMark.x1 = x |
|
currentMark.y1 = y |
|
|
|
let {x0, y0, element} = currentMark |
|
|
|
let circle = x0 === x && y0 === y |
|
element.classList.toggle(prefix + "circle", circle) |
|
element.classList.toggle(prefix + "arrow", !circle) |
|
|
|
element.style.setProperty("--" + prefix + "x1", x) |
|
element.style.setProperty("--" + prefix + "y1", y) |
|
|
|
element.style.setProperty("--" + prefix + "length", Math.sqrt((x - x0) ** 2 + (y - y0) ** 2)) |
|
element.style.setProperty("--" + prefix + "angle", Math.atan2(y - y0, x - x0) + "rad") |
|
}) |
|
|
|
document.addEventListener("pointerup", event => |
|
{ |
|
if (!currentMark) return |
|
|
|
let matching |
|
for (let mark of marks) |
|
{ |
|
if (mark !== currentMark) |
|
if (mark.x0 == currentMark.x0) |
|
if (mark.y0 == currentMark.y0) |
|
if (mark.x1 == currentMark.x1) |
|
if (mark.y1 == currentMark.y1) |
|
{ |
|
matching = mark |
|
break |
|
} |
|
} |
|
|
|
if (matching) |
|
{ |
|
marks.delete(matching) |
|
marks.delete(currentMark) |
|
matching.element.remove() |
|
currentMark.element.remove() |
|
} |
|
|
|
currentMark = undefined |
|
}) |
|
|
|
widget.addEventListener("keydown", event => |
|
{ |
|
switch (event.code) |
|
{ |
|
case "ArrowLeft": |
|
case "ArrowRight": |
|
case "ArrowUp": |
|
case "ArrowDown": |
|
event.preventDefault() |
|
} |
|
}) |
|
|
|
widget.addEventListener("keyup", event => |
|
{ |
|
switch (event.code) |
|
{ |
|
case "ArrowLeft": |
|
update(-1) |
|
break |
|
case "ArrowRight": |
|
update(1) |
|
break |
|
case "ArrowUp": |
|
if (event.shiftKey) |
|
update(-2) |
|
else |
|
update(-3) |
|
break |
|
case "ArrowDown": |
|
if (event.shiftKey) |
|
update(2) |
|
else |
|
update(3) |
|
break |
|
} |
|
}) |
|
|
|
let stops2 = stops.slice().reverse() |
|
let findNextVariation = index => stops.find(i => i > index) ?? lastIndex |
|
let findPreviousVariation = index => stops2.find(i => i < index) ?? 0 |
|
|
|
let findLast = index => |
|
{ |
|
let children = nodes[index].children |
|
if (children.length === 0) return index |
|
let i = 0 |
|
return findLast(children[i]) |
|
} |
|
|
|
let lastIndex = findLast(0) |
|
|
|
let update = n => |
|
{ |
|
let moved |
|
|
|
if (n === -1) |
|
{ |
|
moved = nodes[current].moved |
|
current = nodes[current]?.parent ?? current |
|
} |
|
|
|
if (n === 1) |
|
{ |
|
current = nodes[current]?.children?.[0] ?? current |
|
moved = nodes[current].moved |
|
} |
|
|
|
if (n === -2) current = findPreviousVariation(current) |
|
if (n === 2) current = findNextVariation(current) |
|
|
|
if (n === -3) current = 0 |
|
if (n === 3) current = lastIndex |
|
|
|
if (moved) |
|
{ |
|
for (let {element} of nodes[current].pieces) |
|
{ |
|
if (moved.includes(element)) continue |
|
board.prepend(element) |
|
} |
|
} |
|
|
|
let first = current === 0 |
|
previous.disabled = first |
|
gotoStart.disabled = first |
|
|
|
let last = current === lastIndex |
|
next.disabled = last |
|
gotoEnd.disabled = last |
|
|
|
let whiteToPlay = current % 2 == 1 |
|
|
|
widget.classList.toggle(prefix + "white-to-play", !whiteToPlay) |
|
widget.classList.toggle(prefix + "black-to-play", whiteToPlay) |
|
|
|
widget.classList.toggle(prefix + "from-white", !flipped) |
|
widget.classList.toggle(prefix + "from-black", flipped) |
|
|
|
for (let {x, y, element, captured, type} of nodes[current].pieces) |
|
{ |
|
if (flipped) x = 7 - x |
|
else y = 7 - y |
|
element.style.setProperty("--" + prefix + "x", x) |
|
element.style.setProperty("--" + prefix + "y", y) |
|
element.classList.toggle(prefix + "captured", Boolean(captured)) |
|
for (let type of types) element.classList.toggle(prefix + type, false) |
|
element.classList.toggle(prefix + type, true) |
|
} |
|
|
|
if (whiteTime) |
|
{ |
|
let parent = nodes[current].parent |
|
let grandparent = nodes[parent]?.parent |
|
|
|
let first = nodes[nodes[0].children[0]] |
|
let second = first && nodes[first.children[0]] |
|
|
|
let initialWhiteTime = first?.time |
|
let initialBlackTime = second?.time |
|
|
|
if (whiteToPlay) |
|
{ |
|
whiteTime.textContent = nodes[parent]?.time ?? initialWhiteTime |
|
blackTime.textContent = nodes[grandparent]?.time ?? initialBlackTime |
|
} |
|
else |
|
{ |
|
blackTime.textContent = nodes[parent]?.time ?? initialBlackTime |
|
whiteTime.textContent = nodes[grandparent]?.time ?? initialWhiteTime |
|
} |
|
} |
|
|
|
for (let {button} of nodes.slice(1)) |
|
button.classList.remove(prefix + "current") |
|
|
|
if (current > 0) |
|
{ |
|
nodes[current].button.classList.add(prefix + "current") |
|
|
|
let rect0 = moveList.getBoundingClientRect() |
|
let rect1 = nodes[current].button.getBoundingClientRect() |
|
|
|
let top = rect1.top - rect0.top |
|
let bottom = rect0.bottom - rect1.bottom |
|
|
|
if (top < 0) moveList.scrollBy(0, top) |
|
if (bottom < 0) moveList.scrollBy(0, -bottom) |
|
} |
|
else |
|
{ |
|
moveList.scrollTop = 0 |
|
} |
|
} |
|
|
|
let flipped = false |
|
let current = 0 |
|
|
|
update() |
|
|
|
return widget |
|
} |