Implement the Game of Life on Anything but a Regular Grid

Penrose rhombii in Python, +97 points

I chose a penrose tiling composed of two different shaped rhombuses, meeting 3-8 per vertex. This penrose tiling is proven aperiodic elsewhere. The simulation is graphical (via pygame) and interactive. Comments indicate two places in the code where algorithm implementation was taken from another source.

animation of penrose life ending with p12 oscillator

There are many small neighborhood still lifes:

still life in penrose life still life in penrose life still life in penrose life

Any vertex with four "on" neighbors is a still life:

butterfly still life in penrose life spiky still life in penrose life pacman still life in penrose life

Any loop where no dead interior cells touch three cells on the loop is also a still life:

loop still life in penrose life loop still life in penrose life

There are oscillators at various frequencies:

p2: (many variations)

period 2 oscillator in penrose life

p3:

period 3 oscillator in penrose life

p4:

period 4 oscillator in penrose life period 4 oscillator in penrose life period 4 oscillator in penrose life

p5:

period 5 oscillator in penrose life

p6:

period 6 oscillator in penrose life

p7:

period 7 oscillator in penrose life period 7 oscillator in penrose life

p12:

period 12 oscillator in penrose life

p20:

period 20 oscillator in penrose life

The rules and clarifications as written mostly do not allow for gliders or guns in a non-planned aperiodic tiling. That leaves infinite growth, which I would argue isn't likely, and a p30+ oscillator, which almost certainly exists but will take a while to find.

python penrose-life.py will generate a single randomly colored periodic tiling python -O penrose-life.py or just ./penrose-life.py will actually run the simulation. While running it will try to identify oscillators, and when it finds one (p>2) it will screenshot it. After recording an oscillator, or a stalled board, the board is randomized.

Clicking a cell in the simulation will toggle it.

The following keyboard shortcuts exist in the simulation:

  • Escape - quit the program
  • Space - randomize the whole board
  • P - pause the simulation
  • S - single step the simulation
  • F - toggle "fast" mode, rendering only every 25th frame

The initial seed of the penrose tiling algorithm is a circle of ten narrow triangles. This could be changed to single triangle, or a different arrangement of triangles, symmetric or not.

Source:

#!/usr/bin/env python -O

# tiling generation code originally from http://preshing.com/files/penrose.py

import sys
import math
import time
import cairo
import cmath
import random
import pygame

#TODO: command line parameters
#------ Configuration --------
IMAGE_SIZE = (1200, 1200)
OFFX = 600
OFFY = 600
RADIUS = 600
if __debug__: NUM_SUBDIVISIONS = 5
else: NUM_SUBDIVISIONS = 7
#-----------------------------

goldenRatio = (1 + math.sqrt(5)) / 2

