💾 Archived View for gem.arisamiga.rocks › post › ecasdlc.gmi captured on 2024-07-08 at 23:59:57. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

“I can make that“ - Elementary Cellular Automata

Elooooo! We are back!!!

Now I might have been gone for a bit... but! I have a good reason... uh-

My reason is: *Reason 404 not found*

Anyways! Elementary Cellular Automata!

A couple of days ago I saw a video by

The Coding Train

on Elementary Cellular Automata and I had the urge to make it myself in the future.

{{< youtube Ggxt06qSAe4 >}}

Now I am working on another project which is a huge project and I am going to use C and SDL2

Now... I have never used SDL2 and my knowledge in C is in programming for the Amiga but my project will be on Windows!

So I started thinking about what kind of project could I do to get used to SDL2 and C and I thought of Elementary Cellular Automata!

**So what is Elementary Cellular Automata?**

Elementary Cellular Automata is a 2D grid of cells that can be in one of two states, 0 or 1 (white or black). The cells are updated based on a set of rules that depend on the states of the cell and its two neighbors.

The rules are based on the Wolfram code

https://mathworld.wolfram.com/ElementaryCellularAutomaton.html

So in classic Aris fashion we doing another "I can make that" project WOOOOOO

MoistCritical Gif [IMG]

So first we need to download 3 things.

Now what are these?

GCC Logo [IMG]

**What is GCC?**

GCC is a compiler system produced by the GNU Project supporting various programming languages. GCC is a key component for compiling C code to actual executable files.

SDL2 Logo [IMG]

**What is SDL2?**

Simple DirectMedia Layer is a cross-platform development library designed to provide low-level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D.

Make Logo [IMG]

**What is Make?**

Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles which specify how to derive the target program. We used it before in

Making files with Makefile

Let's get installing!

Now I am on Windows so I am gonna be using

MSYS2

to get all of these things.

MSYS2 Logo [IMG]

**What is MSYS2?**

MSYS2 is a collection of tools and libraries providing you with an environment for building, installing, and running native Windows software. It comes with builds for GCC, mingw-w64, and many other things

For GCC there is a great tutorial made by vscode on how to install it

https://code.visualstudio.com/docs/cpp/config-mingw

so we just follow that to install both GCC and MSYS2

After installing and adding the path of MSYS2 to the PATH variable we can now install and make

pacman -S make

Now note that when installing it you will not be able to use it outside of the MSYS2 terminal so you will need to find the make.exe file usually located in ({Disk}:MSYS2\usr\bin)

We will need to add the path that has the make.exe to our environment variables to use it in the normal command prompt.

Now we need to install SDL2

We need to go to

https://github.com/libsdl-org/SDL/releases/latest

and download the development libraries for Windows (It would be named something like SDL2-devel-2.30.4-mingw.tar.gz)

After downloading it we need to extract it we will use tar to extract it

tar -xvzf SDL2-devel-2.30.4-mingw.tar.gz -C SDL2

Now a folder named SDL2 will be created and inside it will be the SDL2 files

Now two folders are important to us `x86_64-w64-mingw32` and `i686-w64-mingw32`

Since I am on a 64-bit Windows I will be using the `x86_64-w64-mingw32` folder

We will need to copy the `include` and `lib` folders to our project (I will put them under a folder called `src`)

and after that, we will need to go to the `/bin` folder and copy the `SDL2.dll` file to our project as well

Now we need to create a Makefile as we cannot use make without a Makefile

We will use a simple Makefile that will compile our code

all:
 gcc -Isrc/include -Lsrc/lib -o main main.c -lmingw32 -lSDL2main -lSDL2

Now what that does is that when we run `make` it will compile the `main.c` file and output an executable named `main` using the SDL2 libraries

The `-Isrc/include` flag tells the compiler to look for the SDL2 headers in the `src/include` folder

The `-Lsrc/lib` flag tells the compiler to look for the SDL2 libraries in the `src/lib` folder

The `-lmingw32 -lSDL2main -lSDL2` flags tell the compiler to link the mingw32, SDL2main and SDL2 libraries (which we copied)

Time to code!

