UNPKG

@reis/seki

Version:

Seki – A modern javascript based Go board renderer and player, that is simple to use, extensible, compact and intuitive.

492 lines (408 loc) 9.74 kB
import Converter from './converter.js' import Game from '../game.js' import {get} from '../../helpers/object.js' import {markupTypes} from '../../constants/markup.js' import { charCodeA, sgfStoneColors, sgfGameInfoMap, sgfGameTypes, sgfSetupTypes, sgfMarkupTypes } from '../../constants/sgf.js' //Info properties parsing map const gameInfoConversionMap = { 'record.generator': 'convertGenerator', 'game.type': 'convertGameType', settings: 'convertVariationSettings', } //Node parsing map const nodeParsingMap = { name: 'parseNodeName', move: 'parseMove', markup: 'parseMarkup', setup: 'parseSetup', score: 'parseScore', turn: 'parseTurn', comments: 'parseComments', } //Default options const defaultOptions = { includeVariationSettings: false, includeZeroValues: false, } /** * Converter to SGF */ export default class ConvertToSgf extends Converter { /** * Convert Seki game object to SGF */ convert(game, options = {}) { //Not a game instance if (!(game instanceof Game)) { throw new Error('Not a game instance') } //Get options const { includeVariationSettings, includeZeroValues, } = Object.assign({}, defaultOptions, options || {}) //Get game info and initialize sgf root properties object const info = game.getInfo() const root = { FF: 4, CA: 'UTF-8', } //Keys whose zero values get ignored const ignoreZeroValues = !includeZeroValues ? [ 'XL', 'XR', 'XT', 'XB', 'KM', 'HA', 'TM', 'OT', 'TC', 'TT', ] : [] //Loop SGF game info map for (const key in sgfGameInfoMap) { //Get prop and value const prop = sgfGameInfoMap[key] const value = get(info, prop) //No value if (typeof value === 'undefined') { continue } //Ignore variation settings? if (!includeVariationSettings && key === 'ST') { continue } //Zero value if (value === 0 && ignoreZeroValues.includes(key)) { continue } //Parser present? if (gameInfoConversionMap[prop]) { root[key] = this[gameInfoConversionMap[prop]](value, game) } //Otherwise, just copy over else if (value !== '') { root[key] = this.escapeSgf(value) } } //Append data this.appendGenerator(root) this.appendSource(root, game) this.appendBoardSize(root, game) this.appendPlayers(root, game) //Initialize let sgf = '' //Append for (const key in root) { sgf += `\n` sgf += this.makeSgfGroup(key, root[key]) } //Append game tree sgf += `\n;` sgf += this.parseNode(game.root) //Return return `(;${sgf}\n)` } /** * Append generator info */ appendGenerator(root) { root.AP = this.getGeneratorSignature() } /** * Append source info */ appendSource(root, game) { //Get source data const name = game.getSourceName() const url = game.getSourceUrl() const copyright = game.getSourceCopyright() //Set if (name && url) { root.SO = `${name || ''}, ${url || ''}` } else if (name) { root.SO = name } else if (url) { root.SO = url } if (copyright) { root.CP = copyright } } /** * Append baord size */ appendBoardSize(root, game) { //Get size properties const {width, height} = game.getBoardSize() //Get key const key = 'SZ' //Width and height which are the same if (width && height && width === height) { root[key] = width } //Different dimensions else if (width && height) { root[key] = `${width}:${height}` } //Can't determine size else { root[key] = 0 } } /** * Append players */ appendPlayers(root, game) { //Get players const players = game.getPlayers() const colors = Object.keys(players) //Loop for (const color of colors) { //Get color const c = this.convertColor(color) if (!c) { continue } //Get data const {name, rank, team} = players[color] //Set properties root[`P${c}`] = this.escapeSgf(name || '') if (rank) { root[`${c}R`] = this.escapeSgf(rank) } if (team) { root[`${c}T`] = this.escapeSgf(team) } } } /************************************************************************** * Tree and node parsing ***/ /** * Parse a node */ parseNode(node) { //Initialize let sgf = '' //Loop node properties for (const key in node) { if (nodeParsingMap[key]) { sgf += this[nodeParsingMap[key]](node[key]) } } //Multiple children if (node.hasMultipleChildren()) { for (const child of node.children) { sgf += '(\n;' sgf += this.parseNode(child) sgf += '\n)' } } //Just one child else if (node.hasChildren()) { //Already have content? Add separator if (sgf !== '') { sgf += ';' } //Add node contents sgf += this.parseNode(node.getChild(), sgf) } //Return value return sgf } /** * Move parser */ parseMove(move) { //Determine color const color = this.convertColor(move.color) //Pass move if (move.pass) { return this.makeSgfGroup(color) } //Determine coors const coords = this.extractCoordinates(move) return this.makeSgfGroup(color, coords) } /** * Setup parser */ parseSetup(setup) { //Initialise coordinate groups const groups = {} //Loop over setup instructions for (const obj of setup) { //Get SGF setup type and extract coordinates const key = this.getMappedValue(obj.type, sgfSetupTypes) const coords = this.extractCoordinates(obj.coords) //Initialise group and add entry groups[key] = groups[key] || [] groups[key].push(...coords) } //Now convert by type to SGF let sgf = '' for (const key in groups) { sgf += this.makeSgfGroup(key, groups[key]) } //Return return sgf } /** * Markup parser */ parseMarkup(markup) { //Initialise coordinate groups const groups = {} //Loop over markup instructions for (const obj of markup) { //Get SGF markup type and extract coordinates const isLabel = (obj.type === markupTypes.LABEL) const key = this.getMappedValue(obj.type, sgfMarkupTypes) const coords = this.extractCoordinates(obj.coords, isLabel) //Initialise group groups[key] = groups[key] || [] groups[key].push(...coords) } //Now convert by type to SGF let sgf = '' for (const key in groups) { sgf += this.makeSgfGroup(key, groups[key]) } //Return return sgf } /** * Score parser */ parseScore(score) { //Initialise sgf let sgf = '' //Loop entries for (const entry of score) { const color = this.convertColor(entry.color) const key = `T${color}` const coords = this.extractCoordinates(entry.coordinates) sgf += this.makeSgfGroup(key, coords) } //Return return sgf } /** * Turn parser */ parseTurn(turn) { return this.makeSgfGroup('PL', this.convertColor(turn)) } /** * Comments parser */ parseComments(comments) { //No comments if (!comments) { return '' } //Not an array if (!Array.isArray(comments)) { comments = [comments] } //Filter out empty comments comments = comments.filter(Boolean) if (comments.length === 0) { return '' } //Make group return this.makeSgfGroup('C', comments, true) } /** * Node name parser */ parseNodeName(nodeName) { if (!nodeName) { return '' } return this.makeSgfGroup('N', nodeName, true) } /***************************************************************************** * Parsing helpers ***/ /** * Helper to convert to SGF coordinates */ extractCoordinates(obj, isLabel = false) { if (Array.isArray(obj)) { return obj.map(entry => this.extractCoordinates(entry, isLabel)) } if (obj.x < 0 || obj.y < 0) { throw new Error(`Invalid coordinates: (${obj.x},${obj.y})`) } const x = String.fromCharCode(charCodeA + obj.x) const y = String.fromCharCode(charCodeA + obj.y) if (isLabel && obj.text) { return `${x}${y}:${obj.text}` } return `${x}${y}` } /** * Convert a string color value to a numeric color value */ convertColor(color) { return this.getMappedValue(color, sgfStoneColors) } /** * Game type parser */ convertGameType(type) { return this.getMappedValue(type, sgfGameTypes) } /** * Generator parser */ convertGenerator(generator) { return generator .split(' v') .join(':') } /** * Settings parser */ convertVariationSettings(settings) { return ( (settings.showVariations ? 0 : 2) + (settings.showSiblingVariations ? 1 : 0) ) } /** * Helper to escape SGF info */ escapeSgf(str) { if (typeof str === 'string') { return str .replace(/\\/g, '\\\\') .replace(/]/g, '\\]') } return str } /** * Helper to make an SGF key */ makeSgfGroup(key, value = '', escape = false) { if (Array.isArray(value)) { if (value.length === 0) { return '' } value = value .map(value => escape ? this.escapeSgf(value) : value) .join('][') } else if (escape) { value = this.escapeSgf(value) } return `${key}[${value}]` } }