Thread: learning C - Snake game

  1. #1
    Registered User
    Join Date
    Dec 2022
    Posts
    4

    Lightbulb learning C - Snake game

    I'm learning C programming. And wrote a Snake game for practice. Since I'm self-learning I figured I could post my program here and ask for tips and recommendations to improve it and it could turn into a reference for beginners with more experienced programmers comments on it.

    It uses ANSI codes and Linux specific library to handle terminal drawings unfortunately. If there is a platform independent solutions please help me with that.
    It compiles with no additional linking or flag:
    `$ cc snake.c`

    Code:
    Code:
    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    #include <unistd.h>
    // For input handling
    #include <fcntl.h>
    #include <termios.h>
    
    #define W       32
    #define H       16
    #define MAX_INX (H * W)
    #define BODY_C  's'
    #define HEAD_C  'Q'
    #define APPLE_C '*'
    
    // Set this to 1 for passing through walls and comming out from the other side
    #define PASS_WALL 1
    
    // `CSI 0 ; 0 H` puts the cursor at 0, 0 (defaul position).
    // `CSI 0 J` clears from cursor to end of screen.
    // `CSI 3 J` clears entire screen and deletes all lines saved in the scrollback
    // buffer.
    #define cls()            printf("\033[H\033[J\033[3J")
    #define hide_cursor()    printf("\033[?25l")
    #define show_cursor()    printf("\033[?25h")
    #define set_cursor(x, y) printf("\033[%d;%dH", x, y)
    
    typedef struct Point {
        char c;
        struct Point* n;  // next point
    } Point;
    
    typedef struct Snake {
        int len;
        int vx;  // x axis velocity
        int vy;  // y axis velocity
        int inx;
        Point* head;
        Point* tail;
    } Snake;
    
    void drop_apple(Point* table) {
        while (1) {
            int a_inx = rand() % MAX_INX;
    
            if (table[a_inx].c == ' ') {
                table[a_inx].c = APPLE_C;
                break;
            }
        }
    }
    
    void init_game(Point* table, Snake* snake) {
        // Fill table with spaces
        for (int i = 0; i < H; ++i) {
            for (int j = 0; j < W; ++j) {
                table[(i * W) + j].c = ' ';
                table[(i * W) + j].n = NULL;
            }
        }
    
        // Fill vertical table borders with '|'
        for (int j = 0; j < W; j += (W - 1)) {
            for (int i = 0; i < H; ++i) {
                table[(i * W) + j].c = '|';
            }
        }
    
        // Fill horizontal table borders with '-'
        for (int i = 0; i < H; i += (H - 1)) {
            for (int j = 0; j < W; ++j) {
                table[(i * W) + j].c = '-';
            }
        }
    
        // We use a fixed position and body direction for now.
        int x = 1;
        int y = 1;
        int inx = (y * W) + x;
    
        // Tail of the snake
        table[inx].c = BODY_C;
        table[inx].n = &table[inx + 1];
        snake->tail = &table[inx];
    
        // Body of the snake
        for (int i = 1; i < snake->len; ++i) {
            table[inx + i].c = BODY_C;
            table[inx + i].n = &table[inx + i + 1];
        }
    
        // Head of the snake
        table[inx + snake->len].c = HEAD_C;
        table[inx + snake->len].n = NULL;
        snake->head = &table[inx + snake->len];
        snake->inx = inx + snake->len;
    
        srand(time(NULL));  // Initialization, should only be called once.
        drop_apple(table);
    }
    
    void draw_table(const Point* table, const Snake* snake) {
        cls();
        for (int i = 0; i < H; ++i) {
            for (int j = 0; j < W; ++j) {
                printf("%c", table[(i * W) + j].c);
            }
            printf("\r\n");
        }
        printf("j: down, k: up, h: left, l: right\n");
        printf("score: %d\n", snake->len - 3);
    }
    
    // Defined at the end of the file.
    int kbhit(void);
    void get_dir(int* vx, int* vy) {
        char c = ' ';
        if (kbhit()) {
            c = getchar();
        }
    
        switch (c) {
            case 'h':
                *vx = -1;
                *vy = 0;
                break;
            case 'j':
                *vy = 1;
                *vx = 0;
                break;
            case 'k':
                *vy = -1;
                *vx = 0;
                break;
            case 'l':
                *vx = 1;
                *vy = 0;
                break;
            default:; break;
        }
    }
    
    void update_table(Point* table, Snake* snake) {
        int vx = 0;
        int vy = 0;
        get_dir(&vx, &vy);
    
        // Ignore changing the velocity if trying to move backwards.
        if (!(snake->vx == -vx || snake->vy == -vy)) {
            snake->vx = vx;
            snake->vy = vy;
        }
        int inx = snake->inx + snake->vx + (snake->vy * W);
    
        // We are opting to move the tail before the head. So that the player
        // could run in a circle. Therefore we check for invalid index after
        // moving the tail.
    
        if (table[inx].c != APPLE_C) {
            // Move the tail one up in linked list
            Point* temp = snake->tail->n;
            snake->tail->n = NULL;
            snake->tail->c = ' ';
            snake->tail = temp;
        } else {
            // Don't move the tail and increase score (len) drop a new apple
            snake->len += 1;
            drop_apple(table);
        }
    
    #if PASS_WALL == 1
        // Validate the new head position.
        if (table[inx].c == BODY_C) {
            printf("Illegal index, you lost!\n");
            printf("Index %d\n", inx);
            exit(1);
        }
    
        // Move to the other side if hitting walls.
        if (table[inx].c == '|') {
            inx = (inx % W) > (W / 2) ? inx - (W - 2) : inx + (W - 2);
        } else if (table[inx].c == '-') {
            inx = (inx / W) > (H / 2) ? inx - ((H - 2) * W) : inx + ((H - 2) * W);
        }
    #else
        // Validate the new head position.
        if (table[inx].c != ' ' && table[inx].c != APPLE_C) {
            printf("Illegal index, you lost!\n");
            exit(1);
        }
    #endif
    
        // Set the new head position.
        table[inx].n = NULL;
        table[inx].c = HEAD_C;
        snake->head->n = &table[inx];
        snake->head->c = BODY_C;
        snake->head = &table[inx];
        snake->inx = inx;
    }
    
    int main(int argc, char** argv) {
        Point table[W][H];
        Snake snake = {.len = 3, .vx = 1, .vy = 0};
    
        init_game(&table[0][0], &snake);
    
        while (1) {
            update_table(&table[0][0], &snake);
            draw_table(&table[0][0], &snake);
    
            // loop's delay
            usleep(150000);
        }
    }
    
    // Copied from the internet for non-blocking, non-line-bufferd input read
    // Thanks to Thantos's answer in this thread:
    // https://cboard.cprogramming.com/c-programming/63166-kbhit-linux.html
    int kbhit(void) {
        struct termios oldt;
        struct termios newt;
        int ch;
        int oldf;
    
        tcgetattr(STDIN_FILENO, &oldt);
        newt = oldt;
        newt.c_lflag &= ~(ICANON | ECHO);
        tcsetattr(STDIN_FILENO, TCSANOW, &newt);
        oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
        fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
    
        ch = getchar();
    
        tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
        fcntl(STDIN_FILENO, F_SETFL, oldf);
    
        if (ch != EOF) {
            ungetc(ch, stdin);
            return 1;
        }
    
        return 0;
    }
    Last edited by sadegh; 12-21-2022 at 10:24 AM.

  2. #2
    Registered User
    Join Date
    May 2012
    Location
    Arizona, USA
    Posts
    948
    Quote Originally Posted by sadegh View Post
    It uses ANSI codes and Linux specific library to handle terminal drawings unfortunately. If there is a platform independent solutions please help me with that.
    You could look into the ncurses library. I don't have experience with it, but I know it's cross platform and handles the low-level terminal input and output (e.g., cursor control and keyboard input) for you so it can work with virtually any type of terminal (not just ANSI terminals). So you could theoretically play your Snake game on an old Wyse WY-50 terminal.

  3. #3
    Registered User
    Join Date
    Dec 2022
    Posts
    4
    Oh thanks a lot, I will look into that soon.
    Playing it on a Wyse 50 would be cool if you ever get your hands on one.

  4. #4
    and the hat of int overfl Salem's Avatar
    Join Date
    Aug 2001
    Location
    The edge of the known universe
    Posts
    39,661
    > Point table[W][H];
    Not that it makes much of a difference, but conventionally it would be

    Point table[H][W];
    Horizontally adjacent cells on screen are adjacent in memory.

    > init_game(&table[0][0], &snake);
    So do you know how to pass a 2D array to a function?

    All the manual subscripting calculations are somewhat error prone.

    Code:
    void init_game(Point table[][W], Snake* snake) {
      for ( int r = 0 ; r < H ; r++ ) {
        for ( int c = 0 ; c < W ; c++ ) {
          table[r][c].c = ' ';
        }
      }
    }
    
    int main ( ) {
        Point table[H][W];
        init_game(table, &snake);
    }
    > struct Point* n; // next point
    Instead of commenting short meaningless variable names, rename them to be meaningful.

    Other than that, it plays fine.
    If you dance barefoot on the broken glass of undefined behaviour, you've got to expect the occasional cut.
    If at first you don't succeed, try writing your phone number on the exam paper.

  5. #5
    Registered User
    Join Date
    May 2012
    Location
    Arizona, USA
    Posts
    948
    Quote Originally Posted by sadegh View Post
    Playing it on a Wyse 50 would be cool if you ever get your hands on one.
    I actually happen to have one (or one of another similar model) that I acquired from my company's office when we bought the building from the previous owner (they left behind a bunch of really old computer stuff). It even has the original keyboard with ALPs key switches!

    And it works in Linux after I jury-rigged a few paperclips to connect a DE9 serial cable to the larger connector (probably a DB25) on the back of the terminal. It's just really slow at 9600 bps, so it might not be the best choice for a fast-paced Snake game after all.

  6. #6
    Registered User
    Join Date
    Dec 2017
    Posts
    1,633
    I find it a lot easier to play with the arrow keys.
    Code:
    void get_dir(int* vx, int* vy) {
        int kbhit(void); // Defined at the end of the file.
        char c = ' ';
        if (kbhit()) {
            c = getchar(); if (c != 27) return;
            c = getchar(); if (c != 91) return;
            c = getchar();
        }
        switch (c) {
            case 68:      // LEFT  'h':
                *vx = -1;
                *vy = 0;
                break;
            case 66:      // DOWN  'j':
                *vy = 1;
                *vx = 0;
                break;
            case 65:      // UP    'k':
                *vy = -1;
                *vx = 0;
                break;
            case 67:      // RIGHT 'l':
                *vx = 1;
                *vy = 0;
                break;
        }
    }
    A little inaccuracy saves tons of explanation. - H.H. Munro

  7. #7
    Registered User
    Join Date
    Dec 2022
    Posts
    4
    Thank you for your input on all theses, especially on 2D arrays, I actually had no good idea how you deal with then in C.

  8. #8
    Registered User
    Join Date
    Dec 2022
    Posts
    4
    So here is the more polished version:

    Code:
    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    #include <unistd.h>
    // For input handling
    #include <fcntl.h>
    #include <termios.h>
    
    #define W       (32)
    #define H       (16)
    #define MAX_INX (H * W)
    #define BODY_C  's'
    #define HEAD_C  'Q'
    #define APPLE_C '*'
    
    // Set this to 1 for passing through walls and comming out from the other side
    #define PASS_WALL 1
    
    // `CSI 0 ; 0 H` puts the cursor at 0, 0 (defaul position).
    // `CSI 0 J` clears from cursor to end of screen.
    // `CSI 3 J` clears entire screen and deletes all lines saved in the scrollback
    // buffer.
    #define cls()            printf("\033[H\033[J\033[3J")
    #define hide_cursor()    printf("\033[?25l")
    #define show_cursor()    printf("\033[?25h")
    #define set_cursor(x, y) printf("\033[%d;%dH", x, y)
    
    typedef struct Point {
        char c;
        struct Point* next;
    } Point;
    
    // We have to store the index of the snake's head even though we already have
    // a pointer to it. because the `Point` does not store coordinates. We could
    // alos opt to include fields like `x` and `y` in the `Point` struct but they
    // would have no use except for moving the head and maybe passing through    
    // walls which are already solved quite easily without such information provided
    // in the struct.
    typedef struct Snake {
        int len;
        int x;
        int y;
        int vx;   // x axis velocity
        int vy;   // y axis velocity
        Point* head;
        Point* tail;
    } Snake;
    
    void drop_apple(Point table[][W]) {
        while (1) {
            int inx = rand();
            int y = inx % H;
            int x = inx % W;
    
            if (table[y][x].c == ' ') {
                table[y][x].c = APPLE_C;
                break;
            }
        }
    }
    
    void init_game(Point table[][W], Snake* snake) {
        // Fill table with spaces
        for (int i = 0; i < H; ++i) {
            for (int j = 0; j < W; ++j) {
                table[i][j].c = ' ';
                table[i][j].next = NULL;
            }
        }
    
        // Fill vertical table borders with '|'
        for (int j = 0; j < W; j += (W - 1)) {
            for (int i = 0; i < H; ++i) {
                table[i][j].c = '|';
            }
        }
    
        // Fill horizontal table borders with '-'
        for (int i = 0; i < H; i += (H - 1)) {
            for (int j = 0; j < W; ++j) {
                table[i][j].c = '-';
            }
        }
    
        // We use a fixed position and body direction for now.
        int x = 1;
        int y = 1;
    
        // Tail of the snake
        table[y][x].c = BODY_C;
        table[y][x].next = &table[y][x + 1];
        snake->tail = &table[y][x];
    
        // Body of the snake
        for (int j = 1; j < snake->len; ++j) {
            table[y][j + 1].c = BODY_C;
            table[y][j + 1].next = &table[y][x + j +1];
        }
    
        // Head of the snake
        table[y][x + snake->len].c = HEAD_C;
        table[y][x + snake->len].next = NULL;
        snake->head = &table[y][x + snake->len];
        snake->y = y;
        snake->x = x + snake->len;
    
        srand(time(NULL));  // Initialization, should only be called once.
        drop_apple(table);
    }
    
    void get_input_vxy(int* vx, int* vy) {
        int kbhit(void); // Defined at the end of the file.
    
        char c = ' ';
        if (kbhit()) {
            // Skip aditional characters sent by arrow keys
            c = getchar(); if (c != 27) return;
            c = getchar(); if (c != 91) return;
            c = getchar();
        }
        switch (c) {
            case 68:      // LEFT
                *vx = -1;
                *vy = 0;
                break;
            case 66:      // DOWN
                *vy = 1;
                *vx = 0;
                break;
            case 65:      // UP
                *vy = -1;
                *vx = 0;
                break;
            case 67:      // RIGHT
                *vx = 1;
                *vy = 0;
                break;
        }
    }
    
    void update_table(Point table[][W], Snake* snake) {
        int vx = 0;
        int vy = 0;
        get_input_vxy(&vx, &vy);
    
        // Ignore changing the velocity if trying to move backwards.
        if (!(snake->vx == -vx || snake->vy == -vy)) {
            snake->vx = vx;
            snake->vy = vy;
        }
        int y = snake->y + snake->vy;
        int x = snake->x + snake->vx;
    
        // We are opting to move the tail before the head. So that the player could
        // run in a circle. Therefore we check for invalid index after moving the
        // tail.
    
        if (table[y][x].c != APPLE_C) {
            // Move the tail one up in linked list
            Point* temp = snake->tail->next;
            snake->tail->next = NULL;
            snake->tail->c = ' ';
            snake->tail = temp;
        } else {
            // Don't move the tail and increase score (len) drop a new apple
            snake->len += 1;
            drop_apple(table);
        }
    
    #if PASS_WALL == 1
        // Validate the new head position.
        if (table[y][x].c == BODY_C) {
            printf("You lost!\n");
            exit(1);
        }
    
        // Move to the other side if hitting walls.
        if (table[y][x].c == '|') {
            x = x == 0 ? x + (W -2) : x - (W - 2);
        } else if (table[y][x].c == '-') {
            y = y == 0 ? y + (H - 2) : y - (H - 2);
        }
    #else
        // Validate the new head position.
        if (table[inx].c != ' ' && table[inx].c != APPLE_C) {
            printf("You lost!\n");
            exit(1);
        }
    #endif
    
        // Set the new head position.
        table[y][x].next = NULL;
        table[y][x].c = HEAD_C;
        snake->head->next = &table[y][x];
        snake->head->c = BODY_C;
        snake->head = &table[y][x];
        snake->y = y;
        snake->x = x;
    }
    
    void draw_table(const Point table[][W], const Snake* snake) {
        cls();
        for (int i = 0; i < H; ++i) {
            for (int j = 0; j < W; ++j) {
                printf("%c", table[i][j].c);
            }
            printf("\r\n");
        }
        printf("score: %d\n", snake->len - 3);
    }
    
    
    int main(void) {
        Point table[H][W];
        Snake snake = {.len = 3, .vx = 1, .vy = 0};
    
        init_game(table, &snake);
    
        while (1) {
            update_table(table, &snake);
            draw_table(table, &snake);
    
            // loop's delay
            usleep(150000);
        }
    }
    
    // Copied from the internet for non-blocking, non-line-bufferd input read.
    // Thanks to Thantos for his answer in this thread:
    // https://cboard.cprogramming.com/c-programming/63166-kbhit-linux.html
    int kbhit(void) {
        struct termios oldt;
        struct termios newt;
        int ch;
        int oldf;
    
        tcgetattr(STDIN_FILENO, &oldt);
        newt = oldt;
        newt.c_lflag &= ~(ICANON | ECHO);
        tcsetattr(STDIN_FILENO, TCSANOW, &newt);
        oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
        fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
    
        ch = getchar();
    
        tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
        fcntl(STDIN_FILENO, F_SETFL, oldf);
    
        if (ch != EOF) {
            ungetc(ch, stdin);
            return 1;
        }
    
        return 0;
    }
    Next step for me would be to implement it in a graphical environment.
    I've read so far that the SDL library is a fairly good choice for beginners. Do you have any advice or a good resource for study to get started?

  9. #9
    Registered User
    Join Date
    Jun 2011
    Posts
    4,513
    Quote Originally Posted by sadegh View Post
    I've read so far that the SDL library is a fairly good choice for beginners. Do you have any advice or a good resource for study to get started?
    I'd recommend starting right at the source: Simple DirectMedia Layer - Homepage
    There is a lot of information in their wiki to get started.

    I'd also suggest you start getting used to separating data from interface. Any code that performs calculations or stores data should be completely separate from code that displays output.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. help with c snake game
    By stibs in forum C Programming
    Replies: 3
    Last Post: 12-28-2018, 06:19 AM
  2. Snake game
    By coder222 in forum C Programming
    Replies: 2
    Last Post: 12-08-2015, 03:03 PM
  3. Snake Game help on it please...
    By LekhAsh in forum C++ Programming
    Replies: 2
    Last Post: 01-30-2015, 01:45 AM
  4. Dat snake game...
    By esrelmantis in forum C++ Programming
    Replies: 3
    Last Post: 05-19-2013, 04:24 AM
  5. About SnaKE game..
    By ozumsafa in forum C Programming
    Replies: 3
    Last Post: 10-19-2007, 05:46 PM

Tags for this Thread