We will need to make simple window first to make sure everything is working so let's start with just making a window with SDL

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
#define SCREEN_WIDTH 1280 
#define SCREEN_HEIGHT 720
int main(int argc, char** argv){
    if(SDL_Init(SDL_INIT_VIDEO) < 0){
        printf("Error: SDL failed to initialize\nSDL Error: '%s'\n", SDL_GetError());
        return 1;
    }
    SDL_Window *window = SDL_CreateWindow("Epic Window Title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, 0);
    if(!window){
        printf("Error: Failed to open window\nSDL Error: '%s'\n", SDL_GetError());
        return 1;
    }
    SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if(!renderer){
        printf("Error: Failed to create renderer\nSDL Error: '%s'\n", SDL_GetError());
        return 1;
    }
    bool running = true;
    while(running){
        SDL_Event event;
        while(SDL_PollEvent(&event)){
            switch(event.type){
                case SDL_QUIT:
                    running = false;
                    break;
                default:
                    break;
            }
        }
        SDL_SetRenderDrawColor(255,255,255,255);
        SDL_RenderClear(renderer);
        SDL_RenderPresent(renderer);
    }
    return 0;
}

Let's go through what this code does:

First, we start by including all the necessary libraries

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>

Then we define the screen width and height

#define SCREEN_WIDTH 1280
#define SCREEN_HEIGHT 720

The first thing we will need to do in the main function is to initialize SDL

if(SDL_Init(SDL_INIT_VIDEO) < 0){
    printf("Error: SDL failed to initialize\nSDL Error: '%s'\n", SDL_GetError());
    return 1;
}

Not initializing SDL will cause the program to crash so we need to check if it was initialized correctly

Next, we create a window

SDL_Window *window = SDL_CreateWindow("Epic Window Title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, 0);
if(!window){
    printf("Error: Failed to open window\nSDL Error: '%s'\n", SDL_GetError());
    return 1;
}

We can pass various tags in it like the title, the position of the window, the width and height, and the flags (0 for no flags)

After that, we create a renderer

SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if(!renderer){
    printf("Error: Failed to create renderer\nSDL Error: '%s'\n", SDL_GetError());
    return 1;
}

The render is one of the most important parts of SDL as it is what we will use to draw things on the screen

After that, we will create a while loop to keep the window open and check for events

bool running = true;
while(running){
    SDL_Event event;
    while(SDL_PollEvent(&event)){
        switch(event.type){
            case SDL_QUIT:
                running = false;
                break;
            default:
                break;
        }
    }
    SDL_SetRenderDrawColor(255,255,255,255);
    SDL_RenderClear(renderer);
    SDL_RenderPresent(renderer);
}

The `SDL_Event` is a struct that holds the event that is happening (like a key press or a mouse click)

The `SDL_PollEvent` function will check if there is an event happening and if there is it will put it in the `event` struct so we can use it to check what event is happening

The `SDL_QUIT` event is when the window is closed so we check if that event is happening and if it is we set the `running` variable to false to exit the loop

The `SDL_SetRenderDrawColor` function is used to set the color of the renderer (in this case white)

The `SDL_RenderClear` function is used to clear any current drawings on the renderer and set it to the new renderer

The `SDL_RenderPresent` function is used to present the renderer to the window so we can see it

Now we can run the `make` command in the terminal and it will compile the code and output an executable named `main`

SDL2 Window [IMG]

I used a different color in this screenshot :)

So if this works we know that we have both GCC and SDL2 working and we can start working on the Elementary Cellular Automata!

Now I don't wanna go too much into the logic I suggest you watch the video by The Coding Train as he explains it very well but the basic idea is that

We have a set of cells that can be in one of two states, 0 or 1 (white or black) and we update the cells based on a set of rules that depend on the states of the cell and its two neighbors (left and right)

Now The Coding Train coded this in P5.js which is a JavaScript library but we will be writing it in C using SDL2 *yay*

So we will need to create a grid of cells and update them based on the rules

We will do that by making a variable that will hold a list of pixels that will be either black or white

uint32_t pixels[SCREEN_WIDTH * SCREEN_HEIGHT];

We will also use `uint32_t` which is a 32-bit unsigned integer to hold the color of the pixel

Also because we want to color each pixel we will use something called **Texture** in SDL2

SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);

