6
\$\begingroup\$

Made a simple snake game in C.

I am just having some fun with program and using this as a spring board of knowledge in hopes to get some confidence to maybe try different kinds of projects. I am hoping any insight will push me in new directions to develop my coding abilities.

I keep everything in the snake.c file. four functions setup, draw, input and logic. Seemed like most tutorials online were incomplete and used a deprecated library for some reason, so this was the solution route I took. Took me awhile to understand how the movement worked, but code runs. It's fun to play, and it was fun to make as well.

// ALF : arad96/macos-terminal-snake-game-c
// C program to build the complete snake game   
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h>
#include <ncurses.h>
#include <time.h>
  
int i, j, height = 20, width = 20; 
int gameover, score; 
int x, y;                       // current position
int tailX[100], tailY[100];     // memory for all tail segments 
int fruitx, fruity;             // fruit position
int nTail = 1;                    
int flag;                       // direction flag
char ch; 
  
// Function to generate the fruit within the boundary 
void setup() { 
    gameover = 0; 
    x = height / 2; 
    y = width / 2;

    srand(time(0));  // Seed the random number generator
    do {
        fruitx = rand() % (height - 1) + 1;  // generate fruit x,y so always inside boarders 
        fruity = rand() % (width - 1) + 1;   // and not on snakes head
    } while (fruitx == x && fruity == y);

    score = 0; 

} 


// Function to draw the boundaries 
void draw() { 
    
    wclear(stdscr);     // clear window

    // print game board
    for (i = 0; i <= height; i++) { 
        for (j = 0; j <= width; j++) { 
            if (i == 0 || i == height || j == 0 || j == width) {
                // draw boarder 
                printw("#");
            } 
            else if (i == x && j == y){
                    // draw head
                    printw("0");
                }   
            else if (i == fruitx && j == fruity){
                    // draw fruit
                    printw("*");
                } 
            else {
                // check to see if cell is occupied by tail
                int print = 0;
                for(int k = 0; k < nTail; k++){
                    if(tailX[k] == i && tailY[k] == j){
                        // draw tail segments
                        printw("o");
                        print = 1;
                    }        
                }
                if (! print){
                    // not occupied
                    printw(" ");
                }
            }        
        } 
        printw("\n");
    } 
  
    // Print the score after the game ends 
    printw("Score = %d", score); 
    printw("\n"); 
    printw("press X to quit the game"); 
    printw("\n");
    usleep(350000);     // Sleep for x microseconds
    refresh();          // render graphics
}


// Function to take the input 
void input() { 
    
    // Get the keyboard input
    int ch = getch();

    // case 97: Handles the 'a' key press.
    // case 115: Handles the 's' key press.
    // case 100: Handles the 'd' key press.
    // case 119: Handles the 'w' key press.
    // case 120: Handles the 'x' key press.
    
    // Check if a key was pressed
    if (ch != ERR) {
        
        // Get the character code of the key pressed
        int key = ch & 0xFF;

        // Check if the key pressed was a special key, such as an wasd key
        switch (key) { 
            case 'a':       // left
                flag = 1; 
                break; 
            case 's':       // down
                flag = 2; 
                break; 
            case 'd':       // right
                flag = 3; 
                break; 
            case 'w':       // up
                flag = 4; 
                break; 
            case 'x': 
                gameover = 1; 
                break; 
        } 
    }
} 
  

