UNPKG

ts-hashlife

Version:

Efficient TypeScript implementation of HashLife, an optimized algorithm for simulating Conway's Game of Life with memoization and quadtree-based compression.

396 lines (324 loc) 9.63 kB
const MIN_BUFFER_SIZE = 0x100; const MAX_BUFFER_SIZE = 0x1000000; const DENSITY_ESTIMATE = 0.009; export interface Pattern { title: string; description: string; source_url: string; view_url: string; urls: string[]; rule?: string; author?: string; } export interface Result { comment: string; urls: string[]; title?: string; author?: string; rule?: string; pattern_string?: string; width?: number; height?: number; rule_s?: number; rule_b?: number; field_x?: Int32Array | number[]; field_y?: Int32Array | number[]; error?: string; } function parse_rle(pattern_string: string): Result { const result = parse_comments(pattern_string, "#"); let x = 0, y = 0; let expr = /([a-zA-Z]+) *= *([a-zA-Z0-9\/()]+)/g; let header_match: RegExpExecArray | null; pattern_string = result.pattern_string!; let pos = pattern_string.indexOf("\n"); if (pos === -1) { return { ...result, error: "RLE Syntax Error: No Header" }; } while ((header_match = expr.exec(pattern_string.substr(0, pos)))) { switch (header_match[1]) { case "x": result.width = Number(header_match[2]); break; case "y": result.height = Number(header_match[2]); break; case "rule": result.rule_s = parse_rule_rle(header_match[2], true); result.rule_b = parse_rule_rle(header_match[2], false); result.rule = rule2str(result.rule_s, result.rule_b); break; default: return { ...result, error: "RLE Syntax Error: Invalid Header: " + header_match[1], }; } } let initial_size = MIN_BUFFER_SIZE; if (result.width && result.height) { const size = result.width * result.height; if (size > 0) { initial_size = Math.max( initial_size, Math.floor(size * DENSITY_ESTIMATE) ); initial_size = Math.min(MAX_BUFFER_SIZE, initial_size); } } let count = 1; let in_number = false; let chr: number; let field_x = new Int32Array(initial_size); let field_y = new Int32Array(initial_size); let alive_count = 0; const len = pattern_string.length; for (; pos < len; pos++) { chr = pattern_string.charCodeAt(pos); if (chr >= 48 && chr <= 57) { if (in_number) { count *= 10; count += chr ^ 48; } else { count = chr ^ 48; in_number = true; } } else { if (chr === 98) { x += count; } else if ((chr >= 65 && chr <= 90) || (chr >= 97 && chr < 122)) { if (alive_count + count > field_x.length) { field_x = increase_buf_size(field_x); field_y = increase_buf_size(field_y); } while (count--) { field_x[alive_count] = x++; field_y[alive_count] = y; alive_count++; } } else if (chr === 36) { y += count; x = 0; } else if (chr === 33) { break; } count = 1; in_number = false; } } result.field_x = new Int32Array(field_x.buffer, 0, alive_count); result.field_y = new Int32Array(field_y.buffer, 0, alive_count); return result; } function increase_buf_size(buffer: Int32Array): Int32Array { const new_buffer = new Int32Array(Math.floor(buffer.length * 1.5)); new_buffer.set(buffer); return new_buffer; } function parse_comments(pattern_string: string, comment_char: string): Result { const result: Result = { comment: "", urls: [], }; let nl: number; let line: string; const advanced = comment_char === "#"; while (pattern_string[0] === comment_char) { nl = pattern_string.indexOf("\n"); if (nl === -1) nl = pattern_string.length; // Handle the case where there's no newline line = pattern_string.substring(1, nl).trim(); // Check for special comment types (N, O, R) if (advanced && pattern_string.length > 1) { const secondChar = pattern_string[1]; if (secondChar === "N") { result.title = line.substring(1).trim(); } else if (secondChar === "O") { result.author = line.substring(1).trim(); } else if (secondChar === "R") { result.rule = line.substring(1).trim(); } else if (secondChar === "C") { // Handle regular comments and URLs const commentLine = line.substring(1).trim(); if (/^(?:https?:\/\/|www\.)[a-z0-9]/i.test(commentLine)) { let urlLine = commentLine; if (urlLine.substring(0, 4) !== "http") { urlLine = "http://" + urlLine; } result.urls.push(urlLine); } else { result.comment += commentLine + "\n"; } } } pattern_string = pattern_string.substring(nl + 1); } result.pattern_string = pattern_string; result.comment = result.comment.trim(); return result; } function parse_rule_rle(rule_str: string, survived: boolean): number { const tokens = rule_str.split("/"); if (!tokens[1]) { return 0; } if (Number(tokens[0])) { return parse_rule(tokens.join("/"), survived); } if (tokens[0][0].toLowerCase() === "b") { tokens.reverse(); } return parse_rule(tokens[0].substr(1) + "/" + tokens[1].substr(1), survived); } function parse_rule(rule_str: string, survived: boolean): number { let rule = 0; const parsed = rule_str.split("/")[survived ? 0 : 1]; for (const char of parsed) { const n = Number(char); if (isNaN(n) || rule & (1 << n)) { return 0; } rule |= 1 << n; } return rule; } function rule2str(rule_s: number, rule_b: number): string { let rule = ""; for (let i = 0; rule_s; rule_s >>= 1, i++) { if (rule_s & 1) { rule += i; } } rule += "/"; for (let i = 0; rule_b; rule_b >>= 1, i++) { if (rule_b & 1) { rule += i; } } return rule; } function rule2str_rle(rule_s: number, rule_b: number): string { const rule = rule2str(rule_s, rule_b); const tokens = rule.split("/"); return `B${tokens[1]}/S${tokens[0]}`; } function* rle_generator(life: any, bounds: any): Generator<string> { function make(length: number, is_empty: boolean): string { if (length === 0) return ""; const length_tag = length > 1 ? String(length) : ""; return length_tag + (is_empty ? "b" : "o"); } for (let y = bounds.top; y <= bounds.bottom; y++) { let state_is_empty = true; let run_start = bounds.left; for (let x = bounds.left; x <= bounds.right; x++) { const is_empty = !life.get_bit(x, y); const run_length = x - run_start; if (state_is_empty !== is_empty) { yield make(run_length, state_is_empty); run_start = x; state_is_empty = is_empty; } } if (!state_is_empty) { const run_length = bounds.right + 1 - run_start; yield make(run_length, state_is_empty); } if (y !== bounds.bottom) yield "$"; } yield "!"; } function generate_rle(life: any, name: string, comments: string[]): string { const lines: string[] = []; const MAX_LINE_LENGTH = 70; if (name) { // Filter out .rle or .lif extensions from the name const filteredName = name.replace(/\.(rle|lif)$/i, ''); lines.push("#N " + filteredName); } comments.forEach((c) => lines.push("#C " + c)); const bounds = life.get_root_bounds(); const width = bounds.right - bounds.left + 1; const height = bounds.bottom - bounds.top + 1; const rule = rule2str_rle(life.rule_s, life.rule_b); lines.push(`x = ${width}, y = ${height}, rule = ${rule}`); let current_line = ""; for (const fragment of rle_generator(life, bounds)) { if (current_line.length + fragment.length > MAX_LINE_LENGTH) { lines.push(current_line); current_line = ""; } current_line += fragment; } lines.push(current_line); return lines.join("\n"); } function parse_pattern( pattern_text: string ): Partial<Result> | { error: string } { pattern_text = pattern_text.replace(/\r/g, ""); if (pattern_text[0] === "!") { return parse_plaintext(pattern_text); } else if ( /^(?:#[^\n]*\n)*\n*(?:(?:x|y|rule|color|alpha) *= *[a-z0-9\/(),]+,? *)+\s*\n/i.test( pattern_text ) ) { return parse_rle(pattern_text); } else if (pattern_text.substr(0, 10) === "#Life 1.06") { return parse_life106(pattern_text); } else { return { error: "Format detection failed." }; } } function parse_plaintext(pattern_string: string): Result | { error: string } { const result = parse_comments(pattern_string, "!"); pattern_string = result.pattern_string!; const field_x: number[] = []; const field_y: number[] = []; let x = 0, y = 0; const len = pattern_string.length; for (let i = 0; i < len; i++) { switch (pattern_string[i]) { case ".": x++; break; case "O": field_x.push(x++); field_y.push(y); break; case "\n": y++; x = 0; break; case "\r": case " ": break; default: return { error: "Plaintext: Syntax Error" }; } } result.field_x = field_x; result.field_y = field_y; return result; } function parse_life106(pattern_string: string): Partial<Result> { const expr = /\s*(-?\d+)\s+(-?\d+)\s*(?:\n|$)/g; const field_x: number[] = []; const field_y: number[] = []; let match: RegExpExecArray | null; while ((match = expr.exec(pattern_string))) { field_x.push(Number(match[1])); field_y.push(Number(match[2])); } return { field_x, field_y }; } export const formats = { parse_rle, parse_pattern, rule2str, parse_rule, parse_comments, generate_rle, };