From 5e6757190895a930403b81f5b14ee51045d138ce Mon Sep 17 00:00:00 2001 From: jfreegman Date: Tue, 1 Jun 2021 23:00:00 -0400 Subject: [PATCH] Implement Conway's Game of Life --- cfg/checks/games.mk | 2 +- src/game_base.c | 12 + src/game_base.h | 8 +- src/game_centipede.c | 1 + src/game_chess.c | 9 +- src/game_life.c | 602 +++++++++++++++++++++++++++++++++++++++++++ src/game_life.h | 31 +++ src/game_snake.c | 1 + 8 files changed, 659 insertions(+), 7 deletions(-) create mode 100644 src/game_life.c create mode 100644 src/game_life.h diff --git a/cfg/checks/games.mk b/cfg/checks/games.mk index 77f39bd..2a7fd8d 100644 --- a/cfg/checks/games.mk +++ b/cfg/checks/games.mk @@ -1,5 +1,5 @@ # Variables for game support GAMES_CFLAGS = -DGAMES -GAMES_OBJ = game_base.o game_centipede.o game_chess.o game_util.o game_snake.o +GAMES_OBJ = game_base.o game_centipede.o game_chess.o game_life.o game_util.o game_snake.o CFLAGS += $(GAMES_CFLAGS) OBJ += $(GAMES_OBJ) diff --git a/src/game_base.c b/src/game_base.c index 9e74ef7..90cd026 100644 --- a/src/game_base.c +++ b/src/game_base.c @@ -29,6 +29,7 @@ #include "game_centipede.h" #include "game_base.h" #include "game_chess.h" +#include "game_life.h" #include "game_snake.h" #include "line_info.h" #include "misc_tools.h" @@ -77,6 +78,7 @@ struct GameList { static struct GameList game_list[] = { { "centipede", GT_Centipede }, { "chess", GT_Chess }, + { "life", GT_Life }, { "snake", GT_Snake }, { NULL, GT_Invalid }, }; @@ -213,6 +215,11 @@ static int game_initialize_type(GameData *game, const uint8_t *data, size_t leng break; } + case GT_Life: { + ret = life_initialize(game); + break; + } + default: { break; } @@ -980,6 +987,11 @@ void game_update_score(GameData *game, long int points) } } +void game_set_score(GameData *game, long int val) +{ + game->score = val; +} + long int game_get_score(const GameData *game) { return game->score; diff --git a/src/game_base.h b/src/game_base.h index 7a7d30d..c6b49ce 100644 --- a/src/game_base.h +++ b/src/game_base.h @@ -56,7 +56,7 @@ #define GAME_MESSAGE_DEFAULT_TIMEOUT 3 -/***** NETWORKING DEFINES *****/ +/***** NETWORKING CONSTANTS *****/ /* Header starts after custom packet type byte. Comprised of: NetworkVersion (1b) + GameType (1b) + id (4b) */ #define GAME_PACKET_HEADER_SIZE (1 + 1 + sizeof(uint32_t)) @@ -99,6 +99,7 @@ typedef enum GameStatus { typedef enum GameType { GT_Centipede = 0u, GT_Chess, + GT_Life, GT_Snake, GT_Invalid, } GameType; @@ -297,6 +298,11 @@ void game_window_notify(const GameData *game, const char *message); */ void game_update_score(GameData *game, long int points); +/* + * Sets game score to `val`. + */ +void game_set_score(GameData *game, long int score); + /* * Returns the game's current score. */ diff --git a/src/game_centipede.c b/src/game_centipede.c index 1ea4161..098df4b 100644 --- a/src/game_centipede.c +++ b/src/game_centipede.c @@ -1593,6 +1593,7 @@ void cent_cb_kill(GameData *game, void *cb_data) game_set_cb_update_state(game, NULL, NULL); game_set_cb_render_window(game, NULL, NULL); game_set_cb_kill(game, NULL, NULL); + game_set_cb_on_keypress(game, NULL, NULL); game_set_cb_on_pause(game, NULL, NULL); } diff --git a/src/game_chess.c b/src/game_chess.c index e2beb9c..23ef4d8 100644 --- a/src/game_chess.c +++ b/src/game_chess.c @@ -1411,7 +1411,7 @@ static void chess_move_curs_left(ChessState *state) { Board *board = &state->board; - size_t new_x = state->curs_x - CHESS_TILE_SIZE_X; + int new_x = state->curs_x - CHESS_TILE_SIZE_X; if (new_x < board->x_left_bound) { return; @@ -1424,7 +1424,7 @@ static void chess_move_curs_right(ChessState *state) { Board *board = &state->board; - size_t new_x = state->curs_x + CHESS_TILE_SIZE_X; + int new_x = state->curs_x + CHESS_TILE_SIZE_X; if (new_x > board->x_right_bound) { return; @@ -1437,7 +1437,7 @@ static void chess_move_curs_up(ChessState *state) { Board *board = &state->board; - size_t new_y = state->curs_y - CHESS_TILE_SIZE_Y; + int new_y = state->curs_y - CHESS_TILE_SIZE_Y; if (new_y < board->y_top_bound) { return; @@ -1450,7 +1450,7 @@ static void chess_move_curs_down(ChessState *state) { Board *board = &state->board; - size_t new_y = state->curs_y + CHESS_TILE_SIZE_Y; + int new_y = state->curs_y + CHESS_TILE_SIZE_Y; if (new_y >= board->y_bottom_bound) { return; @@ -1716,7 +1716,6 @@ void chess_cb_render_window(GameData *game, WINDOW *win, void *cb_data) move(state->curs_y, state->curs_x); - curs_set(1); chess_draw_board(win, state); diff --git a/src/game_life.c b/src/game_life.c new file mode 100644 index 0000000..b4705de --- /dev/null +++ b/src/game_life.c @@ -0,0 +1,602 @@ +/* game_life.c + * + * + * Copyright (C) 2021 Toxic All Rights Reserved. + * + * This file is part of Toxic. + * + * Toxic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Toxic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Toxic. If not, see . + * + */ + +#include +#include +#include + +#include "game_life.h" + +#define LIFE_DEFAULT_CELL_CHAR 'o' +#define LIFE_CELL_DEFAULT_COLOUR CYAN +#define LIFE_DEFAULT_SPEED 25 +#define LIFE_MAX_SPEED 40 + +/* Determines the additional size of the grid beyond the visible boundaries. + * + * This buffer allows cells to continue growing off-screen giving the illusion of an + * infinite grid to a certain point. + */ +#define LIFE_BOUNDARY_BUFFER 50 + + +typedef struct Cell { + Coords coords; + bool alive; + bool marked; // true if cell should invert alive status at end of current cycle + int display_char; + size_t age; +} Cell; + +typedef struct LifeState { + TIME_MS time_last_cycle; + size_t speed; + size_t generation; + + Cell **cells; + int num_columns; + int num_rows; + + int curs_x; + int curs_y; + + int x_left_bound; + int x_right_bound; + int y_top_bound; + int y_bottom_bound; + + short display_candy; +} LifeState; + + +static void life_increase_speed(LifeState *state) +{ + if (state->speed < LIFE_MAX_SPEED) { + ++state->speed; + } +} + +static void life_decrease_speed(LifeState *state) +{ + if (state->speed > 1) { + --state->speed; + } +} + +static int life_get_display_char(const LifeState *state, const Cell *cell) +{ + if (state->display_candy == 1) { + if (cell->age == 1) { + return '.'; + } + + return '+'; + } + + if (state->display_candy == 2) { + if (cell->age == 1) { + return '.'; + } + + if (cell->age == 2) { + return '-'; + } + + if (cell->age == 3) { + return 'o'; + } + + return 'O'; + } + + return 'o'; +} + +static void life_toggle_display_candy(LifeState *state) +{ + state->display_candy = (state->display_candy + 1) % 3; // magic number depends on life_get_display_char() +} + +static Cell *life_get_cell_at_coords(const LifeState *state, const int x, const int y) +{ + const int i = y - (state->y_top_bound - (LIFE_BOUNDARY_BUFFER / 2)); + const int j = x - (state->x_left_bound - (LIFE_BOUNDARY_BUFFER / 2)); + + if (i >= 0 && j >= 0) { + return &state->cells[i][j]; + } + + return NULL; +} + +static void life_draw_cells(const GameData *game, WINDOW *win, LifeState *state) +{ + for (int i = LIFE_BOUNDARY_BUFFER / 2; i < state->num_rows - (LIFE_BOUNDARY_BUFFER / 2); ++i) { + for (int j = LIFE_BOUNDARY_BUFFER / 2; j < state->num_columns + 1 - (LIFE_BOUNDARY_BUFFER / 2); ++j) { + Cell *cell = &state->cells[i][j]; + + if (cell->alive) { + Coords coords = cell->coords; + wattron(win, A_BOLD | COLOR_PAIR(LIFE_CELL_DEFAULT_COLOUR)); + mvwaddch(win, coords.y, coords.x, cell->display_char); + wattroff(win, A_BOLD | COLOR_PAIR(LIFE_CELL_DEFAULT_COLOUR)); + } + } + } +} + +static void life_toggle_cell(LifeState *state) +{ + Cell *cell = life_get_cell_at_coords(state, state->curs_x, state->curs_y); + + if (cell == NULL) { + return; + } + + cell->alive ^= 1; +} + +/* + * Returns the number of live neighbours of `idx` cell. + */ +static int life_get_live_neighbours(const LifeState *state, const int i, const int j) +{ + Cell *n[8] = {0}; + + if (i > 0 && j > 0) { + n[0] = &state->cells[i - 1][j - 1]; + } + + if (i > 0) { + n[1] = &state->cells[i - 1][j]; + } + + if (i > 0 && j < state->num_columns - 1) { + n[2] = &state->cells[i - 1][j + 1]; + } + + if (j > 0) { + n[3] = &state->cells[i][j - 1]; + } + + if (j < state->num_columns - 1) { + n[4] = &state->cells[i][j + 1]; + } + + if (i < state->num_rows - 1 && j > 0) { + n[5] = &state->cells[i + 1][j - 1]; + } + + if (i < state->num_rows - 1) { + n[6] = &state->cells[i + 1][j]; + } + + if (i < state->num_rows - 1 && j < state->num_columns - 1) { + n[7] = &state->cells[i + 1][j + 1]; + } + + int count = 0; + + for (size_t i = 0; i < 8; ++i) { + if (n[i] == NULL) { + return 0; // If we're at a boundary kill cell + } + + if (n[i]->alive) { + ++count; + } + } + + return count; +} + +static void life_restart(GameData *game, LifeState *state) +{ + for (int i = 0; i < state->num_rows; ++i) { + for (int j = 0; j < state->num_columns; ++j) { + Cell *cell = &state->cells[i][j]; + cell->alive = false; + cell->marked = false; + cell->display_char = LIFE_DEFAULT_CELL_CHAR; + cell->age = 0; + } + } + + game_set_score(game, 0); + + state->generation = 0; +} + +static void life_do_cells(LifeState *state) +{ + + for (int i = 0; i < state->num_rows; ++i) { + for (int j = 0; j < state->num_columns; ++j) { + Cell *cell = &state->cells[i][j]; + + if (cell->marked) { + cell->marked = false; + cell->alive ^= 1; + cell->age = cell->alive; + cell->display_char = life_get_display_char(state, cell); + } else if (cell->alive) { + ++cell->age; + cell->display_char = life_get_display_char(state, cell); + } + } + } +} + +static void life_cycle(GameData *game, LifeState *state) +{ + if (state->generation == 0) { + return; + } + + TIME_MS cur_time = get_time_millis(); + + if (!game_do_object_state_update(game, cur_time, state->time_last_cycle, state->speed)) { + return; + } + + state->time_last_cycle = get_time_millis(); + + ++state->generation; + + size_t live_cells = 0; + + for (int i = 0; i < state->num_rows; ++i) { + for (int j = 0; j < state->num_columns; ++j) { + Cell *cell = &state->cells[i][j]; + + int live_neighbours = life_get_live_neighbours(state, i, j); + + if (cell->alive) { + if (!(live_neighbours == 2 || live_neighbours == 3)) { + cell->marked = true; + } else { + ++live_cells; + } + } else { + if (live_neighbours == 3) { + cell->marked = true; + ++live_cells; + } + } + } + } + + if (live_cells == 0) { + life_restart(game, state); + return; + } + + life_do_cells(state); + + game_update_score(game, 1); +} + +static void life_start(GameData *game, LifeState *state) +{ + state->generation = 1; +} + +void life_cb_update_game_state(GameData *game, void *cb_data) +{ + if (!cb_data) { + return; + } + + LifeState *state = (LifeState *)cb_data; + + life_cycle(game, state); +} + +void life_cb_render_window(GameData *game, WINDOW *win, void *cb_data) +{ + if (!cb_data) { + return; + } + + LifeState *state = (LifeState *)cb_data; + + move(state->curs_y, state->curs_x); + + curs_set(1); + + life_draw_cells(game, win, state); +} + +static void life_move_curs_left(LifeState *state) +{ + int new_x = state->curs_x - 1; + + if (new_x < state->x_left_bound) { + return; + } + + state->curs_x = new_x; +} + +static void life_move_curs_right(LifeState *state) +{ + int new_x = state->curs_x + 1; + + if (new_x > state->x_right_bound) { + return; + } + + state->curs_x = new_x; +} + +static void life_move_curs_up(LifeState *state) +{ + int new_y = state->curs_y - 1; + + if (new_y < state->y_top_bound) { + return; + } + + state->curs_y = new_y; +} + +static void life_move_curs_down(LifeState *state) +{ + int new_y = state->curs_y + 1; + + if (new_y >= state->y_bottom_bound) { + return; + } + + state->curs_y = new_y; +} + +static void life_move_curs_up_left(LifeState *state) +{ + life_move_curs_up(state); + life_move_curs_left(state); +} + +static void life_move_curs_up_right(LifeState *state) +{ + life_move_curs_up(state); + life_move_curs_right(state); +} + +static void life_move_curs_down_right(LifeState *state) +{ + life_move_curs_down(state); + life_move_curs_right(state); +} + +static void life_move_curs_down_left(LifeState *state) +{ + life_move_curs_down(state); + life_move_curs_left(state); +} + +void life_cb_on_keypress(GameData *game, int key, void *cb_data) +{ + if (!cb_data) { + return; + } + + LifeState *state = (LifeState *)cb_data; + + switch (key) { + case KEY_LEFT: { + life_move_curs_left(state); + break; + } + + case KEY_RIGHT: { + life_move_curs_right(state); + break; + } + + case KEY_DOWN: { + life_move_curs_down(state); + break; + } + + case KEY_UP: { + life_move_curs_up(state); + break; + } + + case KEY_HOME: { + life_move_curs_up_left(state); + break; + } + + case KEY_END: { + life_move_curs_down_left(state); + break; + } + + case KEY_PPAGE: { + life_move_curs_up_right(state); + break; + } + + case KEY_NPAGE: { + life_move_curs_down_right(state); + break; + } + + case '\r': { + if (state->generation > 0) { + life_restart(game, state); + } else { + life_start(game, state); + } + + break; + } + + case ' ': { + life_toggle_cell(state); + break; + } + + case '=': + + /* intentional fallthrough */ + + case '+': { + life_increase_speed(state); + break; + } + + case '-': + + /* intentional fallthrough */ + + case '_': { + life_decrease_speed(state); + break; + } + + case '\t': { + life_toggle_display_candy(state); + } + + default: { + return; + } + } +} + +static void life_free_cells(LifeState *state) +{ + if (state->cells == NULL) { + return; + } + + for (int i = 0; i < state->num_rows; ++i) { + if (state->cells[i]) { + free(state->cells[i]); + } + } + + free(state->cells); +} + +void life_cb_kill(GameData *game, void *cb_data) +{ + if (!cb_data) { + return; + } + + LifeState *state = (LifeState *)cb_data; + + life_free_cells(state); + free(state); + + game_set_cb_update_state(game, NULL, NULL); + game_set_cb_render_window(game, NULL, NULL); + game_set_cb_kill(game, NULL, NULL); + game_set_cb_on_keypress(game, NULL, NULL); +} + +static int life_init_state(GameData *game, LifeState *state) +{ + const int x_left = game_x_left_bound(game) ; + const int x_right = game_x_right_bound(game); + const int y_top = game_y_top_bound(game); + const int y_bottom = game_y_bottom_bound(game) + 1; + + state->x_left_bound = x_left; + state->x_right_bound = x_right; + state->y_top_bound = y_top; + state->y_bottom_bound = y_bottom; + + const int x_mid = x_left + ((x_right - x_left) / 2); + const int y_mid = y_top + ((y_bottom - y_top) / 2); + + state->curs_x = x_mid; + state->curs_y = y_mid; + + const int num_rows = (y_bottom - y_top) + LIFE_BOUNDARY_BUFFER; + const int num_columns = (x_right - x_left) + LIFE_BOUNDARY_BUFFER; + + if (num_rows <= 0 || num_columns <= 0) { + return -1; + } + + state->num_columns = num_columns; + state->num_rows = num_rows; + + state->cells = calloc(1, num_rows * sizeof(Cell *)); + + if (state->cells == NULL) { + return -1; + } + + for (int i = 0; i < num_rows; ++i) { + state->cells[i] = calloc(1, num_columns * sizeof(Cell)); + + if (state->cells[i] == NULL) { + return -1; + } + + for (int j = 0; j < num_columns; ++j) { + state->cells[i][j].coords.y = i + (state->y_top_bound - (LIFE_BOUNDARY_BUFFER / 2)); + state->cells[i][j].coords.x = j + (state->x_left_bound - (LIFE_BOUNDARY_BUFFER / 2)); + } + } + + state->speed = LIFE_DEFAULT_SPEED; + + life_restart(game, state); + + return 0; +} + +int life_initialize(GameData *game) +{ + if (game_set_window_shape(game, GW_ShapeRectangle) == -1) { + return -1; + } + + LifeState *state = calloc(1, sizeof(LifeState)); + + if (state == NULL) { + return -1; + } + + if (life_init_state(game, state) == -1) { + life_free_cells(state); + free(state); + return -1; + } + + game_set_update_interval(game, 40); + game_show_score(game, true); + + game_set_cb_update_state(game, life_cb_update_game_state, state); + game_set_cb_render_window(game, life_cb_render_window, state); + game_set_cb_on_keypress(game, life_cb_on_keypress, state); + game_set_cb_kill(game, life_cb_kill, state); + + return 0; +} diff --git a/src/game_life.h b/src/game_life.h new file mode 100644 index 0000000..cdf92b4 --- /dev/null +++ b/src/game_life.h @@ -0,0 +1,31 @@ +/* game_life.h + * + * + * Copyright (C) 2021 Toxic All Rights Reserved. + * + * This file is part of Toxic. + * + * Toxic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Toxic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Toxic. If not, see . + * + */ + +#ifndef GAME_LIFE +#define GAME_LIFE + +#include "game_base.h" + +int life_initialize(GameData *game); + +#endif // GAME_LIFE + diff --git a/src/game_snake.c b/src/game_snake.c index b3a547a..929a8cf 100644 --- a/src/game_snake.c +++ b/src/game_snake.c @@ -801,6 +801,7 @@ void snake_cb_kill(GameData *game, void *cb_data) game_set_cb_update_state(game, NULL, NULL); game_set_cb_render_window(game, NULL, NULL); game_set_cb_kill(game, NULL, NULL); + game_set_cb_on_keypress(game, NULL, NULL); game_set_cb_on_pause(game, NULL, NULL); }