Project

Reverse Game of Life

A Rust-powered exploration into inferring prior states of Conway's Game of Life using SAT solvers and WASM for interactive visualization.

Conway's Game of Life simulation

Tech Stack

Overview

Reverse Game of Life turns Conway’s classic cellular automaton on its head. Instead of only simulating future states, the project asks a harder question: what grid could have produced this pattern? The solution encodes the game rules as a SAT problem, leans on Rust for performance, and ships a WASM front end so visitors can experiment in the browser.

Highlights

  • Encodes cell transitions as boolean constraints and solves them via SAT to backtrack prior states.
  • Ships a WASM-powered playground where you can draw patterns, step forward or backward, and export GIFs of the evolution.
  • Includes Rust crates for simulation, constraint building, and wasm-bindgen bindings to keep the codebase modular and testable.

Lessons learned

Modeling the automaton as SAT exposed performance trade-offs quickly: naïve encodings ballooned in size, so I leaned on Rust’s iterators and memory ownership to keep generation tight. The WASM layer proved invaluable for debugging, because every change could be visualized immediately.

Interactive Playground

Experiment with the reverse solver directly in the browser. Draw on the grid, rewind generations, or export runs as GIFs—all powered by the Rust + WASM engine.

Live Simulation

Every interaction hits the same optimized engine that drives the full reverse solver. Grid updates, text conversions, and GIF recording all execute inside the WASM module.

Grid
Grid: 20x20
Live cells
Live cells: 0

Simulation controls

Step through generations, rewind, or record long runs. Multi-step uses the SAT backtracking heuristics to keep playback fast.

Frame Delay: 200ms

Text-to-pattern tools

Quickly seed the grid from phrases or emoji art. The buffer slider controls spacing so strings stay readable after evolution.

2

Import & export

Bring in RLE or plaintext patterns, drag files straight onto the canvas, or export the current state for debugging.

No file selected
Drag & drop pattern files here

Optimizing The Simulation

Performance notes brought over from the original write-up, organized into a reusable accordion pattern.

Engine Architecture

1 Engine Overview

This represents the best version yet of my Game of Life engine, delivering a ~10,000x performance improvement over the first naive implementation through bit manipulation and hardware-accelerated computation. Benchmarks regularly hit 158 billion cell updates per second on a laptop-class CPU, and there is still room to experiment.

The engine automatically detects hardware capabilities and picks the optimal SIMD configuration, supporting widths of 4, 8, or 16 parallel operations. It uses configurable generic parameters UltimateEngine<const N: usize> where N represents the SIMD lane count, allowing compile-time optimization for specific targets.

2 Bit-Packed Representation

The engine stores 64 cells per u64 integer, achieving 8x memory efficiency compared to byte-per-cell representations. This bit-packing strategy dramatically improves cache performance and enables vectorized operations on large cell groups simultaneously.

Grid layout uses MSB-first bit ordering with strategic padding which eliminates boundary checks during computation, while SIMD alignment ensures optimal memory access patterns. Pre-computed boundary masks handle edge cases efficiently without conditional branching in the hot path.

3 SIMD Vectorization

The engine leverages Rust's portable SIMD to process multiple 64-bit words simultaneously. Custom shift operations shl() and shr() handle cross-lane bit propagation using rotate_elements_left/right with pre-selected masks to maintain bit continuity across SIMD lanes. Using 8 SIMD lanes represents a 512x speedup compared to byte-per-cell representations.

Memory prefetching on x86_64 architectures uses _mm_prefetch with _MM_HINT_T0 to load next rows into L1 cache before processing, reducing memory latency. The engine processes columns in SIMD-aligned chunks, maximizing instruction-level parallelism and minimizing cache misses.

4 Neighbor Counting Algorithm

The core sub_step() function implements an optimized full/half adder algorithm for counting neighbors. Rather than iterating through 8 neighbors individually, it uses a two-stage binary tree reduction to compute neighbor counts for all 64 cells in parallel.

