diff options
Diffstat (limited to 'snake.c')
| -rwxr-xr-x | snake.c | 380 |
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; +} + |