class Triangle():
    def __init__(self, parent = None, color = 0, corners = []):
        self.parent = parent
        self.other_half = None
        # immediate neighbor 0 is on BA side, 1 is on AC side
        self.neighbors = [None, None]
        # all_neighbors includes diagonal neighbors
        self.all_neighbors = set()
        # child 0 is first on BA side, 1 is second, 2 is on AC side
        self.children = []
        self.color = color
        if __debug__: self.debug_color = (random.random(),random.random(),random.random())
        self.state = random.randint(0,1)
        self.new_state = 0
        self.corners = corners
        self.quad = None
    def __repr__(self):
        return "Triangle: state=" + str(self.state) + \
            " color=" + str(self.color) + \
            " parent=" + ("yes" if self.parent else "no") + \
            " corners=" + str(self.corners)
    # break one triangle up into 2-3 smaller triangles
    def subdivide(self):
        result = []
        A,B,C = self.corners
        if self.color == 0:
            # Subdivide red triangle
            P = A + (B - A) / goldenRatio
            result = [Triangle(self, 0, (C, P, B)), Triangle(self, 1, (P, C, A))]
        else:
            # Subdivide blue triangle
            Q = B + (A - B) / goldenRatio
            R = B + (C - B) / goldenRatio
            result = [Triangle(self, 1, (Q, R, B)), Triangle(self, 0, (R, Q, A)), Triangle(self, 1, (R, C, A))]
        self.children.extend(result)
        return result;
    # identify the left and right neighbors of a triangle
    def connect_immediate(self):
        o = None
        n = self.neighbors
        if self.parent:
            if self.color == 0: # red child
                if self.parent.color == 0: # red parent
                    if self.parent.neighbors[0]:
                        if self.parent.neighbors[0].color == 0: # red left neighbor
                            o = self.parent.neighbors[0].children[0]
                        else: # blue left neighbor
                            o = self.parent.neighbors[0].children[1]
                    n[0] = self.parent.children[1]
                    if self.parent.other_half:
                        n[1] = self.parent.other_half.children[0]
                else: # blue parent
                    if self.parent.neighbors[0]:
                        if self.parent.neighbors[0].color == 0: # red left neighbor
                            o = self.parent.neighbors[0].children[0]
                        else: # blue left neighbor
                            o = self.parent.neighbors[0].children[1]
                    n[0] = self.parent.children[0]
                    n[1] = self.parent.children[2]
            else: # blue child
                if self.parent.color == 0: # red parent
                    if self.parent.neighbors[1]:
                        if self.parent.neighbors[1].color == 0: # red right neighbor
                            o = self.parent.neighbors[1].children[1]
                        else: # blue right neighbor
                            o = self.parent.neighbors[1].children[2]
                    n[0] = self.parent.children[0]
                    if self.parent.neighbors[0]:
                        if self.parent.neighbors[0].color == 0: # red left neighbor
                            n[1] = self.parent.neighbors[0].children[1]
                        else: # blue left neighbor
                            n[1] = self.parent.neighbors[0].children[0]
                else: # blue child of blue parent
                    if self.corners[2] == self.parent.corners[1]: # first blue child
                        if self.parent.other_half:
                            o = self.parent.other_half.children[0]
                        n[0] = self.parent.children[1]
                        if self.parent.neighbors[0]:
                            if self.parent.neighbors[0].color == 0: # red left neighbor
                                n[1] = self.parent.neighbors[0].children[1]
                            else: #blue left neighbor
                                n[1] = self.parent.neighbors[0].children[0]
                    else: # second blue child
                        if self.parent.neighbors[1]:
                            if self.parent.neighbors[1].color == 0: # red right neighbor
                                o = self.parent.neighbors[1].children[1]
                            else: # blue right neighbor
                                o = self.parent.neighbors[1].children[2]
                        if self.parent.other_half:
                            n[0] = self.parent.other_half.children[2]
                        n[1] = self.parent.children[1]
        self.other_half = o
        if o:
            self.state = self.other_half.state
            if __debug__: self.debug_color = self.other_half.debug_color

#TODO: different seed triangle configurations
# Create wheel of red triangles around the origin
triangles = [[]]
for i in xrange(10):
    B = cmath.rect(RADIUS, (2*i - 1) * math.pi / 10)+OFFX+OFFY*1j
    C = cmath.rect(RADIUS, (2*i + 1) * math.pi / 10)+OFFX+OFFY*1j
    if i % 2 == 0:
        B, C = C, B  # Make sure to mirror every second triangle
    triangles[0].append(Triangle(None, 0, (OFFX+OFFY*1j, B, C)))

# identify the neighbors of the starting triangles
for i in xrange(10):
    if i%2:
        triangles[0][i].neighbors[0] = triangles[0][(i+9)%10]
        triangles[0][i].neighbors[1] = triangles[0][(i+1)%10]
    else:
        triangles[0][i].neighbors[1] = triangles[0][(i+9)%10]
        triangles[0][i].neighbors[0] = triangles[0][(i+1)%10]

# Perform subdivisions
for i in xrange(NUM_SUBDIVISIONS):
    triangles.append([])
    for t in triangles[i]:
        triangles[i+1].extend(t.subdivide())
    for t in triangles[i+1]:
        t.connect_immediate()

# from here on, we only deal with the most-subdivided triangles
tris = triangles[NUM_SUBDIVISIONS]

# make a dict of every vertex, containing a list of every triangle sharing that vertex
vertices = {}
for t in tris:
    for c in t.corners:
        if c not in vertices:
            vertices[c] = []
        vertices[c].append(t)

# every triangle sharing a vertex are neighbors of each other
for v,triset in vertices.iteritems():
    for t in triset:
        t.all_neighbors.update(triset)

# combine mirrored triangles into quadrilateral cells
quads = []
total_neighbors = 0
for t in tris:
    if t.quad == None and t.other_half != None:
        quads.append(t)
        q = t
        q.corners = (q.corners[0], q.corners[1], q.other_half.corners[0], q.corners[2])
        q.quad = q
        q.other_half.quad = q
        q.all_neighbors.update(q.other_half.all_neighbors)
        q.all_neighbors.remove(q.other_half)
        q.all_neighbors.remove(q)
        total_neighbors += len(q.all_neighbors)

