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.
Text-to-pattern tools
Quickly seed the grid from phrases or emoji art. The buffer slider controls spacing so strings stay readable after evolution.
Import & export
Bring in RLE or plaintext patterns, drag files straight onto the canvas, or export the current state for debugging.
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 . For a finite grid of size , a configuration is a function:
The Game of Life update rule is represented as:
For each cell :
where the neighbor count is:
2 Main Theorem
Theorem: The decision problem REV-GOL is NP-Complete.
In other words, REV-GOL asks: "Given a Game of Life configuration , does there exist a predecessor configuration such that applying one step of the Game of Life rules to produces ?"
3 NP Membership
REV-GOL NP: A nondeterministic algorithm can guess a candidate predecessor and verify in time that .
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.
Additional supporting gadgets handle carry propagation and enforce the timing constraints needed for reversible evolution.
6 Concrete Example
Consider the 3-SAT formula:
Clause 1: is satisfied when at least one of:
Clause 2: is satisfied when at least one of:
7 Conclusion
Thus, has a predecessor if and only if is satisfiable. Since 3-SAT is NP-complete and REV-GOL 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:
Primary Variables
Boolean variable for each cell at each time step
Auxiliary Variables
Helper variables for neighbor counts and state transitions to reduce constraint complexity
Game of Life Constraints
Conway's rules encoded as SAT clauses linking consecutive time steps
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.