An SDL Texture creates a texture that can be used to render things on the screen and we will use it specifically so we can use

SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t));

to update the texture with the pixels array.

The first thing we will need to do is create the first row of the grid so that we can update the rest of the rows based on the first row

// Fill in pixels
for (int i = 0; i < SCREEN_WIDTH; i++){
    pixels[i] = white;
}
pixels[SCREEN_WIDTH / 2] = black;

Based on the Wolfam logic we are meant to have the middle pixel black and the rest white which we do here

Now we will need to update the rest of the rows based on the first row

To be more efficient we will make it into a function

uint32_t* calculateList(uint32_t* pixels) {
    uint32_t *newpixels = malloc(SCREEN_WIDTH * sizeof(uint32_t));
    if (newpixels == NULL) {
        // Handle memory allocation failure
        printf("Error: Failed to allocate memory\n");
        return NULL;
 }
    // Process all rows
    newpixels[0] = calculateState(pixels[SCREEN_WIDTH - 1], pixels[0], pixels[1]);
    for (int x = 1; x < SCREEN_WIDTH - 1; x++) {
        newpixels[x] = calculateState(pixels[x - 1], pixels[x], pixels[x + 1]);
 }
    newpixels[SCREEN_WIDTH - 1] = calculateState(pixels[SCREEN_WIDTH - 2], pixels[SCREEN_WIDTH - 1], pixels[0]);
    return newpixels;
};

This will calculate the new row based on the previous row and the rules and return the new row

You may also notice the use of `calculateState` which is a function that will calculate the state of the cell based on the rules which we will define later but it basically will return either white or black.

Let's break down what we are doing

newpixels[0] = calculateState(pixels[SCREEN_WIDTH - 1], pixels[0], pixels[1]);

This will calculate the first pixel based on the last pixel, the first pixel, and the second pixel

for (int x = 1; x < SCREEN_WIDTH - 1; x++) {
    newpixels[x] = calculateState(pixels[x - 1], pixels[x], pixels[x + 1]);
}

We will calculate the rest of the pixels based on the previous pixel, the current pixel, and the next pixel

newpixels[SCREEN_WIDTH - 1] = calculateState(pixels[SCREEN_WIDTH - 2], pixels[SCREEN_WIDTH - 1], pixels[0]);

After that we will calculate the last pixel based on the second to last pixel, the last pixel, and the first pixel so we dont have to worry about the edge cases in the loop

Now we will need to define the `calculateState` function

Originally the `calculateState` will look something like this

uint32_t calculateState(uint32_t left, uint32_t middle, uint32_t right) {
    if (left == black && middle == black && right == black) return white;
    if (left == black && middle == black && right == white) return white;
    if (left == black && middle == white && right == black) return white;
    if (left == black && middle == white && right == white) return black;
    if (left == white && middle == black && right == black) return black;
    if (left == white && middle == black && right == white) return black;
    if (left == white && middle == white && right == black) return black;
    if (left == white && middle == white && right == white) return white;
    return white;
}

but we want to also use the numbering system that Wolfram used so we will need to ask for the rule number and convert it to binary

int ruleValue = 30;
uint32_t calculateState(uint32_t left, uint32_t middle, uint32_t right) { 
    char ruleset[9];
    
    for (int i = 0; i < 8; i++) {
        ruleset[i] = (ruleValue & (1 << (7 - i))) ? '1' : '0';
 }
    ruleset[8] = '\0';
    
    char rule[4];
    rule[0] = left == white ? '0' : '1';
    rule[1] = middle == white ? '0' : '1';
    rule[2] = right == white ? '0' : '1';
    rule[3] = '\0';
    int index = 7 - parseBase2(rule);
    if (ruleset[index] == '0') {
        return white;
 } else {
        return black;
 }
}

let's go through what we are doing again!

First, we create a char array that will hold the ruleset it will result in a binary number that will be used to determine the state of the cell

char ruleset[9];

Then we loop through the rule number and convert it to binary

