This project demonstrates that John Conway's Game of Life written in C++ using OOP features (inheritance, encapsulation, polymorphism) runs slower than the same implementation using the EO programming language. The broader goal is to show that the EO language is more efficient for object-intensive projects in terms of execution time. According to previous research, measurements showed that the C++ implementation for counting Fibonacci numbers using objects works slowly. Therefore, we decided to implement the Game of Life. Fibonacci number counting is insufficient because using objects for its implementation is artificial, as we can use a simple loop to calculate it.
First, you need to install the Boost library, along with g++,
clang-format and clang-tidy if not already installed.
On Ubuntu, install everything as follows:
sudo apt-get install --yes g++ libboost-all-dev clang-tidy clang-formatAdditionally, install eoc.
To build the project, run:
makeChoose between fast_life or slow_life to start the game.
For fast_life, run:
./fast_life --helpOtherwise:
./slow_life --helpIt will show you all the available options.
Both implementations share the same options.
Examples below use fast_life.
For example, you can enter something like this:
./fast_life --batch 20 --size 40x40 --put 3x6 --put 6x8 --put 12x9This runs an automated game with 20 generations on a 40x40 grid with three initially alive cells.
To clean the environment:
make cleanTo format all .cpp files using clang-format:
make fixTo run tests for the fast or slow version:
make fast_testOr:
make slow_testTo see the Gosper glider gun pattern, run:
./fast_life --batch 1000 --sleep 170 --size 40x40 --put 10x26 --put 11x26 \
--put 11x24 --put 12x14 --put 12x15 --put 12x23 --put 12x22 --put 12x36 \
--put 12x37 --put 13x13 --put 13x17 --put 13x22 --put 13x23 --put 13x36 \
--put 13x37 --put 14x2 --put 14x3 --put 14x12 --put 14x18 --put 14x22 \
--put 14x23 --put 15x2 --put 15x3 --put 15x12 --put 15x16 --put 15x18 \
--put 15x19 --put 15x24 --put 15x26 --put 16x12 --put 16x18 --put 16x26 \
--put 17x13 --put 17x17 --put 18x14 --put 18x15To see an infinite loop pattern:
./fast_life --batch 40 --sleep 500 --size 10x10 --put 5x4 --put 5x5 --put 5x6To run the EO version:
eoc --alone dataize life size 3x3 put 2x1 put 2x2 put 2x3You can use custom arguments. For the size option, use NxM where
N is height and M is width. Use put AxB to place an alive cell
at position AxB:
eoc --alone dataize life size NxM put AxB put CxD put ExF put ... and so on.To recompile after changing the EO file, omit the --alone option:
eoc dataize life size NxM put AxB put CxD put ExF put ... and so on.There are some notable features of Cell and Field objects that
should be mentioned: all for statements are replaced with recursion,
objects are immutable, if we change something, we make a copy
of the object and make changes using the constructor, so to change
the current object we create a new one with changes.
Field stores the playing field and creates the next generation.
Details: rec_line_print and rec_grid_print print the field.
rec_line_print creates the initial field. rec_live creates the next generation.
with changes the Cell at position (x,y) by returning a new Field
object with the modified cell.
count counts alive neighbors for the cell at (x,y). live calls
rec_live with specific arguments.
class Field {
private:
vector<vector<Cell>> grid;
void rec_line_print(int depth);
void rec_grid_print(int x, int y);
public:
Field(int n, int m) : Field(make_grid(n, m)) {}
Field(vector<vector<Cell>> g) : grid(g) {}
vector<vector<Cell>> field(); // getters
Field rec_add(Field cur, vector<pair<int, int>> s, int pos);
Field rec_live(int x, int y, Field cur);
static vector<vector<Cell>> make_grid(int n, int m);
Field live();
Field with(int x, int y, Cell a);
void print(); // next_gen makers
int count(int x, int y);
};Cell stores the cell state (alive/dead). Method live takes an integer
to determine the next generation state and returns a Cell object.
class Cell {
private:
bool state;
public:
Cell(bool st) : state(st) {}
Cell() : Cell(false) {}
bool status() const;
Cell live(int cnt) const;
};Parse parses command line arguments using the BOOST library and validates input.
class Parse {
private:
int n = 100000;
int m = 100000;
vector<pair<int, int>> points;
po::variables_map vm;
public:
Parse() : Parse(nullptr) {}
Parse(po::variables_map vmp) : vm(vmp) {}
int length() const;
int width() const;
vector<pair<int, int>> grid();
po::variables_map opts();
vector<pair<int, int>> rec_cells(int pos, vector<pair<int, int>> p);
static bool has(const string &s, char c);
static bool valid(string const &s);
pair<int, int> point(const string &s) const;
static pair<int, int> size(const string &s);
static pair<int, int> split(const string &s);
void positive();
void cells();
void build();
};The main object is Game(Grid(Size(), Field()), *optional* Repeats()).
Repeats stores the number of iterations.
class Repeats {
public:
int rep;
Repeats();
};Grid stores the grid size and playing field. It provides methods to print the current state and advance to the next iteration.
class Grid {
public:
Size s;
Field g;
Grid(Size &st, Field &ff);
void printGrid();
void nextGen();
};Size stores the playing field dimensions.
class Size {
public:
int n;
int m;
Size(){};
Size(int x, int y);
};Field stores a 2D array of Cell objects. It has methods to set initial
alive cells and count alive neighbors.
class Field {
public:
vector<vector<Cell>> f;
Field(){};
Field(Size sz);
void read_and_set(Size sz);
int count(int x, int y, int sz);
};Cell stores the current state and next generation state. It provides methods to modify these states.
Details: changeNewState stores the next generation state without
immediately applying it. This ensures neighbor cells can still read
the current state when calculating their next state. Once all new states
are determined, old values are updated.
newState stores the cell's state for the next generation during
calculation.
class Cell {
private:
bool curState = false;
bool newState = false;
public:
void changeNewState(bool val);
void changeCurState();
void setState(bool val);
bool getCurState() const;
};
Game runs the game with a configurable interval between generations.
class Game {
public:
Game(Grid gr, Repeats rep, int time);
Game(Grid gr);
};Parse parses and validates console arguments.
class Parse {
public:
Parse(){};
static pair get_size(string const &s);
static vector> get_alive(vector const &a, int n, int m);
};Additional helper objects for input validation and string-to-integer conversion are not shown.

