I’m writing a basic snake game in C. Upon launch, the program renders a snake that stays in place until it receives input from either the 'w', 's', 'a', 'd' keys or the arrow keys. After receiving the 'd' key, the snake moves to the right and continues moving in that direction until it receives input from either the 'w', 's', 'a' keys or the corresponding arrow keys. Once the snake has started moving, it will continue to move until the player presses the ']' key, which quits the game gracefully. Other keys will not stop the snake.
The program takes either 1 or 3 ASCII characters at a time. The 3-ASCII-character case is only for the arrow keys (up, down, left, and right). The 'w', 's', 'a', and 'd' keys control the movement of the snake well, but each of the arrow keys stops the snake from moving.
here is the code
#define map_size 10
struct termios orig_termios;
void clear_screen()
{
printf("\033[H\033[J\033[?25l");
fflush(stdout);
}
void red_square()
{
printf("\033[101m \033[0m");
fflush(stdout);
}
void green_square()
{
printf("\033[102m \033[0m");
fflush(stdout);
}
void move_cursor(int hx, int hy)
{
printf("\033[%d;%dH", hy, hx);
fflush(stdout);
}
void disableRawMode()
{
tcsetattr(0, TCSAFLUSH, &orig_termios);
printf("\033[?25h\n");
}
void enableRawMode()
{
tcgetattr(0, &orig_termios);
atexit(disableRawMode);
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON);
tcsetattr(0, TCSAFLUSH, &raw);
}
int generate_food_x(int lower, int upper)
{
int num = (rand() % (upper - lower + 1)) + lower;
return num * 2 - 1;
}
int generate_food_y(int lower, int upper)
{
int num = (rand() % (upper - lower + 1)) + lower;
return num;
}
bool eats(int snake_x, int snake_y, int food_x, int food_y, int *total)
{
if (snake_x == food_x && snake_y == food_y)
{
(*total)++;
return 1;
}
return 0;
}
bool kbhit()
{
int byteswaiting;
ioctl(0, FIONREAD, &byteswaiting);
bool result = byteswaiting > 0;
return result;
}
int main()
{
int snake_x = 1; // starting position of the green square
int snake_y = 1;
int food_x = 5;
int food_y = 1;
int scoreboard_x = map_size * 2 + 3;
int scoreboard_y = 1;
bool eaten = 0;
int total = 0;
enableRawMode();
srand(time(0));
char ch = 0;
char next1 = 0;
char next2 = 0;
while (1)
{
clear_screen();
move_cursor(snake_x, snake_y);
green_square();
if (eats(snake_x, snake_y, food_x, food_y, &total))
{
food_x = generate_food_x(1, map_size);
food_y = generate_food_y(1, map_size);
}
move_cursor(food_x, food_y);
red_square();
move_cursor(scoreboard_x, scoreboard_y);
printf("total: %d", total);
fflush(stdout);
move_cursor(scoreboard_x, scoreboard_y+1);
printf("next1: %d next2: %d", next1, next2);
fflush(stdout);
if (kbhit()) // check if any key gets hit
{
ch = getchar();
if (ch == ']')
{
exit(0);
}
}
if (ch != 0)
{
if (ch == 'w')
{
snake_y -= 1; // move up 1 line
}
else if (ch == 's')
{
snake_y += 1; // move down 1 line
}
else if (ch == 'a')
{
snake_x -= 2; // move left 2 spaces
}
else if (ch == 'd')
{
snake_x += 2; // move right 2 spaces
}
else if (ch == '\033')
{
next1 = getchar();
next2 = getchar();
if (next1 == '[' && next2 == 'D')
{
snake_x -= 2; // move left 2 spaces
}
else if (next1 == '[' && next2 == 'C')
{
snake_x += 2; // move right 2 spaces
}
else if (next1 == '[' && next2 == 'A')
{
snake_y -= 1; // move up 1 line
}
else if (next1 == '[' && next2 == 'B')
{
snake_y += 1; // move down 1 line
}
}
}
usleep(100000);
}
return 0;
}
the lines around scoreboard_y+1
are for debugging
When the game started, I pressed the right arrow key and the snake moved one step before stopping when next1
was 91 and next2
was 67. When I pressed the right arrow key again, the snake didn't move at all when next1
was 27 and next2
was 91.
there're 2 bugs and I have no clue where to debug deeper.
bug1: the snake is supposed to keep moving with or without further input after initial start. However, any of the arrow keys stops the snake's movement.
bug2: The program is supposed to take 3 ASCII characters for any of the arrow keys though, it sometimes loses some of them.
could someone give a hint?
It seems heavy handed to do an ioctl()
per event loop iteration for 1 bit of information (kbhit()
). Consider using non-blocking I/O and separate processing of input handle_input()
from the action taken based on the current direction switch(d)
. On my machine I saw at most CH_MAX
12 bytes per loop iteration and always all 3 bytes of an arrow key:
#define _DEFAULT_SOURCE
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define map_size 10
#define CH_MAX 12
enum direction {
STOP,
UP,
RIGHT,
DOWN,
LEFT
};
struct termios orig_termios;
void clear_screen() {
printf("\033[H\033[J\033[?25l");
fflush(stdout);
}
void red_square() {
printf("\033[101m \033[0m");
fflush(stdout);
}
void green_square() {
printf("\033[102m \033[0m");
fflush(stdout);
}
void move_cursor(int hx, int hy) {
printf("\033[%d;%dH", hy, hx);
fflush(stdout);
}
void disableRawMode() {
tcsetattr(0, TCSAFLUSH, &orig_termios);
printf("\033[?25h\n");
}
void enableRawMode() {
tcgetattr(0, &orig_termios);
atexit(disableRawMode);
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON);
tcsetattr(0, TCSAFLUSH, &raw);
}
int generate_food_x(int lower, int upper) {
int num = (rand() % (upper - lower + 1)) + lower;
return num * 2 - 1;
}
int generate_food_y(int lower, int upper) {
int num = (rand() % (upper - lower + 1)) + lower;
return num;
}
bool eats(int snake_x, int snake_y, int food_x, int food_y, int *total) {
if (snake_x == food_x && snake_y == food_y) {
(*total)++;
return 1;
}
return 0;
}
void handle_input(enum direction *d) {
char ch[CH_MAX];
ssize_t ch_len = read(0, ch, CH_MAX);
if(ch_len <= 0)
return;
const static struct lookup {
char key;
char *arrow;
enum direction d;
} map[] = {
{'w', "\033[A", UP},
{'d', "\033[C", RIGHT},
{'s', "\033[B", DOWN},
{'a', "\033[D", LEFT}
};
for(const struct lookup *m = map; m < map + sizeof map / sizeof *map; m++) {
if(
ch[ch_len - 1] == m->key ||
(ch_len >= 3 && !strncmp(ch + ch_len - 3, m->arrow, 3))
) {
*d = m->d;
return;
}
}
}
int main() {
int snake_x = 1; // starting position of the green square
int snake_y = 1;
int food_x = 5;
int food_y = 1;
int scoreboard_x = map_size * 2 + 3;
int scoreboard_y = 1;
bool eaten = 0;
int total = 0;
enableRawMode();
fcntl (0, F_SETFL, O_NONBLOCK);
srand(time(0));
enum direction d = STOP;
for(;;) {
clear_screen();
move_cursor(snake_x, snake_y);
green_square();
if (eats(snake_x, snake_y, food_x, food_y, &total)) {
food_x = generate_food_x(1, map_size);
food_y = generate_food_y(1, map_size);
}
move_cursor(food_x, food_y);
red_square();
move_cursor(scoreboard_x, scoreboard_y);
printf("total: %d", total);
fflush(stdout);
handle_input(&d);
switch(d) {
case STOP:
break;
case UP:
snake_y--;
break;
case RIGHT:
snake_x += 2;
break;
case DOWN:
snake_y++;
break;
case LEFT:
snake_x -= 2;
break;
}
usleep(100000);
}
return 0;
}