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
text/typescript
"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
);
}
}
}