# clean up quads who still think they have triangles for neighbors
for q in quads:
    new_neighbors = set()
    for n in q.all_neighbors:
        if len(n.corners)==3:
            if n.other_half:
                if len(n.other_half.corners)==4:
                    new_neighbors.add(n.other_half)
        else:
            new_neighbors.add(n)
    q.all_neighbors = new_neighbors


# # adopt your other half's neighbors, minus them and yourself. mark other half as dead.
# for t in tris:
#     if t.other_half:
#         t.all_neighbors.update(t.other_half.all_neighbors)
#     t.all_neighbors.remove(t)
#     if t.other_half and t.other_half in t.all_neighbors:
#         t.all_neighbors.remove(t.other_half)
#     if t.other_half and not t.dead_half:
#         t.other_half.dead_half = True

pygame.init()
screen = pygame.display.set_mode(IMAGE_SIZE, 0, 32)
pygame.display.set_caption("Penrose Life")
pygame.display.flip()

paused = False
fast = False
randomize = True
found_oscillator = 0
randomized_tick = 0
tick = 0
timed_tick = 0
timed_tick_time = time.clock()
render_countdown = 0

history_length = 45
quad_history = [[0]*len(quads)]*history_length
quad_pointer = 0

myfont = pygame.font.SysFont("monospace", 15)
guidish = random.randint(0,99999999)

while True:

    tick += 1
    if tick - randomized_tick > 1000 and render_countdown == 0:
        randomize = True
    edited = False
    step = False
    if found_oscillator > 0 and render_countdown == 0:
        print "Potential p" + str(found_oscillator) + " osillator"
        render_countdown = found_oscillator
    if render_countdown == 0: # don't handle input while rendering an oscillator
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit(0)
            elif event.type == pygame.KEYDOWN:
                # print event
                if event.scancode == 53: # escape
                    sys.exit(0)
                elif event.unicode == " ": # randomize
                    randomize = True
                    edited = True
                elif event.unicode == "p": # pause
                    paused = not paused
                elif event.unicode == "f": # fast
                    fast = not fast
                elif event.unicode == "s": # step
                    paused = True
                    step = True
            elif event.type == pygame.MOUSEBUTTONDOWN:
            # click to toggle a cell
                x = event.pos[0]
                y = event.pos[1]
                for q in quads:
                    poly = [(c.real,c.imag) for c in q.corners]
                    # http://www.ariel.com.au/a/python-point-int-poly.html
                    n = len(poly)
                    inside = False
                    p1x,p1y = poly[0]
                    for i in range(n+1):
                        p2x,p2y = poly[i % n]
                        if y > min(p1y,p2y):
                            if y <= max(p1y,p2y):
                                if x <= max(p1x,p2x):
                                    if p1y != p2y:
                                        xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
                                    if p1x == p2x or x <= xinters:
                                        inside = not inside
                        p1x,p1y = p2x,p2y
                    if inside:
                        edited = True
                        q.state = 0 if q.state==1 else 1

    if randomize and render_countdown == 0:
        randomized_tick = tick
        randomize = False
        for q in quads:
            q.state = random.randint(0,1)
            edited = True

    if (not fast) or (tick%25==0) or edited or render_countdown > 0:
        # draw filled quads
        for q in quads:
            cs = [(c.real,c.imag) for c in q.corners]
            if __debug__:
                color = q.debug_color
                color = (int(color[0]*256)<<24)+(int(color[1]*256)<<16)+(int(color[2]*256)<<8)+0xFF
            else:
                if q.state == 0:
                    color = 0xFFFFFFFF
                else:
                    color = 0x000000FF
            pygame.draw.polygon(screen, color, cs, 0)
        # draw edges
        for q in quads:
            if len(q.corners)==3:
                exit(1)
            cs = [(c.real,c.imag) for c in q.corners]
            width = 3
            pygame.draw.lines(screen, 0x7F7F7FFF, 1, cs, int(width))
        now = time.clock()
        speed = (tick-timed_tick)/(now-timed_tick_time)
        timed_tick_time = now
        timed_tick = tick
        screen.blit(screen, (0, 0))
        label = myfont.render("%4.2f/s"%speed, 1, (255,255,255))
        screen.fill(pygame.Color("black"), (0, 0, 110, 15))
        screen.blit(label, (0, 0))        
        pygame.display.update()

    if __debug__:
        break

    if paused and not step and render_countdown == 0:
        time.sleep(0.05)
        continue

    # screenshot
    if render_countdown > 0:
        filename = "oscillator_p%03d_%08d_%03d.png" % (found_oscillator, guidish, found_oscillator - render_countdown)
        pygame.image.save(screen,filename)
        render_countdown -= 1
        if render_countdown == 0:
            guidish = random.randint(0,99999999)
            found_oscillator = 0
            randomize = True
            continue


    # calculate new cell states based on the Game of Life rules
    for q in quads:
        a = sum([n.state for n in q.all_neighbors])
        q.new_state = q.state
        # dead cells with three neighbors spawn
        if q.state == 0 and a == 3:
            q.new_state = 1
        # live cells only survive with two or three neighbors
        elif a < 2 or a > 3:
            q.new_state = 0

    # update cell states
    for q in quads:
        q.state = q.new_state

    this_state = [q.state for q in quads]

    # don't bother checking
    if render_countdown == 0:
        # compare this board state to the last N-1 states
        for i in range(1,history_length):
            if quad_history[(quad_pointer-i)%history_length] == this_state:
                if i == 1 or i == 2: # stalled board or p2 oscillator (boring)
                    randomize = True
                    break
                #TODO: give up if the "oscillator" includes border cells
                #TODO: identify cases of two oprime oscillators overlapping
                elif i > 2:
                    found_oscillator = i
                    break # don't keep looking

        # remember this board state
        quad_history[quad_pointer] = this_state
        quad_pointer = (quad_pointer+1)%history_length

