-
-
Save zamfofex/d478de89883e1629ce21de5367b9bfdd to your computer and use it in GitHub Desktop.
play Dummyette in your browser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* | |
!/.gitignore | |
!/index.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width"> | |
<!-- | |
README: | |
To use this, first clone the Dummyette repository within this Gist’s directory, then start an HTTP server from within this Gist’s directory. | |
~~~ | |
git clone https://github.com/zamfofex/dummyette | |
python3 -m http.server 8017 | |
~~~ | |
Then use your favorite web browser to navigate to <http://localhost:8017> | |
--> | |
<title> play Dummyette </title> | |
<style> | |
* | |
{ | |
box-sizing: border-box; | |
} | |
html, body | |
{ | |
height: 100%; | |
user-select: none; | |
} | |
body | |
{ | |
margin: 0; | |
background: #FED; | |
display: grid; | |
align-content: center; | |
justify-items: center; | |
text-align: center; | |
grid-gap: 3.125vmin; | |
color: #654; | |
font-family: | |
"DejaVu Sans", | |
"DejaVu LGC Sans", | |
"Verdana", | |
"Bitstream Vera Sans", | |
"Geneva", | |
sans-serif; | |
font-size: 3.125vmin; | |
} | |
a | |
{ | |
color: inherit; | |
text-decoration: none; | |
} | |
.download | |
{ | |
font-size: 0.75em; | |
margin-top: -1.5625vmin; | |
} | |
.download[href] | |
{ | |
text-decoration: underline; | |
} | |
.board | |
{ | |
border: #654 solid 1.5625vmin; | |
width: 75vmin; | |
height: 75vmin; | |
border-radius: 6.25%; | |
display: grid; | |
grid-template: repeat(8, 1fr) / 1fr; | |
font-size: 1.5625vmin; | |
cursor: pointer; | |
counter-reset: rank 9; | |
position: relative; | |
} | |
.rank:first-child > .square:last-child | |
{ | |
border-radius: 0 1.5625vmin 0 0; | |
} | |
.rank:last-child > .square:first-child | |
{ | |
border-radius: 0 0 0 1.5625vmin; | |
} | |
.rank | |
{ | |
counter-increment: rank -1; | |
display: grid; | |
grid-template: 1fr / repeat(8, 1fr); | |
} | |
.square | |
{ | |
counter-increment: file; | |
display: grid; | |
padding: 12.5%; | |
place-content: end; | |
} | |
.square::before | |
{ | |
content: counter(file, lower-alpha) counter(rank); | |
} | |
.rank:nth-child(even) > .square:nth-child(odd), | |
.rank:nth-child(odd) > .square:nth-child(even) | |
{ | |
background: #654; | |
color: #FED; | |
} | |
.mark | |
{ | |
pointer-events: none; | |
} | |
.arrow | |
{ | |
width: calc(var(--length) * 12.5%); | |
height: 12.5%; | |
background: linear-gradient(#0000 43.75%, #79F 43.75%, #79F 56.25%, #0000 56.25%); | |
position: absolute; | |
opacity: 75%; | |
transform: rotate(var(--angle)); | |
transform-origin: left; | |
left: calc(var(--x) * 12.5% + 6.125%); | |
top: calc(var(--y) * 12.5%); | |
} | |
.arrow::before, | |
.arrow::after | |
{ | |
position: absolute; | |
content: ""; | |
background: #79F; | |
height: 12.5%; | |
width: 3.125vmin; | |
right: 0; | |
top: 50%; | |
} | |
.arrow::before | |
{ | |
transform: translate(0, -50%) rotate(45deg); | |
transform-origin: top right; | |
} | |
.arrow::after | |
{ | |
transform: translate(0, -50%) rotate(-45deg); | |
transform-origin: bottom right; | |
} | |
.circle | |
{ | |
opacity: 75%; | |
position: absolute; | |
width: 12.5%; | |
height: 12.5%; | |
background: radial-gradient(closest-side, #0000 62.5%, #79F 62.5%, #79F 75%, #0000 75%) center / 100%; | |
left: calc(var(--x) * 12.5%); | |
top: calc(var(--y) * 12.5%); | |
} | |
.pieces, .ranks | |
{ | |
display: grid; | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
} | |
.piece | |
{ | |
width: 12.5%; | |
height: 12.5%; | |
background: | |
url("https://upload.wikimedia.org/wikipedia/commons/b/b2/Chess_Pieces_Sprite.svg") | |
0 0 / 600%; | |
left: calc(var(--x) * 12.5%); | |
top: calc(var(--y) * 12.5%); | |
position: absolute; | |
transition: 0.25s ease-in-out; | |
transition-property: top, left, opacity; | |
} | |
.piece.captured | |
{ | |
opacity: 0; | |
} | |
.black { background-position-y: 100%; } | |
.queen { background-position-x: 20%; } | |
.bishop { background-position-x: 40%; } | |
.knight { background-position-x: 60%; } | |
.rook { background-position-x: 80%; } | |
.pawn { background-position-x: 100%; } | |
.move | |
{ | |
opacity: 75%; | |
position: absolute; | |
width: 12.5%; | |
height: 12.5%; | |
background: radial-gradient(closest-side, #7F9 25%, #0000 25%) center / 100%; | |
left: calc(var(--x) * 12.5%); | |
top: calc(var(--y) * 12.5%); | |
} | |
.move:hover | |
{ | |
background-color: #7F96; | |
} | |
/* cheesy hack to avoid gaps in Chrome */ | |
.square:first-child | |
{ | |
margin-left: -2px; | |
padding-left: calc(12.5% + 2px); | |
} | |
.square:last-child | |
{ | |
margin-right: -2px; | |
padding-right: calc(12.5% + 2px); | |
} | |
.rank:first-child > .square | |
{ | |
margin-top: -2px; | |
padding-top: calc(12.5% + 2px); | |
} | |
.rank:last-child > .square | |
{ | |
margin-bottom: -2px; | |
padding-bottom: calc(12.5% + 2px); | |
} | |
</style> | |
<script type="importmap"> | |
{ | |
"imports": | |
{ | |
"dummyette/": "./dummyette/" | |
} | |
} | |
</script> | |
<script type="module"> | |
import {standardBoard} from "dummyette/chess.js" | |
import {AsyncAnalyser} from "dummyette/dummyette.js" | |
import {toSAN} from "dummyette/notation.js" | |
let chessBoard = standardBoard | |
let a = document.createElement("a") | |
a.target = "_blank" | |
a.textContent = "Dummyette" | |
a.href = "https://github.com/zamfofex/dummyette" | |
document.body.append(a) | |
let board = document.createElement("div") | |
board.classList.add("board") | |
document.body.append(board) | |
let status = document.createElement("div") | |
status.textContent = "Your turn!" | |
document.body.append(status) | |
let ranks = document.createElement("div") | |
ranks.classList.add("ranks") | |
board.append(ranks) | |
let download = document.createElement("a") | |
download.classList.add("download") | |
download.textContent = "Have fun" | |
document.body.append(download) | |
for (let y = 0 ; y < 8 ; y++) | |
{ | |
let rank = document.createElement("div") | |
rank.classList.add("rank") | |
ranks.append(rank) | |
for (let x = 0 ; x < 8 ; x++) | |
{ | |
let square = document.createElement("div") | |
square.classList.add("square") | |
rank.append(square) | |
} | |
} | |
let arrow | |
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} | |
} | |
board.addEventListener("contextmenu", event => event.preventDefault()) | |
board.addEventListener("pointerdown", event => | |
{ | |
if (event.button !== 2) return | |
let {x, y} = getPosition(event) | |
arrow = document.createElement("div") | |
arrow.classList.add("mark", "circle") | |
board.append(arrow) | |
arrow.dataset.start = `${x},${y}` | |
arrow.dataset.end = `${x},${y}` | |
arrow.style.setProperty("--x", x) | |
arrow.style.setProperty("--y", y) | |
}) | |
board.addEventListener("pointermove", event => | |
{ | |
if (!arrow) return | |
let {x, y} = getPosition(event) | |
let x0 = Number(arrow.style.getPropertyValue("--x")) | |
let y0 = Number(arrow.style.getPropertyValue("--y")) | |
let circle = x === x0 && y === y0 | |
arrow.classList.toggle("circle", circle) | |
arrow.classList.toggle("arrow", !circle) | |
arrow.dataset.end = `${x},${y}` | |
arrow.style.setProperty("--length", Math.sqrt((x - x0) ** 2 + (y - y0) ** 2)) | |
arrow.style.setProperty("--angle", Math.atan2(y - y0, x - x0) + "rad") | |
}) | |
addEventListener("pointerup", event => | |
{ | |
if (!arrow) return | |
let arrows = board.querySelectorAll(`.mark[data-start="${arrow.dataset.start}"][data-end="${arrow.dataset.end}"]`) | |
if (arrows.length > 1) | |
for (let arrow of arrows) | |
arrow.remove() | |
arrow = null | |
}) | |
let find = (x, y) => | |
{ | |
for (let piece of board.querySelectorAll(".piece:not(.captured)")) | |
{ | |
let x0 = Number(piece.style.getPropertyValue("--x")) | |
let y0 = Number(piece.style.getPropertyValue("--y")) | |
if (x === x0 && y === y0) return piece | |
} | |
} | |
let analyser = AsyncAnalyser() | |
let lastPosition | |
board.addEventListener("click", event => | |
{ | |
for (let mark of board.querySelectorAll(".mark, .move")) | |
mark.remove() | |
if (chessBoard.turn !== "white") return | |
let {x, y} = getPosition(event) | |
if (lastPosition && x === lastPosition.x && y === lastPosition.y) | |
{ | |
lastPosition = null | |
return | |
} | |
lastPosition = {x, y} | |
for (let move of chessBoard.moves) | |
{ | |
if (move.from.x !== x || move.from.y !== 7 - y) continue | |
if (move.name.length === 5 && move.name[4] !== "q") continue | |
let element = document.createElement("div") | |
element.classList.add("move") | |
board.append(element) | |
element.style.setProperty("--x", move.to.x) | |
element.style.setProperty("--y", 7 - move.to.y) | |
element.addEventListener("click", async event => | |
{ | |
for (let mark of board.querySelectorAll(".move")) | |
mark.remove() | |
lastPosition = null | |
event.stopPropagation() | |
status.textContent = "Waiting\u2026" | |
play(move) | |
if (chessBoard.moves.length !== 0) | |
{ | |
let moves = await analyser.analyse(chessBoard) | |
play(moves[0]) | |
} | |
}) | |
} | |
}) | |
let pieces = document.createElement("div") | |
pieces.classList.add("pieces") | |
board.append(pieces) | |
for (let [i, type] of ["rook", "knight", "bishop", "queen", "king", "bishop", "knight", "rook"].entries()) | |
{ | |
let white = document.createElement("div") | |
white.classList.add("piece", "white", type) | |
let black = document.createElement("div") | |
black.classList.add("piece", "black", type) | |
let whitePawn = document.createElement("div") | |
whitePawn.classList.add("piece", "white", "pawn") | |
let blackPawn = document.createElement("div") | |
blackPawn.classList.add("piece", "black", "pawn") | |
white.style.setProperty("--x", i) | |
white.style.setProperty("--y", 7) | |
black.style.setProperty("--x", i) | |
black.style.setProperty("--y", 0) | |
whitePawn.style.setProperty("--x", i) | |
whitePawn.style.setProperty("--y", 6) | |
blackPawn.style.setProperty("--x", i) | |
blackPawn.style.setProperty("--y", 1) | |
pieces.append(white, black, whitePawn, blackPawn) | |
} | |
let state = new Map() | |
for (let piece of board.querySelectorAll(".piece")) | |
state.set(piece, [piece.getAttribute("style"), piece.className]) | |
let states = [state] | |
let moves = [] | |
let date = new Date() | |
let dateString = `${String(date.getFullYear())}.${String(date.getMonth() + 1).padStart(2, "0")}.${String(date.getDate()).padStart(2, "0")}` | |
let time = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}` | |
let dateUTC = `${String(date.getUTCFullYear())}.${String(date.getUTCMonth() + 1).padStart(2, "0")}.${String(date.getUTCDate()).padStart(2, "0")}` | |
let timeUTC = `${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")}:${String(date.getUTCSeconds()).padStart(2, "0")}` | |
let play = move => | |
{ | |
let {x: x0, y: y0} = move.from | |
let {x: x1, y: y1} = move.to | |
chessBoard = move.play() | |
let name = toSAN(move) | |
moves.push(name) | |
let captured = find(x1, 7 - y1) | |
if (captured) captured.classList.add("captured") | |
let piece = find(x0, 7 - y0) | |
piece.style.setProperty("--x", x1) | |
piece.style.setProperty("--y", 7 - y1) | |
let rook | |
if (piece.matches(".king")) | |
{ | |
if (x1 - x0 === 2) | |
{ | |
rook = find(7, 7 - y0) | |
rook.style.setProperty("--x", 5) | |
} | |
if (x1 - x0 === -2) | |
{ | |
rook = find(0, 7 - y0) | |
rook.style.setProperty("--x", 3) | |
} | |
} | |
if (piece.matches(".pawn")) | |
if (x0 !== x1 && !captured) | |
{ | |
captured = find(x1, 7 - y0) | |
captured.classList.add("captured") | |
} | |
if (move.name.length === 5) | |
{ | |
let names = {q: "queen", r: "rook", b: "bishop", k: "knight"} | |
let type = names[move.name[4]] | |
piece.classList.remove("pawn") | |
piece.classList.add(type) | |
} | |
for (let other of board.querySelectorAll(".piece")) | |
{ | |
if (other === piece) continue | |
if (other === rook) continue | |
if (other === captured) continue | |
pieces.prepend(other) | |
} | |
let state = new Map() | |
states.push(state) | |
for (let piece of board.querySelectorAll(".piece")) | |
state.set(piece, [piece.getAttribute("style"), piece.className]) | |
let result = "*" | |
if (chessBoard.moves.length === 0) | |
{ | |
if (chessBoard.checkmate) | |
{ | |
if (chessBoard.turn === "white") | |
result = "0-1" | |
else | |
result = "1-0" | |
} | |
else | |
{ | |
result = "1/2-1/2" | |
} | |
} | |
let pgn = ` | |
[Date "${dateString}"] | |
[Time "${time}"] | |
[UTCDate "${dateUTC}"] | |
[UTCTime "${timeUTC}"] | |
[Event "Casual Game"] | |
[Site "Dummyette Test Page <${location}>"] | |
[Round "-"] | |
[White "Human Player"] | |
[Black "Dummyette"] | |
[WhiteType "human"] | |
[BlackType "program"] | |
[Result "${result}"] | |
`.replace(/\t/g, "").trim() + "\n\n" | |
for (let i = 0 ; i * 2 < moves.length ; i++) | |
{ | |
pgn += String(i + 1) + "." | |
pgn += " " + moves[i * 2] | |
if (moves.length > i * 2 + 1) | |
pgn += " " + moves[i * 2 + 1] | |
pgn += "\n" | |
} | |
pgn += result + "\n" | |
if (download.href) URL.revokeObjectURL(download.href) | |
let url = URL.createObjectURL(new Blob([pgn], {type: "text/plain"})) | |
download.href = url | |
download.textContent = "export game" | |
download.download = "dummyette.pgn" | |
if (chessBoard.moves.length === 0) | |
{ | |
if (chessBoard.draw) | |
status.textContent = "Draw by stalemate!" | |
else if (chessBoard.turn === "white") | |
status.textContent = "Checkmate, you lost!" | |
else | |
status.textContent = "Checkmate, you won!" | |
let i = states.length - 1 | |
addEventListener("keydown", ({code}) => | |
{ | |
if (code === "ArrowLeft" && i > 0) | |
i-- | |
else if (code === "ArrowRight" && i < states.length - 1) | |
i++ | |
else if (code === "ArrowUp" && i > 0) | |
i = 0 | |
else if (code === "ArrowDown" && i < states.length - 1) | |
i = states.length - 1 | |
else | |
return | |
for (let [piece, [style, className]] of states[i]) | |
piece.setAttribute("style", style), | |
piece.className = className | |
}) | |
return | |
} | |
if (chessBoard.turn === "white") | |
status.textContent = "Your turn" | |
else | |
status.textContent = "Waiting\u2026" | |
} | |
</script> | |
<body> | |
<noscript> (JavaScript is required) </noscript> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment