Lars H, 2008-08-27: I stumbled upon a Wikipedia article [L1 ] about this curious little algorithm, and once I had read enough to understand how it works, I couldn't help implementing it (especially since Wikipedia claims "While an amateur programmer might be able to write a simple Life player over an afternoon, few Hashlife implementations exist"). So here it is, as a weekend fun project.
Basic principles of the algorithm:
% encode {{1 1 1} {1 0 1} {1 0 1} {} {} {} {1 1} {1 1}}
10
% decode 10
{1 1 1 0 0 0 0 0} {1 0 1 0 0 0 0 0} {1 0 1 0 0 0 0 0} {0 0 0 0 0 0 0 0} {0 0 0 0 0 0 0 0} {0 0 0 0 0 0 0 0} {1 1 0 0 0 0 0 0} {1 1 0 0 0 0 0 0}
% chardecode 10
OOO
O O
O O
OO
OO
% grow 10
14
% chardecode 14
OOO
O O
O O
OO
OO
% step 14
137
% chardecode 137
OOO
O O
O O
O O
OOO OOO
OO
OO
% set generations
8Since node 14 is a 16×16 grid, stepping it forward advances time 8 generations. growing larger nodes also speeds up the processing.
% step [grow 137] 743 % step [grow 743] 2769 % set generations 56
Larger nodes are difficult to display in character graphics. '''node2xbm" can be used to make a bitmap instead, like so:
% package require Tk % image create bitmap -background yellow image1 % pack [label .l -image image1] % image1 configure -data [node2xbm 2769 3] ; # 3 for making a 3x3 block of pixels for each cell.
Alternatively, that could have been done by
% pack [label .l -image [node2photo 2769 -block 3 -opts {-width 192 -height -192} -bg yellow]]The 1496th generation of that pattern, displayed as above and with one pixel per cell looks like:

High level commands defined below:
Options of node2photo:
Unlike node2xbm, node2photo doesn't make an image large enough to contain all of the node; instead it fills in the image with as much data as it can fit.
# HashLife -- Hashed quadtree-based Life algorithm with memoisation
# The basic data structure is a heap (implemented as a list) and a hash
# table (array) that stores the positions in the heap of particular nodes.
# Heap item contents:
#
# cell <0 or 1>
# 2node <nw> <ne> <sw> <se>
# 4node <nw> <ne> <sw> <se> <self>
# bignode <nw> <ne> <sw> <se> <self>
# cached <nw> <ne> <sw> <se> <next>
# cached1 <nw> <ne> <sw> <se> <next>
#
# Evaluating one of them in the next namespace computes the <next> node
# and mutates the heap item to a [cached] node. Evaluating one of them
# in the next1 namespace computes the <next> node and mutates the heap
# item to a [cached1] node.
#
# The <nw>, <ne>, <sw>, <se>, and <next> are positions of other nodes.
# The <self> is the position of the node itself, which is needed for
# nodes that mutate themselves into [cached] nodes.
# In a [2node], they're [cell]s. In a [4node], they're [2node]s.
# In a [bignode], they're [4node]s, [bignode]s, or [cached]s.
# In a [cached], they're [2node]s or [cached]s.
#
#
set heap {{cell 0} {cell 1}}
# Must be items 0 and 1 -- relied upon by several procs below.
array unset hash
##
# The following procedure finds the heap position (pointer) for a
# particular node, given its four corners. It will *create* the heap
# item for the node if it doesn't already exist.
##
proc find {nw ne sw se} {
variable hash
set key [list $nw $ne $sw $se]
if {![info exists hash($key)]} then {
variable heap
set hash($key) [llength $heap]
switch -- [lindex $heap $nw 0] "cell" {
lappend heap [list 2node $nw $ne $sw $se]
} "2node" {
lappend heap [list 4node $nw $ne $sw $se $hash($key)]
} default {
lappend heap [list bignode $nw $ne $sw $se $hash($key)]
}
}
return $hash($key)
}
##
# side $node -- Compute and return the side of the $node.
##
proc side {node} {expr {
[lindex $::heap $node 0] eq "cell" ? 1 :
2*[side [lindex $::heap $node 1]]
}}
##
# encode $matrix
# Encode a matrix of booleans as a node in the heap.
# The matrix is roughly centered within the node's grid.
#
# encode $matrix $side $col $row
# Encode the $side-by-$side submatrix of $matrix whose
# NW corner is column $col and row $row. This submatrix
# need not be contained within the $matrix. The $side
# must be a power of 2.
##
proc encode {matrix {side 0} {x 0} {y 0}} {
if {!$side} then {
set min [llength $matrix]
foreach row $matrix {
if {[llength $row]>$min} then {set min [llength $row]}
}
set side 1
while {$side<$min} {incr side $side}
set x [expr {($min-$side)/2}]
set y [expr {([llength $matrix]-$side)/2}]
}
if {$side==1} then {
return [lindex {0 1} [string is true -strict [lindex $matrix $y $x]]]
}
set half [expr {$side/2}]
set nw [encode $matrix $half $x $y]
set ne [encode $matrix $half [expr {$x+$half}] $y]
set sw [encode $matrix $half $x [expr {$y+$half}]]
set se [encode $matrix $half [expr {$x+$half}] [expr {$y+$half}]]
return [find $nw $ne $sw $se]
}
##
# Decode a heap node as a matrix of 0s and 1s.
##
proc decode {node} {
if {[lindex $::heap $node 0] == "cell"} then {
return [list [list [lindex $::heap $node 1]]]
}
set res {}
foreach low [decode [lindex $::heap $node 1]]\
high [decode [lindex $::heap $node 2]] {
lappend res [concat $low $high]
}
foreach low [decode [lindex $::heap $node 3]]\
high [decode [lindex $::heap $node 4]] {
lappend res [concat $low $high]
}
return $res
}
##
# Decode a heap node as a character block. Dead cells
# are spaces, live cells some char (defaults to O).
##
proc chardecode {node {cell O}} {
string map [list { } {} 0 { } 1 $cell] [
join [decode $node] \n
]
}
##
# Expand a heap node $level times, into a 2**$level by 2**$level
# matrix of nodes.
##
proc expand {node level} {
if {$level<=0} then {return [list [list $node]]}
incr level -1
set res {}
foreach low [expand [lindex $::heap $node 1] $level]\
high [expand [lindex $::heap $node 2] $level] {
lappend res [concat $low $high]
}
foreach low [expand [lindex $::heap $node 3] $level]\
high [expand [lindex $::heap $node 4] $level] {
lappend res [concat $low $high]
}
return $res
}
##
# Contract the $side-by-$side submatrix, whose NW corner is at
# ($row,$col), of the $matrix of nodes into a single node.
##
proc contract {matrix side row col} {
if {$side<=1} then {return [lindex $matrix $row $col]}
set half [expr {$side/2}]
set nw [contract $matrix $half $row $col]
set ne [contract $matrix $half $row [expr {$col+$half}]]
set sw [contract $matrix $half [expr {$row+$half}] $col]
set se [contract $matrix $half [expr {$row+$half}] [expr {$col+$half}]]
return [find $nw $ne $sw $se]
}
namespace eval next {
proc cached {nw ne sw se next} {return $next}
proc cell {c} {error "Can't advance a single cell"}
proc 2node {nw ne sw se} {error "Can't advance a 2x2 node"}
proc cached1 {nw ne sw se next} {
set self [find $nw $ne $sw $se]
if {[lindex $::heap $nw 0] == "2node"} then {
4node $nw $ne $sw $se $self
} else {
bignode $nw $ne $sw $se $self
}
}
}
##
# The following procedure computes the next generation of the middle
# [2node] in a [4node]. It does not mutate the node itself.
##
proc 4node {nw ne sw se} {
set grid {}
foreach low [decode $nw] high [decode $ne] {
lappend grid [concat $low $high]
}
foreach low [decode $sw] high [decode $se] {
lappend grid [concat $low $high]
}
set call [list find]
foreach y1 {1 2} {
foreach x1 {1 2} {
set sum 0
foreach y2 {-1 0 1} {
foreach x2 {-1 0 1} {
incr sum [lindex $grid [expr {$y1+$y2}] [expr {$x1+$x2}]]
}
}
set sum [expr {2*$sum - [lindex $grid $y1 $x1]}]
lappend call [expr {$sum>=5 && $sum<=7}]
}
}
eval $call
}
##
# The following procedure computes the next generation of the middle
# [2node] in a [4node], and mutates the node into a [cached] node.
##
proc next::4node {nw ne sw se self} {
set next [::4node $nw $ne $sw $se]
lset ::heap $self 0 cached
lset ::heap $self 5 $next
return $next
}
proc next::bignode {nw ne sw se self} {
set grid {}
foreach low [expand $nw 1] high [expand $ne 1] {
lappend grid [concat $low $high]
}
foreach low [expand $sw 1] high [expand $se 1] {
lappend grid [concat $low $high]
}
set n [contract $grid 2 0 1]
set w [contract $grid 2 1 0]
set mid [contract $grid 2 1 1]
set e [contract $grid 2 1 2]
set s [contract $grid 2 2 1]
set grid2 {}
foreach row [list [list $nw $n $ne] [list $w $mid $e] [list $sw $s $se]] {
set row2 {}
foreach node $row {
lappend row2 [eval [lindex $::heap $node]]
}
lappend grid2 $row2
}
set call [list find]
foreach row {0 1} {
foreach col {0 1} {
set node [contract $grid2 2 $row $col]
lappend call [eval [lindex $::heap $node]]
}
}
set next [eval $call]
lset ::heap $self 0 cached
lset ::heap $self 5 $next
return $next
}
namespace eval next1 {
proc cached1 {nw ne sw se next} {return $next}
proc cell {c} {error "Can't advance a single cell"}
proc 2node {nw ne sw se} {error "Can't advance a 2x2 node"}
proc cached {nw ne sw se next} {
set self [find $nw $ne $sw $se]
if {[lindex $::heap $nw 0] == "2node"} then {
lset ::heap $self 0 cached1
set next
} else {
bignode $nw $ne $sw $se $self
}
}
}
##
# The following procedure computes the next generation of the middle
# [2node] in a [4node], and mutates the node into a [cached1] node.
##
proc next1::4node {nw ne sw se self} {
set next [::4node $nw $ne $sw $se]
lset ::heap $self 0 cached1
lset ::heap $self 5 $next
return $next
}
proc next1::bignode {nw ne sw se self} {
set M [expand $self 3]
set call [list find]
foreach {row col} {1 1 1 3 3 1 3 3} {
set node [contract $M 4 $row $col]
lappend call [eval [lindex $::heap $node]]
}
set next [eval $call]
lset ::heap $self 0 cached1
lset ::heap $self 5 $next
return $next
}
##
# Check if a node is a node of all zeroes.
##
proc iszero {node} {
while {$node>=2} {
foreach {type nw ne sw se} [lindex $::heap $node] break
if {$nw!=$ne || $nw!=$sw || $nw!=$se} then {return 0}
set node $nw
}
return [expr {$node==0}]
}
##
# Commands in the zeroed namespace return a node with the same size as
# the node which they are, but with all cells 0.
##
namespace eval zeroed {
proc cell {c} {return 0}
proc 2node {a b c d} {
find 0 0 0 0
}
proc cached {a b c d next} {
set sub [eval [lindex $::heap $a]]
find $sub $sub $sub $sub
}
}
interp alias {} zeroed::bignode {} zeroed::cached
interp alias {} zeroed::4node {} zeroed::cached
interp alias {} zeroed::cached1 {} zeroed::cached
##
# The [grow] procedure return a node whose side is twice
# that of the argument, by padding it with a frame of all
# zeroes.
##
proc grow {node} {
if {[lindex $::heap $node 0] == "cell"} then {
error "You can't grow a single cell"
}
foreach {type nw ne sw se} [lindex $::heap $node] break
set clean [namespace eval ::zeroed [lindex $::heap $nw]]
set call [list find]
lappend call [find $clean $clean $clean $nw]
lappend call [find $clean $clean $ne $clean]
lappend call [find $clean $sw $clean $clean]
lappend call [find $se $clean $clean $clean]
eval $call
}
set generations 0
proc step {node} {
variable generations
set side [side $node]
set node2 [grow $node]
set res [namespace eval ::next [lindex $::heap $node2]]
incr generations [expr {$side/2}]
return $res
}
proc step1 {node} {
variable generations
set node2 [grow $node]
set res [namespace eval ::next1 [lindex $::heap $node2]]
incr generations
return $res
}
##
# The [safe] procedure returns $node if the border of the level
# 2 expansion of the $node is all zeroes, and [grow $node]
# otherwise. This ensures [step [safe $node]] hasn't lost any
# cells at the edge.
##
proc safe {node} {
set grid [expand $node 2]
set clean [namespace eval ::zeroed [lindex $::heap\
[lindex $grid 0 0]]]
foreach row $grid rc {1 0 0 1} {
foreach item $row cc {1 0 0 1} {
if {($rc||$cc) && $item!=$clean} then {
return [grow $node]
}
}
}
return $node
}
##
# Garbage collection
##
namespace eval gc {
##
# Node commands in the gc namespace create a new heap node
# for this old heap element and return the number of that
# new node.
##
variable newheap
variable tonew
##
# The gc::asnew command generally returns the number of a
# new node, given the number of the corresponding old node.
# It may as a side-effect modify the newheap.
##
proc asnew {node} {
variable tonew
if {![info exists tonew($node)]} then {
set tonew($node) [eval [lindex $::heap $node]]
}
return $tonew($node)
}
##
# The gc::find command generally returns the number of a
# new node, given the number of the corresponding old node.
##
proc cell {bit} {return $bit}
proc 2node {nw ne sw se} {
variable newheap
lappend newheap\
[list 2node [asnew $nw] [asnew $ne] [asnew $sw] [asnew $se]]
return [expr {[llength $newheap]-1}]
}
proc 4node {nw ne sw se self} {
variable newheap
lappend newheap\
[list 4node [asnew $nw] [asnew $ne] [asnew $sw] [asnew $se] ""]
set num [expr {[llength $newheap]-1}]
lset newheap end end $num
return $num
}
proc bignode {nw ne sw se self} {
variable newheap
lappend newheap\
[list bignode [asnew $nw] [asnew $ne] [asnew $sw] [asnew $se] ""]
set num [expr {[llength $newheap]-1}]
lset newheap end end $num
return $num
}
proc cached {nw ne sw se next} {
variable newheap
lappend newheap [list cached [asnew $nw] [asnew $ne]\
[asnew $sw] [asnew $se] [asnew $next]]
return [expr {[llength $newheap]-1}]
}
proc cached1 {nw ne sw se next} {
variable newheap
lappend newheap [list cached1 [asnew $nw] [asnew $ne]\
[asnew $sw] [asnew $se] [asnew $next]]
return [expr {[llength $newheap]-1}]
}
}
proc garbagecollect {args} {
set gc::newheap {{cell 0} {cell 1}}
array unset gc::tonew
set res {}
foreach node $args {
lappend res [gc::asnew $node]
}
array unset gc::tonew {[01]}
variable hash
array unset hash
foreach {old node} [array get gc::tonew] {
set hash([lrange [lindex $gc::newheap $node] 1 4]) $node
}
array unset gc::tonew
set ::heap $gc::newheap
return $res
}
# A [bitmap] actually seems more appropriate than a [photo] for
# representing Life patterns, so we need some way of converting
# nodes to XBM data. Intermediate representation:
#
# $side $matrix
#
# If the $side is divisible by 8, then the $matrix is a list of
# $side rows, where each row is a string matching the regexp:
# (0x[[:xdigit:]]{2}, )+
#
# If the $side is not divisible by 8, then the $matrix is a list
# of $side rows, where each row is a string of $side binary digits.
proc node2bm {node mag} {
if {$node <= 1} then {
set row [string repeat $node $mag]
if {!($mag % 8)} then {
binary scan [binary format b* $row] H* row
regsub -all {..} $row {0x&, } row
}
set mat {}
for {set n 1} {$n<=$mag} {incr n} {lappend mat $row}
return [list $mag $mat]
}
set mat {}
foreach {side leftM} [node2bm [lindex $::heap $node 1] $mag] break
foreach l $leftM r [
lindex [node2bm [lindex $::heap $node 2] $mag] 1
] {lappend mat $l$r}
foreach l [
lindex [node2bm [lindex $::heap $node 3] $mag] 1
] r [
lindex [node2bm [lindex $::heap $node 4] $mag] 1
] {lappend mat $l$r}
if {($side%8) && !($side%4)} then {
set mat2 {}
foreach row $mat {
binary scan [binary format b* $row] H* row
regsub -all {..} $row {0x&, } row
lappend mat2 $row
}
set mat $mat2
}
return [list [expr {2*$side}] $mat]
}
proc node2xbm {node mag} {
foreach {side mat} [node2bm $node $mag] break
if {$side % 8} then {
set width [expr {8*(1 + $side/8)}]
set mat2 {}
foreach row $mat {
binary scan [binary format b* $row] H* row
regsub -all {..} $row {0x&, } row
lappend mat2 $row
}
set mat $mat2
} else {
set width $side
}
format {
#define data_width %d
#define data_height %d
static unsigned char data_bits[] = {
%s
};
} $width $side [string trimright [join $mat "\n "] ", "]
}
package require Tk 8.5
namespace eval ::tophoto {
image create photo ::tophoto::dead -width 1 -height 1
image create photo ::tophoto::live -width 1 -height 1
variable where
variable ms
proc idleupdate {period} {
variable ms
if {$ms + $period < [clock milliseconds]} then {
update idletasks
set ms [clock milliseconds]
}
}
proc init {fg bg period} {
dead put $bg
live put $fg
variable where
unset -nocomplain where
if {$period < infinity} then {
interp alias {} ::tophoto::maybe_idleupdate\
{} ::tophoto::idleupdate $period
variable ms [clock milliseconds]
} else {
proc ::tophoto::maybe_idleupdate {} {}
}
}
proc pixelnode {node img side x y w h} {
variable where
if {![info exists where($node)]} then {
if {[iszero $node]} then {
set where($node) [list [namespace which dead]]
} else {
set where($node) [list [namespace which live]]
}
}
$img copy {*}$where($node) -to\
[expr {$x<0 ? 0 : $x}] [expr {$y<0 ? 0 : $y}]\
[expr {$x+$side>=$w ? $w : $x+$side}]\
[expr {$y+$side>=$h ? $h : $y+$side}]
}
proc anynode {node img side unit x y w h} {
maybe_idleupdate
variable where
if {$side <= $unit} then {
pixelnode $node $img $side $x $y $w $h
} elseif {$x>=0 && $y>=0 && $x+$side<=$w && $y+$side<=$h} then {
if {[info exists where($node)]} then {
$img copy {*}$where($node) -to $x $y
} else {
set half [expr {$side/2}]
anynode [lindex $::heap $node 1] $img $half $unit\
$x $y $w $h
anynode [lindex $::heap $node 2] $img $half $unit\
[expr {$x+$half}] $y $w $h
anynode [lindex $::heap $node 3] $img $half $unit\
$x [expr {$y+$half}] $w $h
anynode [lindex $::heap $node 4] $img $half $unit\
[expr {$x+$half}] [expr {$y+$half}] $w $h
set where($node) [list $img -from $x $y\
[expr {$x+$side}] [expr {$y+$side}]]
}
} elseif {$x>-$side && $y>-$side && $x<$w && $y<$h} then {
set half [expr {$side/2}]
anynode [lindex $::heap $node 1] $img $half $unit $x $y $w $h
anynode [lindex $::heap $node 2] $img $half $unit\
[expr {$x+$half}] $y $w $h
anynode [lindex $::heap $node 3] $img $half $unit\
$x [expr {$y+$half}] $w $h
anynode [lindex $::heap $node 4] $img $half $unit\
[expr {$x+$half}] [expr {$y+$half}] $w $h
}
}
}
proc node2photo {node args} {
array set Opt {
-fg black
-bg white
-unzoom 0
-center {0.5 0.5}
-shift {0 0}
-opts {}
-block 1
-period infinity
}
array set Opt $args
if {[info exists Opt(-image)]} then {
set img $Opt(-image)
$img configure {*}$Opt(-opts)
} else {
set img [image create photo {*}$Opt(-opts)]
}
$img blank
set w [image width $img]
set h [image width $img]
tophoto::init $Opt(-fg) $Opt(-bg) $Opt(-period)
set side [expr {[side $node]*$Opt(-block)}]
for {} {$Opt(-unzoom)>0} {incr Opt(-unzoom) -1} {
set side [expr {$side/2}]
}
foreach {rx ry} $Opt(-center) break
foreach {dx dy} $Opt(-shift) break
tophoto::anynode $node $img $side $Opt(-block) [expr {
round(0.5*$w - $side*$rx) + $dx
}] [expr {
round(0.5*$h - $side*$ry) + $dy
}] $w $h
return $img
}