if __debug__:
    filename = "penrose.png"
    pygame.image.save(screen,filename)
    time.sleep(1)

C++ w/ OpenGL (+17)

So I tried 3-Isohedral convex pentagon grid. Works for me ;) Standard game of life rules apply, except the grid is not infinite - there are border cells outside the image. 30% of the cells are initially alive.

This is how the grid looks like:

enter image description here

The live version:

Blue cells are alive, white are dead. Red cells just died, green were just born. Note that the artifacts in the image are the result of gif compression, SO doesn't like 10MB gifs :(.

enter image description here

Still life: (+2)

enter image description here

Oscillators T=2, T=3, T=12: (+9)

enter image description here enter image description here

Oscillators T=6 , T=7: (+6)

enter image description here

There are many more different oscillators... But it seems that the grid is not regular enough for a ship...

This is nothing (no points), but I like it:

enter image description here

The code is a mess :) Uses some ancient fixed OpenGL. Otherwise used GLEW, GLFW, GLM and ImageMagick for gif export.

/**
 * Tile pattern generation is inspired by the code 
 * on http://www.jaapsch.net/tilings/
 * It saved me a lot of thinkink (and debugging) - thank you, sir!
 */

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <FTGL/ftgl.h>  //debug only
#include <ImageMagick-6/Magick++.h> //gif export
#include "glm/glm.hpp" 

#include <iostream>
#include <array>
#include <vector>
#include <set>
#include <algorithm>
#include <unistd.h>

typedef glm::vec2 Point;
typedef glm::vec3 Color;

struct Tile {
    enum State {ALIVE=0, DEAD, BORN, DIED, SIZE};

    static const int VERTICES = 5;
    static constexpr float SCALE = 0.13f;
    static constexpr std::array<std::array<int, 7>, 18> DESC 
    {{
        {{1, 0,0, 0,0,0, 0}},
        {{0, 1,2, 0,2,1, 0}},
        {{2, 2,3, 0,2,3, 1}},
        {{1, 0,4, 0,0,1, 0}},
        {{0, 1,2, 3,2,1, 0}},
        {{2, 2,3, 3,2,3, 1}},
        {{1, 0,4, 3,0,1, 0}},
        {{0, 1,2, 6,2,1, 0}},
        {{2, 2,3, 6,2,3, 1}},
        {{1, 0,4, 6,0,1, 0}},
        {{0, 1,2, 9,2,1, 0}},
        {{2, 2,3, 9,2,3, 1}},
        {{1, 0,4, 9,0,1, 0}},
        {{0, 1,2,12,2,1, 0}},
        {{2, 2,3,12,2,3, 1}},
        {{1, 0,4,12,0,1, 0}},
        {{0, 1,2,15,2,1, 0}},
        {{2, 2,3,15,2,3, 1}}
    }};

