about summary refs log tree commit diff
path: root/snake.c
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2024-01-01 23:36:24 +0100
committerMel <einebeere@gmail.com>2024-01-01 23:43:11 +0100
commit3da3b71fb1305b09fd7ba81781f1b9cce8476aa0 (patch)
treee7200dc3a55e1ea4025f606bb4ad5e328c2f21f7 /snake.c
parent29ac9d8cf84f4412e164c51c139175df6016d319 (diff)
downloadonesies-3da3b71fb1305b09fd7ba81781f1b9cce8476aa0.tar.zst
onesies-3da3b71fb1305b09fd7ba81781f1b9cce8476aa0.zip
Snake! main
Diffstat (limited to 'snake.c')
-rwxr-xr-xsnake.c380
1 files changed, 380 insertions, 0 deletions
diff --git a/snake.c b/snake.c
new file mode 100755
index 0000000..2bebfc0
--- /dev/null
+++ b/snake.c
@@ -0,0 +1,380 @@
+//$(which make) "${0%.c}" && ./"${0%.c}" && exit
+
+#include <assert.h>
+#include <string.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdint.h>
+
+#define SPEED_INIT_MS 250 
+#define SPEED_DECREASE_WITH_SCORE_MS 2 
+#define SPEED_MIN_MS 50 
+
+#define BOARD_WIDTH 30 
+#define BOARD_HEIGHT 10 
+
+#define DISPLAY_PADDING_X 1 
+#define DISPLAY_PADDING_Y 2
+
+#define DISPLAY_WIDTH (BOARD_WIDTH + DISPLAY_PADDING_X * 2)
+#define DISPLAY_HEIGHT (BOARD_HEIGHT + DISPLAY_PADDING_Y * 2)
+
+#define SCORE_MAX (BOARD_WIDTH * BOARD_HEIGHT)
+
+#define TAIL_SIZE_INIT 2 
+#define TAIL_SIZE_MAX (SCORE_MAX + TAIL_SIZE_INIT)
+
+#define FRUIT_COUNT 2 
+
+#define RANDOM_POS_TRIES_MAX 1000
+
+int32_t max(int32_t a, int32_t b) {
+    if (a > b) return a;
+    return b;
+}
+
+int32_t random_in_range(int32_t from, int32_t to) {
+    return (rand() % (to - from)) + from;
+}
+
+void configure_terminal(bool snake_mode) {
+    static struct termios old_terminal_state; struct termios terminal_state;
+    static int32_t old_stdin_fd_state; int32_t stdin_fd_state;
+
+    if (snake_mode) {
+        tcgetattr(STDIN_FILENO, &old_terminal_state);
+
+        terminal_state = old_terminal_state;
+        terminal_state.c_lflag &= ~(ICANON | ECHO);
+
+        old_stdin_fd_state = fcntl(STDIN_FILENO, F_GETFL);
+        stdin_fd_state = old_stdin_fd_state | O_NONBLOCK;
+    } else {
+        terminal_state = old_terminal_state;
+        stdin_fd_state = old_stdin_fd_state;
+    }
+
+    tcsetattr(STDIN_FILENO, TCSANOW, &terminal_state);
+    fcntl(STDIN_FILENO, F_SETFL, stdin_fd_state);
+}
+
+void clear_terminal(void) {
+    printf("\033[2J\033[H");
+}
+
+struct Position {
+    int32_t x, y;
+};
+
+struct Position position_new(int32_t x, int32_t y) {
+    return (struct Position){ x, y };
+}
+
+#define POS(x, y) position_new(x, y)
+
+struct Position position_random(void) {
+    return POS(
+        random_in_range(0, BOARD_WIDTH),
+        random_in_range(0, BOARD_HEIGHT)
+    );
+}
+
+bool position_equals(struct Position a, struct Position b) {
+    return a.x == b.x && a.y == b.y;
+}
+
+bool position_in_bounds(struct Position pos) {
+    return pos.x >= 0 && pos.y >= 0
+        && pos.x < BOARD_WIDTH && pos.y < BOARD_HEIGHT;
+}
+
+struct Display_Position {
+    uint32_t x, y;
+};
+
+#define D_POS(x, y) ((struct Display_Position){ x, y })
+#define D_POS_ZERO ((struct Display_Position){0})
+
+struct Display_Position game_position_to_display_position(struct Position pos) {
+    return D_POS(pos.x + DISPLAY_PADDING_X, pos.y + DISPLAY_PADDING_Y);    
+}
+
+struct Display {
+    uint8_t buffer[DISPLAY_HEIGHT][DISPLAY_WIDTH];
+};
+
+struct Display display_new(void) {
+    struct Display display = {0};
+    memset(&display.buffer, ' ', sizeof(display.buffer));
+    return display;
+}
+
+uint8_t *display_at(struct Display* display, struct Display_Position pos) {
+    assert(pos.x < DISPLAY_WIDTH && pos.y < DISPLAY_HEIGHT);
+    return &display->buffer[pos.y][pos.x];
+}
+
+void display_fill(
+    struct Display* display,
+    struct Display_Position from,
+    struct Display_Position to,
+    uint8_t with
+) {
+    for (uint32_t x = from.x; x < to.x; x++)
+        for (uint32_t y = from.y; y < to.y; y++)
+            *display_at(display, D_POS(x, y)) = with;
+}
+
+void display_frame(
+    struct Display* display,
+    struct Display_Position from,
+    struct Display_Position to
+) {
+    for (uint32_t i = 0; i < 2; i++)
+        for (uint32_t x = from.x + 1; x < to.x - 1; x++)
+            *display_at(display, D_POS(x, from.y + i * (to.y - 1))) = '-';
+
+    for (uint32_t i = 0; i < 2; i++)
+        for (uint32_t y = from.y + 1; y < to.y; y++)
+            *display_at(display, D_POS(from.x + i * (to.x - 1), y)) = '|';
+
+    for (uint32_t x = 0; x < 2; x++)
+        for (uint32_t y = 0; y < 2; y++)
+            *display_at(display, D_POS(from.x + x * (to.x - 1), from.y + y * (to.y - 1))) = '+';
+}
+
+void display_write(
+    struct Display *display,
+    struct Display_Position at,
+    char *text
+) {
+    for (uint32_t i = 0; text[i]; i++) *display_at(display, D_POS(at.x + i, at.y)) = text[i];
+}
+
+void display_display(struct Display* display) {
+    clear_terminal();
+    for (uint32_t y = 0; y < BOARD_HEIGHT + DISPLAY_PADDING_Y * 2; y++)
+        printf("%.*s\n", DISPLAY_WIDTH, display->buffer[y]);
+}
+
+enum Direction {
+    DIRECTION_NONE,
+
+    DIRECTION_RIGHT,
+    DIRECTION_DOWN,
+    DIRECTION_LEFT,
+    DIRECTION_UP,
+};
+
+enum Direction direction_opposite(enum Direction direction) {
+    switch (direction) {
+        case DIRECTION_RIGHT: return DIRECTION_LEFT;
+        case DIRECTION_DOWN: return DIRECTION_UP;
+        case DIRECTION_LEFT: return DIRECTION_RIGHT;
+        case DIRECTION_UP: return DIRECTION_DOWN;
+        default: return DIRECTION_NONE;
+    }
+}
+
+struct Input {
+    enum Direction direction;
+    bool quit;
+};
+
+struct Input input(void) {
+    uint8_t c = 0;
+    scanf("%c", &c);
+
+    struct Input i = {0};
+    switch (c) {
+        case 'w': i.direction = DIRECTION_UP; break;
+        case 'a': i.direction = DIRECTION_LEFT; break;
+        case 's': i.direction = DIRECTION_DOWN; break;
+        case 'd': i.direction = DIRECTION_RIGHT; break;
+
+        case 'q': i.quit = true; break;
+    }
+    
+    return i;
+}
+
+struct Snake {
+    struct Position pos;
+
+    struct Position tail[TAIL_SIZE_MAX];
+    uint32_t tail_size;
+};
+
+struct Game {
+    uint32_t score;
+
+    struct Snake snake;
+    enum Direction last_direction;
+
+    struct Position fruits[FRUIT_COUNT];
+};
+
+struct Game game_new(void) {
+    struct Position snake_pos = POS(BOARD_WIDTH / 2, BOARD_HEIGHT / 2);
+    struct Game game = {
+        .score = 0,
+        .snake = { .pos = snake_pos, .tail_size = TAIL_SIZE_INIT },
+        .last_direction = DIRECTION_RIGHT 
+    };
+
+    for (uint8_t t = 0; t < TAIL_SIZE_INIT; t++)
+        game.snake.tail[t] = POS(snake_pos.x - t, snake_pos.y);
+
+    for (uint8_t i = 0; i < FRUIT_COUNT; i++)
+        game.fruits[i] = position_random();
+
+    return game;
+}
+
+struct Position game_random_free_position(struct Game *game) {
+    struct Position pos; uint32_t try = 0;
+    
+    retry_random:
+    if (try++ > RANDOM_POS_TRIES_MAX) return POS(-1, -1);
+
+    pos = position_random();
+    if (position_equals(game->snake.pos, pos)) goto retry_random;
+    
+    for (uint32_t t = 0; t < game->snake.tail_size; t++)
+        if (position_equals(game->snake.tail[t], pos))
+            goto retry_random;
+    
+    for (uint32_t f = 0; f < FRUIT_COUNT; f++)
+        if (position_equals(game->fruits[f], pos))
+            goto retry_random;
+    
+    return pos;
+}
+
+bool game_step(struct Game *game, enum Direction direction) {
+    if (game->score == SCORE_MAX) return false;
+
+    struct Snake *snake = &game->snake;
+
+    enum Direction move_direction = game->last_direction;
+    if (direction != DIRECTION_NONE && direction != direction_opposite(game->last_direction)) { 
+        move_direction = direction;
+        game->last_direction = direction;
+    }
+
+    int8_t move_x = 0, move_y = 0;
+    switch (move_direction) {
+        case DIRECTION_RIGHT: move_x = 1; break;
+        case DIRECTION_DOWN: move_y = 1; break;
+        case DIRECTION_LEFT: move_x = -1; break;
+        case DIRECTION_UP: move_y = -1; break;
+        default: break;
+    }
+
+    struct Position new_position = POS(
+        snake->pos.x + move_x,
+        snake->pos.y + move_y
+    );
+
+    if (!position_in_bounds(new_position)) return false;
+    struct Position previous_position = snake->pos;
+    snake->pos = new_position;
+
+    int32_t eaten_fruit = -1;
+    for (uint32_t f = 0; f < FRUIT_COUNT; f++) {
+        if (position_equals(snake->pos, game->fruits[f])) {
+            eaten_fruit = f;
+            game->score++;
+            break;
+        }
+    }
+
+    for (uint32_t t = snake->tail_size; t > 0; t--) snake->tail[t] = snake->tail[t - 1];
+    snake->tail[0] = previous_position;
+    if (eaten_fruit >= 0) {
+        snake->tail_size++;
+        game->fruits[eaten_fruit] = game_random_free_position(game);
+    } else {
+        snake->tail[snake->tail_size] = (struct Position){0};
+    }
+
+    for (uint32_t t = 0; t < snake->tail_size; t++)
+        if (position_equals(snake->pos, snake->tail[t]))
+            return false;
+
+    return true;
+}
+
+void game_draw(struct Game *game, struct Display* display) {
+    display_fill(
+        display, 
+        D_POS(DISPLAY_PADDING_X, DISPLAY_PADDING_Y),
+        D_POS(DISPLAY_WIDTH - DISPLAY_PADDING_X, DISPLAY_HEIGHT - DISPLAY_PADDING_Y),
+        '.'
+    );
+
+    display_frame(
+        display, 
+        D_POS(0, 1),
+        D_POS(DISPLAY_WIDTH, DISPLAY_HEIGHT - 2)
+    );
+    
+    for (uint32_t f = 0; f < FRUIT_COUNT; f++) {
+        struct Display_Position fruit_pos = game_position_to_display_position(game->fruits[f]);
+        *display_at(display, fruit_pos) = 'F';
+    }
+    
+    for (uint32_t t = 0; t < game->snake.tail_size; t++) {
+        struct Display_Position tail_pos = game_position_to_display_position(game->snake.tail[t]);
+        *display_at(display, tail_pos) = 'T';
+    }
+   
+    struct Display_Position snake_pos = game_position_to_display_position(game->snake.pos);
+    *display_at(display, snake_pos) = 'S';
+
+    display_write(display, D_POS_ZERO, "snake!");
+    
+    char score_message[] = "score: 000";
+    uint32_t score_message_size = sizeof(score_message) / sizeof(char) - 1;
+    score_message[score_message_size - 1] = (game->score % 10) + '0';
+    score_message[score_message_size - 2] = (game->score / 10) + '0';
+    score_message[score_message_size - 3] = (game->score / 100) + '0';
+    display_write(display, D_POS(DISPLAY_WIDTH - score_message_size, 0), score_message);
+
+    display_write(display, D_POS(0, DISPLAY_HEIGHT - 1), "controls: (w a s d) q");
+}
+
+uint32_t game_current_sleep_duration_ms(struct Game *game) {
+    return max(SPEED_INIT_MS - (SPEED_DECREASE_WITH_SCORE_MS * (int64_t)game->score), SPEED_MIN_MS);
+}
+
+int main() {
+    srand(333);
+    configure_terminal(true);
+    
+    struct Game game = game_new();
+    struct Display display = display_new();
+
+    while (true) {
+        struct Input player_input = input();
+        if (player_input.quit) break;
+
+        if(!game_step(&game, player_input.direction)) {
+            printf("game over! score: %d\n", game.score);
+            break;
+        }
+
+        game_draw(&game, &display);
+        display_display(&display);
+
+        usleep(game_current_sleep_duration_ms(&game) * 1000);
+    }
+
+    configure_terminal(false);
+    return 0;
+}
+