UNPKG

lichess-pgn-viewer

Version:

PGN viewer widget, designed to be embedded in content pages.

140 lines (131 loc) 5 kB
import { Color, makeUci, Position } from 'chessops'; import { scalachessCharPair } from 'chessops/compat'; import { makeFen } from 'chessops/fen'; import { parsePgn, parseComment, PgnNodeData, startingPosition, transform, Node } from 'chessops/pgn'; import { makeSanAndPlay, parseSan } from 'chessops/san'; import { Game } from './game'; import { MoveData, Initial, Players, Player, Comments, Metadata, Clocks, Lichess } from './interfaces'; import { Path } from './path'; class State { constructor( readonly pos: Position, public path: Path, public clocks: Clocks, ) {} clone = () => new State(this.pos.clone(), this.path, { ...this.clocks }); } export const parseComments = (strings: string[]): Comments => { const comments = strings.map(parseComment); const reduceTimes = (times: Array<number | undefined>) => times.reduce<number | undefined>((last, time) => (typeof time == undefined ? last : time), undefined); return { texts: comments.map(c => c.text).filter(t => !!t), shapes: comments.flatMap(c => c.shapes), clock: reduceTimes(comments.map(c => c.clock)), emt: reduceTimes(comments.map(c => c.emt)), }; }; export const makeGame = (pgn: string, lichess: Lichess = false): Game => { const game = parsePgn(pgn)[0] || parsePgn('*')[0]; const start = startingPosition(game.headers).unwrap(); const fen = makeFen(start.toSetup()); const comments = parseComments(game.comments || []); const headers = new Map(Array.from(game.headers, ([key, value]) => [key.toLowerCase(), value])); const metadata = makeMetadata(headers, lichess); const initial: Initial = { fen, turn: start.turn, check: start.isCheck(), pos: start.clone(), comments: comments.texts, shapes: comments.shapes, clocks: { white: metadata.timeControl?.initial || comments.clock, black: metadata.timeControl?.initial || comments.clock, }, }; const moves = makeMoves(start, game.moves, metadata); const players = makePlayers(headers, metadata); return new Game(initial, moves, players, metadata); }; const makeMoves = (start: Position, moves: Node<PgnNodeData>, metadata: Metadata) => transform<PgnNodeData, MoveData, State>(moves, new State(start, Path.root, {}), (state, node, _index) => { const move = parseSan(state.pos, node.san); if (!move) return undefined; const moveId = scalachessCharPair(move); const path = state.path.append(moveId); const san = makeSanAndPlay(state.pos, move); state.path = path; const setup = state.pos.toSetup(); const comments = parseComments(node.comments || []); const startingComments = parseComments(node.startingComments || []); const shapes = [...comments.shapes, ...startingComments.shapes]; const ply = (setup.fullmoves - 1) * 2 + (state.pos.turn === 'white' ? 0 : 1); let clocks = (state.clocks = makeClocks(state.clocks, state.pos.turn, comments.clock)); if (ply < 2 && metadata.timeControl) clocks = { white: metadata.timeControl.initial, black: metadata.timeControl.initial, ...clocks, }; const moveNode: MoveData = { path, ply, move, san, uci: makeUci(move), fen: makeFen(state.pos.toSetup()), turn: state.pos.turn, check: state.pos.isCheck(), comments: comments.texts, startingComments: startingComments.texts, nags: node.nags || [], shapes, clocks, emt: comments.emt, }; return moveNode; }); const makeClocks = (prev: Clocks, turn: Color, clk?: number): Clocks => turn == 'white' ? { ...prev, black: clk } : { ...prev, white: clk }; type Headers = Map<string, string>; function makePlayers(headers: Headers, metadata: Metadata): Players { const get = (color: Color, field: string): string | undefined => { const raw = headers.get(`${color}${field}`); return raw == '?' || raw == '' ? undefined : raw; }; const makePlayer = (color: Color): Player => { const name = get(color, ''); return { name, title: get(color, 'title'), rating: parseInt(get(color, 'elo') || '') || undefined, isLichessUser: metadata.isLichess && !!name?.match(/^[a-z0-9][a-z0-9_-]{0,28}[a-z0-9]$/i), }; }; return { white: makePlayer('white'), black: makePlayer('black'), }; } function makeMetadata(headers: Headers, lichess: Lichess): Metadata { const site = headers.get('source') || headers.get('site'); const tcs = headers .get('timecontrol') ?.split('+') .map(x => parseInt(x)); const timeControl = tcs && tcs[0] ? { initial: tcs[0], increment: tcs[1] || 0, } : undefined; const orientation = headers.get('orientation'); return { externalLink: site && site.match(/^https?:\/\//) ? site : undefined, isLichess: !!(lichess && site?.startsWith(lichess)), timeControl, orientation: orientation === 'white' || orientation === 'black' ? orientation : undefined, }; }