    const int ID;
    std::vector<Point> coords;
    std::set<Tile*> neighbours;
    State state;
    State nextState;
    Color color;

    Tile() : ID(-1), state(DEAD), nextState(DEAD), color(1, 1, 1) {
        const float ln = 0.6f;
        const float h = ln * sqrt(3) / 2.f;
        coords = {
            Point(0.f,      0.f), 
            Point(ln,       0.f), 
            Point(ln*3/2.f,h), 
            Point(ln,       h*4/3.f), 
            Point(ln/2.f,   h)
        };
        for(auto &c : coords) {
            c *= SCALE;
        }
    }

    Tile(const int id, const std::vector<Point> coords_) : 
        ID(id), coords(coords_), state(DEAD), nextState(DEAD), color(1, 1, 1) {}

    bool operator== (const Tile &other) const {
        return ID == other.ID;
    }

    const Point & operator[] (const int i) const {
        return coords[i];
    }
    void updateState() {
        state = nextState;
    }
    /// returns "old" state
    bool isDead() const {
        return state == DEAD || state == DIED;
    }
    /// returns "old" state
    bool isAlive() const {
        return state == ALIVE || state == BORN;
    }

    void translate(const Point &p) {
       for(auto &c : coords) {
           c += p;
       }
    }

    void rotate(const Point &p, const float angle) {
        const float si = sin(angle);
        const float co = cos(angle);
        for(auto &c : coords) {
            Point tmp = c - p;
            c.x = tmp.x * co - tmp.y * si + p.x;
            c.y = tmp.y * co + tmp.x * si + p.y;
        }      
    }

    void mirror(const float y2) {
       for(auto &c : coords) {
          c.y = y2 - (c.y - y2);
       }
    }

};
std::array<std::array<int, 7>, 18> constexpr Tile::DESC;
constexpr float Tile::SCALE;

class Game {
    static const int    CHANCE_TO_LIVE  = 30;       //% of cells initially alive
    static const int    dim             = 4;        //evil grid param

    FTGLPixmapFont &font;
    std::vector<Tile> tiles;
    bool animate; //animate death/birth
    bool debug; //show cell numbers (very slow)
    bool exportGif;     //save gif
    bool run;

public: 
    Game(FTGLPixmapFont& font) : font(font), animate(false), debug(false), exportGif(false), run(false) {
        //create the initial pattern
        std::vector<Tile> init(18);
        for(int i = 0; i < Tile::DESC.size(); ++i) {
            auto &desc = Tile::DESC[i];
            Tile &tile = init[i];
            switch(desc[0]) {   //just to check the grid
                case 0: tile.color = Color(1, 1, 1);break;
                case 1: tile.color = Color(1, 0.7, 0.7);break;
                case 2: tile.color = Color(0.7, 0.7, 1);break;
            }

            if(desc[3] != i) {
                const Tile &tile2 = init[desc[3]];
                tile.translate(tile2[desc[4]] - tile[desc[1]]);
                if(desc[6] != 0) {
                   float angleRad = getAngle(tile[desc[1]], tile[desc[2]]);
                   tile.rotate(tile[desc[1]], -angleRad);
                   tile.mirror(tile[desc[1]].y);
                   angleRad = getAngle(tile[desc[1]], tile2[desc[5]]);
                   tile.rotate(tile[desc[1]], angleRad);
                }
                else {
                   float angleRad = getAngle(tile[desc[1]], tile[desc[2]], tile2[desc[5]]);
                   tile.rotate(tile[desc[1]], angleRad);
                }
            }
        }

        const float offsets[4] {
            init[2][8].x - init[8][9].x,
            init[2][10].y - init[8][11].y,
            init[8][12].x - init[14][13].x,
            init[8][14].y - init[14][15].y 
        };

        // create all the tiles
        for(int dx = -dim; dx <= dim; ++dx) { //fuck bounding box, let's hardcode it
            for(int dy = -dim; dy <= dim; ++dy) {

                for(auto &tile : init) {
                    std::vector<Point> vert;
                    for(auto &p : tile.coords) {
                        float ax = dx * offsets[0] + dy * offsets[2];
                        float ay = dx * offsets[1] + dy * offsets[3];
                        vert.push_back(Point(p.x + ax, p.y + ay));
                    }
                    tiles.push_back(Tile(tiles.size(), vert));
                    tiles.back().color = tile.color;
                    tiles.back().state = tile.state;
                }
            }
        }

        //stupid bruteforce solution, but who's got time to think..
        for(Tile &tile : tiles) { //find neighbours for each cell 
            for(Tile &t : tiles) {
                if(tile == t) continue;
                for(Point &p : t.coords) {
                    for(Point &pt : tile.coords) {
                        if(glm::distance(p, pt) < 0.01 ) {
                            tile.neighbours.insert(&t);
                            break;
                        }
                    }
                }
            }
            assert(tile.neighbours.size() <= 9);
        }   
    }

