For an Information System Security course I've applied to, I need to learn C and decided to start learning it yesterday.
Conway's Game of Life has historically been a good project to start with, so I picked it. Here's a GIF of the end result (sorry about the wobble; it's just simple console printing so the result isn't perfect. I might try using Curses next):
I'd like comments on anything. I've never worked this low down before, so I'm likely doing some things incorrectly. I have a few main concerns though:
Am I allocating and handling memory correctly? Do I have any undefined behavior?
Is there a better way of writing
formatWorld
? In Clojure, that would be like 5 lines. It ended up getting a little messy on me here though. String operations seem incredibly difficult to pull off.Is my use of
size_t
appropriate here? It seems to be the type to use when dealing with data that shouldn't be allowed to be negative, but as soon as I started looking for places to use it, I realized that it seems it should be used everywhere.Am I using headers properly? It seems like I should declare functions in the header that I expect the end user to use, and don't declare internal "private" functions.
Any stylistic/convention concerns.
world.h
#pragma once
#include <stdbool.h>
#include <stdlib.h>
typedef struct {
bool* readCells;
bool* writeCells;
size_t width;
size_t height;
} World;
// All write operations are done to writeArray, and all read operations are read from
// read array.
// Returns a pointer to a newly allocated World with the given dimensions
World* newWorld(size_t width, size_t height);
// Gets/Sets the Cell at the given position
void setCell(World* world, size_t x, size_t y, bool isAlive);
bool getCell(World*, size_t x, size_t y);
// Returns a char array representing the World
char* formatWorld(World*);
// Helper that prints the array returned from the function
void printWorld(World*);
// Advances the world by one "tick"
void advanceWorld(World*);
world.c
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <windows.h> // For sleep
#include "world.h"
#define MOORE_SEARCH_DEPTH 1
#define WORLD_WIDTH 50
#define WORLD_HEIGHT 50
#define LOOP_ADVANCE_DELAY 100
// Returns a representation of a non-ragged 2D array with the given dimensions
// where all the cells are dead
bool* newDeadCells(size_t width, size_t height) {
bool* cells = malloc(sizeof(bool) * width * height);
for (size_t i = 0; i < (width * height); i++) {
cells[i] = false;
}
return cells;
}
World* newWorld(size_t width, size_t height) {
World* w = malloc(sizeof(World));
w->width = width;
w->height = height;
w->readCells = newDeadCells(width, height);
w->writeCells = newDeadCells(width, height);
return w;
}
// Randomizes the cells so that each cell has an aliveChance chance of being alive
void randomizeCells(World* world, float aliveChance) {
size_t width = world->width;
size_t height = world->height;
for (size_t i = 0; i < (width * height); i++) {
world->readCells[i] = (rand() % 100) < aliveChance * 100;
}
}
// Overwrites the readable cells with the writable cells
void copyWritableToReadable(World* world) {
memcpy(world->readCells, world->writeCells,
sizeof(bool) * world->width * world->height);
}
// Frees the given World and any memory associated with it
void freeWorld(World* world) {
free(world->readCells);
free(world->writeCells);
free(world);
}
size_t indexOf(size_t width, size_t x, size_t y) {
return width * y + x;
}
void setCell(World* world, size_t x, size_t y, bool isAlive) {
size_t index = indexOf(world->width, x, y);
world->writeCells[index] = isAlive;
}
bool getCell(World* world, size_t x, size_t y) {
int index = indexOf(world->width, x, y);
return world->readCells[index];
}
// Returns the number of live neighbors surrounding the given position.
// depth returns how many squares to look in each direction. 1 = Standard Moore Neighborhood.
size_t nAliveNeighborsSurrounding(World* world, size_t x, size_t y, size_t depth) {
size_t xBound = min(x + depth + 1, world->width);
size_t yBound = min(y + depth + 1, world->height);
size_t aliveCount = 0;
for (size_t ny = max(0, y - depth); ny < yBound; ny++) {
for (size_t nx = max(0, x - depth); nx < xBound; nx++) {
if (getCell(world, nx, ny) && !(nx == x && ny == y)) {
aliveCount++;
}
}
}
return aliveCount;
}
bool cellShouldLive(bool isAlive, size_t nNeighbors) {
return (isAlive && nNeighbors >= 2 && nNeighbors <= 3)
|| (!isAlive && nNeighbors == 3);
}
// Decides if a cell should live or die, and sets it accordingly
void advanceCellAt(World* world, size_t x, size_t y) {
size_t nNeighbors = nAliveNeighborsSurrounding(world, x, y, MOORE_SEARCH_DEPTH);
bool isAlive = getCell(world, x, y);
setCell(world, x, y, cellShouldLive(isAlive, nNeighbors));
}
void advanceWorld(World* world) {
size_t width = world->width;
size_t height = world->height;
for (size_t y = 0; y < height; y++) {
for (size_t x = 0; x < width; x++) {
advanceCellAt(world, x, y);
}
}
copyWritableToReadable(world);
}
char* formatWorld(World* world) {
size_t width = world->width;
size_t height = world->height;
size_t nCells = width * height;
// total cells needed + extra for newlines + NL term
size_t buffSize = sizeof(char) * nCells + height + 1;
char* buffer = malloc(buffSize);
buffer[buffSize - 1] = '\0';
size_t i = 0;
for (size_t y = 0; y < height; y++) {
for (size_t x = 0; x < width; x++) {
bool isAlive = getCell(world, x, y);
char rep = isAlive ? '#' : ' ';
buffer[i] = rep;
i++;
}
buffer[i] = '\n';
i++;
}
return buffer;
}
void printWorld(World* world) {
char* formatted = formatWorld(world);
printf("%s", formatted);
free(formatted);
}
void simpleConsoleRoutine() {
srand(NULL);
World* world = newWorld(WORLD_WIDTH, WORLD_HEIGHT);
randomizeCells(world, 0.3);
// Leaving it with a counter so I can limit it easily later
for (size_t i = 0; ; i++) {
printWorld(world);
printf("----------\n");
advanceWorld(world);
Sleep(LOOP_ADVANCE_DELAY);
}
// No need to free world?
}
int main() {
simpleConsoleRoutine();
}
windows.h
is very limiting for justsleep
: en.wikipedia.org/wiki/Sleep_(system_call). I'd go with the more general POSIX, but then Windows will probably have trouble. Perhaps an#ifdef
? \$\endgroup\$#includes
. I'm using it here purely for demo purposes, so I didn't think it would be a big deal. Ideally I wouldn't be usingSleep
at all in a "real program". \$\endgroup\$#ifdef DEMO
/#endif
around the#include <windows.h>
andSleep()
; then one can defineDEMO
in your compiler settings (Windows version with theSleep
) or not, and it will change the code. \$\endgroup\$