1

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?

JJJohn
  • 915
  • 8
  • 26
  • Aside: `getchar()` returns an `int`, not a `char`. – Harith May 02 '23 at 01:25
  • 1
    Please revise your program with the missing includes. – Allan Wind May 02 '23 at 02:16
  • I would make all `getchar()` conditioned on `kbhit()`. If the first byte is `\033` set a variable saying you expect to read a 2nd byte and when you get that a 3rd byte. Or at least make each of the`getchar()` in `ch == '\033' conditioned on kbkit() – Allan Wind May 02 '23 at 02:29
  • If your kbhit happens and you read an escape then you go and read two more (correct). But then kbhit doesn't happen and ch is still an escape and then you go and read two more (incorrect). So after reading a ch the first time you don't want to read two more characters every loop. – Jerry Jeremiah May 02 '23 at 02:36
  • `ioctl()` per loop iteration doesn't seem right... or at least if you do that expensive op use all the information (there n bytes to read). In raw mode isn't there a non-blocking getchar()? – Allan Wind May 02 '23 at 02:37
  • See https://stackoverflow.com/questions/20588002/nonblocking-get-character – Allan Wind May 02 '23 at 02:46
  • 1
    If `kbhit` returns 0, then `ch` could be stale (holding the last valid character but _not_ the current one). Try adding `ch = 0;` to the bottom of the loop. Or, put it just above the `kbhit` call. – Craig Estey May 02 '23 at 02:54
  • Also, I wouldn't mix stdio buffered calls like `getchar` with raw TTY mode. For a `getchar` replacement, I'd use `char buf[1]; read(0,buf,sizeof(buf)); ch = buf[0];` You may also wish to use `tcsetattr` to set `VMIN` and `VTIME` – Craig Estey May 02 '23 at 02:59
  • @CraigEstey He can't change ch to 0 because then his snake won't keep moving. – Jerry Jeremiah May 02 '23 at 03:11
  • This would fix it: `if(kbhit()){ch=getchar(); if(ch=='\033'){next1=getchar(); next2=getchar(); if(next1=='['&&next2=='D')ch='a'; if(next1=='['&&next2=='C')ch='d'; if(next1=='['&&next2=='A')ch='w'; if(next1=='['&&next2=='B')ch='s'; } } if(ch!=0){if(ch=='w')snake_y-=1; if(ch=='s')snake_y+=1; if(ch=='a')snake_x-=2; if(ch=='d')snake_x+=2; } if(snake_y<1||snake_y>map_size||snake_x<1||snake_x>map_size*2){break; } ` https://onlinegdb.com/B7Ym3A232 – Jerry Jeremiah May 02 '23 at 03:13
  • @JerryJeremiah You should put your correction in an answer. We often complain about lack of indentation :-) – Craig Estey May 02 '23 at 03:17
  • @CraigEstey It wasn't an answer - it was the second part of my first comment. However, it could be an answer... – Jerry Jeremiah May 02 '23 at 03:41

1 Answers1

1

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;
}
Allan Wind
  • 23,068
  • 5
  • 28
  • 38
  • Please use a consistent indentation style, preferable something readable. – 12431234123412341234123 May 02 '23 at 08:57
  • @12431234123412341234123 I am pretty sure it was consistent when I entered/pasted it but stackoverflow then reformats it when you save it. Not entirely yet what happens but I think tab is converted to space on display, but if you use space intend when editing code it comes out differently than the tabs when saved. Revised answer to use a table to DRY up the code. It should help with readability, too. – Allan Wind May 02 '23 at 19:02