import { useEffect, useRef, useState } from "react"; import type { PuzzleType } from "../lib/api"; type Props = { puzzleType: PuzzleType; problemAscii: string; boardAscii: string; onChange: (nextAscii: string) => void; }; type BridgesOpportunity = { id: string; kind: "horizontal" | "vertical"; cells: Array<[number, number]>; from: [number, number]; to: [number, number]; }; type LoopyEdge = { id: string; kind: "horizontal" | "vertical"; row: number; col: number; boardRow: number; boardCol: number; }; type GalaxyBoundary = { id: string; kind: "horizontal" | "vertical"; row: number; col: number; boardRow: number; boardCol: number; }; type GalaxyDot = { rowCoord: number; colCoord: number; }; type PatternRow = { prefix: string; cells: string[]; border: string; }; type UndeadRow = { left: string; cells: string[]; right: string; }; const FLOW_FREE_COLORS = [ "#f15b5d", "#f5a623", "#f6df5a", "#7ed957", "#36cfc9", "#4b8ef7", "#7a5cff", "#ff8bd1", "#7d5a50", "#5f6a6b", ]; function splitLines(board: string): string[] { return board.replace(/\r/g, "").split("\n"); } function cloneGrid(lines: string[]): string[][] { return lines.map((line) => [...line]); } function joinGrid(grid: string[][]): string { return grid.map((row) => row.join("")).join("\n"); } function padLines(lines: string[], width: number): string[] { return lines.map((line) => line.padEnd(width, " ")); } function isClueChar(char: string): boolean { return /[0-9A-G]/.test(char); } function addBridgeOpportunityForCell( cellToOpportunities: Map, r: number, c: number, opportunity: BridgesOpportunity, ) { const key = `${r}:${c}`; cellToOpportunities.set(key, [...(cellToOpportunities.get(key) ?? []), opportunity]); } function bridgeSymbol(opportunity: BridgesOpportunity, level: 1 | 2): string { return opportunity.kind === "horizontal" ? (level === 2 ? "=" : "-") : level === 2 ? '"' : "|"; } function bridgeLevelFromChar(opportunity: BridgesOpportunity, char: string): 0 | 1 | 2 { if (opportunity.kind === "horizontal") { return char === "=" ? 2 : char === "-" ? 1 : 0; } return char === '"' ? 2 : char === "|" ? 1 : 0; } function bridgeOpportunitiesShareCell(a: BridgesOpportunity, b: BridgesOpportunity): boolean { const bCells = new Set(b.cells.map(([r, c]) => `${r}:${c}`)); return a.cells.some(([r, c]) => bCells.has(`${r}:${c}`)); } function parseBridges(problemAscii: string) { const lines = splitLines(problemAscii); const opportunities: BridgesOpportunity[] = []; const cellToOpportunities = new Map(); for (let r = 0; r < lines.length; r += 1) { for (let c = 0; c < lines[r].length; c += 1) { if (!isClueChar(lines[r][c])) { continue; } let next = c + 1; while (next < lines[r].length && lines[r][next] === ".") { next += 1; } if (next < lines[r].length && isClueChar(lines[r][next]) && next > c + 1) { const opp: BridgesOpportunity = { id: `h-${r}-${c}-${next}`, kind: "horizontal", cells: [], from: [r, c], to: [r, next], }; for (let cell = c + 1; cell < next; cell += 1) { opp.cells.push([r, cell]); addBridgeOpportunityForCell(cellToOpportunities, r, cell, opp); } opportunities.push(opp); } let nextRow = r + 1; while (nextRow < lines.length && c < lines[nextRow].length && lines[nextRow][c] === ".") { nextRow += 1; } if ( nextRow < lines.length && c < lines[nextRow].length && isClueChar(lines[nextRow][c]) && nextRow > r + 1 ) { const opp: BridgesOpportunity = { id: `v-${r}-${c}-${nextRow}`, kind: "vertical", cells: [], from: [r, c], to: [nextRow, c], }; for (let cell = r + 1; cell < nextRow; cell += 1) { opp.cells.push([cell, c]); addBridgeOpportunityForCell(cellToOpportunities, cell, c, opp); } opportunities.push(opp); } } } return { lines, opportunities, cellToOpportunities }; } function bridgeLevelFromCells(opportunity: BridgesOpportunity, boardLines: string[]): 0 | 1 | 2 { let level: 0 | 1 | 2 = 0; for (const [r, c] of opportunity.cells) { const cellLevel = bridgeLevelFromChar(opportunity, boardLines[r]?.[c] ?? "."); if (cellLevel === 2) { return 2; } if (cellLevel === 1) { level = 1; } } return level; } function sortedBridgeCandidates(opportunities: BridgesOpportunity[]): BridgesOpportunity[] { return [...opportunities].sort((a, b) => { if (a.kind !== b.kind) { return a.kind === "horizontal" ? -1 : 1; } return a.id.localeCompare(b.id); }); } function clearBridgeConflicts( parsed: ReturnType, states: Record, selected: BridgesOpportunity, ) { for (const opportunity of parsed.opportunities) { if (opportunity.id !== selected.id && bridgeOpportunitiesShareCell(opportunity, selected)) { states[opportunity.id] = 0; } } } function bridgeClickCountForOpportunity( opportunities: BridgesOpportunity[], opportunity: BridgesOpportunity, level: 0 | 1 | 2, ): number { if (level === 0 || opportunities.length <= 1) { return level; } const index = sortedBridgeCandidates(opportunities).findIndex( (candidate) => candidate.id === opportunity.id, ); return index === -1 ? level : index * 2 + level; } function cycleBridgesOpportunities( parsed: ReturnType, states: Record, opportunities: BridgesOpportunity[], ): Record { if (opportunities.length === 0) { return states; } const nextStates = { ...states }; const candidates = sortedBridgeCandidates(opportunities); if (candidates.length === 1) { const opportunity = candidates[0]; const level = (((nextStates[opportunity.id] ?? 0) + 1) % 3) as 0 | 1 | 2; nextStates[opportunity.id] = level; if (level > 0) { clearBridgeConflicts(parsed, nextStates, opportunity); } return nextStates; } const sequence: Array<{ opportunity: BridgesOpportunity; level: 1 | 2 } | null> = [ ...candidates.flatMap((opportunity) => [ { opportunity, level: 1 as const }, { opportunity, level: 2 as const }, ]), null, ]; const currentIndex = sequence.findIndex((entry) => candidates.every((opportunity) => { const expected = entry?.opportunity.id === opportunity.id ? entry.level : 0; return (nextStates[opportunity.id] ?? 0) === expected; }), ); const fallbackIndex = sequence.findIndex( (entry) => entry !== null && (nextStates[entry.opportunity.id] ?? 0) === entry.level, ); const safeCurrentIndex = currentIndex === -1 ? (fallbackIndex === -1 ? sequence.length - 1 : fallbackIndex) : currentIndex; const nextEntry = sequence[(safeCurrentIndex + 1) % sequence.length]; for (const opportunity of candidates) { nextStates[opportunity.id] = 0; } if (nextEntry) { nextStates[nextEntry.opportunity.id] = nextEntry.level; clearBridgeConflicts(parsed, nextStates, nextEntry.opportunity); } return nextStates; } function bridgesBoardFromStates( problemAscii: string, states: Record, ): string { const parsed = parseBridges(problemAscii); const grid = cloneGrid(parsed.lines); for (const opportunity of parsed.opportunities) { const level = states[opportunity.id] ?? 0; // The grid starts as a fresh copy of the problem (non-clue cells are "."), // so a level-0 opportunity has nothing to add. Skip it: writing "." here // would clobber the crossing cell of an *active* perpendicular bridge that // shares this cell (e.g. a vertical bridge passing through a cell also owned // by an unused horizontal opportunity), leaving a gap in the drawn bridge. if (level === 0) { continue; } const symbol = bridgeSymbol(opportunity, level); for (const [r, c] of opportunity.cells) { grid[r][c] = symbol; } } return joinGrid(grid); } function bridgesStatesFromBoard( problemAscii: string, boardAscii: string, ): Record { const parsed = parseBridges(problemAscii); const boardLines = splitLines(boardAscii); const states: Record = {}; for (const opportunity of parsed.opportunities) { states[opportunity.id] = bridgeLevelFromCells(opportunity, boardLines); } return states; } function parseLoopy(problemAscii: string) { const lines = splitLines(problemAscii); const rows = Math.max(0, Math.floor((lines.length - 3) / 2)); const cols = Math.max(0, Math.floor(((lines[0]?.length ?? 0) - 3) / 2)); const clues = new Map(); const horizontalEdges: LoopyEdge[] = []; const verticalEdges: LoopyEdge[] = []; for (let r = 0; r < rows; r += 1) { for (let c = 0; c < cols; c += 1) { const char = lines[2 + 2 * r]?.[2 + 2 * c] ?? " "; if (/\d/.test(char)) { clues.set(`${r}:${c}`, char); } } } for (let r = 0; r <= rows; r += 1) { for (let c = 0; c < cols; c += 1) { horizontalEdges.push({ id: `h-${r}-${c}`, kind: "horizontal", row: r, col: c, boardRow: 1 + 2 * r, boardCol: 2 + 2 * c, }); } } for (let r = 0; r < rows; r += 1) { for (let c = 0; c <= cols; c += 1) { verticalEdges.push({ id: `v-${r}-${c}`, kind: "vertical", row: r, col: c, boardRow: 2 + 2 * r, boardCol: 1 + 2 * c, }); } } return { lines, rows, cols, clues, horizontalEdges, verticalEdges }; } function loopyEdgeStates( problemAscii: string, boardAscii: string, ): Record { const puzzle = parseLoopy(problemAscii); const width = puzzle.lines[0]?.length ?? 0; const boardLines = padLines(splitLines(boardAscii), width); const states: Record = {}; for (const edge of [...puzzle.horizontalEdges, ...puzzle.verticalEdges]) { const char = boardLines[edge.boardRow]?.[edge.boardCol] ?? " "; states[edge.id] = char === "-" || char === "|" ? "line" : char === "x" ? "blocked" : "unknown"; } return states; } function loopyBoardFromStates( problemAscii: string, states: Record, ): string { const puzzle = parseLoopy(problemAscii); const width = puzzle.lines[0]?.length ?? 0; const grid = cloneGrid(padLines(puzzle.lines, width)); for (const edge of puzzle.horizontalEdges) { const state = states[edge.id] ?? "blocked"; grid[edge.boardRow][edge.boardCol] = state === "line" ? "-" : state === "blocked" ? "x" : " "; } for (const edge of puzzle.verticalEdges) { const state = states[edge.id] ?? "blocked"; grid[edge.boardRow][edge.boardCol] = state === "line" ? "|" : state === "blocked" ? "x" : " "; } return joinGrid(grid); } function parseGalaxies(problemAscii: string) { const lines = splitLines(problemAscii); const rows = Math.max(0, Math.floor(((lines.length ?? 0) - 1) / 2)); const cols = Math.max(0, Math.floor(((lines[0]?.length ?? 0) - 1) / 2)); const horizontalBoundaries: GalaxyBoundary[] = []; const verticalBoundaries: GalaxyBoundary[] = []; const dots: GalaxyDot[] = []; for (let boardRow = 0; boardRow < lines.length; boardRow += 1) { const line = lines[boardRow] ?? ""; for (let boardCol = 0; boardCol < line.length; boardCol += 1) { if (line[boardCol] === "o") { dots.push({ rowCoord: boardRow, colCoord: boardCol }); } } } for (let r = 1; r < rows; r += 1) { for (let c = 0; c < cols; c += 1) { if ((lines[2 * r]?.[1 + 2 * c] ?? " ") === "o") { continue; } horizontalBoundaries.push({ id: `h-${r}-${c}`, kind: "horizontal", row: r, col: c, boardRow: 2 * r, boardCol: 1 + 2 * c, }); } } for (let r = 0; r < rows; r += 1) { for (let c = 1; c < cols; c += 1) { if ((lines[1 + 2 * r]?.[2 * c] ?? " ") === "o") { continue; } verticalBoundaries.push({ id: `v-${r}-${c}`, kind: "vertical", row: r, col: c, boardRow: 1 + 2 * r, boardCol: 2 * c, }); } } return { lines, rows, cols, horizontalBoundaries, verticalBoundaries, dots }; } function galaxiesBoundaryStates( problemAscii: string, boardAscii: string, ): Record { const puzzle = parseGalaxies(problemAscii); const width = puzzle.lines[0]?.length ?? 0; const boardLines = padLines(splitLines(boardAscii), width); const states: Record = {}; for (const boundary of [...puzzle.horizontalBoundaries, ...puzzle.verticalBoundaries]) { const char = boardLines[boundary.boardRow]?.[boundary.boardCol] ?? " "; states[boundary.id] = char === "-" || char === "|"; } return states; } function galaxiesBoardFromStates(problemAscii: string, states: Record): string { const puzzle = parseGalaxies(problemAscii); const width = puzzle.lines[0]?.length ?? 0; const grid = cloneGrid(padLines(puzzle.lines, width)); for (const boundary of puzzle.horizontalBoundaries) { grid[boundary.boardRow][boundary.boardCol] = states[boundary.id] ? "-" : " "; } for (const boundary of puzzle.verticalBoundaries) { grid[boundary.boardRow][boundary.boardCol] = states[boundary.id] ? "|" : " "; } return joinGrid(grid); } function parsePattern(boardAscii: string) { const lines = splitLines(boardAscii); const firstContentIndex = lines.findIndex((line) => line.includes("|")); const topLines = lines.slice(0, firstContentIndex); const rows: PatternRow[] = []; for (let idx = firstContentIndex; idx < lines.length; idx += 2) { const content = lines[idx]; const border = lines[idx + 1]; if (!content || !border) { break; } const firstBar = content.indexOf("|"); const prefix = content.slice(0, firstBar); const parts = content.slice(firstBar).split("|").slice(1, -1); rows.push({ prefix, cells: parts, border }); } return { topLines, rows }; } function normalizePatternAscii(boardAscii: string) { const template = parsePattern(boardAscii); let changed = false; for (const row of template.rows) { for (let index = 0; index < row.cells.length; index += 1) { if (row.cells[index] === " ") { row.cells[index] = ".."; changed = true; } } } return changed ? patternToAscii(template) : boardAscii; } function parsePatternColumnClues(template: ReturnType): string[][] { const topBorder = template.topLines.find((line) => line.includes("+")); const clueLines = template.topLines.filter((line) => !line.includes("+")); const firstRow = template.rows[0]; if (!topBorder || !firstRow) { return []; } const firstBar = topBorder.indexOf("+"); const columnCount = firstRow.cells.length; const columns: string[][] = Array.from({ length: columnCount }, () => []); for (const line of clueLines) { for (let col = 0; col < columnCount; col += 1) { const cellSlice = line.slice(firstBar + 1 + col * 3, firstBar + 3 + col * 3).trim(); if (cellSlice) { columns[col].push(cellSlice); } } } return columns; } function patternToAscii(template: ReturnType): string { const lines = [...template.topLines]; for (const row of template.rows) { lines.push(`${row.prefix}|${row.cells.join("|")}|`); lines.push(row.border); } return lines.join("\n"); } function parseUndead(boardAscii: string) { const lines = splitLines(boardAscii); const header = lines[0] ?? ""; const topLine = lines[2] ?? ""; const bottomLine = lines.at(-1) ?? ""; const rows: UndeadRow[] = lines.slice(3, -1).map((line) => { const tokens = line.trim().split(/\s+/); return { left: tokens[0] ?? "", cells: tokens.slice(1, -1), right: tokens.at(-1) ?? "", }; }); return { header, topLine, bottomLine, rows }; } function undeadToAscii(template: ReturnType): string { const lines = [template.header, "", template.topLine]; for (const row of template.rows) { lines.push(` ${row.left} ${row.cells.join(" ")} ${row.right}`); } lines.push(template.bottomLine); return lines.join("\n"); } function legendColor(letter: string) { return FLOW_FREE_COLORS[(letter.charCodeAt(0) - 65) % FLOW_FREE_COLORS.length]; } function BridgesEditor({ problemAscii, boardAscii, onChange }: Omit) { const parsed = parseBridges(problemAscii); const [states, setStates] = useState(() => bridgesStatesFromBoard(problemAscii, boardAscii)); const lastEmittedBoard = useRef(null); const renderedBoardAscii = bridgesBoardFromStates(problemAscii, states); const boardLines = splitLines(renderedBoardAscii); useEffect(() => { if (lastEmittedBoard.current === boardAscii) { return; } setStates(bridgesStatesFromBoard(problemAscii, boardAscii)); lastEmittedBoard.current = null; }, [problemAscii, boardAscii]); function cycle(opportunities: BridgesOpportunity[]) { const nextStates = cycleBridgesOpportunities(parsed, states, opportunities); const nextBoard = bridgesBoardFromStates(problemAscii, nextStates); setStates(nextStates); lastEmittedBoard.current = nextBoard; onChange(nextBoard); } return (
Click a route between islands to cycle no bridge, single bridge, and double bridge.
{parsed.lines.flatMap((line, r) => [...line].map((char, c) => { const opportunities = parsed.cellToOpportunities.get(`${r}:${c}`) ?? []; const boardChar = boardLines[r]?.[c] ?? char; const className = isClueChar(char) ? "board-cell fixed island-cell" : opportunities.length > 0 ? "board-cell bridge-cell" : "board-cell water"; const display = boardChar === "." ? "" : boardChar === '"' ? "‖" : boardChar === "=" ? "═" : boardChar; return ( ); }), )}
); } function FlowFreeEditor({ problemAscii, boardAscii, onChange }: Omit) { const problemLines = splitLines(problemAscii); const boardLines = splitLines(boardAscii); const letters = [...new Set(problemAscii.replace(/[^A-Z]/g, "").split(""))].sort(); const [activeColor, setActiveColor] = useState(letters[0] ?? "A"); function updateCell(r: number, c: number) { if (problemLines[r][c] !== ".") { setActiveColor(problemLines[r][c]); return; } const grid = cloneGrid(boardLines); grid[r][c] = grid[r][c] === activeColor ? "." : activeColor; onChange(joinGrid(grid)); } return (
Pick a color, then fill open cells. Clicking an endpoint picks that color.
{letters.map((letter) => ( ))}
{boardLines.flatMap((line, r) => [...line].map((char, c) => { const fixed = problemLines[r][c] !== "."; const letter = fixed ? problemLines[r][c] : char; const color = letter === "." ? "transparent" : legendColor(letter); return ( ); }), )}
); } function LoopyEditor({ problemAscii, boardAscii, onChange }: Omit) { const puzzle = parseLoopy(problemAscii); const states = loopyEdgeStates(problemAscii, boardAscii); const cell = 52; const margin = 28; const width = margin * 2 + puzzle.cols * cell; const height = margin * 2 + puzzle.rows * cell; function cycle(edge: LoopyEdge) { const current = states[edge.id] ?? "unknown"; const next = current === "line" ? "blocked" : "line"; onChange( loopyBoardFromStates(problemAscii, { ...states, [edge.id]: next, }), ); } return (
Click any segment, including the outer border, to toggle between part of the loop and definitely not part of it.
{Array.from({ length: puzzle.rows + 1 }).map((_, row) => Array.from({ length: puzzle.cols + 1 }).map((__, col) => ( )), )} {Array.from({ length: puzzle.rows }).map((_, row) => Array.from({ length: puzzle.cols }).map((__, col) => ( {puzzle.clues.has(`${row}:${col}`) ? ( {puzzle.clues.get(`${row}:${col}`)} ) : null} )), )} {puzzle.horizontalEdges.map((edge) => { const x1 = margin + edge.col * cell; const y = margin + edge.row * cell; const x2 = x1 + cell; const state = states[edge.id] ?? "unknown"; return ( cycle(edge)}> {state === "line" ? : null} {state === "unknown" ? : null} {state === "blocked" ? ( × ) : null} ); })} {puzzle.verticalEdges.map((edge) => { const x = margin + edge.col * cell; const y1 = margin + edge.row * cell; const y2 = y1 + cell; const state = states[edge.id] ?? "unknown"; return ( cycle(edge)}> {state === "line" ? : null} {state === "unknown" ? : null} {state === "blocked" ? ( × ) : null} ); })}
); } function GalaxiesEditor({ problemAscii, boardAscii, onChange }: Omit) { const puzzle = parseGalaxies(problemAscii); const states = galaxiesBoundaryStates(problemAscii, boardAscii); const cell = 56; const half = cell / 2; const margin = 28; const width = margin * 2 + puzzle.cols * cell; const height = margin * 2 + puzzle.rows * cell; function toggle(boundary: GalaxyBoundary) { onChange( galaxiesBoardFromStates(problemAscii, { ...states, [boundary.id]: !states[boundary.id], }), ); } return (
Click an interior wall segment to add or remove it. Dots mark each galaxy center.
{Array.from({ length: puzzle.rows }).map((_, row) => Array.from({ length: puzzle.cols }).map((__, col) => ( )), )} {Array.from({ length: puzzle.rows + 1 }).map((_, row) => Array.from({ length: puzzle.cols + 1 }).map((__, col) => ( )), )} {Array.from({ length: puzzle.rows + 1 }).map((_, row) => Array.from({ length: puzzle.cols }).map((__, col) => ( )), )} {Array.from({ length: puzzle.rows }).map((_, row) => Array.from({ length: puzzle.cols + 1 }).map((__, col) => ( )), )} {puzzle.horizontalBoundaries.map((boundary) => { const x1 = margin + boundary.col * cell; const y = margin + boundary.row * cell; const x2 = x1 + cell; return ( toggle(boundary)}> ); })} {puzzle.verticalBoundaries.map((boundary) => { const x = margin + boundary.col * cell; const y1 = margin + boundary.row * cell; const y2 = y1 + cell; return ( toggle(boundary)}> ); })} {puzzle.dots.map((dot) => ( ))}
); } function PatternEditor({ boardAscii, onChange }: Omit) { const normalizedBoard = normalizePatternAscii(boardAscii); useEffect(() => { if (normalizedBoard !== boardAscii) { onChange(normalizedBoard); } }, [boardAscii, normalizedBoard, onChange]); const template = parsePattern(normalizedBoard); const columnClues = parsePatternColumnClues(template); const maxColumnClueDepth = Math.max(...columnClues.map((column) => column.length), 1); function cycle(rowIndex: number, cellIndex: number) { const next = parsePattern(normalizedBoard); const current = next.rows[rowIndex].cells[cellIndex]; next.rows[rowIndex].cells[cellIndex] = current === "##" ? ".." : "##"; onChange(patternToAscii(next)); } return (
Every cell starts white. Click a square to toggle it between white and filled black.
{columnClues.map((column, index) => (
{Array.from({ length: maxColumnClueDepth }).map((_, rowIndex) => { const clue = column[rowIndex - (maxColumnClueDepth - column.length)] ?? ""; return (
{clue}
); })}
))}
{template.rows.map((row, rowIndex) => (
{row.prefix .trim() .split(/\s+/) .filter(Boolean) .map((clue, clueIndex) => ( {clue} ))}
{row.cells.map((cell, cellIndex) => ( ))}
))}
); } function UndeadEditor({ boardAscii, onChange }: Omit) { const template = parseUndead(boardAscii); const topClues = template.topLine.trim().split(/\s+/); const bottomClues = template.bottomLine.trim().split(/\s+/); const boardWidth = template.rows[0]?.cells.length ?? 0; const rowTemplate = `40px repeat(${boardWidth}, 38px) 40px`; function cycle(rowIndex: number, cellIndex: number) { const current = template.rows[rowIndex].cells[cellIndex]; if (current === "/" || current === "\\") { return; } const next = parseUndead(boardAscii); next.rows[rowIndex].cells[cellIndex] = current === "." ? "G" : current === "G" ? "V" : current === "V" ? "Z" : "."; onChange(undeadToAscii(next)); } return (
Click a cell to cycle ghost, vampire, zombie, and blank. Mirror cells are fixed.
{template.header}
{topClues.map((clue, index) => (
{clue}
))}
{template.rows.map((row, rowIndex) => (
{row.left}
{row.cells.map((cell, cellIndex) => { const fixedMirror = cell === "/" || cell === "\\"; return ( ); })}
{row.right}
))}
{bottomClues.map((clue, index) => (
{clue}
))}
); } export function PuzzleEditor(props: Props) { if (props.puzzleType === "bridges") { return ; } if (props.puzzleType === "flow_free") { return ; } if (props.puzzleType === "galaxies") { return ; } if (props.puzzleType === "loopy") { return ; } if (props.puzzleType === "pattern") { return ; } if (props.puzzleType === "undead") { return ; } return null; } // Test-only barrel: exposes the pure encode/decode helpers so the round-trip // pipeline tests can exercise the *real* editor functions (not reimplementations). // These mirror exactly what each Editor's onChange emits. export const __testables = { splitLines, cloneGrid, joinGrid, parseBridges, cycleBridgesOpportunities, bridgeClickCountForOpportunity, bridgesBoardFromStates, bridgesStatesFromBoard, parseLoopy, loopyEdgeStates, loopyBoardFromStates, parseGalaxies, galaxiesBoundaryStates, galaxiesBoardFromStates, parsePattern, patternToAscii, normalizePatternAscii, parseUndead, undeadToAscii, };