// Function for the logic behind each movement 
void logic() {
    
    // store head from previous iteration 
    int prevX = tailX[0];          
    int prevY = tailY[0];
    int prev2X, prev2Y;

    // update x, y based on input direction wasd
    switch (flag) {
        // x goes up and down
        // y goes left and right 
        // ik its backwards im dyslexic   
        case 1: 
            y--;    // Move left
            break; 
        case 2: 
            x++;    // Move down
            break; 
        case 3: 
            y++;    // Move right
            break; 
        case 4: 
            x--;    // Move up
            break; 
        default: 
            break; 
    } 

    // Update the position of the head in the tail arrays   
    tailX[0] = x;
    tailY[0] = y;

    // update position of tail segments
    for (int ix = 1; ix < nTail; ix++) {
        prev2X = tailX[ix];
        prev2Y = tailY[ix];
        tailX[ix] = prevX;
        tailY[ix] = prevY;
        prevX = prev2X;
        prevY = prev2Y;
    }
  
    // check boarder collision
    if (x < 1 || x > height - 1 || y < 1 || y > width - 1){ // (subtract 1 bc boarders)
        gameover = 1;
        printw("Boundary hit: GAME OVER");
        printw("\n");
        refresh();
        sleep(2);
        return;
    }

    // check self collision
    for(int k = 1; k < nTail; k++){
        if(tailX[k] == x && tailY[k] == y){
            gameover = 1;
            printw("Self hit: GAME OVER");
            printw("\n");
            refresh();
            sleep(3);
            return;
        }
    }

    // check for fruit collision  
    if (x == fruitx && y == fruity) {  
        
        int fruit_on_snake;

        // After eating the above fruit generate new fruit on non occupied space
        do {
            fruit_on_snake = 0;
            fruitx = rand() % (height - 1) + 1;  // generate fruit x,y so always inside boarders and non occupied spot 
            fruity = rand() % (width - 1) + 1;
            for(int k = 0; k < nTail; k++){
                if(tailX[k] == fruitx && tailY[k] == fruity){
                    fruit_on_snake = 1;
                    // printw("Fruit on Snake"); printw("\n");refresh();
                }        
            }
        } while (fruit_on_snake);

        nTail++;
        score += 10;

    } 
}


// Driver Code 
int main() { 
    
    // init screen, Enable keypad mode, unbuffer input   
    initscr();              // Start curses mode
    cbreak();               // Line buffering disabled
    noecho();               // Don't echo() while we do getch
    nodelay(stdscr, TRUE);  // Non-blocking input
    keypad(stdscr, TRUE); 

    // Generate boundary 
    setup(); 
  
    // Until the game is over 
    while (!gameover) { 
        draw(); 
        input(); 
        logic();
    }

    // Disable keypad mode End curses mode
    keypad(stdscr, FALSE); 
    endwin();

return 0;

} 
\$\endgroup\$
7
  • 2
    \$\begingroup\$ Dyslexia is a weak excuse... Recommend r for rows and c for cols (or even better, use 3-letter variable names)... Using generic i and j (and k) WILL get you in trouble one day. Especially if those variables are not scoped to be extremely local... \$\endgroup\$
    – Fe2O3
    Commented May 26 at 6:29
  • 1
    \$\begingroup\$ Thanks for reviewing. That is a good point, I can see that variable naming and scoping is an important thing to consider especially as the program scales it could cause dangerous time consuming debugging nightmares. \$\endgroup\$
    – Alf
    Commented May 26 at 6:47
  • 1
    \$\begingroup\$ Ask any experienced coder. ALL of us, I dare say, have been bitten at one time or another. Any time you think "Ah, I can save 3 seconds by just ...", you're risking wasting 3 hours down the track trying to find the problem created by saving those 3 seconds... \$\endgroup\$
    – Fe2O3
    Commented May 26 at 7:53
  • 2
    \$\begingroup\$ Consider: do { draw(); input(); } while( logic() ); to replace loop in main(), and get rid of gameover. Don't kill the game if user mis-types a letter; just ignore it... Consider how to use variables at local scope, and return values from functions, instead of file scope. One-by-one... Cheers! \$\endgroup\$
    – Fe2O3
    Commented May 26 at 7:57
  • 1
    \$\begingroup\$ // Get the keyboard input int ch = getch(); ==> Why do you feel the need to comment this? \$\endgroup\$
    – Harith
    Commented May 26 at 9:51

1 Answer 1

