UNPKG

@logic-pad/core

Version:
467 lines (466 loc) 18 kB
import GridData from '../grid.js'; import GridConnections from '../gridConnections.js'; import TileData from '../tile.js'; import { ConfigType } from '../config.js'; import { Color, DIRECTIONS, ORIENTATIONS, Orientation, directionToggle, orientationToggle, } from '../primitives.js'; import { array, escape, unescape } from '../dataHelper.js'; import { allRules } from '../rules/index.js'; import { allSymbols } from '../symbols/index.js'; import SerializerBase from './serializerBase.js'; import { ControlLine, Row } from '../rules/musicControlLine.js'; import GridZones from '../gridZones.js'; const OFFSETS = [ { x: 0, y: -1 }, { x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }, ]; const orientationChars = { [Orientation.Up]: 'u', [Orientation.UpRight]: 'x', [Orientation.Right]: 'r', [Orientation.DownRight]: 'z', [Orientation.Down]: 'd', [Orientation.DownLeft]: 'y', [Orientation.Left]: 'l', [Orientation.UpLeft]: 'w', }; export default class SerializerV0 extends SerializerBase { constructor() { super(...arguments); Object.defineProperty(this, "version", { enumerable: true, configurable: true, writable: true, value: 0 }); } stringifyTile(tile) { if (!tile.exists) return '.'; const char = tile.color === Color.Gray ? 'n' : tile.color === Color.Dark ? 'b' : 'w'; return tile.fixed ? char.toUpperCase() : char; } parseTile(str) { return TileData.create(str); } stringifyControlLine(line) { const result = []; result.push(`c${line.column}`); if (line.bpm !== null) result.push(`b${line.bpm}`); if (line.pedal !== null) result.push(`p${line.pedal ? '1' : '0'}`); if (line.checkpoint) result.push('s'); result.push(`r${line.rows .map(row => `v${row.velocity ?? ''}n${row.note ?? ''}`) .join(',')}`); return result.join('|'); } parseControlLine(str) { let column = null; let bpm = null; let pedal = null; let checkpoint = false; const rows = []; const data = str.split('|'); for (const entry of data) { const key = entry.charAt(0); const value = entry.slice(1); switch (key) { case 'c': column = value === '' ? null : Number(value); break; case 'b': bpm = value === '' ? null : Number(value); break; case 'p': pedal = value === '1' ? true : value === '0' ? false : null; break; case 's': checkpoint = true; break; case 'r': rows.push(...value.split(',').map(row => { const match = row.match(/^v([\d.]*?)n(.*)$/); if (!match) return new Row(null, null); const [, velocity, note] = match; return new Row(note === '' ? null : note, velocity === '' ? null : Number(velocity)); })); break; } } return new ControlLine(column ?? 0, bpm, pedal, checkpoint, rows); } stringifyConfig(instruction, config) { switch (config.type) { case ConfigType.Boolean: return (config.field + '=' + (instruction[config.field] ? '1' : '0')); case ConfigType.Number: case ConfigType.Color: case ConfigType.Comparison: case ConfigType.Wrapping: case ConfigType.Direction: case ConfigType.Orientation: return (config.field + '=' + String(instruction[config.field])); case ConfigType.NullableBoolean: return (config.field + '=' + (instruction[config.field] === null ? '' : instruction[config.field] ? '1' : '0')); case ConfigType.NullableNumber: return (config.field + '=' + (instruction[config.field] === null ? '' : String(instruction[config.field]))); case ConfigType.DirectionToggle: return (config.field + '=' + DIRECTIONS.filter(dir => instruction[config.field][dir]) .map(x => orientationChars[x]) .join('')); case ConfigType.OrientationToggle: return (config.field + '=' + ORIENTATIONS.filter(dir => instruction[config.field][dir]) .map(x => orientationChars[x]) .join('')); case ConfigType.String: case ConfigType.Icon: return (config.field + '=' + escape(String(instruction[config.field]))); case ConfigType.NullableNote: return (config.field + '=' + escape(instruction[config.field] === null ? '' : escape(String(instruction[config.field])))); case ConfigType.Tile: case ConfigType.Grid: return (config.field + '=' + escape(this.stringifyGrid(instruction[config.field]))); case ConfigType.NullableGrid: return (config.field + '=' + escape(instruction[config.field] === null ? '' : this.stringifyGrid(instruction[config.field]))); case ConfigType.ControlLines: return (config.field + '=' + escape(instruction[config.field] .map(line => this.stringifyControlLine(line)) .join(':'))); case ConfigType.SolvePath: return (config.field + '=' + escape(instruction[config.field] ?.map(pos => `${pos.x}_${pos.y}`) .join('/') ?? '')); } } parseConfig(configs, entry) { const [key, value] = entry.split('='); const config = configs.find(x => x.field === key); if (!config) { console.warn(`Unknown config: ${key} when parsing ${entry}`); return [key, value]; } switch (config.type) { case ConfigType.Boolean: return [config.field, value === '1']; case ConfigType.NullableBoolean: return [config.field, value === '' ? null : value === '1']; case ConfigType.Number: return [config.field, Number(value)]; case ConfigType.NullableNumber: return [config.field, value === '' ? null : Number(value)]; case ConfigType.Color: return [config.field, value]; case ConfigType.Comparison: return [config.field, value]; case ConfigType.Wrapping: return [config.field, value]; case ConfigType.Direction: return [config.field, value]; case ConfigType.DirectionToggle: { const toggle = directionToggle(); for (const dir of DIRECTIONS) { toggle[dir] = value.includes(orientationChars[dir]); } return [config.field, toggle]; } case ConfigType.Orientation: return [config.field, value]; case ConfigType.OrientationToggle: { const toggle = orientationToggle(); for (const dir of ORIENTATIONS) { toggle[dir] = value.includes(orientationChars[dir]); } return [config.field, toggle]; } case ConfigType.String: case ConfigType.Icon: return [config.field, unescape(value)]; case ConfigType.Tile: case ConfigType.Grid: return [config.field, this.parseGrid(unescape(value))]; case ConfigType.NullableGrid: return [ config.field, value === '' ? null : this.parseGrid(unescape(value)), ]; case ConfigType.ControlLines: return [ config.field, unescape(value) .split(':') .map(line => this.parseControlLine(line)), ]; case ConfigType.NullableNote: return [config.field, value === '' ? null : unescape(value)]; case ConfigType.SolvePath: return [ config.field, value === '' ? [] : value.split('/').map(pos => { const [x, y] = pos.split('_'); return { x: Number(x), y: Number(y) }; }), ]; } } stringifyInstruction(instruction) { return `${instruction.id},${instruction.configs?.map(config => this.stringifyConfig(instruction, config)).join(',') ?? ''}`; } stringifyRule(rule) { return this.stringifyInstruction(rule); } stringifySymbol(symbol) { return this.stringifyInstruction(symbol); } parseRule(str) { const [id, ...entries] = str.split(','); const instruction = allRules.get(id); if (!instruction) throw new Error(`Unknown rule: ${id}`); const configs = instruction.configs; if (configs == null) return instruction.copyWith({}); return instruction.copyWith(Object.fromEntries(entries .filter(entry => entry !== '') .map(entry => this.parseConfig(configs, entry)))); } parseSymbol(str) { const [id, ...entries] = str.split(','); const instruction = allSymbols.get(id); if (!instruction) throw new Error(`Unknown symbol: ${id}`); const configs = instruction.configs; if (configs == null) return instruction.copyWith({}); return instruction.copyWith(Object.fromEntries(entries.map(entry => this.parseConfig(configs, entry)))); } stringifyConnections(connections) { const maxX = connections.edges.reduce((max, edge) => Math.max(max, edge.x1, edge.x2), 0); const maxY = connections.edges.reduce((max, edge) => Math.max(max, edge.y1, edge.y2), 0); const result = array(maxX + 1, maxY + 1, () => '.'); for (let y = 0; y <= maxY; y++) { for (let x = 0; x <= maxX; x++) { if (result[y][x] !== '.') { continue; } const tiles = connections.getConnectedTiles({ x, y }); if (tiles.length < 2) { continue; } const existingChars = []; for (const { x: tx, y: ty } of tiles) { for (const { x: dx, y: dy } of OFFSETS) { if (tx + dx > maxX || ty + dy > maxY || tx + dx < 0 || ty + dy < 0) { continue; } if (result[ty + dy][tx + dx] !== '.') existingChars.push(result[ty + dy][tx + dx]); } } let char = 'A'; while (existingChars.includes(char)) { char = String.fromCharCode(char.charCodeAt(0) + 1); } for (const connection of tiles) { result[connection.y][connection.x] = char; } } } return `C${maxX + 1}:${result.map(row => row.join('')).join('')}`.replace(/\.+$/, ''); } parseConnections(input) { if (!input.startsWith('C')) { throw new Error('Invalid grid connections\n' + input); } const [size, data] = input.slice(1).split(':'); const width = Number(size); const tiles = array(width, Math.ceil(data.length / width), (x, y) => data[y * width + x]); return GridConnections.create(tiles.map(row => row.join(''))); } stringifyZones(zones) { return `Z${zones.edges.map(edge => `${edge.x1}_${edge.y1}_${edge.x2 - edge.x1}_${edge.y2 - edge.y1}`).join(':')}`; } parseZones(input) { if (!input.startsWith('Z')) { throw new Error('Invalid grid zones\n' + input); } const data = input.slice(1).split(':'); return new GridZones(data.map(entry => { const [x1, y1, w, h] = entry.split('_').map(Number); return { x1, y1, x2: x1 + w, y2: y1 + h }; })); } stringifyTiles(tiles) { return `T${tiles[0]?.length ?? 0}:${tiles.map(row => row.map(tile => this.stringifyTile(tile)).join('')).join('')}`; } parseTiles(input) { if (!input.startsWith('T')) { throw new Error('Invalid grid data\n' + input); } const [size, data] = input.slice(1).split(':'); const width = Number(size); return array(width, Math.ceil(data.length / width), (x, y) => this.parseTile(data.charAt(y * width + x))); } stringifyRules(rules) { return `R${rules.map(rule => this.stringifyRule(rule)).join(':')}`; } parseRules(input) { if (!input.startsWith('R')) { throw new Error('Invalid rules\n' + input); } return input .slice(1) .split(':') .filter(rule => rule !== '') .map(rule => this.parseRule(rule)); } stringifySymbols(symbols) { return `S${Array.from(symbols.values()) .flat() .map(symbol => this.stringifySymbol(symbol)) .join(':')}`; } parseSymbols(input) { if (!input.startsWith('S')) { throw new Error('Invalid symbols\n' + input); } const symbols = new Map(); input .slice(1) .split(':') .filter(symbol => symbol !== '') .forEach(symbol => { const parsed = this.parseSymbol(symbol); if (symbols.has(parsed.id)) { symbols.set(parsed.id, [...symbols.get(parsed.id), parsed]); } else { symbols.set(parsed.id, [parsed]); } }); return symbols; } stringifyGrid(grid) { const data = [ this.stringifyTiles(grid.tiles), this.stringifyConnections(grid.connections), this.stringifyZones(grid.zones), this.stringifySymbols(grid.symbols), this.stringifyRules(grid.rules), ]; return `${grid.width}x${grid.height}|${data.join('|')}`; } parseGrid(input) { const data = input.split('|'); let width; let height; let tiles; let connections; let zones; let symbols; let rules; for (const d of data) { if (/^\d+x\d+$/.test(d)) { [width, height] = d.split('x').map(Number); } else if (d.startsWith('T')) { tiles = this.parseTiles(d); } else if (d.startsWith('C')) { connections = this.parseConnections(d); } else if (d.startsWith('Z')) { zones = this.parseZones(d); } else if (d.startsWith('S')) { symbols = this.parseSymbols(d); } else if (d.startsWith('R')) { rules = this.parseRules(d); } else { throw new Error(`Invalid data: ${d}`); } } return GridData.create(width ?? tiles?.[0].length ?? 0, height ?? tiles?.length ?? 0, tiles, connections, zones, symbols, rules); } stringifyPuzzle(puzzle) { let grid = puzzle.grid; if (puzzle.solution !== null) { const tiles = array(puzzle.grid.width, puzzle.grid.height, (x, y) => { const tile = puzzle.grid.getTile(x, y); const solutionTile = puzzle.solution.getTile(x, y); return tile.exists && !tile.fixed && solutionTile.exists && solutionTile.color !== Color.Gray ? tile.copyWith({ color: puzzle.solution.tiles[y][x].color, }) : tile; }); grid = puzzle.grid.copyWith({ tiles }); } return JSON.stringify({ title: puzzle.title, grid: this.stringifyGrid(grid), difficulty: puzzle.difficulty, link: puzzle.link, author: puzzle.author, description: puzzle.description, }); } parsePuzzle(input) { const { grid: gridString, ...metadata } = JSON.parse(input); const grid = this.parseGrid(gridString); const reset = grid.resetTiles(); return { ...metadata, grid: reset, solution: grid.colorEquals(reset) ? null : grid, }; } }