    void init() {
        for(auto &t : tiles) {
            if(rand() % 100 < CHANCE_TO_LIVE) {
                t.state = Tile::BORN;
            }
            else {
                t.state = Tile::DEAD;           
            }
        }
    }

    void update() {
        for(auto &tile: tiles) {
            //check colors
            switch(tile.state) {
                case Tile::BORN:    //animate birth
                    tile.color.g -= 0.05;
                    tile.color.b += 0.05;
                    if(tile.color.b > 0.9) {
                        tile.state = Tile::ALIVE;
                    }
                    break;
                case Tile::DIED:    //animate death
                    tile.color += 0.05;
                    if(tile.color.g > 0.9) {
                        tile.state = Tile::DEAD;
                    }
                    break;
            }
            //fix colors after animation
            switch(tile.state) {
                case Tile::ALIVE:
                    tile.color = Color(0, 0, 1);
                    break;
                case Tile::DEAD:
                    tile.color = Color(1, 1, 1);
                    break;
            }

            //draw polygons
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            glBegin(GL_POLYGON);
            glColor3f(tile.color.r, tile.color.g, tile.color.b);
            for(auto &pt : tile.coords) {
                glVertex2f(pt.x, pt.y); //haha so oldschool!
            }
            glEnd();
        }

        //draw grid
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        glColor3f(0, 0, 0);
        for(auto &tile : tiles) {
            glBegin(GL_POLYGON);
            Point c;    //centroid of tile
            for(auto &pt : tile.coords) {
                glVertex2f(pt.x, pt.y);
                c += pt;
            }
            glEnd();
            if(debug) {
                c /= (float) Tile::VERTICES;
                glRasterPos2f(c.x - 0.025, c.y - 0.01);
                font.Render(std::to_string(tile.ID).c_str()); // 
            }
        }

        if(!run) {
            return;
        }

        //compute new generation
        for(Tile &tile: tiles) {

            tile.nextState = tile.state; //initialize next state
            int c = 0;
            for(auto *n : tile.neighbours) {
                if(n->isAlive()) c++;
            }
            switch(c) {
                case 2:
                    break;
                case 3:
                    if(tile.isDead()) {
                        tile.nextState = animate ? Tile::BORN : Tile::ALIVE;
                        tile.color = Color(0, 1, 0);
                    }
                    break;
                default:
                    if(tile.isAlive()) {
                        tile.nextState = animate ? Tile::DIED : Tile::DEAD;
                        tile.color = Color(1, 0, 0);
                    }
                    break;
            }
        }
        //switch state to new
        for(Tile &tile: tiles) {
            tile.updateState();
        }
    }

    void stop() {run = false;}
    void switchRun() {run = !run;}
    bool isRun() {return run;}
    void switchAnim() {animate = !animate;}
    bool isAnim() {return animate;}
    void switchExportGif() {exportGif = !exportGif;}
    bool isExportGif() {return exportGif;}
    void switchDebug() {debug = !debug;}
    bool isDebug() const {return debug;}
 private:
    static float getAngle(const Point &p0, const Point &p1, Point const &p2) {
       return atan2(p2.y - p0.y, p2.x - p0.x) - atan2(p1.y - p0.y, p1.x - p0.x);
    }

    static float getAngle(const Point &p0, const Point &p1) {
       return atan2(p1.y - p0.y, p1.x - p0.x);
    }
};

class Controlls {
    Game *game;
    std::vector<Magick::Image> *gif;
    Controlls() : game(nullptr), gif(nullptr) {}
public:
    static Controlls& getInstance() {
        static Controlls instance;
        return instance;
    }

    static void keyboardAction(GLFWwindow* window, int key, int scancode, int action, int mods) {
        getInstance().keyboardActionImpl(key, action);
    }