8
\$\begingroup\$

It works on more than just MacOS

Congratulations, you have written code that compiles just fine on Linux as well, and probably works on every other UNIX-like operating system. However, that's just because most standard libraries still support the obsoleted POSIX function usleep():

Use a portable sleep function

usleep() was marked obsolete in POSIX.1-2001, and officially removed from POSIX.1-2008. So you should no longer use this function. There are several alternatives:

Sleep after refreshing the screen

Your game feels not very responsive to inputs. The reason for that is that you sleep between reading the input and refreshing the screen. This causes an additional delay of one frame before one sees the result of the input. The fix is to simply call refresh() before the sleep command.

Use werase() instead of wclear()

wclear() forces the whole window to be redrawn, but this should almost never be needed. Use werase() instead, as then ncurses will only send the minimum number of updates to the window to make it show the new state. This will also reduce the amount of visible flickering.

About swapping x and y

You wrote in the comments that you swapped x and y because of dyslexia. Still, you noticed this issue yourself, so why not change x and y back? If that still is hard to do, then Fe2O3's suggestion to rename these variables to something much different would be helpful. But instead of using r and c, I would use row and column. A bit more typing, but much easier to read and understand what they mean (even for those without dyslexia by the way).

Split large functions up into multiple smaller ones

logic() is a very large function. I recommend you split it up into several smaller ones. Consider writing logic() itself like so:

void logic() {
    update_head_position();
    update_tail();
    check_border_collisions();
    check_self_collision();
    check_fruit_collision();
}

Note how you can give functions very descriptive names; this makes the code more self-documenting, and can often avoid the need to add comments explaining what a section of the code does.

Use an enum to name the directions

flag is just an integer, and now you have to remember which value means which direction. It's would be much better if you could write something like this in input():

switch(key) {
    case 'a':
        direction = LEFT;
        break;
    …
}

And then in logic() you can write:

switch(direction) {
    case LEFT:
        x--;
        break;
    …
}

You can do this by declaring an enum:

enum {
    LEFT,
    RIGHT,
    UP,
    DOWN,
} direction;

What if you eat 100 pieces of fruit?

You hardcoded the size of tailX[] and tailY[] to 100 elements. But if you eat 100 pieces of fruit, you will start accessing these arrays out of bounds. In the best case, that will result in the program crashing. However, before that happens you will likely first overwrite other variables, causing the program to start behaving incorrectly.

There are two ways to solve this issue. First, you could store the tail positions in a more dynamic data structure, for example by (re)allocating memory for these arrays when necessary, or by using a linked list.

Second, just ensure that you never increate nTail past 100. Maybe instead reward the player for eating 100 pieces of fruit, by declaring that they won?

\$\endgroup\$
3
  • \$\begingroup\$ Thank you for trying on a different OS! I am definitely going to refactor and continue to try and improve the code. I was not aware of the obsolete sleep function, I will probably lean towards C11's 'thrd_sleep()' or ncurses 'timeout()'. I did notice the weird lag I was not aware it was bc the way I was refreshing, good catch! Yes at the time swapping the x and y felt like, "if it ain't broke don't fix it", but i will refactor this now that I've had some feedback :) The reward at the end is a good idea, i only chose 100 bc it is ambiguously large. \$\endgroup\$
    – Alf
    Commented May 26 at 19:20
  • \$\begingroup\$ I see in the usleep() link the note saying the the function is obsolete. Does that mean the calls to sleep() in my code are also obsolete? They are both called from the same library but link doesn't mention that it is obsolete? \$\endgroup\$
    – Alf
    Commented May 29 at 0:57
  • \$\begingroup\$ No, sleep() is not obsolete and can still be used. Note that it is a POSIX function, not C, so it might not be available on platforms that are not POSIX-compliant. \$\endgroup\$
    – G. Sliepen
    Commented May 29 at 5:25

Not the answer you're looking for? Browse other questions tagged or ask your own question.