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.

609 lines 23 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LifeUniverse = exports.TreeNode = void 0; const event_bus_1 = __importDefault(require("./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; class TreeNode { constructor(nw, ne, sw, se, id, level, population) { 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; } } exports.TreeNode = TreeNode; class LifeUniverse { constructor() { this._step = 0; this._generation = 0; 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() { return this._step; } set step(value) { this._step = value; event_bus_1.default.emit("step", Math.pow(2, value)); } get generation() { return this._generation; } set generation(value) { this._generation = value; event_bus_1.default.emit("generation", value); } pow2(x) { if (x >= 1024) return Infinity; return this._powers[x]; } save_rewind_state() { this.rewind_state = this.root; } restore_rewind_state() { this.generation = 0; this.root = this.rewind_state; this.garbage_collect(); } eval_mask(bitmask) { const rule = bitmask & 32 ? this.rule_s : this.rule_b; return (rule >> this._bitcounts[bitmask & 0x757]) & 1; } level1_create(bitmask) { 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, y, living) { 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, y) { 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() { 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) { if (this.empty_tree_cache[level]) { return this.empty_tree_cache[level]; } let t; 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) { 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) { 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) { 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) { 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; 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, ne, sw, se) { const hash = this.calc_hash(nw.id, ne.id, sw.id, se.id) & this.hashmap_size; let node = this.hashmap[hash]; let prev; 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) { 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() { 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, ne_id, sw_id, se_id) { return (((nw_id * 23) ^ ne_id) * 23) ^ (sw_id * 23) ^ se_id; } clear_pattern() { 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, field_y) { 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) { let max = 4; const keys = Object.keys(bounds); for (let i = 0; i < keys.length; i++) { const coordinate = bounds[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, field_y, bounds) { 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, field_y, offset_x, offset_y) { 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, field_y, bounds) { 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, end, test_field, other_field, offset) { let i = start, j = end, swap; 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, end, field_x, field_y, level) { 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, end, field_x, field_y) { let set = 0, x, y; 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) { if (step !== this.step) { this.step = step; this.uncache(false); this.empty_tree_cache = []; this.level2_cache = Array(0x10000); } } set_rules(s, b) { 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, x, y, living) { 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, x, y) { 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, left, top, field) { 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) { 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) { 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) { 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) { 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, left, top, find_mask, boundary) { 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); } } } exports.LifeUniverse = LifeUniverse; //# sourceMappingURL=life-universe.js.map