UNPKG

puz-parser

Version:
297 lines (257 loc) 7.86 kB
/** * Quick parser for .puz files * @author Dylan Armstrong * @license MIT * * Information taken from: * http://www.muppetlabs.com/~breadbox/txt/acre.html * and * https://archive.is/N547D */ import type { CellDirection, Grid, Header, Puz, } from './types'; const nMap = (n: number) => String.fromCharCode(n); const stringify = (n: number[]) => n.map(nMap).join(''); const verify = (data: number[]) => { const magic = '41 43 52 4f 53 53 26 44 4f 57 4e 00'.split(' '); for (let i = 0, len = magic.length; i < len; i++) { if (data[i] !== Number.parseInt(magic[i], 16)) { return false; } } return true; }; // Pass in UInt8Array const parse = (data: Uint8Array): Puz => { if (data.length < 52) { return { error: 'data.length < 52', valid: false, }; } try { const maxLength = data.length; const get = (index: number, length?: number) => { const s = []; const useNullDelimiter = typeof length === 'undefined'; const max = index + (length || maxLength); for (let i = index; i < max; i++) { const n = data[i]; if (useNullDelimiter && n === 0x00) { return { index: i, value: s }; } s.push(n); } return { index: max, value: s }; }; /* eslint-disable sort-keys */ const header: Header = { checksum: get(0x00, 0x2).value, magic: get(0x02, 0xc).value, cibChecksum: get(0x0e, 0x2).value, maskedLowChecksum: get(0x10, 0x4).value, maskedHighChecksum: get(0x14, 0x4).value, versionString: get(0x18, 0x4).value, reserved1c: get(0x1c, 0x2).value, unknown: get(0x1e, 0x2).value, reserved20: get(0x20, 0xb).value, bic: get(0x2c, 0x8).value, width: get(0x2c, 0x1).value, height: get(0x2d, 0x1).value, clues: get(0x2e, 0x2).value, unknownMask: get(0x30, 0x2).value, unknown32: get(0x32, 0x2).value, }; /* eslint-enable sort-keys */ const height = header.height[0]; const width = header.width[0]; const { clues: nClues, magic } = header; // The following all require the width & height and the indices // start at end of the previous values const solution = get(0x34, width * height).value; const titleDetails = get(0x34 + 2 * width * height); const title = stringify(titleDetails.value); const authorDetails = get(titleDetails.index + 1); const author = stringify(authorDetails.value); const copyrightDetails = get(authorDetails.index + 1); const copyright = stringify(copyrightDetails.value); let { index } = copyrightDetails; const clues = []; // Is adding the clues the right way to do this? Not sure for (let i = 0, max = nClues[0] + nClues[1]; i < max; i++) { const clue = get(index + 1); clues.push(stringify(clue.value)); ({ index } = clue); } const grid = get(0x34 + width * height, width * height).value; // [y][x] const computedGrid: Grid = new Array(height); for (let i = 0; i < height; i++) { computedGrid[i] = new Array(width); } const isBlackSq = (n: number) => n === 46; const getX = (n: number) => n % width; const getY = (n: number) => Math.floor(n / width); const getStartingCellForAcross = (posY: number, n: number): number => { const newY = getY(n); if (posY !== newY || n < 0 || isBlackSq(grid[n])) { return n + 1; } return getStartingCellForAcross(posY, n - 1); }; const getStartingCellForDown = (posX: number, n: number): number => { const newX = getX(n); if (posX !== newX || n < 0 || isBlackSq(grid[n])) { return n + width; } return getStartingCellForDown(posX, n - width); }; const getDown = (start: number, pos: number): CellDirection => { const posX = getX(pos); const getEndingCell = (n: number): number => { const posY = getY(n); const newX = getX(n); if (posX !== newX || posY > height - 1 || isBlackSq(grid[n])) { return n - width; } return getEndingCell(n + width); }; const end = getEndingCell(pos); const cells = []; for (let i = 0; i < (end - start) / width + 1; i++) { cells.push(start + (i * width)); } return { cells, len: cells.length, }; }; const getAcross = (start: number, pos: number): CellDirection => { // On across, this cannot change const posY = getY(pos); const getEndingCell = (n: number): number => { const posX = getX(n); const newY = getY(n); if (posY !== newY || posX > width || isBlackSq(grid[n])) { return n - 1; } return getEndingCell(n + 1); }; const end = getEndingCell(pos); const cells = []; for (let i = 0; i < end - start + 1; i++) { cells.push(start + i); } return { cells, len: cells.length, }; }; const getClue = (start: number, direction: 'across' | 'down') => { const y = getY(start); const x = getX(start); const dir = computedGrid[y][x]?.[direction]; return dir ? { clue: dir.clue, clueIndex: dir.clueIndex, } : null; }; let clueIndex = 0; let acrossClueIndex = 0; let downClueIndex = 0; let visibleClueIndex = 0; let isStart = false; let x = 0; let y = 0; for (let i = 0, max = grid.length; i < max; i++) { x = i % width; y = Math.floor(i / width); const n = grid[i]; // Empty const isBlack = isBlackSq(n); const startAcross = getStartingCellForAcross(y, i); const startDown = getStartingCellForDown(x, i); const across = getAcross(startAcross, i); const down = getDown(startDown, i); const isAcross = startAcross === i && across.len > 1; const isDown = startDown === i && down.len > 1; isStart = true; if (isBlack) { isStart = false; } else if (isAcross && isDown) { acrossClueIndex = clueIndex; clueIndex += 1; downClueIndex = clueIndex; clueIndex += 1; visibleClueIndex += 1; } else if (isAcross) { acrossClueIndex = clueIndex; clueIndex += 1; visibleClueIndex += 1; } else if (isDown) { downClueIndex = clueIndex; clueIndex += 1; visibleClueIndex += 1; } else { isStart = false; } let acrossClue = null; let downClue = null; if (isAcross) { acrossClue = { clue: clues[acrossClueIndex], clueIndex: visibleClueIndex, }; } else if (!isBlack) { acrossClue = getClue(startAcross, 'across'); } if (isDown) { downClue = { clue: clues[downClueIndex], clueIndex: visibleClueIndex, }; } else if (!isBlack) { downClue = getClue(startDown, 'down'); } computedGrid[y][x] = { across: { cells: across.cells, len: across.len, ...(acrossClue || {}), }, cell: i, clueIndex: visibleClueIndex, down: { cells: down.cells, len: down.len, ...(downClue || {}), }, isAcross, isBlack, isDown, isStart, value: n === 45 ? '' : String.fromCharCode(n), }; } return { author, clues, copyright, grid: computedGrid, header, solution, title, valid: verify(magic), }; } catch (e) { return { error: e.message, valid: false, }; } }; export default parse;