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.

902 lines (782 loc) 23.1 kB
"use strict"; import eventBus from "./event-bus"; const LOAD_FACTOR = 0.9; const INITIAL_SIZE = 16; const HASHMAP_LIMIT = 24; const MASK_LEFT = 1; const MASK_TOP = 2; const MASK_RIGHT = 4; const MASK_BOTTOM = 8; export class TreeNode { nw: TreeNode | null; ne: TreeNode | null; sw: TreeNode | null; se: TreeNode | null; id: number; level: number; population: number; cache: TreeNode | null; quick_cache: TreeNode | null; hashmap_next: TreeNode | undefined; constructor( nw: TreeNode | null, ne: TreeNode | null, sw: TreeNode | null, se: TreeNode | null, id: number, level?: number, population?: number ) { this.nw = nw || null; this.ne = ne || null; this.sw = sw || null; this.se = se || null; this.id = id; if (level !== undefined && population !== undefined) { this.level = level; this.population = population; } else if (nw !== null && nw.level !== undefined) { this.level = nw.level + 1; this.population = (nw.population || 0) + (ne!.population || 0) + (sw!.population || 0) + (se!.population || 0); } else { // Leaf node this.level = 0; this.population = 0; } this.cache = null; this.quick_cache = null; this.hashmap_next = undefined; } } export class LifeUniverse { last_id: number; hashmap_size: number; max_load: number; hashmap: (TreeNode | undefined)[]; empty_tree_cache: TreeNode[]; level2_cache: (TreeNode | undefined)[]; _powers: Float64Array; _bitcounts: Int8Array; rule_b: number; rule_s: number; root: TreeNode | null; rewind_state: TreeNode | null; false_leaf: TreeNode; true_leaf: TreeNode; private _step: number = 0; private _generation: number = 0; constructor() { this.last_id = 0; this.hashmap_size = 0; this.max_load = 0; this.hashmap = []; this.empty_tree_cache = []; this.level2_cache = []; this._powers = new Float64Array(1024); this._powers[0] = 1; for (let i = 1; i < 1024; i++) { this._powers[i] = this._powers[i - 1] * 2; } this._bitcounts = new Int8Array(0x758); this._bitcounts.set([0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]); for (let i = 0x10; i < 0x758; i++) { this._bitcounts[i] = this._bitcounts[i & 0xf] + this._bitcounts[(i >> 4) & 0xf] + this._bitcounts[i >> 8]; } this.rule_b = 1 << 3; this.rule_s = (1 << 2) | (1 << 3); this.root = null; this.rewind_state = null; this.step = 0; this.generation = 0; this.false_leaf = new TreeNode(null, null, null, null, 3, 0, 0); this.true_leaf = new TreeNode(null, null, null, null, 2, 0, 1); this.clear_pattern(); } get step(): number { return this._step; } set step(value: number) { this._step = value; eventBus.emit("step", Math.pow(2, value)); } get generation(): number { return this._generation; } set generation(value: number) { this._generation = value; eventBus.emit("generation", value); } pow2(x: number): number { if (x >= 1024) return Infinity; return this._powers[x]; } save_rewind_state(): void { this.rewind_state = this.root; } restore_rewind_state(): void { this.generation = 0; this.root = this.rewind_state; this.garbage_collect(); } eval_mask(bitmask: number): number { const rule = bitmask & 32 ? this.rule_s : this.rule_b; return (rule >> this._bitcounts[bitmask & 0x757]) & 1; } level1_create(bitmask: number): TreeNode { return this.create_tree( bitmask & 1 ? this.true_leaf : this.false_leaf, bitmask & 2 ? this.true_leaf : this.false_leaf, bitmask & 4 ? this.true_leaf : this.false_leaf, bitmask & 8 ? this.true_leaf : this.false_leaf ); } set_bit(x: number, y: number, living: boolean): void { const level = this.get_level_from_bounds({ x: x, y: y }); if (this.root === null) { this.root = this.empty_tree(level); } if (living) { while (level > this.root.level) { this.root = this.expand_universe(this.root); } } else { if (level > this.root.level) { return; } } this.root = this.node_set_bit(this.root, x, y, living); } get_bit(x: number, y: number): boolean { const level = this.get_level_from_bounds({ x: x, y: y }); if (this.root === null || level > this.root.level) { return false; } else { return this.node_get_bit(this.root, x, y); } } get_root_bounds(): { top: number; left: number; bottom: number; right: number; } { if (this.root === null || this.root.population === 0) { return { top: 0, left: 0, bottom: 0, right: 0, }; } const bounds = { top: Infinity, left: Infinity, bottom: -Infinity, right: -Infinity, }, offset = this.pow2(this.root.level - 1); this.node_get_boundary( this.root, -offset, -offset, MASK_TOP | MASK_LEFT | MASK_BOTTOM | MASK_RIGHT, bounds ); return bounds; } empty_tree(level: number): TreeNode { if (this.empty_tree_cache[level]) { return this.empty_tree_cache[level]; } let t: TreeNode; if (level === 1) { t = this.false_leaf; } else { t = this.empty_tree(level - 1); } return (this.empty_tree_cache[level] = this.create_tree(t, t, t, t)); } expand_universe(node: TreeNode): TreeNode { const t = this.empty_tree(node.level - 1); return this.create_tree( this.create_tree(t, t, t, node.nw!), this.create_tree(t, t, node.ne!, t), this.create_tree(t, node.sw!, t, t), this.create_tree(node.se!, t, t, t) ); } uncache(also_quick: boolean): void { for (let i = 0; i <= this.hashmap_size; i++) { const node = this.hashmap[i]; if (node !== undefined) { node.cache = null; node.hashmap_next = undefined; if (also_quick) node.quick_cache = null; } } } in_hashmap(n: TreeNode): boolean { const hash = this.calc_hash(n.nw!.id, n.ne!.id, n.sw!.id, n.se!.id) & this.hashmap_size; let node = this.hashmap[hash]; while (true) { if (node === undefined) { return false; } else if (node === n) { return true; } node = node.hashmap_next; } } hashmap_insert(n: TreeNode): void { const hash = this.calc_hash(n.nw!.id, n.ne!.id, n.sw!.id, n.se!.id) & this.hashmap_size; let node = this.hashmap[hash]; let prev: TreeNode | undefined; while (true) { if (node === undefined) { if (prev !== undefined) { prev.hashmap_next = n; } else { this.hashmap[hash] = n; } return; } prev = node; node = node.hashmap_next; } } create_tree( nw: TreeNode, ne: TreeNode, sw: TreeNode, se: TreeNode ): TreeNode { const hash = this.calc_hash(nw.id, ne.id, sw.id, se.id) & this.hashmap_size; let node = this.hashmap[hash]; let prev: TreeNode | undefined; while (true) { if (node === undefined) { if (this.last_id > this.max_load) { this.garbage_collect(); return this.create_tree(nw, ne, sw, se); } const new_node = new TreeNode(nw, ne, sw, se, this.last_id++); if (prev !== undefined) { prev.hashmap_next = new_node; } else { this.hashmap[hash] = new_node; } return new_node; } else if ( node.nw === nw && node.ne === ne && node.sw === sw && node.se === se ) { return node; } prev = node; node = node.hashmap_next; } } next_generation(is_single: boolean): void { let root = this.root!; while ( (is_single && root.level <= this.step + 2) || root.nw!.population !== root.nw!.se!.se!.population || root.ne!.population !== root.ne!.sw!.sw!.population || root.sw!.population !== root.sw!.ne!.ne!.population || root.se!.population !== root.se!.nw!.nw!.population ) { root = this.expand_universe(root); } if (is_single) { this.generation += this.pow2(this.step); root = this.node_next_generation(root); } else { this.generation += this.pow2(this.root!.level - 2); root = this.node_quick_next_generation(root); } this.root = root; } garbage_collect(): void { if (this.hashmap_size < (1 << HASHMAP_LIMIT) - 1) { this.hashmap_size = (this.hashmap_size << 1) | 1; this.hashmap = []; } this.max_load = (this.hashmap_size * LOAD_FACTOR) | 0; for (let i = 0; i <= this.hashmap_size; i++) this.hashmap[i] = undefined; this.last_id = 4; this.node_hash(this.root!); } calc_hash( nw_id: number, ne_id: number, sw_id: number, se_id: number ): number { return (((nw_id * 23) ^ ne_id) * 23) ^ (sw_id * 23) ^ se_id; } clear_pattern(): void { this.last_id = 4; this.hashmap_size = (1 << INITIAL_SIZE) - 1; this.max_load = (this.hashmap_size * LOAD_FACTOR) | 0; this.hashmap = []; this.empty_tree_cache = []; this.level2_cache = Array(0x10000); for (let i = 0; i <= this.hashmap_size; i++) this.hashmap[i] = undefined; this.root = this.empty_tree(3); this.generation = 0; } get_bounds( field_x: Int32Array | number[], field_y: Int32Array | number[] ): { top: number; left: number; bottom: number; right: number } { if (!field_x.length) { return { top: 0, left: 0, bottom: 0, right: 0, }; } const bounds = { top: field_y[0], left: field_x[0], bottom: field_y[0], right: field_x[0], }; const len = field_x.length; for (let i = 1; i < len; i++) { const x = field_x[i], y = field_y[i]; if (x < bounds.left) { bounds.left = x; } else if (x > bounds.right) { bounds.right = x; } if (y < bounds.top) { bounds.top = y; } else if (y > bounds.bottom) { bounds.bottom = y; } } return bounds; } get_level_from_bounds(bounds: { x?: number; y?: number; left?: number; top?: number; bottom?: number; right?: number; }): number { let max = 4; const keys = Object.keys(bounds); for (let i = 0; i < keys.length; i++) { const coordinate = (bounds as any)[keys[i]]; if (coordinate + 1 > max) { max = coordinate + 1; } else if (-coordinate > max) { max = -coordinate; } } return Math.ceil(Math.log(max) / Math.LN2) + 1; } make_center( field_x: Int32Array | number[], field_y: Int32Array | number[], bounds: { left: number; right: number; top: number; bottom: number } ): void { const offset_x = Math.round((bounds.left - bounds.right) / 2) - bounds.left, offset_y = Math.round((bounds.top - bounds.bottom) / 2) - bounds.top; this.move_field(field_x, field_y, offset_x, offset_y); bounds.left += offset_x; bounds.right += offset_x; bounds.top += offset_y; bounds.bottom += offset_y; } move_field( field_x: Int32Array | number[], field_y: Int32Array | number[], offset_x: number, offset_y: number ): void { const len = field_x.length; for (let i = 0; i < len; i++) { field_x[i] += offset_x; field_y[i] += offset_y; } } setup_field( field_x: Int32Array | number[], field_y: Int32Array | number[], bounds?: { left: number; right: number; top: number; bottom: number } ): void { if (bounds === undefined) { bounds = this.get_bounds(field_x, field_y); } const level = this.get_level_from_bounds(bounds), offset = this.pow2(level - 1), count = field_x.length; this.move_field(field_x, field_y, offset, offset); this.root = this.setup_field_recurse(0, count - 1, field_x, field_y, level); } partition( start: number, end: number, test_field: Int32Array | number[], other_field: Int32Array | number[], offset: number ): number { let i = start, j = end, swap: number; while (i <= j) { while (i <= end && (test_field[i] & offset) === 0) { i++; } while (j > start && test_field[j] & offset) { j--; } if (i >= j) { break; } swap = test_field[i]; test_field[i] = test_field[j]; test_field[j] = swap; swap = other_field[i]; other_field[i] = other_field[j]; other_field[j] = swap; i++; j--; } return i; } setup_field_recurse( start: number, end: number, field_x: Int32Array | number[], field_y: Int32Array | number[], level: number ): TreeNode { if (start > end) { return this.empty_tree(level); } if (level === 2) { return this.level2_setup(start, end, field_x, field_y); } level--; const offset = 1 << level, part3 = this.partition(start, end, field_y, field_x, offset), part2 = this.partition(start, part3 - 1, field_x, field_y, offset), part4 = this.partition(part3, end, field_x, field_y, offset); return this.create_tree( this.setup_field_recurse(start, part2 - 1, field_x, field_y, level), this.setup_field_recurse(part2, part3 - 1, field_x, field_y, level), this.setup_field_recurse(part3, part4 - 1, field_x, field_y, level), this.setup_field_recurse(part4, end, field_x, field_y, level) ); } level2_setup( start: number, end: number, field_x: Int32Array | number[], field_y: Int32Array | number[] ): TreeNode { let set = 0, x: number, y: number; for (let i = start; i <= end; i++) { x = field_x[i]; y = field_y[i]; set |= 1 << ((x & 1) | (((y & 1) | (x & 2)) << 1) | ((y & 2) << 2)); } if (this.level2_cache[set]) { return this.level2_cache[set]!; } return (this.level2_cache[set] = this.create_tree( this.level1_create(set), this.level1_create(set >> 4), this.level1_create(set >> 8), this.level1_create(set >> 12) )); } set_step(step: number): void { if (step !== this.step) { this.step = step; this.uncache(false); this.empty_tree_cache = []; this.level2_cache = Array(0x10000); } } set_rules(s: number, b: number): void { if (this.rule_s !== s || this.rule_b !== b) { this.rule_s = s; this.rule_b = b; this.uncache(true); this.empty_tree_cache = []; this.level2_cache = Array(0x10000); } } node_set_bit( node: TreeNode, x: number, y: number, living: boolean ): TreeNode { if (node.level === 0) { return living ? this.true_leaf : this.false_leaf; } const offset = node.level === 1 ? 0 : this.pow2(node.level - 2); let nw = node.nw, ne = node.ne, sw = node.sw, se = node.se; if (x < 0) { if (y < 0) { nw = this.node_set_bit(nw!, x + offset, y + offset, living); } else { sw = this.node_set_bit(sw!, x + offset, y - offset, living); } } else { if (y < 0) { ne = this.node_set_bit(ne!, x - offset, y + offset, living); } else { se = this.node_set_bit(se!, x - offset, y - offset, living); } } return this.create_tree(nw!, ne!, sw!, se!); } node_get_bit(node: TreeNode, x: number, y: number): boolean { if (node.population === 0) { return false; } if (node.level === 0) { return true; } const offset = node.level === 1 ? 0 : this.pow2(node.level - 2); if (x < 0) { if (y < 0) { return this.node_get_bit(node.nw!, x + offset, y + offset); } else { return this.node_get_bit(node.sw!, x + offset, y - offset); } } else { if (y < 0) { return this.node_get_bit(node.ne!, x - offset, y + offset); } else { return this.node_get_bit(node.se!, x - offset, y - offset); } } } node_get_field( node: TreeNode, left: number, top: number, field: { x: number; y: number }[] ): void { if (node.population === 0) { return; } if (node.level === 0) { field.push({ x: left, y: top }); } else { const offset = this.pow2(node.level - 1); this.node_get_field(node.nw!, left, top, field); this.node_get_field(node.sw!, left, top + offset, field); this.node_get_field(node.ne!, left + offset, top, field); this.node_get_field(node.se!, left + offset, top + offset, field); } } node_level2_next(node: TreeNode): TreeNode { const nw = node.nw!, ne = node.ne!, sw = node.sw!, se = node.se!; const bitmask = (nw.nw!.population << 15) | (nw.ne!.population << 14) | (ne.nw!.population << 13) | (ne.ne!.population << 12) | (nw.sw!.population << 11) | (nw.se!.population << 10) | (ne.sw!.population << 9) | (ne.se!.population << 8) | (sw.nw!.population << 7) | (sw.ne!.population << 6) | (se.nw!.population << 5) | (se.ne!.population << 4) | (sw.sw!.population << 3) | (sw.se!.population << 2) | (se.sw!.population << 1) | se.se!.population; return this.level1_create( this.eval_mask(bitmask >> 5) | (this.eval_mask(bitmask >> 4) << 1) | (this.eval_mask(bitmask >> 1) << 2) | (this.eval_mask(bitmask) << 3) ); } node_next_generation(node: TreeNode): TreeNode { if (node.cache) { return node.cache; } if (this.step === node.level - 2) { return this.node_quick_next_generation(node); } if (node.level === 2) { if (node.quick_cache) { return node.quick_cache; } else { return (node.quick_cache = this.node_level2_next(node)); } } const nw = node.nw!, ne = node.ne!, sw = node.sw!, se = node.se!, n00 = this.create_tree(nw.nw!.se!, nw.ne!.sw!, nw.sw!.ne!, nw.se!.nw!), n01 = this.create_tree(nw.ne!.se!, ne.nw!.sw!, nw.se!.ne!, ne.sw!.nw!), n02 = this.create_tree(ne.nw!.se!, ne.ne!.sw!, ne.sw!.ne!, ne.se!.nw!), n10 = this.create_tree(nw.sw!.se!, nw.se!.sw!, sw.nw!.ne!, sw.ne!.nw!), n11 = this.create_tree(nw.se!.se!, ne.sw!.sw!, sw.ne!.ne!, se.nw!.nw!), n12 = this.create_tree(ne.sw!.se!, ne.se!.sw!, se.nw!.ne!, se.ne!.nw!), n20 = this.create_tree(sw.nw!.se!, sw.ne!.sw!, sw.sw!.ne!, sw.se!.nw!), n21 = this.create_tree(sw.ne!.se!, se.nw!.sw!, sw.se!.ne!, se.sw!.nw!), n22 = this.create_tree(se.nw!.se!, se.ne!.sw!, se.sw!.ne!, se.se!.nw!); return (node.cache = this.create_tree( this.node_next_generation(this.create_tree(n00, n01, n10, n11)), this.node_next_generation(this.create_tree(n01, n02, n11, n12)), this.node_next_generation(this.create_tree(n10, n11, n20, n21)), this.node_next_generation(this.create_tree(n11, n12, n21, n22)) )); } node_quick_next_generation(node: TreeNode): TreeNode { if (node.quick_cache !== null) { return node.quick_cache; } if (node.level === 2) { return (node.quick_cache = this.node_level2_next(node)); } const nw = node.nw!, ne = node.ne!, sw = node.sw!, se = node.se!, n00 = this.node_quick_next_generation(nw), n01 = this.node_quick_next_generation( this.create_tree(nw.ne!, ne.nw!, nw.se!, ne.sw!) ), n02 = this.node_quick_next_generation(ne), n10 = this.node_quick_next_generation( this.create_tree(nw.sw!, nw.se!, sw.nw!, sw.ne!) ), n11 = this.node_quick_next_generation( this.create_tree(nw.se!, ne.sw!, sw.ne!, se.nw!) ), n12 = this.node_quick_next_generation( this.create_tree(ne.sw!, ne.se!, se.nw!, se.ne!) ), n20 = this.node_quick_next_generation(sw), n21 = this.node_quick_next_generation( this.create_tree(sw.ne!, se.nw!, sw.se!, se.sw!) ), n22 = this.node_quick_next_generation(se); return (node.quick_cache = this.create_tree( this.node_quick_next_generation(this.create_tree(n00, n01, n10, n11)), this.node_quick_next_generation(this.create_tree(n01, n02, n11, n12)), this.node_quick_next_generation(this.create_tree(n10, n11, n20, n21)), this.node_quick_next_generation(this.create_tree(n11, n12, n21, n22)) )); } node_hash(node: TreeNode): void { if (!this.in_hashmap(node)) { node.id = this.last_id++; node.hashmap_next = undefined; if (node.level > 1) { this.node_hash(node.nw!); this.node_hash(node.ne!); this.node_hash(node.sw!); this.node_hash(node.se!); if (node.cache) { this.node_hash(node.cache); } if (node.quick_cache) { this.node_hash(node.quick_cache); } } this.hashmap_insert(node); } } node_get_boundary( node: TreeNode, left: number, top: number, find_mask: number, boundary: { top: number; left: number; bottom: number; right: number } ): void { if (node.population === 0 || !find_mask) { return; } if (node.level === 0) { if (left < boundary.left) boundary.left = left; if (left > boundary.right) boundary.right = left; if (top < boundary.top) boundary.top = top; if (top > boundary.bottom) boundary.bottom = top; } else { const offset = this.pow2(node.level - 1); if ( left >= boundary.left && left + offset * 2 <= boundary.right && top >= boundary.top && top + offset * 2 <= boundary.bottom ) { return; } let find_nw = find_mask, find_sw = find_mask, find_ne = find_mask, find_se = find_mask; if (node.nw!.population) { find_sw &= ~MASK_TOP; find_ne &= ~MASK_LEFT; find_se &= ~MASK_TOP & ~MASK_LEFT; } if (node.sw!.population) { find_se &= ~MASK_LEFT; find_nw &= ~MASK_BOTTOM; find_ne &= ~MASK_BOTTOM & ~MASK_LEFT; } if (node.ne!.population) { find_nw &= ~MASK_RIGHT; find_se &= ~MASK_TOP; find_sw &= ~MASK_TOP & ~MASK_RIGHT; } if (node.se!.population) { find_sw &= ~MASK_RIGHT; find_ne &= ~MASK_BOTTOM; find_nw &= ~MASK_BOTTOM & ~MASK_RIGHT; } this.node_get_boundary(node.nw!, left, top, find_nw, boundary); this.node_get_boundary(node.sw!, left, top + offset, find_sw, boundary); this.node_get_boundary(node.ne!, left + offset, top, find_ne, boundary); this.node_get_boundary( node.se!, left + offset, top + offset, find_se, boundary ); } } }