aboutsummaryrefslogtreecommitdiff
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
parent29ac9d8cf84f4412e164c51c139175df6016d319 (diff)
downloadonesies-3da3b71fb1305b09fd7ba81781f1b9cce8476aa0.tar.zst
onesies-3da3b71fb1305b09fd7ba81781f1b9cce8476aa0.zip
Snake!main
-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;
+}
+