Skip to content

Instantly share code, notes, and snippets.

@zamfofex
Last active February 19, 2022 11:19
Show Gist options
  • Save zamfofex/d478de89883e1629ce21de5367b9bfdd to your computer and use it in GitHub Desktop.
Save zamfofex/d478de89883e1629ce21de5367b9bfdd to your computer and use it in GitHub Desktop.
play Dummyette in your browser
*
!/.gitignore
!/index.html
<!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