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;
}
1 Answer 1
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:
- Use the POSIX functions
nanosleep()
orclock_nanosleep()
. - Use C11's
thrd_sleep()
. - Use ncurses itself to sleep, using
timeout()
to letgetch()
wait for a fixed amount of time for input.
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?
-
\$\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– Alf2024年05月26日 19:20:16 +00:00Commented May 26, 2024 at 19:20
-
\$\begingroup\$ I see in the
usleep()
link the note saying the the function is obsolete. Does that mean the calls tosleep()
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– Alf2024年05月29日 00:57:06 +00:00Commented May 29, 2024 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– G. Sliepen2024年05月29日 05:25:51 +00:00Commented May 29, 2024 at 5:25
r
for rows andc
for cols (or even better, use 3-letter variable names)... Using generici
andj
(andk
) WILL get you in trouble one day. Especially if those variables are not scoped to be extremely local... \$\endgroup\$do { draw(); input(); } while( logic() );
to replace loop inmain()
, and get rid ofgameover
. 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\$// Get the keyboard input int ch = getch();
==> Why do you feel the need to comment this? \$\endgroup\$