Published
Edited
Feb 22, 2021
Importers
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// games for archival storage are required to provide following tags (as tag-value pairs),
// known as the "Seven Tag Roster"
sevenTagRoster = new Set([
// the name of the tournament or match event
"Event",

// the location of the event, in 'City, Region COUNTRY' format ('City COUNTRY' in our case),
// where COUNTRY is the three-letter International Olympic Committee country code
"Site",

// the starting date of the game, in YYYY.MM.DD format, '??' used for unknown values ('?' in our case)
"Date",

// the playing round ordinal of the game within the event
"Round",

// the player of the white pieces, in 'LastName, FirstName' format (in reverse order without comma, in our case)
"White",

// the player of the black pieces
"Black",

// the result of the game, in 'WhiteScore-BlackScore' format, or '*' for other, e.g., the game is ongoing
"Result"
])
Insert cell
// half-moves (aka ply, in plural) are not unique
halfMoves = game.history({verbose: true}) // returns moves in Standard (Short) Algebraic Notation (SAN) only otherwise
Insert cell
// see: https://www.chessgames.com/perl/chessgame?gid=1060353
game = {
const game = new Chess();
game.load_pgn(pgn);
return game;
}
Insert cell
Chess = require("chess.js@0.11")
Insert cell
pgn = FileAttachment("ivanchuk-wolff-1993.pgn").text();
Insert cell
Insert cell
illustrateHeader = () => {
let literal = ""
const addTagValuePair = (tag, value) => {
if (value === "?") {
return;
}
if (literal.length > 0) {
literal += " ";
}
literal += `<span class="text-nowrap">[${decode(tag)} "${decode(value)}"]</span>`;
};
const addMandatoryTagValues = () => {
sevenTagRoster.forEach((tag) => addTagValuePair(tag, header[tag]));
};

const addOptionalTagValues = () => {
Object.keys(header).forEach((tag) => {
if (!sevenTagRoster.has(tag)) {
addTagValuePair(tag, header[tag]);
}
});
};
addMandatoryTagValues();
literal += "<br>"
addOptionalTagValues();
return md`<code>${literal}</code>`;
}
Insert cell
decode = (text) => {
if (clarifications.has(text)) {
return clarify(text);
}

let result = "";
const words = text.split(" ");
words.forEach((word) => {
if (result.length > 0) {
result += " ";
}

if (clarifications.has(word)) {
result += clarify(word);
} else {
result += word;
}
});

return result;
}
Insert cell
clarifications = new Map([
["Interzonal", "Tournament organized by FIDE as a stage in the triennial World Championship cycle."],
["SUI", "International Olympic Committee country code for Switzerland."],
["1/2-1/2", "Draw, awarding each player a half-point in the traditional chess tournament scoring system."],
["ECO", "Encyclopedia of Chess Openings, a reference system published in five volumes."],
["D20", "Queen's Gambit Accepted, an ECO Volume D opening."],
["PlyCount", "Number of half-moves played: one move consists of a turn by each player."]
])
Insert cell
clarify = (text) => `<abbr title="${clarifications.get(text)}">${text}</abbr>`
Insert cell
// in Forsyth-Edwards Notation (FEN)
currentPosition = {
const fen = positions[positionIndex];
board.set({fen});
highlightHalfMove(positionIndex);
return fen;
}
Insert cell
positions = {
const game = new Chess();
const initialPosition = game.fen();
const positions = [initialPosition];
halfMoves.forEach((move) => {
game.move(move);
const position = game.fen();
positions.push(position);
});
return positions;
}
Insert cell
// see: https://github.com/ornicar/chessground/blob/master/src/config.ts
board = {
const container = document.getElementById("board-container");
return Chessground(container, {viewOnly: true});
}
Insert cell
Chessground = (await require('https://bundle.run/chessground@7.9.3')).Chessground
Insert cell
illustrateMoves = () => {
let literal = "";
const prependSpace = () => {
if (literal.length > 0) {
literal += " ";
}
};
const addHalfMove = (move, number) => literal += `<span class="half-move" data-number="${number}">${move.san}</span>`;
const addMove = ([whiteMove, blackMove], number) => {
prependSpace();
literal += `${number}.`;
const whiteMoveNumber = (number * 2) - 1;
addHalfMove(whiteMove, whiteMoveNumber);
if (blackMove !== undefined) {
prependSpace();
const blackMoveNumber = whiteMoveNumber + 1;
addHalfMove(blackMove, blackMoveNumber);
}
};
const moves = getMoves(halfMoves);
moves.forEach((move, index) => addMove(move, index + 1));
const isClosedGame = () => header.Result !== "*";
if (isClosedGame()) {
prependSpace();
literal += ` ${header.Result}`; // repeating the result in the header
}
return md`<code class="movetext">${literal}</code>`;
}
Insert cell
highlightHalfMove = (positionIndex) => {
let element = document.querySelector(".half-move.current");
if (element != null) {
element.classList.remove("current");
const previousHalfMoveNumber = element.dataset.number; // might be the last move of the game when looping
element.innerText = halfMoves[previousHalfMoveNumber - 1].san;
};
if (positionIndex > 0) {
const currentHalfMoveNumber = positionIndex;
element = document.querySelector(`.half-move[data-number="${currentHalfMoveNumber}"]`);
element.classList.add("current");
const currentHalfMove = halfMoves[currentHalfMoveNumber - 1];
const description = getHalfMoveDescription(currentHalfMove);
element.innerHTML = `<abbr title="${description}">${currentHalfMove.san}</abbr>`;
}
}
Insert cell
getHalfMoveDescription = {
const colors = new Map([
["w", "white"],
["b", "black"]
]);
const pieces = new Map([
["k", "king"],
["q", "queen"],
["r", "rook"],
["b", "bishop"],
["n", "knight"],
["p", "pawn"]
]);
const getHalfMoveDescription = (move) => {
let text = `${colors.get(move.color)} ${pieces.get(move.piece)}`;

if (move.flags.includes("b") || move.flags.includes("n")) {
text += ` goes to ${move.to} (from ${move.from}).`;
}

if (move.flags.includes("c")) {
text += ` takes ${pieces.get(move.captured)} at ${move.to} (from ${move.from}).`;
}

if (move.flags.includes("k")) {
if (move.color === "w") {
text = `${colors.get(move.color)} castles kingside (king goes from e1 to g1, rook goes from h1 to f1).`;
} else {
text = `${colors.get(move.color)} castles kingside (king goes from e8 to g8, rook goes from h8 to f8).`;
}
}
return toSentenceCase(text);
}
return getHalfMoveDescription;
}
Insert cell
toSentenceCase = (text) => text[0].toUpperCase() + text.slice(1, text.length)
Insert cell
getMoves = (halfMoves) =>
halfMoves.reduce((result, value, index, array) => {
if ((index % 2) === 0) {
const move = array.slice(index, index + 2)
result.push(move);
}

return result;
}, []);
Insert cell
createStepper = (visibility) => {
const positionIndices = Array.from({length: positions.length}, (_, i) => i);
const options = {
delay: 1000,
loop: false,
firstStepDelay: 100,
format: (_, index, values) => {
const halfMoveCount = (values.length - 1).toString();
const halfMoveNumber = index.toString().padStart(halfMoveCount.length, "0");
return `Ply: ${halfMoveNumber}/${halfMoveCount}`;
},
visibility
};
return stepper(positionIndices, options);
}
Insert cell
Insert cell
html`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">`
Insert cell
html`<style>
.text-nowrap {
white-space: nowrap;
}

abbr[title] {
text-underline-position: under; /* 'auto' appears broken (Chrome) */
}

#board-container {
width: ${sideLength}px;
height: ${sideLength}px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12);
}

${chessgroundStylesheets}

cg-board {
cursor: default;
}

.cg-wrap coords {
font-size: 10px;
font-weight: bold;
}

.cg-wrap coords.ranks {
top: -30px;
}

.cg-wrap coords.files {
bottom: -1px;
left: 33px;
text-transform: none;
}

.stepper {
display: flex;
flex-direction: column;
align-items: center;
max-width: 640px;
}

.stepper output {
font-family: var(--mono_fonts);
font-size: 16px;
line-height: 1;
}

.stepper input[name="slider"] {
width: 100%;
}

.stepper button i[class^="bi-"]::before {
display: block;
}

.half-move.current {
font-weight: bold;
}
<style>`
Insert cell
sideLength = Math.min(640, width)
Insert cell
// see: https://github.com/ornicar/chessground/tree/master/assets
chessgroundStylesheets = {
const fetchStylesheet = (name) => {
const url = `https://raw.githubusercontent.com/ornicar/chessground/master/assets/chessground.${name}.css`;
return fetch(url).then(response => response.text());
};
const names = ["base", "brown", "cburnett"]
const requests = names.map(fetchStylesheet);
return Promise.all(requests)
.then(values => values.join("\n"))
.catch(() => FileAttachment("chessground.css").text());
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more