    void setGame(Game *game) {
        this->game = game;
    }
    void setGif(std::vector<Magick::Image> *gif) {
        this->gif = gif;
    }
private:    
    void keyboardActionImpl(int key, int action) {
        if(!game || action == GLFW_RELEASE) {
            return;
        }
        switch (key) {
            case 'R':
                game->stop();
                game->init();
                if(gif) gif->clear();
                break;
            case GLFW_KEY_SPACE:
                game->switchRun();
                break;
            case 'A':
                game->switchAnim();
                break;
            case 'D':
                game->switchDebug();
                break;
                break;
            case 'G':
                game->switchExportGif();
                break;
        };
    }
};

int main(int argc, char** argv) {
    const int width         = 620;      //window size
    const int height        = 620;
    const std::string window_title  ("Game of life!");
    const std::string font_file     ("/usr/share/fonts/truetype/arial.ttf");
    const std::string gif_file      ("./gol.gif");

    if(!glfwInit()) return 1;

    GLFWwindow* window = glfwCreateWindow(width, height, window_title.c_str(), NULL, NULL);
    glfwSetWindowPos(window, 100, 100);
    glfwMakeContextCurrent(window);

    GLuint err = glewInit();
    if (err != GLEW_OK) return 2;

    FTGLPixmapFont font(font_file.c_str());
    if(font.Error()) return 3;
    font.FaceSize(8);

    std::vector<Magick::Image> gif; //gif export
    std::vector<GLfloat> pixels(3 * width * height);

    Game gol(font);
    gol.init();
    Controlls &controlls = Controlls::getInstance();
    controlls.setGame(&gol);
    controlls.setGif(&gif);

    glfwSetKeyCallback(window, Controlls::keyboardAction);

    glClearColor(1.f, 1.f, 1.f, 0);
    while(!glfwWindowShouldClose(window) && !glfwGetKey(window, GLFW_KEY_ESCAPE)) {
        glClear(GL_COLOR_BUFFER_BIT);

        gol.update();

        //add layer to gif
        if(gol.isExportGif()) {
            glReadPixels(0, 0, width, height, GL_RGB, GL_FLOAT, &pixels[0]);
            Magick::Image image(width, height, "RGB", Magick::FloatPixel, &pixels[0]);
            image.animationDelay(50);
            gif.push_back(image);
        }

        std::string info = "ANIMATE (A): ";
        info += gol.isAnim() ? "ON " : "OFF";
        info += " | DEBUG (D): ";
        info += gol.isDebug() ? "ON " : "OFF";
        info += " | EXPORT GIF (G): ";
        info += gol.isExportGif() ? "ON " : "OFF";
        info += gol.isRun() ? " | STOP (SPACE)" : " | START (SPACE)";
        font.FaceSize(10);
        glRasterPos2f(-.95f, -.99f);
        font.Render(info.c_str());

        if(gol.isDebug()) font.FaceSize(8);
        if(!gol.isDebug()) usleep(50000); //not so fast please!

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    //save gif to file
    if(gol.isExportGif()) {
        std::cout << "saving " << gif.size() << " frames to gol.gif\n";
        gif.back().write("./last.png");
        Magick::writeImages(gif.begin(), gif.end(), gif_file);
    }

    glfwTerminate();
    return 0;
}

Go, ? points

So rather than pin myself down to a particular tiling, I wrote a program that takes a gif or png of a tiling and runs life on it. The gif/png must use a single color for all the tiles.

package main

import (
    "flag"
    "image"
    "image/color"
    "image/gif"
    "image/png"
    "math/rand"
    "os"
    "strings"
)

func main() {
    flag.Parse()
    filename := flag.Args()[0]
    r, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
    var i image.Image
    if strings.HasSuffix(filename, ".gif") {
        i, err = gif.Decode(r)
        if err != nil {
            panic(err)
        }
    }
    if strings.HasSuffix(filename, ".png") {
        i, err = png.Decode(r)
        if err != nil {
            panic(err)
        }
    }

    // find background color
    back := background(i)

    // find connected regions
    n, m := regions(i, back)

    // find edges between regions
    edges := graph(i, m)

    // run life on the tiling
    life(i, n, m, edges)
}

// Find the most-common occurring color.
// This is the "background" color.
func background(i image.Image) color.Color {
    hist := map[color.Color]int{}
    b := i.Bounds()
    for y := b.Min.Y; y < b.Max.Y; y++ {
        for x := b.Min.X; x < b.Max.X; x++ {
            hist[i.At(x, y)]++
        }
    }
    maxn := 0
    var maxc color.Color
    for c, n := range hist {
        if n > maxn {
            maxn = n
            maxc = c
        }
    }
    return maxc
}

// find connected regions.  Returns # of regions and a map from pixels to their region numbers.
func regions(i image.Image, back color.Color) (int, map[image.Point]int) {

    // m maps each background point to a region #
    m := map[image.Point]int{}

    // number regions consecutively
    id := 0

    b := i.Bounds()
    for y := b.Min.Y; y < b.Max.Y; y++ {
        for x := b.Min.X; x < b.Max.X; x++ {
            if i.At(x, y) != back {
                continue
            }
            p := image.Point{x, y}
            if _, ok := m[p]; ok {
                continue // already in a region
            }
            q := []image.Point{p}
            m[p] = id
            k := 0
            for k < len(q) {
                z := q[k]
                k++
                for _, n := range [4]image.Point{{z.X - 1, z.Y}, {z.X + 1, z.Y}, {z.X, z.Y - 1}, {z.X, z.Y + 1}} {
                    if !n.In(b) || i.At(n.X, n.Y) != back {
                        continue
                    }
                    if _, ok := m[n]; ok {
                        continue
                    }
                    m[n] = id
                    q = append(q, n)

                }
            }
            if len(q) < 10 {
                // really tiny region - probably junk in input data
                for _, n := range q {
                    delete(m, n)
                }
                continue
            }
            id++
        }
    }
    return id, m
}

// edge between two regions.  r < s.
type edge struct {
    r, s int
}

// returns a set of edges between regions.
func graph(i image.Image, m map[image.Point]int) map[edge]struct{} {
    // delta = max allowed spacing between adjacent regions
    const delta = 6
    e := map[edge]struct{}{}
    for p, r := range m {
        for dx := -delta; dx <= delta; dx++ {
            for dy := -delta; dy <= delta; dy++ {
                n := image.Point{p.X + dx, p.Y + dy}
                if _, ok := m[n]; !ok {
                    continue
                }
                if m[n] > r {
                    e[edge{r, m[n]}] = struct{}{}
                }
            }
        }
    }
    return e
}

// run life engine
// i = image
// n = # of regions
// m = map from points to their region #
// edges = set of edges between regions
func life(i image.Image, n int, m map[image.Point]int, edges map[edge]struct{}) {
    b := i.Bounds()
    live := make([]bool, n)
    nextlive := make([]bool, n)
    palette := []color.Color{color.RGBA{0, 0, 0, 255}, color.RGBA{128, 0, 0, 255}, color.RGBA{255, 255, 128, 255}} // lines, on, off
    var frames []*image.Paletted
    var delays []int

    // pick random starting lives
    for j := 0; j < n; j++ {
        if rand.Int()%2 == 0 {
            live[j] = true
            nextlive[j] = true
        }
    }
    for round := 0; round < 100; round++ {
        // count live neighbors
        neighbors := make([]int, n)
        for e := range edges {
            if live[e.r] {
                neighbors[e.s]++
            }
            if live[e.s] {
                neighbors[e.r]++
            }
        }

        for j := 0; j < n; j++ {
            nextlive[j] = neighbors[j] == 3 || (live[j] && neighbors[j] == 2)
        }

        // add a frame
        frame := image.NewPaletted(b, palette)
        for y := b.Min.Y; y < b.Max.Y; y++ {
            for x := b.Min.X; x < b.Max.X; x++ {
                frame.SetColorIndex(x, y, 0)
            }
        }
        for p, r := range m {
            if live[r] {
                frame.SetColorIndex(p.X, p.Y, 1)
            } else {
                frame.SetColorIndex(p.X, p.Y, 2)
            }
        }
        frames = append(frames, frame)
        delays = append(delays, 30)

        live, nextlive = nextlive, live
    }

    // write animated gif of result
    w, err := os.Create("animated.gif")
    if err != nil {
        panic(err)
    }
    gif.EncodeAll(w, &gif.GIF{Image: frames, Delay: delays, LoopCount: 100})
    w.Close()
}

Then I just went on the web, grabbed some fun tiling images and ran the program on them.

go run life.go penrose1.go

It generates a file called "animated.gif" which contains a 100-step life simulation of the given tiling.

Standard life:

enter image description here enter image description here

Penrose tiles:

enter image description here enter image description here

enter image description here enter image description here

Above one has an oscillator of period 12.

enter image description here enter image description here

Above one has an oscillator of period 3.