This is my near-final version of Conway's Game of Life, with inherited colors using PDCurses. Any new spawned cells take on the most frequent color surrounding it when it spawns. This leads to single-colored populations roaming around, and causes interesting "battles" between 2 populations; which usually end in one taking over the other.
I already had one review of it, which increased its speed considerably, but now I'm looking for more general feedback about best practices, and feedback regarding the following:
- An alternative to the struct
NeighborData
. It seems overkill to use a struct like this just to return 2 pieces of data at once (The most prevalent color, and the number of neighbors). - If there's a more efficient way of counting the colors than
consumeColorFreqs
- Anywhere else I can squeeze more speed out of.
Population.h:
#ifndef POPULATION_H
#define POPULATION_H
#include <set>
#include <vector>
#include <array>
#include "curses.h"
#define NCOLORS 16
typedef unsigned char Color;
struct NeighborData {
unsigned int count = 0;
Color color = COLOR_WHITE;
NeighborData(unsigned int ct, Color cr);
};
class Population {
//To hold the "finished" generation, and the generation
// currently being constructed
std::vector<Color> cells;
std::vector<Color> newCells;
int width = 0, height = 0;
public:
Population(int newWidth, int newHeight);
bool pointIsOccupied(int x, int y) const;
void addPoint(int x, int y, Color color);
void killPoint(int x, int y);
Color getPointColor(int x, int y) const;
NeighborData getNeighborData(int x, int y, int depth = 1) const;
void decideLifeOf(int, int);
int getIndexOf(int, int) const;
void replacePopulation();
Color consumeColorFrequencies(const Color colorFreqs[]) const;
};
Color randomColor(Color starting = 1);
#endif
Population.cpp:
#include "Population.h"
#include <cstdlib>
#include <algorithm>
#include <array>
#include "curses.h"
NeighborData::NeighborData(unsigned int ct, Color cr) {
count = ct, color = cr;
}
Population::Population(int newWidth, int newHeight) {
width = newWidth;
height = newHeight;
cells.resize(width * height);
newCells.resize(width * height);
}
bool Population::pointIsOccupied(int x, int y) const {
return cells[getIndexOf(x, y)] != '\0';
}
Color Population::getPointColor(int x, int y) const {
return cells[getIndexOf(x, y)];
}
void Population::addPoint(int x, int y, Color color) {
newCells[getIndexOf(x, y)] = color;
}
void Population::killPoint(int x, int y) {
newCells[getIndexOf(x, y)] = '\0';
}
NeighborData Population::getNeighborData(int x, int y, int depth) const {
//To temporarily hold frequencies of colors
//Index is the color, value is the number of occurances
Color colorFreqs[NCOLORS];
int count = 0;
for (int cY = y - depth; cY <= y + depth; cY++) {
if (cY < 0 || cY >= height) continue;
for (int cX = x - depth; cX <= x + depth; cX++) {
if (cX < 0 || cX >= width || (cX == x && cY == y)) continue;
Color color = getPointColor(cX, cY);
if (color != '\0') {
count += 1;
colorFreqs[color] += 1;
}
}
}
Color c = consumeColorFrequencies(colorFreqs);
return NeighborData(count,c);
}
void Population::decideLifeOf(int x, int y) {
NeighborData nD = getNeighborData(x, y, 1);
unsigned int ns = nD.count;
Color color = nD.color;
if (ns < 2 || ns > 3) killPoint(x, y);
else if (ns == 3) addPoint(x, y, color);
}
int Population::getIndexOf(int x, int y) const {
return y * width + x;
}
void Population::replacePopulation() {
cells = newCells;
}
Color randomColor(Color starting) {
return (rand() % (NCOLORS - starting)) + starting;
}
Color Population::consumeColorFrequencies(const Color colorFreqs[]) const {
Color hIndex = 0, highest = 0;
for (Color i = 0; i < NCOLORS; i++) {
Color freq = colorFreqs[i];
if (freq > highest) {
hIndex = i, highest = freq;
}
}
return hIndex;
}
World.h:
#ifndef WORLD_H
#define WORLD_H
#include <set>
#include <sstream>
#include <limits>
#include <vector>
#include "Population.h"
class World {
Population pop;
int worldWidth = 0, worldHeight = 0;
public:
World(int, int);
void compileOutput(std::string disp = "#") const;
void simGeneration();
void randomizeCells(double chanceOfLife = 0.3, int newSeed = -1);
};
#endif
World.cpp:
#include "World.h"
#include <iomanip>
#include <set>
#include <cstdlib>
#include <string>
#include "curses.h"
World::World(int xMax, int yMax) :
pop(xMax,yMax) {
worldWidth = xMax;
worldHeight = yMax;
}
void World::compileOutput(std::string disp) const {
for (int cY = 0; cY < worldHeight; cY++) {
for (int cX = 0; cX < worldWidth; cX++) {
char c = pop.getPointColor(cX, cY);
init_pair(c, c, COLOR_BLACK); //(Pair number, fore color, back color)
attron(COLOR_PAIR(c));
mvprintw(cY, cX, (pop.pointIsOccupied(cX, cY) ? disp.c_str() : " ") );
attroff(COLOR_PAIR(c));
}
}
}
void World::simGeneration() {
for (int y = 0; y < worldHeight; y++) {
for (int x = 0; x < worldWidth; x++) {
pop.decideLifeOf(x,y);
}
}
pop.replacePopulation();
}
void World::randomizeCells(double chanceOfLife, int newSeed) {
if (newSeed > 0) srand(newSeed);
for (int y = 0; y < worldHeight; y++) {
for (int x = 0; x < worldWidth; x++) {
if ((rand() % int(1.0 / chanceOfLife)) == 0) {
unsigned char color = randomColor();
pop.addPoint(x, y, color);
}
}
}
pop.replacePopulation();
}
Timer.h (Not entirely necessary for the program to run, but it's used in the main):
#ifndef TIMER_H
#define TIMER_H
#include <chrono>
class Timer {
std::chrono::system_clock::time_point start;
public:
Timer();
void restart();
std::chrono::system_clock::time_point now();
double getMS();
double getSecs();
};
#endif
Timer.cpp:
#include "Timer.h"
#include <ctime>
Timer::Timer() {
start = now();
}
void Timer::restart() {
start = now();
}
std::chrono::system_clock::time_point Timer::now() {
return std::chrono::system_clock::now();
}
double Timer::getMS() {
return (now() - start).count() / 10000.0;
}
double Timer::getSecs() {
return getMS() / 1000.0;
}
Main.cpp:
#include "Timer.h"
#include "World.h"
#include <iostream>
#include <sstream>
#include <cstdlib>
#include <vector>
#include <chrono>
#include <thread>
#include "curses.h"
int strToInt(std::string str) {
std::stringstream ss;
int ret = 0;
ss << str; ss >> ret;
return ret;
}
void resetCin() {
std::cin.clear();
std::cin.ignore(255, '\n');
}
int main(int argc, char* argv[]) {
using namespace std;
//The input bit below is ugly and unsafe.
string inX = "10",
inY = "10",
tempSeed = "", inSeed = "-1";
cout << "Board Dimensions? (2 space separated numbers): ";
cin >> inX >> inY;
resetCin();
cout << "Random seed? (Leave blank for random): ";
getline(cin, tempSeed);
inSeed = tempSeed == "" ? inSeed : tempSeed;
initscr(); /* Start curses mode */
start_color();
noecho(); // Don't echo any keypresses
curs_set(FALSE); // Don't display a cursor
int maxX = strToInt(inX), maxY = strToInt(inY);
World w(maxX, maxY);
//w.randomizeCells(0.4, 10);
w.randomizeCells(0.4, strToInt(inSeed));
int updateDataEvery = 500;
double lastDur = 0, lastOutput = updateDataEvery;
Timer t;
for (int gens = 0; gens < 100000; gens++) {
//Clearing isn't necessary when using just updating pixels, because they're
// constantly being overwritten
//It causes areas outside the grid to "streak"
clear();
w.compileOutput("#");
mvprintw(maxY + 1, 0, "Generation: %d", gens);
w.simGeneration();
//Update block every n milliseconds
lastOutput += lastDur;
if (lastOutput >= updateDataEvery) {
lastDur = t.getMS();
lastOutput = 0;
}
t.restart();
mvprintw(maxY + 2, 0, "%0.1f fps", 1000.0 / lastDur);
refresh();
this_thread::sleep_for(chrono::milliseconds( 25 ) );
}
endwin();
}
getNeighborData()
. If the resulting number is 3, the cell will be alive; if it is 4, the cell will remain the same; for any other number, the cell will be dead. This approach would save you one comparison indecideLifeOf()
and one comparison ingetNeighborData()
. \$\endgroup\$