Neighbors are grouped into pairs using XOR for sum bits and AND for carry bits and then combined. The Game of Life rules are applied using bitwise operations: center |= ab; center &= b3 ^ b4; center &= !c0; eliminating conditional branching in the hotpath, allowing the vectorization to remain efficient.

5 Parallel Processing

Workloads are divided into row blocks that can be processed independently across threads. The scheduler balances work dynamically to avoid idle cores during uneven workloads.

Prefetch distance is tuned per architecture to maximize cache utilization; block sizes are chosen to maintain cache locality while minimizing synchronization overhead between worker threads.

6 WebAssembly Integration

Wasm SIMD is used when supported, providing significant performance improvements in modern browsers. The engine gracefully handles hardware limitations with automatic fallback to scalar implementations, ensuring compatibility across all platforms while maximizing performance on capable hardware.

Because the UI is powered by wasm-bindgen, the same Rust codebase drives both the native solver and the interactive web experience.

7 Future Work

Several optimization vectors remain unexplored. Current benchmarks show memory bandwidth saturation as the primary bottleneck, with CPU utilization plateauing at roughly 50% because of DRAM throughput. Future work includes experimenting with alternative memory layouts, smarter software prefetching, and adaptive block sizes tuned per architecture.

The scalar build already handled ~12 million cell updates per second—which is more than enough for the web demo—but this project is an excuse to chase theoretical limits. GPU-backed variants and distributed solving strategies are on the roadmap.

Proving Reverse GOL Is NP-Complete

The original MathJax-heavy proof has been migrated as-is using the new accordion components.

Proof Structure

1 Definitions & Setup

Let Σ={0,1}\Sigma = \{0,1\}. For a finite grid of size N×NN \times N, a configuration is a function:

c:{0,1,,N1}2{0,1}c : \{0,1,\dots,N-1\}^2 \to \{0,1\}

The Game of Life update rule is represented as:

f:{0,1}N×N{0,1}N×Nf : \{0,1\}^{N \times N} \to \{0,1\}^{N \times N}

For each cell (i,j)(i,j):

