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:
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 :(.
Still life: (+2)
Oscillators T=2, T=3, T=12: (+9)
Oscillators T=6 , T=7: (+6)
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:
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;
}
I'm probably going to post an answer that breaks one of the rules. I'm simulating Life on a fractal grid, with just one shape of cell at an infinite number of sizes. So either it breaks the "2+ shapes" rule or the "finite number of shapes" rule. But I think it's very neat and worth posting here. I don't mind if it gets downvoted. – Sparr – 2015-11-09T19:50:48.147
I did a 3D (cubic) implementation in Minecraft once. Should I go dig up that code and post it? – Draco18s no longer trusts SE – 2018-12-10T15:44:45.927
has anyone done equilateral triangles game of life before? I have heard of hexlife but not equilateral triangles – cmarangu – 2019-03-06T20:29:17.237
I think you should specifically define what counts as "consistently in some direction", so it's clear whether that only works for periodic tilings or might work for aperiodic tilings, too. – Martin Ender – 2014-08-06T14:03:34.120
@MartinBüttner Done – Calvin's Hobbies – 2014-08-06T14:11:49.660
You might want to put a size constraint on it as well (e.g. "a spaceship must never exceed an arbitrary but constant number of cells in size"), or you'd count moving infinite growth as a spaceship as well. – Martin Ender – 2014-08-06T14:14:27.327
7There's a strong case to be made that if it's not on a regular square grid it's not Conway's Life, but a Life-like automaton. Certainly if you want to talk about "the standard rules of Conway's Game of Life" and exclude tilings in which every cell has exactly 8 neighbours you're asking for an oxymoron. – Peter Taylor – 2014-08-06T14:18:11.817
2@PeterTaylor That's a pretty semantic difference that I can't imagine would be confusing in this context, but just to be sure I've changed it (along with Martin's suggestions). – Calvin's Hobbies – 2014-08-06T14:32:47.640
Do you accept automations with not-all-dead background conditions? – John Dvorak – 2014-08-06T14:49:30.500
4Do I need to tile the euclidean plane? – John Dvorak – 2014-08-06T14:56:57.180
3Your "topologically distinct" condition also leaves a massive loophole which allows direct implantation of the standard Life by means of a grid of squares each of which has a triangular wedge removed from its top edge. The result is a tiling of triangles and square-minus-triangles in which each triangle has two squares for neighbours, each square has two triangles and eight squares, and the triangles can simply be ignored. That's a cheap 10230-point base score. – Peter Taylor – 2014-08-06T14:57:38.063
@PeterTaylor the triangles can't be ignored. They break connectivity along one diagonal. Triangles that are always white and wedged along an edge will not, though. – John Dvorak – 2014-08-06T14:59:11.630
@JanDvorak Yes the euclidean plane, and the background should default to dead. – Calvin's Hobbies – 2014-08-06T15:00:59.883
You could break the square-tiling-plus-some-edges tiling by requiring that two faces either connect at an edge or at a vertex, never at two places at once. – John Dvorak – 2014-08-06T15:02:48.480
@PeterTaylor I consider two edges joined at a degree-2 vertex as a single edge. You could introduce extra tiles to distinguish when a vertex is binary or not. – John Dvorak – 2014-08-06T15:07:41.070
@PeterTaylor How about requiring that all cells must have at least 3 neighbors so they always have a chance to come alive? (And if it's not clear, two cells can only neighbor each other once.) – Calvin's Hobbies – 2014-08-06T15:08:35.430
@Calvin'sHobbies that doesn't break the "square-grid-plus-extra-faces" loophole. You can bevel any edge with a face that only touches two cells by edge, and a bunch of others by vertex. – John Dvorak – 2014-08-06T15:09:25.157
You could specify that removing any combination of prototiles doesn't leave behind one of the forgotten grids. It sounds exacly like what you want. – John Dvorak – 2014-08-06T15:11:53.193
@PeterTaylor it fails under the subset-by-prototiles, though. The partial squares form a square grid. – John Dvorak – 2014-08-06T15:13:41.127
@PeterTaylor roger that. Voting too. – John Dvorak – 2014-08-06T15:14:06.137
If you must. I don't have the time to sort this out now however. – Calvin's Hobbies – 2014-08-06T15:15:12.997
I hope you don't mind if I point out the sandbox to you?
– John Dvorak – 2014-08-06T15:16:19.8604The inability to sort it out immediately is precisely the reason for closing it. It pre-empts answers being posted which prevent it from being fixed. – Peter Taylor – 2014-08-06T15:17:48.117
@PeterTaylor how does it? If you remove the square tiles, you are left with a grid that only has four neighbors per tile. In fact, the connectivity graph of a truncated square tiling is planar, and thus does not have the connectivity graph of a square or triangle tiling as a subgraph or even as a minor. The hexagonal tiling connectivity graph is not a subgraph either. It is a minor, but it is not a graph formed by deleting some prototiles and contracting order-2 nodes. – John Dvorak – 2014-08-06T15:59:20.450
@PeterTaylor Could you give it a look now? I've used Jan's suggestion and also removed the UTM bonus since it was kind of a joke anyway. – Calvin's Hobbies – 2014-08-07T04:42:41.477
@JanDvorak Same question – Calvin's Hobbies – 2014-08-07T04:43:03.073
@PeterTaylor I really would like to include that tiling if I could. What if the rule was simply that a tiling cannot trivially boil down to the regular square/tri/hex tilings? It's slightly vague but I'm not expecting a flurry of answers intentionally trying to find loopholes. – Calvin's Hobbies – 2014-08-07T11:30:04.153
Would it remove ambiguity to say something along the lines of "the boundary between neighbours must be connected"? So two cells can have as many vertices and edges adjacent as you like, as long as there are no gaps in that boundary. That might avoid excluding some interesting cases by "can only neighbour other cells once". – trichoplax – 2014-08-07T11:46:10.660
Hopefully that will avoid people thinking that Penrose Tiling P1 is excluded, due to some tiles sharing 3 vertices and 2 edges. – trichoplax – 2014-08-07T11:48:00.000
@githubphagocyte I like the connected boundary idea but I had never planned for cells to neighbor each other more than once. After all, two adjacent tiles on a square grid share two corners and an edge but that doesn't mean they neighbor each other more than once. – Calvin's Hobbies – 2014-08-07T11:54:16.610
I see that you need to rule out cells touching in two different places due to the loophole pointed out by Peter Taylor, so this suggested wording was just to clarify in case someone is put off by thinking that two adjacent edges between neighbours is excluded. – trichoplax – 2014-08-07T11:56:35.233
For most people it should already be clear what you mean, but the extra clarity might attract slightly more competition. – trichoplax – 2014-08-07T11:57:30.210
@githubphagocyte Ah, I see I see. I will clarify that. – Calvin's Hobbies – 2014-08-07T11:59:29.120
@PeterTaylor with the truncated hexagonal tiling I don't think there is a way of recreating hexagonal life behaviour with the same rule since the triangles would kill many valid hexagonal patterns. Is there a way round that I can't picture? – trichoplax – 2014-08-07T12:00:05.683
@githubphagocyte No no, he was saying that my old spec ruled that tiling out, which was not desirable since it can't trivially recreate the hex tiling. – Calvin's Hobbies – 2014-08-07T12:02:40.100
1Now to design an aperiodic tiling with wrap around boundary conditions... – trichoplax – 2014-08-07T12:05:41.937
Can there be some triangles and squares? Let's say that there are dozens of different tilings, but some of them are triangles. Is that possible? – Jaa-c – 2014-08-07T17:47:53.437
1@Jaa-c Sure. Squares triangles and hexagons are allowed, just not their regular tilings. – Calvin's Hobbies – 2014-08-07T18:03:39.823
@Calvin'sHobbies So oscillators with periods from 9 to 29 don't give any points? :( – Martin Ender – 2014-08-08T13:22:23.827
@MartinBüttner Nope, sorry. I had to make the cutoff somewhere. – Calvin's Hobbies – 2014-08-08T13:41:29.543
@MartinBüttner Actually, after seeing some of the really neat large oscillators below I've decided to change the oscillator point scale. – Calvin's Hobbies – 2014-08-08T14:06:07.190
Hm your definition of glider is interesting. Say I'd be using some radial tiling which is isomorphic to the regular square grid with periodic boundary conditions along some x+y=c (which would be illegal, I know). Then the standard GoL glider would loop around the tangent and would by your definitions result in arbitrarily many oscillators with period of my polar resolution instead of being defined as a glider. If I rotate it by 90 degrees, I get a glider along the radial. I'm not saying you should fix it, I just thought I'd point it out so you can decide whether that's your intention. – Martin Ender – 2014-08-09T00:02:40.093
@MartinBüttner I guess in that case you would have all the oscillators and spaceships. But can such a radial tiling have finitely many prototiles? – Calvin's Hobbies – 2014-08-09T00:22:55.047
@Calvin'sHobbies How can a glider (or a gun) exist on an aperiodic tiling? – mniip – 2014-08-09T07:19:11.600
I presume that for the oscillator of period 30+ you'll disqualify one which is just two oscillators such that the lcm of their periods is 30+? – Peter Taylor – 2014-08-09T07:37:03.350
@mniip Chances are they cant't. That's why aperiodic tiling get 40 points automatically. – Calvin's Hobbies – 2014-08-09T09:19:31.627
@PeterTaylor Yes. They can't just be two independent 3 and 10 oscillators (for example). – Calvin's Hobbies – 2014-08-09T09:22:09.120
For infinite growth, is the "enough evidence of the pattern that it is practically certain" criterion met by showing a quadratic increase in population over 3000 generations? – Peter Taylor – 2014-08-12T15:43:46.653
@PeterTaylor That sounds like a good contender, please show us. Ideally there would be an simple pattern to the growth (like the glider gun) for easy verification. – Calvin's Hobbies – 2014-08-12T16:32:41.703
No simple pattern, alas: http://i.imgur.com/tteyVyu.gif is the first 300 generations
– Peter Taylor – 2014-08-12T16:48:05.807@PeterTaylor You can assume that is infinite until shown otherwise. – Calvin's Hobbies – 2014-08-13T02:17:21.657