for (int i = 0; i < 8; i++) {
    ruleset[i] = (ruleValue & (1 << (7 - i))) ? '1' : '0';
}
ruleset[8] = '\0';

This works by checking if the bit at position `7 - i` in `ruleValue` is 1. If it is, we will return a `1` and if it's not, we will return a `0`.

So something like `30` would be `00011110` in binary

Also we check from left to right so we make sure to add the leading 0s so we have a full 8-bit number.

We will also make sure to add a null terminator at the end of the array

Then we create a char array that will hold the current state of the cell

char rule[4];

Then we convert the current state of the cell to binary

rule[0] = left == white ? '0' : '1';
rule[1] = middle == white ? '0' : '1';
rule[2] = right == white ? '0' : '1';
rule[3] = '\0';

We parse the binary number to a decimal number and subtract it from 7 to get the index of the ruleset

int index = 7 - parseBase2(rule);

Now unfortunately in C, you can't just parse an int to a base 2 number. In Javascript you would do

parseInt(binary, 2)

but in C you will need to make a function to do that

int parseBase2(char *str) {
    int result = 0;
    int length = strlen(str); // Get the length of the binary string
    for (int i = 0; i < length; i++) {
        if (str[i] == '1') {
            // Calculate the power of 2 based on the position from the right
            int pos = length - i - 1;
            result += (1 << (pos));
        }
    }
    return result;
}

Ultimately what it does is that it will loop through the binary number and if it finds a 1 it then calculates it to the power of 2 based on the position from the right and adds it to the result

So something like `110` would be `6` in decimal

Now based on the ruleset and the index we will return either white or black

if (ruleset[index] == '0') {
    return white;
} else {
    return black;
}

Now we will need to update the pixels based on the new row

for (int y = 1; y < SCREEN_HEIGHT; y++) { // Start from the second row
    uint32_t *previousRow = &pixels[(y - 1) * SCREEN_WIDTH]; // Get the previous row
    uint32_t *newRow = calculateList(previousRow); // Calculate the new row based on the previous row
    if (newRow != NULL) {
        for (int i = 0; i < SCREEN_WIDTH; i++) {
            pixels[i + (y * SCREEN_WIDTH)] = newRow[i]; // Update the current row with the new state
        }
        free(newRow);
    }
}

This will loop through the rows and update the pixels based on the previous row

Now we will need to update the texture with the new pixels

SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t));

Then we render the texture to the screen

SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);

Now we also need to remember to destroy the texture and the renderer when we exit as well

SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);