f(c)i,j={1if ci,j=1 and n(i,j){2,3},1if ci,j=0 and n(i,j)=3,0otherwise.f(c)_{i,j} = \begin{cases} 1 & \text{if } c_{i,j} = 1 \text{ and } n(i,j) \in \{2,3\}, \\ 1 & \text{if } c_{i,j} = 0 \text{ and } n(i,j) = 3, \\ 0 & \text{otherwise}. \end{cases}

where the neighbor count is:

n(i,j)=(d1,d2){1,0,1}2{(0,0)}c(i+d1,j+d2)n(i,j) = \sum_{(d_1,d_2) \in \{-1,0,1\}^2 \setminus \{(0,0)\}} c(i+d_1,j+d_2)
2 Main Theorem

Theorem: The decision problem REV-GOL is NP-Complete.

REV-GOL={c{0,1}N×Np{0,1}N×N such that f(p)=c}\text{REV-GOL} = \{\, c \in \{0,1\}^{N \times N} \mid \exists\, p \in \{0,1\}^{N \times N} \text{ such that } f(p)=c \,\}

In other words, REV-GOL asks: "Given a Game of Life configuration cc, does there exist a predecessor configuration pp such that applying one step of the Game of Life rules to pp produces cc?"

3 NP Membership

REV-GOL \in NP: A nondeterministic algorithm can guess a candidate predecessor pp and verify in O(N2)O(N^2) time that f(p)=cf(p) = c.

The verification process simply applies the Game of Life rules to each cell in the guessed configuration and checks if the result matches the given configuration.

4 NP-Hardness

We reduce from 3-SAT. Each boolean variable is modeled using paired wires that encode its true/false states, and clause gadgets ensure at least one incident literal satisfies the clause.

The reduction ensures the constructed configuration has a predecessor if and only if the original 3-SAT formula is satisfiable.

5 Gadget Construction

Variable gadgets propagate signals along synchronized wires so clauses receive consistent timing. Clause gadgets OR the incoming literals and feed a verification grid.

G=(i=1nVi)(i=1mGCi)(background)G = \left( \bigcup_{i=1}^n V_i \right) \cup \left( \bigcup_{i=1}^m G_{C_i} \right) \cup (\text{background})

Additional supporting gadgets handle carry propagation and enforce the timing constraints needed for reversible evolution.

6 Concrete Example

Consider the 3-SAT formula:

φ=(x1¬x2x3)(¬x1x2¬x3)\varphi = (x_1 \lor \neg x_2 \lor x_3) \land (\neg x_1 \lor x_2 \lor \neg x_3)

Clause 1: (x1¬x2x3)(x_1 \lor \neg x_2 \lor x_3) is satisfied when at least one of:

x1 is True (P1,T), +x2 is False (P2,F), +x3 is True (P3,T)x_1 \text{ is True } (P_{1,T}), \ + x_2 \text{ is False } (P_{2,F}), \ + x_3 \text{ is True } (P_{3,T})

Clause 2: (¬x1x2¬x3)(\neg x_1 \lor x_2 \lor \neg x_3) is satisfied when at least one of:

x1 is False (P1,F), +x2 is True (P2,T), +x3 is False (P3,F)x_1 \text{ is False } (P_{1,F}), \ + x_2 \text{ is True } (P_{2,T}), \ + x_3 \text{ is False } (P_{3,F})
7 Conclusion

Thus, cφc_\varphi has a predecessor if and only if φ\varphi is satisfiable. Since 3-SAT is NP-complete and REV-GOL \in NP, it follows that REV-GOL is NP-complete.

All progress will be posted on my GitHub first before making its way here.

Implementation: SAT-Based Reverse Solver

System Architecture

The implementation is structured as a modular Rust application with the following core components:

Configuration Management

YAML-based configuration system supporting flexible parameter tuning for grid size, generations, solver backends, optimization levels, and output formats.

Game of Life Engine

Core cellular automaton implementation with grid representation, rule application, I/O operations, and support for multiple boundary conditions (dead, wrap, mirror).

SAT Encoding & Solving

Converts Game of Life constraints into SAT problems with support for multiple backends: CaDiCaL (single-threaded) and ParKissat-RS (multithreaded) solvers.

Reverse Problem Solving

Handles problem definition, solution extraction, validation through forward simulation, and comprehensive output formatting with pattern analysis.

Utility & Display

Provides display utilities, output formatting, benchmarking tools, and comprehensive logging for performance analysis and debugging.

SAT Encoding Strategy

The solver employs a hybrid encoding approach that balances constraint complexity with solving efficiency:

1

Primary Variables

Boolean variable cell(x,y,t)cell(x,y,t) for each cell at each time step

Variables: {cell(i,j,t)0i<W,0j<H,0tG}\text{Variables: } \{cell(i,j,t) \mid 0 \leq i < W, 0 \leq j < H, 0 \leq t \leq G\}
2

Auxiliary Variables

Helper variables for neighbor counts and state transitions to reduce constraint complexity

Neighbor count: n(i,j,t)=(dx,dy)Ncell(i+dx,j+dy,t)\text{Neighbor count: } n(i,j,t) = \sum_{(d_x,d_y) \in N} cell(i+d_x, j+d_y, t)
3

Game of Life Constraints

Conway's rules encoded as SAT clauses linking consecutive time steps

cell(i,j,t+1)    (cell(i,j,t)n(i,j,t){2,3})(¬cell(i,j,t)n(i,j,t)=3)cell(i,j,t+1) \iff (cell(i,j,t) \land n(i,j,t) \in \{2,3\}) \lor (\neg cell(i,j,t) \land n(i,j,t) = 3)

Key Features

Multiple SAT Backends

Supports both CaDiCaL (single-threaded) and ParKissat-RS (multithreaded) solvers for optimal performance across different problem sizes

Multithreaded Solving

ParKissat-RS backend provides parallel SAT solving capabilities for complex problems with large grids and multiple generations

Multiple Solution Finding

Discovers all valid predecessor states up to a configurable limit using iterative SAT solving with hybrid encoding strategies

Solution Validation

Verifies found solutions by forward simulation to ensure correctness and detects known Game of Life patterns

Comprehensive Benchmarking

Built-in performance analysis tools to compare solver backends and optimization strategies across different problem complexities

Multiple Output Formats

Supports text, JSON, and visual representations with evolution path visualization and pattern analysis

Command Line Interface

The solver provides a comprehensive CLI with multiple commands for different use cases:

cargo run -- solve --config config/examples/simple.yaml

Find predecessor states using configuration file with customizable solver backends and optimization levels

cargo run -- solve --target input/target_states/glider.txt --generations 3 --max-solutions 5

Find multiple predecessor states for a given target configuration with custom parameters

cargo run -- setup --directory . --force

Initialize project structure with example configurations and target states

cargo run -- analyze --target input/target_states/blinker.txt

Analyze target state solvability and complexity estimates before attempting to solve

cargo run -- validate --predecessor p.txt --target t.txt --show-evolution

Validate that a predecessor correctly evolves to the target with complete evolution visualization

Crates and Command Line Interface

Game of Life Engine

A highly optimized Conway's Game of Life simulator featuring advanced SIMD parallelism and automatic hardware detection. The engine delivers 10,000x performance improvements through bit-packed representation (64 cells per u64), vectorized operations, and multi-threading with Rayon.

Key Features: Automatic SIMD width selection, memory prefetching, boundary optimization with ghost cells, and support for multiple input formats.

cargo run --release

Run visual simulation with default settings

cargo run --release -- --input my_pattern.txt --generations 20

Custom pattern simulation with specified generations

Repository Link: Link

Reverse SAT Solver

A Rust implementation that solves the NP-Complete problem of reversing Conway's Game of Life by converting it into boolean satisfiability (SAT) problems. Supports both CaDiCaL (single-threaded) and ParKissat-RS (multithreaded) solver backends for optimal performance across different problem sizes.

Key Features: Multiple SAT backends, hybrid encoding strategies, configurable parameters via YAML, comprehensive benchmarking tools, pattern analysis, and multiple output formats (text, JSON, visual).

cargo run -- solve --config config/examples/simple.yaml

Solve using configuration file with custom solver settings

cargo run -- analyze --target input/target_states/blinker.txt

Analyze target state solvability before attempting to solve

Repository Link: Link

ParKissat-RS Bindings

Safe Rust bindings for the ParKissat-RS parallel SAT solver, winner of SAT Competition 2022. Provides a memory-safe, idiomatic Rust API while maintaining high performance through minimal overhead FFI calls. Combines Kissat solver efficiency with parallel processing capabilities. Multithreaded may be a bit of a stretch since parallelizing SAT solving is very difficult, but some problems will be able to utilize multiple cores.

Key Features: Multi-threaded SAT solving, DIMACS format support, configurable solver parameters, comprehensive error handling, interruption support.

let mut solver = ParkissatSolver::new()?;

Create solver instance with safe Rust API

solver.load_dimacs("problem.cnf")?;

Load SAT problems from standard DIMACS format files

Repository Link: Link

Text-to-ASCII Converter

A Rust crate that converts text input into pixel art using 1s and 0s with a sophisticated variable-width font system. Supports A-Z uppercase, a-z lowercase, and space characters with optimal spacing through 1-5 pixel wide characters for compact, readable output.

Key Features: Variable-width font optimization, automatic buffer generation, case-sensitive rendering, graceful error handling without unwrap() calls, and performance optimization with pre-allocated collections.

cargo run

Interactive text-to-pixel art conversion with error messages

use text_to_input::text_to_pixel_art;

Library usage with comprehensive error handling

Repository Link: Link

Make Your Name Emerge From Chaos

Generate ASCII Pattern

Create your text pattern using either the website functionality above or the text_to_input crate. You can also manually draw on the grid with your mouse if you prefer custom patterns.

Website Method: Enter your name in the text input above, adjust the buffer size, and click "Generate from Text". Then use the download button to save your pattern.

CLI Method: Use the text_to_input crate for more control:

Important: No matter which method you use, you must download/save the pattern file for the next steps.

cd text_to_input && cargo run

Interactive text-to-ASCII conversion with custom settings

Setup Reverse Solver

Clone and set up the reverse Game of Life crate. This is the only crate not implemented on the website because running a SAT solver in the browser is not advisable.

Prerequisites: You must have Rust installed on your system. Check the crate documentation for additional requirements.

Repository Link: Link

git clone https://github.com/rrumana/game_of_life_reverse

Clone the reverse Game of Life repository

cd game_of_life_reverse && cargo run -- setup

Initialize the project structure and dependencies

Find Predecessor State

Move your generated pattern file into the input area of the reverse Game of Life crate, then run the solver to find a predecessor state.

Performance Guide: For a first name, 3-4 generations will take on the order of minutes, whereas 10 generations took about 12 hours on my laptop.

cp your_pattern.txt game_of_life_reverse/input/target_states/

Move your pattern file to the input directory

cargo run -- solve --target input/target_states/your_pattern.txt --generations 4

Run the reverse solver with your pattern and desired generations

Visualize the Magic

Take the new file from the reverse solver and upload it to the website above, or load it into the game_of_life crate. The initial state will look like random static - this is expected!

File Location: The solver will output a predecessor state file in the output directory. This file will contain multiple generations - this is completely normal! The one labelled generation 0 is the one you want, copy this into a new text file for upload.

Upload the solution file using the file upload button above

Load the predecessor state into the web simulation

Set Multi-Step generations ≥ the number used in CLI

Configure the simulation to run enough generations

cargo run --release -- --input solution_file.txt --generations 4

Alternative: Use the game_of_life crate CLI for visualization

Watch Name Emerge

Click the Multi-Step button and watch as your name emerges from the apparent chaos! The seemingly random initial state will evolve through the deterministic rules of Conway's Game of Life to reveal your text pattern.

Create GIF (Coming Soon)

TODO: I am planning on adding a function to turn this evolution into a downloadable GIF. The implementation approach is still being determined, but this feature will allow you to easily share your name emergence animation.

This section will be updated once the GIF generation functionality is implemented.

Tips & Tricks

No Solutions Found? If your name has no solutions, try adding a bigger buffer to the pattern. This increases the search space and makes it more likely to find a solution, though it will also increase computation time.

Performance Optimization: Start with fewer generations (3-4) for initial testing. You can always increase the generation count for more dramatic emergence effects once you've confirmed the pattern works.

Pattern Design: Simpler patterns with more spacing tend to have more solutions. Complex, tightly-packed text may require longer solve times or larger buffers.

Results and Analysis

Performance Results

The project successfully demonstrates the feasibility of using SAT solving to reverse-engineer Conway's Game of Life states. By encoding cellular automaton rules and constraints into boolean logic clauses, the system can efficiently discover predecessor states that would evolve into a given target configuration.

Performance Benchmarks: The system reveals impressive scalability for practical use cases: a 300-cell grid can be traced back 4 generations in under a second, while more ambitious reverse-engineering tasks like 6 generations require 8 minutes and 10 generations complete in approximately 2 hours. Even computationally intensive 20-generation lookbacks finish within 48 hours, anthough your results may vary, bigger grids will result in much longer search times.

Key Lessons Learned

The most significant challenge encountered was SAT encoding optimization, where initial implementations generated an excessive number of clauses that severely degraded solver performance. This experience highlighted how the formulation of constraints can dramatically impact computational efficiency, even when the underlying logical relationships remain equivalent.

Optimization Success: Through careful pruning I was able to reduce the number of clauses by a factor of 10 without compromising the encoding accuracy. When it comes to problems with exponential runtime complexity, pre-compute optimization becomes increasingly important for practical applications.

Future Work

The next phase will focus on leveraging parallelization to further enhance performance, specifically investigating multithreaded SAT solvers with robust Rust bindings for seamless integration into the existing codebase. The ParKissat-RS integration represents a significant step in this direction.

Additional Opportunities: Optimizing auxiliary algorithms and data structures presents opportunities for incremental improvements, though these optimizations will likely yield diminishing returns given that core SAT solving dominates the computational workload for large-scale problems. While I do not anticipate much more active development, I will post any breakthroughs here.