So our code should look like this

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#define SCREEN_WIDTH 1280
#define SCREEN_HEIGHT 720
#define black 0xFF000000
#define white 0xFFFFFFFF
int parseBase2(char * str) {
    int result = 0;
    int length = strlen(str); // Get the length of the binary string
    for (int i = 0; i < length; i++) {
        if (str[i] == '1') {
            // Calculate the power of 2 based on the position from the right
            int pos = length - i - 1;
            result += (1 << (pos));
        }
    }
    return result;
}
uint32_t calculateState(uint32_t left, uint32_t middle, uint32_t right) {
    char ruleset[9];
    for (int i = 0; i < 8; i++) {
        ruleset[i] = (ruleValue & (1 << (7 - i))) ? '1' : '0';
    }
    ruleset[8] = '\0';
    char rule[4];
    rule[0] = left == white ? '0' : '1';
    rule[1] = middle == white ? '0' : '1';
    rule[2] = right == white ? '0' : '1';
    rule[3] = '\0';
    int index = 7 - parseBase2(rule);
    if (ruleset[index] == '0') {
        return white;
    } else {
        return black;
    }
}
uint32_t * calculateList(uint32_t * pixels) {
    uint32_t * newpixels = malloc(SCREEN_WIDTH * sizeof(uint32_t));
    if (newpixels == NULL) {
        // Handle memory allocation failure
        printf("Error: Failed to allocate memory\n");
        return NULL;
    }
    // Process all rows
    newpixels[0] = calculateState(pixels[SCREEN_WIDTH - 1], pixels[0], pixels[1]);
    for (int x = 1; x < SCREEN_WIDTH - 1; x++) {
        newpixels[x] = calculateState(pixels[x - 1], pixels[x], pixels[x + 1]);
    }
    newpixels[SCREEN_WIDTH - 1] = calculateState(pixels[SCREEN_WIDTH - 2], pixels[SCREEN_WIDTH - 1], pixels[0]);
    return newpixels;
};
int main(int argc, char ** argv) {
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("Error: SDL failed to initialize\nSDL Error: '%s'\n", SDL_GetError());
        return 1;
    }
    SDL_Window * window = SDL_CreateWindow("Elementary Cellular Automata", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_RESIZABLE);
    if (!window) {
        printf("Error: Failed to open window\nSDL Error: '%s'\n", SDL_GetError());
        return 1;
    }
    SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if (!renderer) {
        printf("Error: Failed to create renderer\nSDL Error: '%s'\n", SDL_GetError());
        return 1;
    }
    // Fill in pixels
    for (int i = 0; i < SCREEN_WIDTH; i++) {
        pixels[i] = white;
    }
    pixels[SCREEN_WIDTH / 2] = black;
    bool running = true;
    SDL_Texture * texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);
    int finished = 0; // 0 = not finished, 1 = finished
    while (running) {
        SDL_Event event;
        while (SDL_PollEvent( & event)) {
            switch (event.type) {
            case SDL_QUIT:
                running = false;
                break;
            case SDL_WINDOWEVENT:
                if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
                    int newWidth = event.window.data1;
                    int newHeight = event.window.data2;
                    // Calculate the new scale based on the resized window dimensions
                    float scaleX = (float) newWidth / SCREEN_WIDTH;
                    float scaleY = (float) newHeight / SCREEN_HEIGHT;
                    // Set the new scale for rendering
                    SDL_RenderSetScale(renderer, scaleX, scaleY);
                }
                break;
            default:
                break;
            }
        }
        if (finished == 1) {
            SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t));
            SDL_RenderClear(renderer);
            SDL_RenderCopy(renderer, texture, NULL, NULL);
            SDL_RenderPresent(renderer);
            continue;
        }
        for (int y = 1; y < SCREEN_HEIGHT; y++) { // Start from the second row
            uint32_t * previousRow = & pixels[(y - 1) * SCREEN_WIDTH]; // Get the previous row
            uint32_t * newRow = calculateList(previousRow); // Calculate the new row based on the previous row
            if (newRow != NULL) {
                for (int i = 0; i < SCREEN_WIDTH; i++) {
                    pixels[i + (y * SCREEN_WIDTH)] = newRow[i]; // Update the current row with the new state
                }
                free(newRow);
                // Update the texture once after all rows are processed
                SDL_UpdateTexture(texture, NULL, pixels, SCREEN_WIDTH * sizeof(uint32_t));
                SDL_RenderClear(renderer);
                SDL_RenderCopy(renderer, texture, NULL, NULL);
                SDL_RenderPresent(renderer);
                SDL_Delay(10);
            }
        }
        if (finished == 0) {
            finished = 1;
        }
    }
    // Destroy the texture after the loop if it exists
    if (texture) {
        SDL_DestroyTexture(texture);
    }
    SDL_DestroyRenderer(renderer); // Destroy the renderer
    SDL_DestroyWindow(window); // Destroy the window
    SDL_Quit(); // Quit SDL
    return 0;
}

We will also add a finished variable that will be used to check if the program is finished or not

int finished = 0; // 0 = not finished, 1 = finished

We do that so the program will not overwrite any finished rows and will not update the texture when it is finished. It will keep updating just the finished texture

We will also add a delay so we can see the animation

SDL_Delay(10);

And when running it we get something like this!

Elementary Cellular Automata [IMG]

And that is it! We have made Elementary Cellular Automata in C using SDL2!

This was a great, complicated, and fun project to do.

I posted all the code with some improvements as well on my github at

https://github.com/Arisamiga/ECA-SDL-C

so you can check it out there!

I highly suggest you watch the video by The Coding Train and try to make it yourself!

Now I will be working on my other project which I might post progress about soon... maybe... I dunno... we will see!

But until then! Hope you have an amazing day and Thanks so much for reading :D