From b9ea73347055b0e0aeb7123f4187b0e2931a2ad7 Mon Sep 17 00:00:00 2001 From: jfreegman Date: Fri, 11 Dec 2020 01:46:01 -0500 Subject: [PATCH] Add game module (WIP) --- Makefile | 6 +- src/chat.c | 1 + src/conference.c | 1 + src/execute.c | 1 + src/game_base.c | 914 ++++++++++++++++++++++ src/game_base.h | 295 +++++++ src/game_centipede.c | 1738 +++++++++++++++++++++++++++++++++++++++++ src/game_centipede.h | 30 + src/game_snake.c | 901 +++++++++++++++++++++ src/game_snake.h | 30 + src/game_util.c | 158 ++++ src/game_util.h | 92 +++ src/global_commands.c | 53 ++ src/global_commands.h | 1 + src/prompt.c | 1 + src/toxic.c | 6 +- src/windows.c | 50 +- src/windows.h | 6 + 18 files changed, 4273 insertions(+), 11 deletions(-) create mode 100644 src/game_base.c create mode 100644 src/game_base.h create mode 100644 src/game_centipede.c create mode 100644 src/game_centipede.h create mode 100644 src/game_snake.c create mode 100644 src/game_snake.h create mode 100644 src/game_util.c create mode 100644 src/game_util.h diff --git a/Makefile b/Makefile index c956edc..b01f21a 100644 --- a/Makefile +++ b/Makefile @@ -13,9 +13,9 @@ LDFLAGS ?= LDFLAGS += ${USER_LDFLAGS} OBJ = autocomplete.o avatars.o bootstrap.o chat.o chat_commands.o configdir.o curl_util.o execute.o -OBJ += file_transfers.o friendlist.o global_commands.o conference_commands.o conference.o help.o input.o -OBJ += line_info.o log.o message_queue.o misc_tools.o name_lookup.o notify.o prompt.o qr_code.o settings.o -OBJ += term_mplex.o toxic.o toxic_strings.o windows.o +OBJ += file_transfers.o friendlist.o game_base.o game_centipede.o game_util.o game_snake.o global_commands.o conference_commands.o +OBJ += conference.o help.o input.o line_info.o log.o message_queue.o misc_tools.o name_lookup.o notify.o +OBJ += prompt.o qr_code.o settings.o term_mplex.o toxic.o toxic_strings.o windows.o # Check if debug build is enabled RELEASE := $(shell if [ -z "$(ENABLE_RELEASE)" ] || [ "$(ENABLE_RELEASE)" = "0" ] ; then echo disabled ; else echo enabled ; fi) diff --git a/src/chat.c b/src/chat.c index 3853594..d2d2be1 100644 --- a/src/chat.c +++ b/src/chat.c @@ -76,6 +76,7 @@ static const char *chat_cmd_list[] = { "/connect", "/exit", "/conference", + "/game", "/help", "/invite", "/join", diff --git a/src/conference.c b/src/conference.c index fb92907..02d8ddf 100644 --- a/src/conference.c +++ b/src/conference.c @@ -88,6 +88,7 @@ static const char *conference_cmd_list[] = { "/decline", "/exit", "/conference", + "/game", "/help", "/log", #ifdef AUDIO diff --git a/src/execute.c b/src/execute.c index 884b40f..5636415 100644 --- a/src/execute.c +++ b/src/execute.c @@ -49,6 +49,7 @@ static struct cmd_func global_commands[] = { { "/decline", cmd_decline }, { "/exit", cmd_quit }, { "/conference", cmd_conference }, + { "/game", cmd_game }, { "/help", cmd_prompt_help }, { "/log", cmd_log }, { "/myid", cmd_myid }, diff --git a/src/game_base.c b/src/game_base.c new file mode 100644 index 0000000..3bc3df9 --- /dev/null +++ b/src/game_base.c @@ -0,0 +1,914 @@ +/* game_base.c + * + * + * Copyright (C) 2020 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 + +#include "game_centipede.h" +#include "game_base.h" +#include "game_snake.h" +#include "line_info.h" +#include "misc_tools.h" + +/* + * Determines the base rate at which game objects should update their state. + * Inversely correlated with frame rate. + */ +#define GAME_OBJECT_UPDATE_INTERVAL_MULTIPLIER 25 + +/* Determines overall game speed; lower makes it faster and vice versa. + * Inversely correlated with frame rate. + */ +#define GAME_DEFAULT_UPDATE_INTERVAL 10 +#define GAME_MAX_UPDATE_INTERVAL 50 + + +#define GAME_BORDER_COLOUR BAR_TEXT + +/* Determines if window is large enough for a respective window type */ +#define WINDOW_SIZE_LARGE_SQUARE_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_SQUARE_Y))\ + && ((max_x) >= (GAME_MAX_SQUARE_X))) + +#define WINDOW_SIZE_SMALL_SQUARE_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_SQUARE_Y_SMALL))\ + && ((max_x) >= (GAME_MAX_SQUARE_X_SMALL))) + +#define WINDOW_SIZE_LARGE_RECT_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_RECT_Y))\ + && ((max_x) >= (GAME_MAX_RECT_X))) + +#define WINDOW_SIZE_SMALL_RECT_VALID(max_x, max_y)((((max_y) - 4) >= (GAME_MAX_RECT_Y_SMALL))\ + && ((max_x) >= (GAME_MAX_RECT_X_SMALL))) + + +static ToxWindow *game_new_window(GameType type); + +struct GameList { + const char *name; + GameType type; +}; + +static struct GameList game_list[] = { + { "centipede", GT_Centipede }, + { "snake", GT_Snake }, + { NULL, GT_Invalid }, +}; + + +static void game_get_parent_max_x_y(const ToxWindow *parent, int *max_x, int *max_y) +{ + getmaxyx(parent->window, *max_y, *max_x); + *max_y -= (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT); +} + +/* + * Returns the GameType associated with `game_string`. + */ +GameType game_get_type(const char *game_string) +{ + const char *match_string = NULL; + + for (size_t i = 0; (match_string = game_list[i].name); ++i) { + if (strcasecmp(game_string, match_string) == 0) { + return game_list[i].type; + } + } + + return GT_Invalid; +} + +/* Returns the string name associated with `type`. */ +static const char *game_get_name_string(GameType type) +{ + GameType match_type; + + for (size_t i = 0; (match_type = game_list[i].type) < GT_Invalid; ++i) { + if (match_type == type) { + return game_list[i].name; + } + } + + return NULL; +} + +/* + * Prints all available games to window associated with `self`. + */ +void game_list_print(ToxWindow *self) +{ + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Available games:"); + + const char *name = NULL; + + for (size_t i = 0; (name = game_list[i].name); ++i) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%d: %s", i + 1, name); + } +} + +/* Returns the current wall time in milliseconds */ +TIME_MS get_time_millis(void) +{ + struct timespec t; + timespec_get(&t, TIME_UTC); + return ((TIME_MS) t.tv_sec) * 1000 + ((TIME_MS) t.tv_nsec) / 1000000; +} + +void game_kill(ToxWindow *self) +{ + GameData *game = self->game; + + if (game->cb_game_kill) { + game->cb_game_kill(game, game->cb_game_kill_data); + } + + delwin(game->window); + free(game->messages); + free(game); + del_window(self); +} + +static void game_toggle_pause(GameData *game) +{ + GameStatus status = game->status; + + if (status == GS_Running) { + game->status = GS_Paused; + } else if (status == GS_Paused) { + game->status = GS_Running; + } else { + return; + } + + if (game->cb_game_pause) { + game->cb_game_pause(game, game->status == GS_Paused, game->cb_game_pause_data); + } +} + +static int game_initialize_type(GameData *game) +{ + int ret = -1; + + switch (game->type) { + case GT_Snake: { + ret = snake_initialize(game); + break; + } + + case GT_Centipede: { + ret = centipede_initialize(game); + break; + } + + default: { + break; + } + } + + return ret; +} + +/* + * Initializes game instance. + * + * Return 0 on success. + * Return -1 if screen is too small. + * Return -2 on other failure. + */ +int game_initialize(const ToxWindow *parent, Tox *m, GameType type, bool force_small_window) +{ + int max_x; + int max_y; + game_get_parent_max_x_y(parent, &max_x, &max_y); + + int max_game_window_x = GAME_MAX_SQUARE_X; + int max_game_window_y = GAME_MAX_SQUARE_Y; + + if (!force_small_window && !WINDOW_SIZE_LARGE_SQUARE_VALID(max_x, max_y)) { + return -1; + } + + if (force_small_window) { + max_game_window_x = GAME_MAX_SQUARE_X_SMALL; + max_game_window_y = GAME_MAX_SQUARE_Y_SMALL; + + if (!WINDOW_SIZE_SMALL_SQUARE_VALID(max_x, max_y)) { + return -1; + } + } + + ToxWindow *self = game_new_window(type); + + if (self == NULL) { + return -2; + } + + GameData *game = self->game; + + int window_id = add_window(m, self); + + if (window_id == -1) { + free(game); + free(self); + return -2; + } + + game->parent = parent; + game->window_shape = GW_ShapeSquare; + game->game_max_x = max_game_window_x; + game->game_max_y = max_game_window_y; + game->update_interval = GAME_DEFAULT_UPDATE_INTERVAL; + game->type = type; + game->window_id = window_id; + game->level = 0; + game->window = subwin(self->window, max_y, max_x, 0, 0); + + if (game->window == NULL) { + game_kill(self); + return -2; + } + + if (game_initialize_type(game) == -1) { + game_kill(self); + return -1; + } + + game->status = GS_Running; + + set_active_window_index(window_id); + + return 0; +} + +int game_set_window_shape(GameData *game, GameWindowShape shape) +{ + if (shape >= GW_ShapeInvalid) { + return -1; + } + + if (game->status != GS_None) { + return -2; + } + + if (shape == game->window_shape) { + return 0; + } + + if (shape == GW_ShapeSquare) { + game->game_max_x = GAME_MAX_SQUARE_X; + return 0; + } + + const ToxWindow *parent = game->parent; + + int max_x; + int max_y; + game_get_parent_max_x_y(parent, &max_x, &max_y); + + if (WINDOW_SIZE_LARGE_RECT_VALID(max_x, max_y)) { + game->game_max_x = GAME_MAX_RECT_X; + game->game_max_y = GAME_MAX_RECT_Y; + return 0; + } + + if (WINDOW_SIZE_SMALL_RECT_VALID(max_x, max_y)) { + game->game_max_x = GAME_MAX_RECT_X_SMALL; + game->game_max_y = GAME_MAX_RECT_Y_SMALL; + return 0; + } + + return -1; +} + +static void game_fix_message_coords(const GameData *game, Direction direction, Coords *coords, size_t length) +{ + if (direction >= INVALID_DIRECTION) { + return; + } + + if (direction == EAST || direction == WEST) { + coords->y = game_coordinates_in_bounds(game, coords->x, coords->y + 2) ? coords->y + 2 : coords->y - 2; + } else { + coords->x = game_coordinates_in_bounds(game, coords->x + 2, coords->y) + ? coords->x + 2 : coords->x - (length + 2); + } + + if (!game_coordinates_in_bounds(game, coords->x + length, coords->y) + || !game_coordinates_in_bounds(game, coords->x, coords->y)) { + int max_x; + int max_y; + getmaxyx(game->window, max_y, max_x); + + const int x_left_bound = (max_x - game->game_max_x) / 2; + const int x_right_bound = (max_x + game->game_max_x) / 2; + const int y_top_bound = (max_y - game->game_max_y) / 2; + const int y_bottom_bound = (max_y + game->game_max_y) / 2; + + if (coords->x + length >= x_right_bound) { + coords->x -= (length + 2); + } else if (coords->x <= x_left_bound) { + coords->x = x_left_bound + 2; + } + + if (coords->y >= y_bottom_bound) { + coords->y -= 2; + } else if (coords->y <= y_top_bound) { + coords->y += 2; + } + } +} + +static void game_clear_message(GameData *game, size_t index) +{ + memset(&game->messages[index], 0, sizeof(GameMessage)); +} + +static void game_clear_all_messages(GameData *game) +{ + for (size_t i = 0; i < game->messages_size; ++i) { + game_clear_message(game, i); + } +} + +static GameMessage *game_get_new_message_holder(GameData *game) +{ + size_t i; + + for (i = 0; i < game->messages_size; ++i) { + GameMessage *m = &game->messages[i]; + + if (m->length == 0) { + break; + } + } + + if (i == game->messages_size) { + GameMessage *tmp = realloc(game->messages, sizeof(GameMessage) * (i + 1)); + + if (tmp == NULL) { + return NULL; + } + + memset(&tmp[i], 0, sizeof(GameMessage)); + + game->messages = tmp; + game->messages_size = i + 1; + } + + return &game->messages[i]; + +} + +int game_set_message(GameData *game, const char *message, size_t length, Direction dir, int attributes, int colour, + TIME_S timeout, const Coords *coords, bool sticky, bool priority) +{ + if (length == 0 || length > GAME_MAX_MESSAGE_SIZE) { + return -1; + } + + if (coords == NULL) { + return -1; + } + + GameMessage *m = game_get_new_message_holder(game); + + if (m == NULL) { + return -1; + } + + memcpy(m->message, message, length); + m->message[length] = 0; + + m->length = length; + m->timeout = timeout > 0 ? timeout : GAME_MESSAGE_DEFAULT_TIMEOUT; + m->set_time = get_unix_time(); + m->attributes = attributes; + m->colour = colour; + m->direction = dir; + m->coords = coords; + m->sticky = sticky; + m->priority = priority; + + m->original_coords = (Coords) { + coords->x, coords->y + }; + + if (GAME_UTIL_DIRECTION_VALID(dir)) { + game_fix_message_coords(game, dir, &m->original_coords, length); + } + + return 0; +} + +static int game_restart(GameData *game) +{ + if (game->cb_game_kill) { + game->cb_game_kill(game, game->cb_game_kill_data); + } + + game->update_interval = GAME_DEFAULT_UPDATE_INTERVAL; + game->status = GS_Running; + game->score = 0; + game->level = 0; + game->lives = 0; + game->last_frame_time = 0; + + game_clear_all_messages(game); + + if (game_initialize_type(game) == -1) { + return -1; + } + + return 0; +} + +static void game_draw_help_bar(WINDOW *win) +{ + int max_x; + int max_y; + UNUSED_VAR(max_x); + + getmaxyx(win, max_y, max_x); + + wmove(win, max_y - 1, 1); + + wprintw(win, "Pause: "); + wattron(win, A_BOLD); + wprintw(win, "F2 "); + wattroff(win, A_BOLD); + + wprintw(win, "Quit: "); + wattron(win, A_BOLD); + wprintw(win, "F9"); + wattroff(win, A_BOLD); +} + +static void game_draw_border(const GameData *game, const int max_x, const int max_y) +{ + WINDOW *win = game->window; + + const int game_max_x = game->game_max_x; + const int game_max_y = game->game_max_y; + + const int x = (max_x - game_max_x) / 2; + const int y = (max_y - game_max_y) / 2; + + wattron(win, A_BOLD | COLOR_PAIR(GAME_BORDER_COLOUR)); + + mvwaddch(win, y, x, ' '); + mvwhline(win, y, x + 1, ' ', game_max_x - 1); + mvwvline(win, y + 1, x, ' ', game_max_y - 1); + mvwvline(win, y, x - 1, ' ', game_max_y + 1); + mvwaddch(win, y, x + game_max_x, ' '); + mvwvline(win, y + 1, x + game_max_x, ' ', game_max_y - 1); + mvwvline(win, y, x + game_max_x + 1, ' ', game_max_y + 1); + mvwaddch(win, y + game_max_y, x, ' '); + mvwhline(win, y + game_max_y, x + 1, ' ', game_max_x - 1); + mvwaddch(win, y + game_max_y, x + game_max_x, ' '); + + wattroff(win, A_BOLD | COLOR_PAIR(GAME_BORDER_COLOUR)); +} + +static void game_draw_status(const GameData *game, const int max_x, const int max_y) +{ + WINDOW *win = game->window; + + int x = ((max_x - game->game_max_x) / 2) - 1; + int y = ((max_y - game->game_max_y) / 2) - 1; + + wattron(win, A_BOLD); + mvwprintw(win, y, x, "Score: %zu", game->score); + + mvwprintw(win, y + game->game_max_y + 2, x, "High Score: %zu", game->high_score); + + x = ((max_x / 2) + (game->game_max_x / 2)) - 7; + + if (game->level > 0) { + mvwprintw(win, y, x, "Level: %zu", game->level); + } + + if (game->lives >= 0) { + mvwprintw(win, y + game->game_max_y + 2, x, "Lives: %zu", game->lives); + } + + wattroff(win, A_BOLD); +} + +static void game_draw_game_over(const GameData *game) +{ + WINDOW *win = game->window; + + int max_x; + int max_y; + getmaxyx(win, max_y, max_x); + + const int x = max_x / 2; + const int y = max_y / 2; + + const char *message = "GAME OVER!"; + size_t length = strlen(message); + + wattron(win, A_BOLD | COLOR_PAIR(RED)); + mvwprintw(win, y - 1, x - (length / 2), "%s", message); + wattroff(win, A_BOLD | COLOR_PAIR(RED)); + + message = "Press F5 to play again"; + length = strlen(message); + + mvwprintw(win, y + 1, x - (length / 2), "%s", message); +} + +static void game_draw_pause_screen(const GameData *game) +{ + WINDOW *win = game->window; + + int max_x; + int max_y; + getmaxyx(win, max_y, max_x); + + const int x = max_x / 2; + const int y = max_y / 2; + + wattron(win, A_BOLD | COLOR_PAIR(YELLOW)); + mvwprintw(win, y, x - 3, "PAUSED"); + wattroff(win, A_BOLD | COLOR_PAIR(YELLOW)); +} + +static void game_draw_messages(GameData *game, bool priority) +{ + WINDOW *win = game->window; + + for (size_t i = 0; i < game->messages_size; ++i) { + GameMessage *m = &game->messages[i]; + + if (m->length == 0 || m->coords == NULL) { + continue; + } + + if (timed_out(m->set_time, m->timeout)) { + game_clear_message(game, i); + continue; + } + + if (m->priority != priority) { + continue; + } + + if (!m->sticky) { + wattron(win, m->attributes | COLOR_PAIR(m->colour)); + mvwprintw(win, m->original_coords.y, m->original_coords.x, "%s", m->message); + wattroff(win, m->attributes | COLOR_PAIR(m->colour)); + continue; + } + + Coords fixed_coords = { + m->coords->x, + m->coords->y + }; + + // TODO: we should only have to do this if the coordinates changed + game_fix_message_coords(game, m->direction, &fixed_coords, m->length); + + wattron(win, m->attributes | COLOR_PAIR(m->colour)); + mvwprintw(win, fixed_coords.y, fixed_coords.x, "%s", m->message); + wattroff(win, m->attributes | COLOR_PAIR(m->colour)); + } +} + +static void game_update_state(GameData *game) +{ + if (!game->cb_game_update_state) { + return; + } + + TIME_MS cur_time = get_time_millis(); + + if (cur_time - game->last_frame_time > 500) { + game->last_frame_time = cur_time; + } + + size_t iterations = (cur_time - game->last_frame_time) / game->update_interval; + + for (size_t i = 0; i < iterations; ++i) { + game->cb_game_update_state(game, game->cb_game_update_state_data); + game->last_frame_time += game->update_interval; + } +} + +void game_onDraw(ToxWindow *self, Tox *m) +{ + UNUSED_VAR(m); // Note: This function is not thread safe if we ever need to use `m` + + curs_set(0); + + game_draw_help_bar(self->window); + draw_window_bar(self); + + GameData *game = self->game; + + int max_x; + int max_y; + getmaxyx(game->window, max_y, max_x); + + wclear(game->window); + + game_draw_border(game, max_x, max_y); + + game_draw_messages(game, false); + + if (game->cb_game_render_window) { + game->cb_game_render_window(game, game->window, game->cb_game_render_window_data); + } + + game_draw_status(game, max_x, max_y); + + switch (game->status) { + case GS_Running: { + game_update_state(game); + break; + } + + case GS_Paused: { + game_draw_pause_screen(game); + break; + } + + case GS_Finished: { + game_draw_game_over(game); + break; + } + + default: { + fprintf(stderr, "Unknown game status: %d\n", game->status); + break; + } + } + + game_draw_messages(game, true); + + wnoutrefresh(self->window); +} + +bool game_onKey(ToxWindow *self, Tox *m, wint_t key, bool is_printable) +{ + UNUSED_VAR(m); // Note: this function is not thread safe if we ever need to use `m` + UNUSED_VAR(is_printable); + + GameData *game = self->game; + + if (key == KEY_F(9)) { + game_kill(self); + return true; + } + + if (key == KEY_F(2)) { + game_toggle_pause(self->game); + return true; + } + + if (game->status == GS_Finished && key == KEY_F(5)) { + if (game_restart(self->game) == -1) { + fprintf(stderr, "Warning: game_restart() failed\n"); + } + + return true; + } + + if (game->cb_game_key_press) { + game->cb_game_key_press(game, key, game->cb_game_key_press_data); + } + + return true; +} + +void game_onInit(ToxWindow *self, Tox *m) +{ + UNUSED_VAR(m); + + int max_x; + int max_y; + getmaxyx(self->window, max_y, max_x); + + if (max_y <= 0 || max_x <= 0) { + exit_toxic_err("failed in game_onInit", FATALERR_CURSES); + } + + self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, max_x, max_y - 2, 0); +} + +static ToxWindow *game_new_window(GameType type) +{ + const char *window_name = game_get_name_string(type); + + if (window_name == NULL) { + return NULL; + } + + ToxWindow *ret = calloc(1, sizeof(ToxWindow)); + + if (ret == NULL) { + return NULL; + } + + ret->type = WINDOW_TYPE_GAME; + + ret->onInit = &game_onInit; + ret->onDraw = &game_onDraw; + ret->onKey = &game_onKey; + + ret->game = calloc(1, sizeof(GameData)); + + if (ret->game == NULL) { + free(ret); + return NULL; + } + + snprintf(ret->name, sizeof(ret->name), "%s", window_name); + + return ret; +} + +bool game_coordinates_in_bounds(const GameData *game, int x, int y) +{ + const int game_max_x = game->game_max_x; + const int game_max_y = game->game_max_y; + + int max_x; + int max_y; + getmaxyx(game->window, max_y, max_x); + + const int x_left_bound = (max_x - game_max_x) / 2; + const int x_right_bound = (max_x + game_max_x) / 2; + const int y_top_bound = (max_y - game_max_y) / 2; + const int y_bottom_bound = (max_y + game_max_y) / 2; + + return x > x_left_bound && x < x_right_bound && y > y_top_bound && y < y_bottom_bound; +} + +void game_random_coords(const GameData *game, Coords *coords) +{ + const int game_max_x = game->game_max_x; + const int game_max_y = game->game_max_y; + + int max_x; + int max_y; + getmaxyx(game->window, max_y, max_x); + + const int x_left_bound = ((max_x - game_max_x) / 2) + 1; + const int x_right_bound = ((max_x + game_max_x) / 2) - 1; + const int y_top_bound = ((max_y - game_max_y) / 2) + 1; + const int y_bottom_bound = ((max_y + game_max_y) / 2) - 1; + + coords->x = (rand() % (x_right_bound - x_left_bound + 1)) + x_left_bound; + coords->y = (rand() % (y_bottom_bound - y_top_bound + 1)) + y_top_bound; +} + +void game_max_x_y(const GameData *game, int *x, int *y) +{ + getmaxyx(game->window, *y, *x); +} + +int game_y_bottom_bound(const GameData *game) +{ + int max_x; + int max_y; + UNUSED_VAR(max_x); + + getmaxyx(game->window, max_y, max_x); + + return ((max_y + game->game_max_y) / 2) - 1; +} + +int game_y_top_bound(const GameData *game) +{ + int max_x; + int max_y; + UNUSED_VAR(max_x); + + getmaxyx(game->window, max_y, max_x); + + return ((max_y - game->game_max_y) / 2) + 1; +} + +int game_x_right_bound(const GameData *game) +{ + int max_x; + int max_y; + UNUSED_VAR(max_y); + + getmaxyx(game->window, max_y, max_x); + + return ((max_x + game->game_max_x) / 2) - 1; +} + +int game_x_left_bound(const GameData *game) +{ + int max_x; + int max_y; + UNUSED_VAR(max_y); + + getmaxyx(game->window, max_y, max_x); + + return ((max_x - game->game_max_x) / 2) + 1; +} + +void game_update_score(GameData *game, long int points) +{ + game->score += points; + + if (game->score > game->high_score) { + game->high_score = game->score; + } +} + +long int game_get_score(const GameData *game) +{ + return game->score; +} + +void game_increment_level(GameData *game) +{ + ++game->level; +} + +void game_update_lives(GameData *game, int lives) +{ + fprintf(stderr, "%d\n", lives); + game->lives += lives; +} + +int game_get_lives(const GameData *game) +{ + return game->lives; +} + +size_t game_get_current_level(const GameData *game) +{ + return game->level; +} + +void game_set_status(GameData *game, GameStatus status) +{ + if (status < GS_Invalid) { + game->status = status; + } +} + +void game_set_update_interval(GameData *game, TIME_MS update_interval) +{ + game->update_interval = MIN(update_interval, GAME_MAX_UPDATE_INTERVAL); +} + +bool game_do_object_state_update(const GameData *game, TIME_MS current_time, TIME_MS last_moved_time, TIME_MS speed) +{ + TIME_MS delta = (current_time - last_moved_time) * speed; + return delta > game->update_interval * GAME_OBJECT_UPDATE_INTERVAL_MULTIPLIER; +} + +void game_set_cb_update_state(GameData *game, cb_game_update_state *func, void *cb_data) +{ + game->cb_game_update_state = func; + game->cb_game_update_state_data = cb_data; +} + +void game_set_cb_on_keypress(GameData *game, cb_game_key_press *func, void *cb_data) +{ + game->cb_game_key_press = func; + game->cb_game_key_press_data = cb_data; +} + +void game_set_cb_render_window(GameData *game, cb_game_render_window *func, void *cb_data) +{ + game->cb_game_render_window = func; + game->cb_game_render_window_data = cb_data; +} + +void game_set_cb_kill(GameData *game, cb_game_kill *func, void *cb_data) +{ + game->cb_game_kill = func; + game->cb_game_kill_data = cb_data; +} + +void game_set_cb_on_pause(GameData *game, cb_game_pause *func, void *cb_data) +{ + game->cb_game_pause = func; + game->cb_game_pause_data = cb_data; +} diff --git a/src/game_base.h b/src/game_base.h new file mode 100644 index 0000000..2aae676 --- /dev/null +++ b/src/game_base.h @@ -0,0 +1,295 @@ +/* game_base.h + * + * + * Copyright (C) 2020 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_BASE +#define GAME_BASE + +#include +#include + +#include + +#include "game_util.h" +#include "windows.h" + +/* Max size of a default size square game window */ +#define GAME_MAX_SQUARE_Y 26 +#define GAME_MAX_SQUARE_X (GAME_MAX_SQUARE_Y * 2) + +/* Max size of a small square game window */ +#define GAME_MAX_SQUARE_Y_SMALL 18 +#define GAME_MAX_SQUARE_X_SMALL (GAME_MAX_SQUARE_Y_SMALL * 2) + +/* Max size of a default size rectangle game window */ +#define GAME_MAX_RECT_Y 24 +#define GAME_MAX_RECT_X (GAME_MAX_RECT_Y * 4) + +/* Max size of a small rectangle game window */ +#define GAME_MAX_RECT_Y_SMALL 12 +#define GAME_MAX_RECT_X_SMALL (GAME_MAX_RECT_Y_SMALL * 4) + +/* Maximum length of a game message set with game_set_message() */ +#define GAME_MAX_MESSAGE_SIZE 64 + +#define GAME_MESSAGE_DEFAULT_TIMEOUT 3 + +typedef void cb_game_update_state(GameData *game, void *cb_data); +typedef void cb_game_render_window(GameData *game, WINDOW *window, void *cb_data); +typedef void cb_game_kill(GameData *game, void *cb_data); +typedef void cb_game_pause(GameData *game, bool is_paused, void *cb_data); +typedef void cb_game_key_press(GameData *game, int key, void *cb_data); + + +typedef enum GameWindowShape { + GW_ShapeSquare = 0, + GW_ShapeRectangle, + GW_ShapeInvalid, +} GameWindowShape; + +typedef enum GameStatus { + GS_None = 0, + GS_Paused, + GS_Running, + GS_Finished, + GS_Invalid, +} GameStatus; + +typedef enum GameType { + GT_Snake = 0, + GT_Centipede, + GT_Invalid, +} GameType; + +typedef struct GameMessage { + char message[GAME_MAX_MESSAGE_SIZE + 1]; + size_t length; + const Coords *coords; // pointer to coords so we can track movement + Coords original_coords; // static coords at time of being set + time_t timeout; + time_t set_time; + int attributes; + int colour; + Direction direction; + bool sticky; + bool priority; +} GameMessage; + +struct GameData { + TIME_MS last_frame_time; + TIME_MS update_interval; // determines the refresh rate (lower means faster) + long int score; + size_t high_score; + int lives; + size_t level; + GameStatus status; + GameType type; + + GameMessage *messages; + size_t messages_size; + + int game_max_x; // max dimensions of game window + int game_max_y; + int window_id; + WINDOW *window; + const ToxWindow *parent; + GameWindowShape window_shape; + + cb_game_update_state *cb_game_update_state; + void *cb_game_update_state_data; + + cb_game_render_window *cb_game_render_window; + void *cb_game_render_window_data; + + cb_game_kill *cb_game_kill; + void *cb_game_kill_data; + + cb_game_pause *cb_game_pause; + void *cb_game_pause_data; + + cb_game_key_press *cb_game_key_press; + void *cb_game_key_press_data; +}; + + +/* + * Sets the callback for game state updates. + */ +void game_set_cb_update_state(GameData *game, cb_game_update_state *func, void *cb_data); + +/* + * Sets the callback for frame rendering. + */ +void game_set_cb_render_window(GameData *game, cb_game_render_window *func, void *cb_data); + +/* + * Sets the callback for game termination. + */ +void game_set_cb_kill(GameData *game, cb_game_kill *func, void *cb_data); + +/* + * Sets the callback for the game pause event. + */ +void game_set_cb_on_pause(GameData *game, cb_game_pause *func, void *cb_data); + +/* + * Sets the callback for the key press event. + */ +void game_set_cb_on_keypress(GameData *game, cb_game_key_press *func, void *cb_data); + +/* + * Initializes game instance. + * + * Return 0 on success. + * Return -1 if screen is too small. + * Return -2 on other failure. + */ +int game_initialize(const ToxWindow *self, Tox *m, GameType type, bool force_small_window); + +/* + * Sets game window to `shape` and attempts to adjust size for best fit. + * + * This should be called in the game's initialize function. + * + * Return 0 on success. + * Return -1 if window is too small or shape is invalid. + * Return -2 if function is called while the game state is valid. + */ +int game_set_window_shape(GameData *game, GameWindowShape shape); + +/* + * Returns the GameType associated with `game_string`. + */ +GameType game_get_type(const char *game_string); + +/* + * Prints all available games to window associated with `self`. + */ +void game_list_print(ToxWindow *self); + +/* + * Returns true if coordinates designated by `x` and `y` are within the game window boundaries. + */ +bool game_coordinates_in_bounds(const GameData *game, int x, int y); + +/* + * Put random coordinates that fit within the game window in `coords`. + */ +void game_random_coords(const GameData *game, Coords *coords); + +/* + *Gets the current max dimensions of the game window. + */ +void game_max_x_y(const GameData *game, int *x, int *y); + +/* + * Returns the respective coordinate boundary of the game window. + */ +int game_y_bottom_bound(const GameData *game); +int game_y_top_bound(const GameData *game); +int game_x_right_bound(const GameData *game); +int game_x_left_bound(const GameData *game); + +/* + * Updates game score. + */ +void game_update_score(GameData *game, long int points); + +/* + * Returns the game's current score. + */ +long int game_get_score(const GameData *game); + +/* + * Increments level. + * + * This function should be called on initialization if game wishes to display level. + */ +void game_increment_level(GameData *game); + +/* + * Updates lives with `amount`. + * + * If lives becomes negative the lives counter will not be drawn. + */ +void game_update_lives(GameData *game, int amount); + +/* + * Returns the remaining number of lives for the game. + */ +int game_get_lives(const GameData *game); + +/* + * Returns the current level. + */ +size_t game_get_current_level(const GameData *game); + +/* + * Sets the game status to `status`. + */ +void game_set_status(GameData *game, GameStatus status); + +/* + * Sets the game base update interval. + * + * Lower values of `update_interval` make the game faster, where 1 is the fastest and 50 is slowest. + * If this function is never called the game chooses a reasonable default. + */ +void game_set_update_interval(GameData *game, TIME_MS update_interval); + +/* + * Creates a message `message` of size `length` to be displayed at `coords` for `timeout` seconds. + * + * Message must be no greater than GAME_MAX_MESSAGE_SIZE bytes in length. + * + * If `sticky` is true the message will follow coords if they move. + * + * If `dir` is a valid direction, the message will be positioned a few squares away from `coords` + * so as to not overlap with its associated object. + * + * If `timeout` is zero, the default timeout value will be used. + * + * If `priority` true, messages will be printed on top of game objects. + * + * Return 0 on success. + * Return -1 on failure. + */ +int game_set_message(GameData *game, const char *message, size_t length, Direction dir, int attributes, int colour, + time_t timeout, const Coords *coords, bool sticky, bool priority); + +/* + * Returns true if game should update an object's state according to its last moved time and current speed. + * + * This is used to independently control the speed of various game objects. + */ +bool game_do_object_state_update(const GameData *game, TIME_MS current_time, TIME_MS last_moved_time, TIME_MS speed); + +/* + * Returns the current wall time in milliseconds. + */ +TIME_MS get_time_millis(void); + +/* + * Ends game associated with `self` and cleans up. + */ +void game_kill(ToxWindow *self); + +#endif // GAME_BASE diff --git a/src/game_centipede.c b/src/game_centipede.c new file mode 100644 index 0000000..60d74db --- /dev/null +++ b/src/game_centipede.c @@ -0,0 +1,1738 @@ +/* game_centipede.c + * + * + * Copyright (C) 2020 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_centipede.h" +#include "game_base.h" +#include "game_util.h" +#include "misc_tools.h" + +/* Determines how many mushrooms are spawned at the start of a game relative to window size (higher values means fewer) */ +#define CENT_MUSHROOMS_POP_CONSTANT 35000 + +/* Max number of mushrooms */ +#define CENT_MUSHROOMS_LENGTH (GAME_MAX_SQUARE_X * GAME_MAX_SQUARE_Y) + +/* Max number of individual centipedes at any given time */ +#define CENT_MAX_NUM_HEADS 20 + +/* Max number of segments that a centipede can have */ +#define CENT_MAX_NUM_SEGMENTS 12 + +/* Get a free life every time we get this many points. Needs to be > the most points we can get in a single shot. */ +#define CENT_SCORE_ONE_UP 5000 + +/* Max number of lives we can have */ +#define CENT_MAX_LIVES 6 + +/* How many lives we start with */ +#define CENT_START_LIVES 3 + +/* Max speed of an enemy agent */ +#define CENT_MAX_ENEMY_AGENT_SPEED 8 + +/* How often a head that reachest he bottom can repdoduce */ +#define CENT_REPRODUCE_TIMEOUT 10 + +#define CENT_CENTIPEDE_DEFAULT_SPEED 5 +#define CENT_CENTIPEDE_ATTR A_BOLD +#define CENT_CENTIPEDE_SEG_CHAR '8' +#define CENT_CENTIPEDE_HEAD_CHAR '8' + +#define CENT_BULLET_COLOUR YELLOW +#define CENT_BULLET_ATTR A_BOLD +#define CENT_BULLET_CHAR '|' +#define CENT_BULLET_SPEED 150 + +#define CENT_BLASTER_ATTR A_BOLD +#define CENT_BLASTER_CHAR 'U' +#define CENT_BLASTER_SPEED 10 + +#define CENT_MUSH_DEFAULT_HEALTH 4 +#define CENT_MUSH_DEFAULT_ATTR A_BOLD +#define CENT_MUSH_DEFAULT_CHAR '0' + +#define CENT_SPIDER_SPAWN_TIMER 7 // has a 75% chance of spawning each timeout +#define CENT_SPIDER_DEFAULT_SPEED 1 +#define CENT_SPIDER_DEFAULT_ATTR A_BOLD +#define CENT_SPIDER_CHAR 'X' +#define CENT_SPIDER_START_HEALTH 1 + +#define CENT_FLEA_SPAWN_TIMER 15 // has a 75% chance of spawning each timeout +#define CENT_FLEA_DEFAULT_SPEED 2 +#define CENT_FLEA_CHAR 'Y' +#define CENT_FLEA_DEFAULT_ATTR A_BOLD +#define CENT_FLEA_POINTS 200 +#define CENT_FLEA_START_HEALTH 2 + +#define CENT_SCORPION_BASE_SPAWN_TIMER 30 +#define CENT_SCORPION_DEFAULT_SPEED 2 +#define CENT_SCORPION_DEFAULT_ATTR A_BOLD +#define CENT_SCORPTION_CHAR '&' +#define CENT_SCORPTION_POINTS 1000 +#define CENT_SCORPTION_START_HEALTH 1 + + +/* + * Determines how far north on the Y axis the blaster can move, how far north centipedes can move when + * moving north, and the point at which fleas will stop creating mushrooms. + */ +#define CENT_INVISIBLE_H_WALL 5 + +#define CENT_KEY_FIRE ' ' + + +typedef struct EnemyAgent { + Coords coords; + Direction direction; + Direction start_direction; + int colour; + int attributes; + char display_char; + size_t speed; + TIME_MS last_time_moved; + TIME_S last_time_despawned; + bool was_killed; + size_t health; +} EnemyAgent; + +typedef struct Mushroom { + Coords coords; + size_t health; + int colour; + int attributes; + char display_char; + bool is_poisonous; +} Mushroom; + +typedef struct Blaster { + Coords coords; + Coords start_coords; + size_t speed; + TIME_MS last_time_moved; + int colour; + int attributes; + Direction direction; +} Blaster; + +typedef struct Projectile { + Coords coords; + size_t speed; + TIME_MS last_time_moved; + int colour; + int attributes; +} Projectile; + +/* + * A centipede is a doubly linked list comprised of one or more Segments. The head of the centipede + * is the root. All non-head segments follow their `prev` segment. When a non-tail segment is destroyed, + * its `next` segment becomes a head. + */ +typedef struct Segment Segment; +struct Segment { + Coords coords; + Direction h_direction; + Direction v_direction; + int colour; + int attributes; + int display_char; + size_t poison_rot; + bool is_fertile; + TIME_S last_time_reproduced; + TIME_MS last_time_moved; + Segment *prev; + Segment *next; +}; + +typedef struct Centipedes { + Segment *heads[CENT_MAX_NUM_HEADS]; + size_t heads_length; +} Centipedes; + +typedef struct CentState { + Centipedes centipedes; + Mushroom *mushrooms; + size_t mushrooms_length; + EnemyAgent spider; + EnemyAgent flea; + EnemyAgent scorpion; + Projectile bullet; + Blaster blaster; + + TIME_S pause_time; + bool game_over; +} CentState; + + +/* + * Evaluates to true if vertically moving bullet (x2,y2) has hit or passed vertically moving object (x1,y1). + * This should only be used if both the bullet and object never move on the x axis. + */ +#define CENT_VERTICAL_IMPACT(x1, y1, x2, y2)(((x1) == (x2)) && ((y1) >= (y2))) + + +#define CENT_LEVEL_COLOURS_SIZE 19 +const static int cent_level_colours[] = { + RED, + CYAN, + MAGENTA, + BLUE, + BLUE, + RED, + YELLOW, + GREEN, + GREEN, + CYAN, + YELLOW, + MAGENTA, + BLUE, + GREEN, + RED, + MAGENTA, + CYAN, + YELLOW, + WHITE, +}; + +static int cent_mushroom_colour(size_t level) +{ + return cent_level_colours[level % CENT_LEVEL_COLOURS_SIZE]; +} + +static int cent_head_colour(size_t level) +{ + return cent_level_colours[(level + 1) % CENT_LEVEL_COLOURS_SIZE]; +} + +static int cent_spider_colour(size_t level) +{ + return cent_level_colours[(level + 2) % CENT_LEVEL_COLOURS_SIZE]; +} + +static int cent_segment_colour(size_t level) +{ + return cent_level_colours[(level + 3) % CENT_LEVEL_COLOURS_SIZE]; +} + +static int cent_flea_colour(size_t level) +{ + return cent_level_colours[(level + 4) % CENT_LEVEL_COLOURS_SIZE]; +} + +static int cent_scorpion_colour(size_t level) +{ + return cent_level_colours[(level + 5) % CENT_LEVEL_COLOURS_SIZE]; +} + +static int cent_blaster_colour(size_t level) +{ + return cent_level_colours[(level + 6) % CENT_LEVEL_COLOURS_SIZE]; +} + +static int cent_poisonous_mush_colour(size_t level) +{ + int colour = cent_mushroom_colour(level); + + switch (colour) { + case RED: + return YELLOW; + + case YELLOW: + return RED; + + case CYAN: + return MAGENTA; + + case MAGENTA: + return CYAN; + + case BLUE: + return GREEN; + + case GREEN: + return BLUE; + + default: + return RED; + } +} + +static size_t cent_enemy_agent_speed(size_t base_speed, size_t level) +{ + if (level < 2) { + return base_speed; + } + + int r = rand() % (level / 2); + + return MIN(base_speed + r, CENT_MAX_ENEMY_AGENT_SPEED); +} + +static void cent_update_score(GameData *game, const CentState *state, long int points, const Coords *coords) +{ + char buf[GAME_MAX_MESSAGE_SIZE + 1]; + + long int prev_score = game_get_score(game); + game_update_score(game, points); + long int score = game_get_score(game); + + int lives = game_get_lives(game); + + if ((lives < CENT_MAX_LIVES) && ((score % CENT_SCORE_ONE_UP) < (prev_score % CENT_SCORE_ONE_UP))) { + game_update_lives(game, 1); + + snprintf(buf, sizeof(buf), "%s", "1UP!"); + + if (game_set_message(game, buf, strlen(buf), NORTH, A_BOLD, WHITE, 0, &state->blaster.coords, false, false) == -1) { + fprintf(stderr, "failed to set points message\n"); + } + } + + if (coords == NULL) { + return; + } + + snprintf(buf, sizeof(buf), "%ld", points); + + if (game_set_message(game, buf, strlen(buf), NORTH, A_BOLD, WHITE, 0, coords, false, true) == -1) { + fprintf(stderr, "failed to set points message\n"); + } +} + +static void cent_enemy_despawn(EnemyAgent *agent, bool was_killed) +{ + memset(agent, 0, sizeof(EnemyAgent)); + agent->last_time_despawned = get_unix_time(); + agent->was_killed = was_killed; +} + +static void cent_bullet_reset(Projectile *bullet) +{ + bullet->coords.x = -1; + bullet->coords.y = -1; +} + +static long int cent_spider_points(const Coords *spider_coords, const Coords *blaster_coords) +{ + const int y_dist = blaster_coords->y - spider_coords->y; + + if (y_dist > 3) { + return 300; + } + + if (y_dist > 1) { + return 600; + } + + return 900; +} + +static bool cent_centipedes_are_dead(Centipedes *centipedes) +{ + for (size_t i = 0; i < centipedes->heads_length; ++i) { + if (centipedes->heads[i] != NULL) { + return false; + } + } + + return true; +} + +static void cent_kill_centipede(Centipedes *centipedes, size_t index) +{ + Segment *head = centipedes->heads[index]; + + while (head) { + Segment *tmp1 = head->next; + free(head); + head = tmp1; + } + + centipedes->heads[index] = NULL; +} + +static void cent_poison_centipede(Segment *segment) +{ + if (segment->poison_rot == 0) { + while (segment != NULL) { + segment->poison_rot = 1; + segment = segment->next; + } + } +} + +static void cent_cure_centipede(Segment *segment) +{ + if (segment->poison_rot > 0) { + while (segment != NULL) { + segment->poison_rot = 0; + segment = segment->next; + } + } +} + +static void cent_exterminate_centipedes(Centipedes *centipedes) +{ + for (size_t i = 0; i < centipedes->heads_length; ++i) { + if (centipedes->heads[i] != NULL) { + cent_kill_centipede(centipedes, i); + } + } + + centipedes->heads_length = 0; +} + +static void cent_move_segments(Segment *head) +{ + if (head == NULL) { + return; + } + + Segment *current = head; + + while (current->next) { + current = current->next; + } + + Segment *prev = current->prev; + + while (prev) { + current->coords.x = prev->coords.x; + current->coords.y = prev->coords.y; + current->h_direction = prev->h_direction; + current->v_direction = prev->v_direction; + current = prev; + prev = prev->prev; + } +} + +static int cent_new_centipede_head_index(Centipedes *centipedes) +{ + for (size_t i = 0; i < CENT_MAX_NUM_HEADS; ++i) { + if (centipedes->heads[i] != NULL) { + continue; + } + + if (i == centipedes->heads_length) { + ++centipedes->heads_length; + } + + return i; + } + + return -1; +} + +static int cent_birth_centipede(const GameData *game, CentState *state, size_t length, Direction direction, + const Coords *coords) +{ + if (length > CENT_MAX_NUM_SEGMENTS) { + return -1; + } + + Centipedes *centipedes = &state->centipedes; + + int head_idx = cent_new_centipede_head_index(centipedes); + + if (head_idx == -1) { + return -1; + } + + Segment *new_head = calloc(1, sizeof(Segment)); + + if (new_head == NULL) { + return -1; + } + + if (coords == NULL) { + 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); + + new_head->coords.x = direction == EAST ? x_left : x_right; + new_head->coords.y = y_top; + } else { + new_head->coords.x = coords->x; + new_head->coords.y = coords->y; + } + + size_t level = game_get_current_level(game); + + new_head->h_direction = direction; + new_head->v_direction = SOUTH; + new_head->colour = cent_head_colour(level); + new_head->attributes = CENT_CENTIPEDE_ATTR; + new_head->display_char = CENT_CENTIPEDE_HEAD_CHAR; + new_head->prev = NULL; + + Segment *prev = new_head; + + for (size_t i = 0; i < length; ++i) { + Segment *new_seg = calloc(1, sizeof(Segment)); + + if (new_seg == NULL) { + cent_kill_centipede(centipedes, head_idx); + return -1; + } + + new_seg->colour = cent_segment_colour(level); + new_seg->attributes = CENT_CENTIPEDE_ATTR; + new_seg->display_char = CENT_CENTIPEDE_SEG_CHAR; + new_seg->coords.x = -1; // coords will update as it moves + new_seg->coords.y = -1; + new_seg->h_direction = direction; + new_seg->v_direction = SOUTH; + + new_seg->prev = prev; + prev->next = new_seg; + prev = new_seg; + } + + centipedes->heads[head_idx] = new_head; + + return 0; +} + +static int cent_init_level_centipedes(const GameData *game, CentState *state, size_t level) +{ + Direction dir = rand() % 2 == 0 ? WEST : EAST; + + // First level we spawn one full size centipede + if (level == 1) { + if (cent_birth_centipede(game, state, CENT_MAX_NUM_SEGMENTS, dir, NULL) == -1) { + return -1; + } + + return 0; + } + + const int c = ((int)level - 1); + const int diff = CENT_MAX_NUM_SEGMENTS - c; + + const int y_top = game_y_top_bound(game); + const int x_left = game_x_left_bound(game); + const int x_right = game_x_right_bound(game); + + const int remainder = diff > 4 ? c : CENT_MAX_NUM_SEGMENTS; + + // for the next few levels we spawn one multi-seg centipede + // decreasing in size and progressively more lone heads + if (diff > 4) { + if (cent_birth_centipede(game, state, diff, dir, NULL) == -1) { + return -1; + } + } + + // Spawn loan heads + for (size_t i = 0; i < remainder; ++i) { + dir = i % 2 == 0 ? EAST : WEST; + + Coords coords; + coords.x = dir == EAST ? x_left - i : x_right + i; + coords.y = i % 2 == 0 ? y_top + 1 : y_top + 2; + + if (cent_birth_centipede(game, state, 0, dir, &coords) == -1) { + return -1; + } + } + + return 0; +} + +static int cent_restart_level(GameData *game, CentState *state) +{ + Mushroom *mushrooms = state->mushrooms; + + for (size_t i = 0; i < state->mushrooms_length; ++i) { + Mushroom *mush = &mushrooms[i]; + + if (mush->health > 0) { + if (mush->health < CENT_MUSH_DEFAULT_HEALTH) { + cent_update_score(game, state, 5, NULL); + } + + mush->health = CENT_MUSH_DEFAULT_HEALTH; + mush->display_char = CENT_MUSH_DEFAULT_CHAR; + mush->attributes = CENT_MUSH_DEFAULT_ATTR; + } + } + + cent_enemy_despawn(&state->spider, false); + cent_enemy_despawn(&state->flea, false); + cent_enemy_despawn(&state->scorpion, false); + cent_exterminate_centipedes(&state->centipedes); + + size_t level = game_get_current_level(game); + + if (cent_init_level_centipedes(game, state, level) == -1) { + return -1; + } + + + state->blaster.coords.x = state->blaster.start_coords.x; + state->blaster.coords.y = state->blaster.start_coords.y; + + cent_bullet_reset(&state->bullet); + + return 0; +} + +static int cent_next_level(GameData *game, CentState *state) +{ + game_increment_level(game); + + size_t level = game_get_current_level(game); + + Mushroom *mushrooms = state->mushrooms; + + for (size_t i = 0; i < state->mushrooms_length; ++i) { + Mushroom *mush = &mushrooms[i]; + + if (mush->health > 0) { + mush->colour = mush->is_poisonous ? cent_poisonous_mush_colour(level) : cent_mushroom_colour(level); + } + } + + cent_exterminate_centipedes(&state->centipedes); + + if (cent_init_level_centipedes(game, state, level) == -1) { + return -1; + } + + state->blaster.colour = cent_blaster_colour(level); + state->spider.colour = cent_spider_colour(level); + state->flea.colour = cent_flea_colour(level); + state->scorpion.colour = cent_scorpion_colour(level); + + cent_bullet_reset(&state->bullet); + + return 0; +} + +static void cent_deduct_life(GameData *game, CentState *state) +{ + game_update_lives(game, -1); + + int lives = game_get_lives(game); + + if (lives == 0) { + game_set_status(game, GS_Finished); + state->game_over = true; + } else { + if (cent_restart_level(game, state) != 0) { + fprintf(stderr, "Failed to restart level\n"); + } + } +} + +static void cent_update_mush_appearance(const GameData *game, const CentState *state, Mushroom *mushroom) +{ + if (mushroom->health > CENT_MUSH_DEFAULT_HEALTH) { + return; + } + + switch (mushroom->health) { + case CENT_MUSH_DEFAULT_HEALTH: { + size_t level = game_get_current_level(game); + mushroom->colour = mushroom->is_poisonous ? cent_poisonous_mush_colour(level) : cent_mushroom_colour(level); + mushroom->attributes = CENT_MUSH_DEFAULT_ATTR; + mushroom->display_char = CENT_MUSH_DEFAULT_CHAR; + break; + } + + case 3: { + mushroom->display_char = 'o'; + break; + } + + case 2: { + mushroom->display_char = 'c'; + break; + } + + case 1: { + mushroom->display_char = ';'; + break; + } + + default: { + return; + } + } +} + +static Mushroom *cent_get_mushroom_at_coords(CentState *state, const Coords *coords) +{ + for (size_t i = 0; i < state->mushrooms_length; ++i) { + Mushroom *mush = &state->mushrooms[i]; + + if (mush->health == 0) { + continue; + } + + if (COORDINATES_OVERLAP(coords->x, coords->y, mush->coords.x, mush->coords.y)) { + return mush; + } + } + + return NULL; +} + +static Mushroom *cent_mushroom_new(CentState *state) +{ + Mushroom *mushrooms = state->mushrooms; + + for (size_t i = 0; i < CENT_MUSHROOMS_LENGTH; ++i) { + Mushroom *mush = &mushrooms[i]; + + if (mush->health != 0) { + continue; + } + + if (i == state->mushrooms_length) { + state->mushrooms_length = i + 1; + } + + return &mushrooms[i]; + } + + return NULL; +} + +static void cent_mushroom_grow(const GameData *game, CentState *state, const Coords *coords, bool is_poisonous) +{ + if (cent_get_mushroom_at_coords(state, coords) != NULL) { + return; + } + + if (!game_coordinates_in_bounds(game, coords->x, coords->y)) { + return; + } + + if (game_y_bottom_bound(game) == coords->y) { // can't be hit by blaster on the floor + return; + } + + Mushroom *mush = cent_mushroom_new(state); + + if (mush == NULL) { + return; + } + + mush->is_poisonous = is_poisonous; + mush->health = CENT_MUSH_DEFAULT_HEALTH; + mush->coords.x = coords->x; + mush->coords.y = coords->y; + + cent_update_mush_appearance(game, state, mush); +} + +static bool cent_blaster_enemy_collision(CentState *state, const EnemyAgent *enemy) +{ + Blaster *blaster = &state->blaster; + + if (enemy->health == 0) { + return false; + } + + if (COORDINATES_OVERLAP(blaster->coords.x, blaster->coords.y, enemy->coords.x, enemy->coords.y)) { + return true; + } + + return false; +} + +static void cent_blaster_move(GameData *game, CentState *state, TIME_MS cur_time) +{ + Blaster *blaster = &state->blaster; + + if (!GAME_UTIL_DIRECTION_VALID(blaster->direction)) { + return; + } + + const TIME_MS real_speed = GAME_UTIL_REAL_SPEED(blaster->direction, blaster->speed); + + if (!game_do_object_state_update(game, cur_time, blaster->last_time_moved, real_speed)) { + return; + } + + blaster->last_time_moved = cur_time; + + Coords new_coords = (Coords) { + blaster->coords.x, + blaster->coords.y + }; + + game_util_move_coords(blaster->direction, &new_coords); + + blaster->direction = INVALID_DIRECTION; + + const int y_bottom = game_y_bottom_bound(game); + + if (new_coords.y < y_bottom - CENT_INVISIBLE_H_WALL) { + return; + } + + if (!game_coordinates_in_bounds(game, new_coords.x, new_coords.y)) { + return; + } + + if (cent_get_mushroom_at_coords(state, &new_coords) != NULL) { + return; + } + + blaster->coords.x = new_coords.x; + blaster->coords.y = new_coords.y; +} + +static bool cent_bullet_mushroom_collision(GameData *game, CentState *state, const Coords *coords) +{ + Mushroom *mush = cent_get_mushroom_at_coords(state, coords); + + if (mush == NULL) { + return false; + } + + --mush->health; + cent_update_mush_appearance(game, state, mush); + + if (mush->health == 0) { + memset(mush, 0, sizeof(Mushroom)); + cent_update_score(game, state, 1, NULL); + } + + return true; +} + + +static void cent_bullet_move(const GameData *game, CentState *state, TIME_MS cur_time) +{ + Projectile *bullet = &state->bullet; + + if (!game_do_object_state_update(game, cur_time, bullet->last_time_moved, bullet->speed)) { + return; + } + + bullet->last_time_moved = cur_time; + + const int y_top = game_y_top_bound(game); + + --bullet->coords.y; + + if (bullet->coords.y < y_top) { + cent_bullet_reset(bullet); + } +} + +void cent_bullet_spawn(CentState *state) +{ + Projectile *bullet = &state->bullet; + + if (bullet->coords.x > 0) { + return; + } + + Blaster blaster = state->blaster; + + bullet->coords.x = blaster.coords.x; + bullet->coords.y = blaster.coords.y; + bullet->speed = CENT_BULLET_SPEED; +} + +/* Destroys centipede segment `seg`. If seg is a head, its `next` segment is assigned as the new head for + * the rest of the centipede. If seg is a tail, its `prev` segment is assigned as the new tail. + * if seg is somewher in the middle, the centipede is split into two; its prev is assigned as a new + * tail, and its `next` is assigned as a new head. + * + * Returns points value for the segment (100 for head, 10 for non-head). + * Returns 0 if head limit has been reached. + */ +static long int cent_kill_centipede_segment(const GameData *game, Centipedes *centipedes, Segment *seg, size_t index) +{ + if (seg->prev == NULL) { // head + if (seg->next == NULL) { // lone head + free(seg); + centipedes->heads[index] = NULL; + return 100; + } + + seg->next->display_char = seg->display_char; + seg->next->colour = seg->colour; + seg->next->attributes = seg->attributes; + seg->next->poison_rot = seg->poison_rot; + seg->next->prev = NULL; + + centipedes->heads[index] = seg->next; + free(seg); + + return 100; + } + + if (seg->next == NULL) { // tail + seg->prev->next = NULL; + free(seg); + return 10; + } + + // somewhere in the middle + + int idx = cent_new_centipede_head_index(centipedes); + + if (idx == -1) { + return 0; + } + + seg->next->prev = NULL; // next becomes head + seg->prev->next = NULL; // prev becomes tail + + seg->next->display_char = CENT_CENTIPEDE_HEAD_CHAR; + seg->next->attributes = CENT_CENTIPEDE_ATTR; + seg->next->poison_rot = seg->prev->poison_rot; + + size_t level = game_get_current_level(game); + seg->next->colour = cent_head_colour(level); + + centipedes->heads[idx] = seg->next; + + free(seg); + + return 10; +} + +static bool cent_bullet_centipede_collision(GameData *game, CentState *state) +{ + Centipedes *centipedes = &state->centipedes; + Projectile *bullet = &state->bullet; + + for (size_t i = 0; i < centipedes->heads_length; ++i) { + Segment *seg = centipedes->heads[i]; + + if (seg == NULL) { + continue; + } + + while (seg) { + if (COORDINATES_OVERLAP(seg->coords.x, seg->coords.y, bullet->coords.x, bullet->coords.y)) { + Coords mush_coords; + mush_coords.x = seg->h_direction == WEST ? seg->coords.x - 1 : seg->coords.x + 1; + mush_coords.y = seg->coords.y; + cent_mushroom_grow(game, state, &mush_coords, false); + + long int points = cent_kill_centipede_segment(game, centipedes, seg, i); + cent_update_score(game, state, points, NULL); + + return true; + } + + seg = seg->next; + } + } + + return false; +} + +/* + * Checks if bullet has collided with a mushroom or enemy and updates score appropriately. + * If objects overlap they're all hit. + */ +static void cent_bullet_collision_check(GameData *game, CentState *state) +{ + Projectile *bullet = &state->bullet; + + if (bullet->coords.x <= 0) { + return; + } + + bool collision = false; + + if (cent_bullet_mushroom_collision(game, state, &bullet->coords)) { + collision = true; + } + + EnemyAgent *spider = &state->spider; + + if (spider->health > 0 && COORDINATES_OVERLAP(spider->coords.x, spider->coords.y, bullet->coords.x, bullet->coords.y)) { + long int points = cent_spider_points(&spider->coords, &state->blaster.coords); + cent_update_score(game, state, points, &spider->coords); + cent_enemy_despawn(spider, true); + collision = true; + } + + EnemyAgent *scorpion = &state->scorpion; + + if (scorpion->health > 0 + && COORDINATES_OVERLAP(scorpion->coords.x, scorpion->coords.y, bullet->coords.x, bullet->coords.y)) { + cent_update_score(game, state, CENT_SCORPTION_POINTS, NULL); + cent_enemy_despawn(scorpion, true); + collision = true; + } + + EnemyAgent *flea = &state->flea; + + if (flea->health > 0 && CENT_VERTICAL_IMPACT(flea->coords.x, flea->coords.y, bullet->coords.x, bullet->coords.y)) { + if (--flea->health == 0) { + cent_update_score(game, state, CENT_FLEA_POINTS, NULL); + cent_enemy_despawn(flea, true); + } else { + flea->speed += 5; + } + + collision = true; + } + + if (cent_bullet_centipede_collision(game, state)) { + collision = true; + + if (cent_centipedes_are_dead(&state->centipedes)) { + cent_next_level(game, state); + } + } + + if (collision) { + cent_bullet_reset(bullet); + } +} + +static void cent_set_head_direction(CentState *state, Segment *head, int y_bottom, int x_left, int x_right) +{ + Coords next_coords = (Coords) { + head->coords.x, head->coords.y + }; + + // Move horizontally until we hit a mushroom or a wall, at which point we move down one square + // and continue in the other direction. + if (head->h_direction == WEST) { + next_coords.x = head->coords.x - 1; + Mushroom *mush = cent_get_mushroom_at_coords(state, &next_coords); + + if (head->coords.x <= x_left || mush != NULL) { + if (mush && mush->is_poisonous) { + cent_poison_centipede(head); + } + + head->h_direction = EAST; + head->coords.y = head->v_direction == SOUTH ? head->coords.y + 1 : head->coords.y - 1; + } else { + --head->coords.x; + } + } else if (head->h_direction == EAST) { + next_coords.x = head->coords.x + 1; + Mushroom *mush = cent_get_mushroom_at_coords(state, &next_coords); + + if (head->coords.x >= x_right || mush != NULL) { + if (mush && mush->is_poisonous) { + cent_poison_centipede(head); + } + + head->h_direction = WEST; + head->coords.y = head->v_direction == SOUTH ? head->coords.y + 1 : head->coords.y - 1; + } else { + ++head->coords.x; + } + } + + // if we touched a poison mushroom we move south every two steps + if (head->poison_rot == 2) { + ++head->coords.y; + head->h_direction = head->h_direction == EAST ? WEST : EAST; + head->poison_rot = 1; + } else if (head->poison_rot > 0) { + ++head->poison_rot; + } + + // if we hit the bottom boundary we turn north. if we're going north we only go up to the invisible wall + // and turn back around. + if (head->v_direction == SOUTH && head->coords.y > y_bottom) { + head->coords.y -= 2; + head->v_direction = NORTH; + cent_cure_centipede(head); + head->is_fertile = true; + head->last_time_reproduced = get_unix_time(); + } else if (head->v_direction == NORTH && head->coords.y < y_bottom - CENT_INVISIBLE_H_WALL) { + head->coords.y += 2; + head->v_direction = SOUTH; + } +} + +/* + * If a head has reached the bottom it reproduces (spawns an additional head) every time it + * makes two round-trips across the screen. + */ +static void cent_do_reproduce(const GameData *game, CentState *state, Segment *head, int x_right, int x_left, + int y_bottom) +{ + if (!head->is_fertile) { + return; + } + + if (!timed_out(head->last_time_reproduced, CENT_REPRODUCE_TIMEOUT)) { + return; + } + + Direction dir = rand() % 2 == 0 ? WEST : EAST; + + Coords new_coords; + new_coords.x = dir == EAST ? x_left : x_right; + new_coords.y = y_bottom - (rand() % CENT_INVISIBLE_H_WALL); + + if (cent_birth_centipede(game, state, 0, dir, &new_coords) == 0) { + head->last_time_reproduced = get_unix_time(); + } +} + +static void cent_do_centipede(const GameData *game, CentState *state, TIME_MS cur_time) +{ + Centipedes *centipedes = &state->centipedes; + + if (centipedes->heads_length == 0) { + return; + } + + const int y_bottom = game_y_bottom_bound(game); + const int x_left = game_x_left_bound(game); + const int x_right = game_x_right_bound(game); + + for (size_t i = 0; i < centipedes->heads_length; ++i) { + Segment *head = centipedes->heads[i]; + + if (head == NULL) { + continue; + } + + // half speed if poisoned + TIME_MS real_speed = head->poison_rot > 0 ? (CENT_CENTIPEDE_DEFAULT_SPEED / 2) + 1 : CENT_CENTIPEDE_DEFAULT_SPEED; + + if (!game_do_object_state_update(game, cur_time, head->last_time_moved, real_speed)) { + continue; + } + + head->last_time_moved = cur_time; + + cent_move_segments(head); + cent_set_head_direction(state, head, y_bottom, x_left, x_right); + + if (head->coords.x == x_left || head->coords.x == x_right) { + cent_do_reproduce(game, state, head, x_right, x_left, y_bottom); + } + } +} + +static void cent_try_spawn_flea(const GameData *game, EnemyAgent *flea) +{ + if (!flea->was_killed) { + if (!timed_out(flea->last_time_despawned, CENT_FLEA_SPAWN_TIMER)) { + return; + } + } + + flea->was_killed = false; + + if (rand() % 4 == 0) { + return; + } + + size_t level = game_get_current_level(game); + + flea->colour = cent_flea_colour(level); + flea->speed = cent_enemy_agent_speed(CENT_FLEA_DEFAULT_SPEED, level); + flea->attributes = CENT_FLEA_DEFAULT_ATTR; + flea->display_char = CENT_FLEA_CHAR; + flea->direction = SOUTH; + flea->health = CENT_FLEA_START_HEALTH; + + const int y_top = game_y_top_bound(game); + const int x_left = game_x_left_bound(game); + const int x_right = game_x_right_bound(game); + + flea->coords.y = y_top; + flea->coords.x = (rand() % (x_right - x_left + 1)) + x_left; +} + +static void cent_do_flea(GameData *game, CentState *state, TIME_MS cur_time) +{ + EnemyAgent *flea = &state->flea; + + if (flea->health == 0) { + cent_try_spawn_flea(game, flea); + return; + } + + if (!game_do_object_state_update(game, cur_time, flea->last_time_moved, flea->speed)) { + return; + } + + flea->last_time_moved = cur_time; + + const int y_bottom = game_y_bottom_bound(game); + + if (flea->coords.y < (y_bottom - 5) && rand() % 4 == 0) { + cent_mushroom_grow(game, state, &flea->coords, false); + } + + ++flea->coords.y; + + if (flea->coords.y > game_y_bottom_bound(game)) { + cent_enemy_despawn(flea, false); + } +} + +/* + * Scorption spawn timeout is reduced linearly according to the level. She has a 75% chance of + * spawning when the timeout expires, at which point it's reset whether she spawns or not. + */ +static bool cent_scorpion_spawn_check(EnemyAgent *scorpion, size_t level) +{ + size_t decay = level * 2; + TIME_S timeout = CENT_SCORPION_BASE_SPAWN_TIMER > decay ? CENT_SCORPION_BASE_SPAWN_TIMER - decay : 1; + + if (!timed_out(scorpion->last_time_despawned, timeout)) { + return false; + } + + scorpion->last_time_despawned = get_unix_time(); + + return (rand() % 4) < 3; +} + +static void cent_try_spawn_scorpion(const GameData *game, CentState *state, EnemyAgent *scorpion) +{ + size_t level = game_get_current_level(game); + + if (level < 2) { + return; + } + + if (!cent_scorpion_spawn_check(scorpion, level)) { + return; + } + + scorpion->colour = cent_scorpion_colour(level); + scorpion->speed = CENT_SCORPION_DEFAULT_SPEED; + scorpion->attributes = CENT_SCORPION_DEFAULT_ATTR; + scorpion->display_char = CENT_SCORPTION_CHAR; + scorpion->health = CENT_SCORPTION_START_HEALTH; + scorpion->direction = rand() % 2 == 0 ? WEST : EAST; + + const int y_bottom = game_y_bottom_bound(game); + 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_mid = y_top + ((y_bottom - y_top) / 2); + + scorpion->coords.x = scorpion->direction == WEST ? x_right : x_left; + scorpion->coords.y = (y_mid - 5) + (rand() % 5); +} + +static void cent_do_scorpion(GameData *game, CentState *state, TIME_MS cur_time) +{ + EnemyAgent *scorpion = &state->scorpion; + + if (scorpion->health == 0) { + cent_try_spawn_scorpion(game, state, scorpion); + return; + } + + if (!game_do_object_state_update(game, cur_time, scorpion->last_time_moved, scorpion->speed)) { + return; + } + + scorpion->last_time_moved = cur_time; + + Mushroom *mush = cent_get_mushroom_at_coords(state, &scorpion->coords); + + if (mush != NULL) { + size_t level = game_get_current_level(game); + mush->is_poisonous = true; + mush->colour = cent_poisonous_mush_colour(level); + } + + const int x_left = game_x_left_bound(game); + const int x_right = game_x_right_bound(game); + + scorpion->coords.x = scorpion->direction == WEST ? scorpion->coords.x - 1 : scorpion->coords.x + 1; + + if (scorpion->coords.x > x_right || scorpion->coords.x < x_left) { + cent_enemy_despawn(scorpion, false); + } +} + +static void cent_try_spawn_spider(const GameData *game, EnemyAgent *spider) +{ + if (!timed_out(spider->last_time_despawned, CENT_SPIDER_SPAWN_TIMER)) { + return; + } + + if (rand() % 4 == 0) { + spider->last_time_despawned = get_unix_time(); + return; + } + + size_t level = game_get_current_level(game); + + spider->colour = cent_spider_colour(level); + spider->speed = cent_enemy_agent_speed(CENT_SPIDER_DEFAULT_SPEED, level); + spider->attributes = CENT_SPIDER_DEFAULT_ATTR; + spider->display_char = CENT_SPIDER_CHAR; + spider->start_direction = rand() % 2 == 0 ? WEST : EAST; + spider->direction = spider->start_direction; + spider->health = CENT_SPIDER_START_HEALTH; + + const int y_bottom = game_y_bottom_bound(game); + const int x_left = game_x_left_bound(game); + const int x_right = game_x_right_bound(game); + + spider->coords.x = spider->direction == WEST ? x_right : x_left; + spider->coords.y = (rand() % (y_bottom - (y_bottom - CENT_INVISIBLE_H_WALL))) + (y_bottom - CENT_INVISIBLE_H_WALL); +} + +static void cent_do_spider(GameData *game, CentState *state, TIME_MS cur_time) +{ + EnemyAgent *spider = &state->spider; + + if (spider->health == 0) { + cent_try_spawn_spider(game, spider); + return; + } + + if (!game_do_object_state_update(game, cur_time, spider->last_time_moved, spider->speed)) { + return; + } + + spider->last_time_moved = cur_time; + + Coords new_coords = (Coords) { + spider->coords.x, + spider->coords.y, + }; + + int r = rand(); + + if (spider->direction == spider->start_direction) { + if (r % 4 == 0) { + spider->direction = r % 3 == 0 ? NORTH : SOUTH; + } + } else { + if (r % 5 == 0) { + spider->direction = spider->start_direction; + } + } + + game_util_move_coords(spider->direction, &new_coords); + + const int y_bottom = game_y_bottom_bound(game); + const int x_left = game_x_left_bound(game); + const int x_right = game_x_right_bound(game); + const int top_limit = y_bottom - CENT_INVISIBLE_H_WALL; + + if (new_coords.x > x_right || new_coords.x < x_left) { + cent_enemy_despawn(spider, false); + return; + } + + if (new_coords.y > y_bottom) { + new_coords.y = y_bottom; + spider->direction = NORTH; + } else if (new_coords.y < top_limit) { + new_coords.y = new_coords.y + 1; + spider->direction = SOUTH; + } + + spider->coords = (Coords) { + new_coords.x, + new_coords.y + }; + + Mushroom *mush = cent_get_mushroom_at_coords(state, &new_coords); + + if (mush != NULL) { + memset(mush, 0, sizeof(Mushroom)); + } +} + +static bool cent_blaster_centipede_collision(CentState *state) +{ + Centipedes *centipedes = &state->centipedes; + Blaster blaster = state->blaster; + + for (size_t i = 0; i < centipedes->heads_length; ++i) { + Segment *seg = centipedes->heads[i]; + + if (seg == NULL) { + continue; + } + + while (seg) { + if (COORDINATES_OVERLAP(seg->coords.x, seg->coords.y, blaster.coords.x, blaster.coords.y)) { + return true; + } + + seg = seg->next; + } + } + + return false; +} + +static void cent_blaster_collision_check(GameData *game, CentState *state) +{ + if (state->blaster.coords.x <= 0) { + return; + } + + if (cent_blaster_enemy_collision(state, &state->flea)) { + cent_deduct_life(game, state); + return; + } + + if (cent_blaster_enemy_collision(state, &state->spider)) { + cent_deduct_life(game, state); + return; + } + + if (cent_blaster_centipede_collision(state)) { + cent_deduct_life(game, state); + return; + } +} + +static void cent_blaster_draw(WINDOW *win, const CentState *state) +{ + Blaster blaster = state->blaster; + + wattron(win, blaster.attributes | COLOR_PAIR(blaster.colour)); + mvwaddch(win, blaster.coords.y, blaster.coords.x, CENT_BLASTER_CHAR); + wattroff(win, blaster.attributes | COLOR_PAIR(blaster.colour)); +} + +static void cent_projectiles_draw(WINDOW *win, const CentState *state) +{ + Projectile bullet = state->bullet; + Coords blaster_coords = state->blaster.coords; + + if (bullet.coords.x > 0 && bullet.coords.y != blaster_coords.y) { + wattron(win, bullet.attributes | COLOR_PAIR(bullet.colour)); + mvwaddch(win, bullet.coords.y, bullet.coords.x, CENT_BULLET_CHAR); + wattroff(win, bullet.attributes | COLOR_PAIR(bullet.colour)); + } +} + +static void cent_enemy_draw(WINDOW *win, const CentState *state) +{ + EnemyAgent spider = state->spider; + + if (spider.health > 0) { + wattron(win, spider.attributes | COLOR_PAIR(spider.colour)); + mvwaddch(win, spider.coords.y, spider.coords.x, spider.display_char); + wattroff(win, spider.attributes | COLOR_PAIR(spider.colour)); + } + + EnemyAgent flea = state->flea; + + if (flea.health > 0) { + wattron(win, flea.attributes | COLOR_PAIR(flea.colour)); + mvwaddch(win, flea.coords.y, flea.coords.x, CENT_FLEA_CHAR); + wattroff(win, flea.attributes | COLOR_PAIR(flea.colour)); + } + + EnemyAgent scorpion = state->scorpion; + + if (scorpion.health > 0) { + wattron(win, scorpion.attributes | COLOR_PAIR(scorpion.colour)); + mvwaddch(win, scorpion.coords.y, scorpion.coords.x, scorpion.display_char); + wattroff(win, scorpion.attributes | COLOR_PAIR(scorpion.colour)); + } +} + +static void cent_centipede_draw(const GameData *game, WINDOW *win, CentState *state) +{ + Centipedes *centipedes = &state->centipedes; + + for (size_t i = 0; i < centipedes->heads_length; ++i) { + Segment *seg = centipedes->heads[i]; + + while (seg) { + // sometimes we spawn heads outside of game bounds + if (game_coordinates_in_bounds(game, seg->coords.x, seg->coords.y)) { + wattron(win, seg->attributes | COLOR_PAIR(seg->colour)); + mvwaddch(win, seg->coords.y, seg->coords.x, seg->display_char); + wattroff(win, seg->attributes | COLOR_PAIR(seg->colour)); + } + + seg = seg->next; + } + } +} + +static void cent_mushrooms_draw(WINDOW *win, const CentState *state) +{ + Mushroom *mushrooms = state->mushrooms; + + for (size_t i = 0; i < state->mushrooms_length; ++i) { + Mushroom mush = mushrooms[i]; + + if (mush.health == 0) { + continue; + } + + wattron(win, mush.attributes | COLOR_PAIR(mush.colour)); + mvwaddch(win, mush.coords.y, mush.coords.x, mush.display_char); + wattroff(win, mush.attributes | COLOR_PAIR(mush.colour)); + } +} + +void cent_cb_update_game_state(GameData *game, void *cb_data) +{ + if (!cb_data) { + return; + } + + CentState *state = (CentState *)cb_data; + + TIME_MS cur_time = get_time_millis(); + + cent_blaster_collision_check(game, state); + cent_bullet_collision_check(game, state); + cent_blaster_move(game, state, cur_time); + cent_bullet_move(game, state, cur_time); + cent_do_centipede(game, state, cur_time); + cent_do_spider(game, state, cur_time); + cent_do_flea(game, state, cur_time); + cent_do_scorpion(game, state, cur_time); +} + +void cent_cb_render_window(GameData *game, WINDOW *win, void *cb_data) +{ + if (!cb_data) { + return; + } + + CentState *state = (CentState *)cb_data; + + cent_blaster_draw(win, state); + cent_projectiles_draw(win, state); + cent_mushrooms_draw(win, state); + cent_enemy_draw(win, state); + cent_centipede_draw(game, win, state); +} + +void cent_cb_on_keypress(GameData *game, int key, void *cb_data) +{ + if (!cb_data) { + return; + } + + CentState *state = (CentState *)cb_data; + + if (key == CENT_KEY_FIRE) { + cent_bullet_spawn(state); + return; + } + + Direction dir = game_util_get_direction(key); + + if (dir < INVALID_DIRECTION) { + state->blaster.direction = dir; + } +} + +void cent_cb_pause(GameData *game, bool is_paused, void *cb_data) +{ + if (!cb_data) { + return; + } + + CentState *state = (CentState *)cb_data; + + TIME_S t = get_unix_time(); + + if (is_paused) { + state->pause_time = t; + } else { + state->spider.last_time_despawned += (t - state->pause_time); + } +} + +void cent_cb_kill(GameData *game, void *cb_data) +{ + if (!cb_data) { + return; + } + + CentState *state = (CentState *)cb_data; + + cent_exterminate_centipedes(&state->centipedes); + + free(state->mushrooms); + 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_pause(game, NULL, NULL); +} + +/* Gives `mush` new coordinates that don't overlap with another mushroom and are within boundaries. + * + * Return false if we fail to find a vacant coordinate. + */ +static bool cent_new_mush_coordinates(const GameData *game, CentState *state, Mushroom *mush, int y_floor_bound) +{ + size_t tries = 0; + Coords new_coords; + + do { + if (++tries > 10) { + return false; + } + + game_random_coords(game, &new_coords); + } while (new_coords.y >= (y_floor_bound - 1) || cent_get_mushroom_at_coords(state, &new_coords) != NULL); + + mush->coords = (Coords) { + new_coords.x, + new_coords.y + }; + + return true; +} + +static void cent_populate_mushrooms(const GameData *game, CentState *state, int population_const) +{ + int max_x; + int max_y; + game_max_x_y(game, &max_x, &max_y); + + const int y_floor_bound = game_y_bottom_bound(game); + + for (size_t i = 0; i < CENT_MUSHROOMS_LENGTH; ++i) { + if (rand() % population_const != 0) { + continue; + } + + size_t idx = state->mushrooms_length; + Mushroom *mush = &state->mushrooms[idx]; + + if (!cent_new_mush_coordinates(game, state, mush, y_floor_bound)) { + continue; + } + + mush->is_poisonous = false; + mush->health = CENT_MUSH_DEFAULT_HEALTH; + + cent_update_mush_appearance(game, state, mush); + + ++state->mushrooms_length; + } +} + +static int cent_init_state(GameData *game, CentState *state) +{ + game_update_lives(game, CENT_START_LIVES); + + const int y_bottom = game_y_bottom_bound(game); + 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); + + Mushroom *mushrooms = calloc(1, sizeof(Mushroom) * CENT_MUSHROOMS_LENGTH); + + if (mushrooms == NULL) { + return -1; + } + + state->mushrooms = mushrooms; + + Centipedes *centipedes = &state->centipedes; + memset(centipedes->heads, 0, sizeof(centipedes->heads)); + + Direction dir = rand() % 2 == 0 ? WEST : EAST; + + if (cent_birth_centipede(game, state, CENT_MAX_NUM_SEGMENTS, dir, NULL) == -1) { + free(mushrooms); + return -1; + } + + state->spider.last_time_despawned = get_unix_time(); + state->flea.last_time_despawned = get_unix_time(); + state->scorpion.last_time_despawned = get_unix_time(); + + state->blaster.colour = cent_blaster_colour(0); + state->blaster.attributes = CENT_BLASTER_ATTR; + state->blaster.direction = INVALID_DIRECTION; + state->blaster.speed = CENT_BLASTER_SPEED; + + state->blaster.coords = (Coords) { + (x_left + x_right) / 2, + y_bottom + }; + + state->blaster.start_coords = (Coords) { + (x_left + x_right) / 2, + y_bottom + }; + + state->bullet.colour = CENT_BULLET_COLOUR; + state->bullet.attributes = CENT_BULLET_ATTR; + + const int grid_size = (y_bottom - y_top) * (x_right - x_left); + + if (grid_size >= CENT_MUSHROOMS_POP_CONSTANT) { + return -1; + } + + const int population = CENT_MUSHROOMS_POP_CONSTANT / grid_size; + cent_populate_mushrooms(game, state, population); + + return 0; +} + +int centipede_initialize(GameData *game) +{ + if (game_set_window_shape(game, GW_ShapeSquare) == -1) { + return -1; + } + + CentState *state = calloc(1, sizeof(CentState)); + + if (state == NULL) { + return -1; + } + + game_increment_level(game); + game_set_update_interval(game, 10); + + if (cent_init_state(game, state) == -1) { + free(state); + return -1; + } + + game_set_cb_update_state(game, cent_cb_update_game_state, state); + game_set_cb_render_window(game, cent_cb_render_window, state); + game_set_cb_on_keypress(game, cent_cb_on_keypress, state); + game_set_cb_kill(game, cent_cb_kill, state); + game_set_cb_on_pause(game, cent_cb_pause, state); + + return 0; +} diff --git a/src/game_centipede.h b/src/game_centipede.h new file mode 100644 index 0000000..f048550 --- /dev/null +++ b/src/game_centipede.h @@ -0,0 +1,30 @@ +/* game_centipede.h + * + * + * Copyright (C) 2020 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_CENTIPEDE +#define GAME_CENTIPEDE + +#include "game_base.h" + +int centipede_initialize(GameData *game); + +#endif // GAME_CENTIPEDE diff --git a/src/game_snake.c b/src/game_snake.c new file mode 100644 index 0000000..3765cff --- /dev/null +++ b/src/game_snake.c @@ -0,0 +1,901 @@ +/* game_snake.c + * + * + * Copyright (C) 2020 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_base.h" +#include "game_util.h" +#include "game_snake.h" +#include "misc_tools.h" + +#define SNAKE_MAX_SNAKE_LENGTH (GAME_MAX_SQUARE_X * GAME_MAX_SQUARE_Y) +#define SNAKE_AGENT_MAX_LIST_SIZE (GAME_MAX_SQUARE_X * GAME_MAX_SQUARE_Y) + +#define SNAKE_DEFAULT_SNAKE_SPEED 6 +#define SNAKE_DEFAULT_AGENT_SPEED 1 +#define SNAKE_MAX_SNAKE_SPEED 12 +#define SNAKE_MAX_AGENT_SPEED (SNAKE_MAX_SNAKE_SPEED / 2) + +#define SNAKE_DEFAULT_UPDATE_INTERVAL 20 + +/* How often we update frames independent of the speed of the game or objects */ +#define SNAKE_FRAME_DRAW_SPEED 5 + +/* How long a regular message stays on the screen */ +#define SNAKE_DEFAULT_MESSAGE_TIMER 5 + +/* Increment snake speed by 1 every time level increases by this amount */ +#define SNAKE_LEVEL_SPEED_INTERVAL 5 + +/* Increment level by 1 every time snake eats this many foods */ +#define SNAKE_LEVEL_UP_FOOD_LIMIT 4 + +/* Increment agent speed by 1 every time level increases by this amount */ +#define SNAKE_AGENT_LEVEL_SPEED_INTERVAL 2 + +/* Points multiplier for getting a powerup */ +#define SNAKE_POWERUP_BONUS_MULTIPLIER 5 + +/* Extra bonus for running over a glowing agent */ +#define SNAKE_AGENT_GLOWING_MULTIPLIER 2 + +/* Agents begin glowing if their speed is greater than this */ +#define SNAKE_AGENT_GLOWING_SPEED (SNAKE_DEFAULT_AGENT_SPEED + 2) + +/* A new powerup is placed on the board after this many seconds since last one wore off */ +#define SNAKE_POWERUP_INTERVAL 45 + +/* How long a powerup lasts */ +#define SNAKE_POWERUP_TIMER 12 + +/* Number of key presses to queue; one key press is retrieved per state update */ +#define SNAKE_KEY_PRESS_QUEUE_SIZE 3 + +/* These decide how many points to decay and how often to penalize camping for powerups */ +#define SNAKE_DECAY_POINTS_INTERVAL 1 +#define SNAKE_DECAY_POINTS_FRACTION 10 + +#define SNAKE_HEAD_COLOUR GREEN + +#define SNAKE_DEAD_BODY_CHAR 'o' +#define SNAKE_DEAD_BODY_COLOUR RED + +#define SNAKE_BODY_COLOUR CYAN +#define SNAKE_BODY_CHAR 'o' + +#define SNAKE_FOOD_COLOUR YELLOW +#define SNAKE_FOOD_CHAR '*' + +#define SNAKE_AGENT_NORMAL_COLOUR RED +#define SNAKE_AGENT_GLOWING_COLOUR GREEN +#define SNAKE_AGENT_NORMAL_CHAR 'x' +#define SNAKE_AGENT_GLOWING_CHAR 'X' + +#define SNAKE_POWERUP_CHAR 'P' + +typedef struct NasaAgent { + Coords coords; + bool is_alive; + bool is_glowing; + TIME_MS last_time_moved; + size_t speed; + char display_char; + int colour; + int attributes; +} NasaAgent; + +typedef struct Snake { + Coords coords; + char display_char; + int colour; + int attributes; +} Snake; + +typedef struct SnakeState { + Snake *snake; + size_t snake_length; + size_t snake_speed; + TIME_MS snake_time_last_moved; + bool has_powerup; + Direction direction; + + Coords powerup; + TIME_S powerup_timer; + TIME_S last_powerup_time; + + Coords food; + + NasaAgent *agents; + size_t agent_list_size; + + TIME_S last_time_points_decayed; + TIME_S pause_time; + + int key_press_queue[SNAKE_KEY_PRESS_QUEUE_SIZE]; + size_t keys_skip_counter; + + TIME_MS last_draw_update; + bool game_over; +} SnakeState; + + +static void snake_create_points_message(GameData *game, Direction dir, long int points, const Coords *coords) +{ + char buf[GAME_MAX_MESSAGE_SIZE + 1]; + snprintf(buf, sizeof(buf), "%ld", points); + + if (game_set_message(game, buf, strlen(buf), dir, A_BOLD, WHITE, 0, coords, false, false) == -1) { + fprintf(stderr, "failed to set points message\n"); + } +} + +static void snake_create_message(GameData *game, Direction dir, const char *message, int attributes, + int colour, TIME_S timeout, const Coords *coords, bool priority) +{ + if (game_set_message(game, message, strlen(message), dir, attributes, colour, timeout, coords, false, priority) == -1) { + fprintf(stderr, "failed to set message\n"); + } +} + +static Coords *snake_get_head_coords(const SnakeState *state) +{ + return &state->snake[0].coords; +} + +static void snake_set_head_char(SnakeState *state) +{ + Snake *snake_head = &state->snake[0]; + + switch (state->direction) { + case NORTH: + snake_head->display_char = '^'; + break; + + case SOUTH: + snake_head->display_char = 'v'; + break; + + case EAST: + snake_head->display_char = '>'; + return; + + case WEST: + snake_head->display_char = '<'; + break; + + default: + snake_head->display_char = '?'; + break; + } +} + +static bool snake_validate_direction(const SnakeState *state, Direction dir) +{ + if (!GAME_UTIL_DIRECTION_VALID(dir)) { + return false; + } + + const int diff = abs((int)state->direction - (int)dir); + + return diff != 1; +} + +static void snake_update_direction(SnakeState *state) +{ + for (size_t i = 0; i < SNAKE_KEY_PRESS_QUEUE_SIZE; ++i) { + int key = state->key_press_queue[i]; + + if (key == 0) { + continue; + } + + Direction dir = game_util_get_direction(key); + + if (!GAME_UTIL_DIRECTION_VALID(dir)) { + state->key_press_queue[i] = 0; + continue; + } + + if (snake_validate_direction(state, dir)) { + state->direction = dir; + snake_set_head_char(state); + state->key_press_queue[i] = 0; + state->keys_skip_counter = 0; + break; + } + + if (state->keys_skip_counter++ >= SNAKE_KEY_PRESS_QUEUE_SIZE) { + state->keys_skip_counter = 0; + memset(state->key_press_queue, 0, sizeof(state->key_press_queue)); + } + } +} + +static void snake_set_key_press(SnakeState *state, int key) +{ + for (size_t i = 0; i < SNAKE_KEY_PRESS_QUEUE_SIZE; ++i) { + if (state->key_press_queue[i] == 0) { + state->key_press_queue[i] = key; + return; + } + } + + memset(state->key_press_queue, 0, sizeof(state->key_press_queue)); + state->key_press_queue[0] = key; +} + +static void snake_update_score(GameData *game, const SnakeState *state, long int points) +{ + const Coords *head = snake_get_head_coords(state); + + snake_create_points_message(game, state->direction, points, head); + + game_update_score(game, points); +} + +static long int snake_get_move_points(const SnakeState *state) +{ + return state->snake_length + (2 * state->snake_speed); +} + +/* Return true if snake body is occupying given coordinates */ +static bool snake_coords_contain_body(const SnakeState *state, const Coords *coords) +{ + for (size_t i = 1; i < state->snake_length; ++i) { + Coords snake_coords = state->snake[i].coords; + + if (COORDINATES_OVERLAP(coords->x, coords->y, snake_coords.x, snake_coords.y)) { + return true; + } + } + + return false; +} + +static bool snake_self_consume(const SnakeState *state) +{ + const Coords *head = snake_get_head_coords(state); + return snake_coords_contain_body(state, head); +} + +/* + * Returns a pointer to the agent at x and y coordinates. + * + * Returns NULL if no living agent is at coords. + */ +static NasaAgent *snake_get_agent_at_coords(const SnakeState *state, const Coords *coords) +{ + for (size_t i = 0; i < state->agent_list_size; ++i) { + NasaAgent *agent = &state->agents[i]; + + if (!agent->is_alive) { + continue; + } + + if (COORDINATES_OVERLAP(coords->x, coords->y, agent->coords.x, agent->coords.y)) { + return agent; + } + } + + return NULL; +} + +/* + * Return true if snake got caught by an agent and doesn't have a powerup. + * + * If snake runs over agent and does have a powerup the agent is killed and score updated. + */ +static bool snake_agent_caught(GameData *game, SnakeState *state, const Coords *coords) +{ + NasaAgent *agent = snake_get_agent_at_coords(state, coords); + + if (agent == NULL) { + return false; + } + + if (!state->has_powerup) { + return true; + } + + agent->is_alive = false; + + long int points = snake_get_move_points(state) * (agent->speed + 1); + + if (agent->is_glowing) { + points *= SNAKE_AGENT_GLOWING_MULTIPLIER; + } + + snake_update_score(game, state, points); + + return false; +} + +static bool snake_state_valid(GameData *game, SnakeState *state) +{ + const Coords *head = snake_get_head_coords(state); + + if (!game_coordinates_in_bounds(game, head->x, head->y)) { + snake_create_message(game, state->direction, "Ouch!", A_BOLD, WHITE, SNAKE_DEFAULT_MESSAGE_TIMER, head, true); + return false; + } + + if (snake_self_consume(state)) { + snake_create_message(game, state->direction, "Tastes like chicken", A_BOLD, WHITE, SNAKE_DEFAULT_MESSAGE_TIMER, head, + true); + return false; + } + + if (snake_agent_caught(game, state, head)) { + snake_create_message(game, state->direction, "ARGH they got me!", A_BOLD, WHITE, SNAKE_DEFAULT_MESSAGE_TIMER, head, + true); + return false; + } + + return true; +} + +/* + * Sets colour and attributes for entire snake except head. + * + * If colour is set to -1 a random colour is chosen for each body part. + */ +static void snake_set_body_attributes(Snake *snake, size_t length, int colour, int attributes) +{ + for (size_t i = 1; i < length; ++i) { + Snake *body = &snake[i]; + + if (colour == -1) { + body->colour = game_util_random_colour(); + } else { + body->colour = colour; + } + + body->attributes = attributes; + } +} + +static void snake_move_body(SnakeState *state) +{ + for (size_t i = state->snake_length - 1; i > 0; --i) { + Coords *curr = &state->snake[i].coords; + Coords prev = state->snake[i - 1].coords; + curr->x = prev.x; + curr->y = prev.y; + } +} + +static void snake_move_head(SnakeState *state) +{ + Coords *head = snake_get_head_coords(state); + game_util_move_coords(state->direction, head); +} + +static void snake_grow(SnakeState *state) +{ + size_t index = state->snake_length; + + if (index >= SNAKE_MAX_SNAKE_LENGTH) { + return; + } + + state->snake[index].coords.x = -1; + state->snake[index].coords.y = -1; + state->snake[index].display_char = SNAKE_BODY_CHAR; + state->snake[index].colour = SNAKE_BODY_COLOUR; + state->snake[index].attributes = A_BOLD; + + state->snake_length = index + 1; +} + +static long int snake_check_food(const GameData *game, SnakeState *state) +{ + Coords *food = &state->food; + const Coords *head = snake_get_head_coords(state); + + if (!COORDINATES_OVERLAP(head->x, head->y, food->x, food->y)) { + return 0; + } + + snake_grow(state); + + game_random_coords(game, food); + + return snake_get_move_points(state); +} + +static long int snake_check_powerup(GameData *game, SnakeState *state) +{ + Coords *powerup = &state->powerup; + const Coords *head = snake_get_head_coords(state); + + if (!COORDINATES_OVERLAP(head->x, head->y, powerup->x, powerup->y)) { + return 0; + } + + snake_create_message(game, state->direction, "AAAAA", A_BOLD, RED, 2, head, false); + + TIME_S t = get_unix_time(); + + state->has_powerup = true; + state->powerup_timer = t; + + powerup->x = -1; + powerup->y = -1; + + return snake_get_move_points(state) * SNAKE_POWERUP_BONUS_MULTIPLIER; +} + +/* + * Returns the first unoccupied index in agent array. + */ +static size_t snake_get_empty_agent_index(const NasaAgent *agents) +{ + for (size_t i = 0; i < SNAKE_AGENT_MAX_LIST_SIZE; ++i) { + if (!agents[i].is_alive) { + return i; + } + } + + fprintf(stderr, "Warning: Agent array is full. This should be impossible\n"); + + return 0; +} + +static void snake_initialize_agent(SnakeState *state, const Coords *coords) +{ + size_t idx = snake_get_empty_agent_index(state->agents); + + if ((idx >= state->agent_list_size) && (idx + 1 <= SNAKE_AGENT_MAX_LIST_SIZE)) { + state->agent_list_size = idx + 1; + } + + NasaAgent *agent = &state->agents[idx]; + + agent->coords = (Coords) { + coords->x, + coords->y + }; + + agent->is_alive = true; + agent->is_glowing = false; + agent->display_char = SNAKE_AGENT_NORMAL_CHAR; + agent->colour = SNAKE_AGENT_NORMAL_COLOUR; + agent->attributes = A_BOLD; + agent->last_time_moved = 0; + agent->speed = SNAKE_DEFAULT_AGENT_SPEED; +} + +static void snake_dispatch_new_agent(const GameData *game, SnakeState *state) +{ + Coords new_coords; + const Coords *head = snake_get_head_coords(state); + + size_t tries = 0; + + do { + if (tries++ >= 10) { + return; + } + + game_random_coords(game, &new_coords); + } while (COORDINATES_OVERLAP(new_coords.x, new_coords.y, head->x, head->y) + || snake_get_agent_at_coords(state, &new_coords) != NULL); + + snake_initialize_agent(state, &new_coords); +} + +static void snake_place_powerup(const GameData *game, SnakeState *state) +{ + Coords *powerup = &state->powerup; + + if (powerup->x != -1) { + return; + } + + if (!timed_out(state->last_powerup_time, SNAKE_POWERUP_INTERVAL)) { + return; + } + + game_random_coords(game, powerup); +} + +static void snake_do_powerup(const GameData *game, SnakeState *state) +{ + if (!state->has_powerup) { + snake_place_powerup(game, state); + return; + } + + if (timed_out(state->powerup_timer, SNAKE_POWERUP_TIMER)) { + state->last_powerup_time = get_unix_time(); + state->has_powerup = false; + snake_set_body_attributes(state->snake, state->snake_length, SNAKE_BODY_COLOUR, A_BOLD); + } +} + +static void snake_decay_points(GameData *game, SnakeState *state) +{ + + long int score = game_get_score(game); + long int decay = snake_get_move_points(state) / SNAKE_DECAY_POINTS_FRACTION; + + if (score > decay && timed_out(state->last_time_points_decayed, SNAKE_DECAY_POINTS_INTERVAL)) { + game_update_score(game, -decay); + state->last_time_points_decayed = get_unix_time(); + } +} + +static void snake_do_points_update(GameData *game, SnakeState *state, long int points) +{ + snake_update_score(game, state, points); + + if (state->snake_length % SNAKE_LEVEL_UP_FOOD_LIMIT != 0) { + return; + } + + game_increment_level(game); + + size_t level = game_get_current_level(game); + + if (level % SNAKE_LEVEL_SPEED_INTERVAL == 0 && state->snake_speed < SNAKE_MAX_SNAKE_SPEED) { + ++state->snake_speed; + } + + if (level % SNAKE_AGENT_LEVEL_SPEED_INTERVAL == 0) { + for (size_t i = 0; i < state->agent_list_size; ++i) { + NasaAgent *agent = &state->agents[i]; + + if (!agent->is_alive) { + continue; + } + + if (agent->speed < SNAKE_MAX_AGENT_SPEED) { + ++agent->speed; + } + + if (agent->speed > SNAKE_AGENT_GLOWING_SPEED && !agent->is_glowing) { + agent->is_glowing = true; + agent->display_char = SNAKE_AGENT_GLOWING_CHAR; + agent->colour = SNAKE_AGENT_GLOWING_COLOUR; + snake_create_message(game, state->direction, "*glows*", A_BOLD, GREEN, 2, &agent->coords, false); + } + } + } + + snake_dispatch_new_agent(game, state); +} + +static void snake_game_over(SnakeState *state) +{ + state->game_over = true; + state->has_powerup = false; + state->snake[0].colour = SNAKE_DEAD_BODY_COLOUR; + state->snake[0].attributes = A_BOLD | A_BLINK; + + snake_set_body_attributes(state->snake, state->snake_length, SNAKE_DEAD_BODY_COLOUR, A_BOLD | A_BLINK); +} + +static void snake_move(GameData *game, SnakeState *state, TIME_MS cur_time) +{ + const TIME_MS real_speed = GAME_UTIL_REAL_SPEED(state->direction, state->snake_speed); + + if (!game_do_object_state_update(game, cur_time, state->snake_time_last_moved, real_speed)) { + return; + } + + state->snake_time_last_moved = cur_time; + + snake_update_direction(state); + snake_move_body(state); + snake_move_head(state); + + if (!snake_state_valid(game, state)) { + snake_game_over(state); + game_set_status(game, GS_Finished); + return; + } + + long int points = snake_check_food(game, state) + snake_check_powerup(game, state); + + if (points > 0) { + snake_do_points_update(game, state, points); + } +} + +/* + * Attempts to move every agent in list. + * + * If an agent is normal it will move in a random direction. If it's glowing it will + * move towards the snake. + */ +static void snake_agent_move(GameData *game, SnakeState *state, TIME_MS cur_time) +{ + const Coords *head = snake_get_head_coords(state); + + for (size_t i = 0; i < state->agent_list_size; ++i) { + NasaAgent *agent = &state->agents[i]; + + if (!agent->is_alive) { + continue; + } + + Coords *coords = &agent->coords; + + Coords new_coords = (Coords) { + coords->x, + coords->y + }; + + Direction dir = !agent->is_glowing ? game_util_random_direction() + : game_util_move_towards(coords, head, state->has_powerup); + + const TIME_MS real_speed = agent->is_glowing ? GAME_UTIL_REAL_SPEED(dir, agent->speed) : agent->speed; + + if (!game_do_object_state_update(game, cur_time, agent->last_time_moved, real_speed)) { + continue; + } + + agent->last_time_moved = cur_time; + + game_util_move_coords(dir, &new_coords); + + if (!game_coordinates_in_bounds(game, new_coords.x, new_coords.y)) { + continue; + } + + if (snake_coords_contain_body(state, &new_coords)) { + continue; + } + + if (snake_get_agent_at_coords(state, &new_coords) != NULL) { + continue; + } + + coords->x = new_coords.x; + coords->y = new_coords.y; + + if (!state->has_powerup && COORDINATES_OVERLAP(head->x, head->y, new_coords.x, new_coords.y)) { + snake_game_over(state); + game_set_status(game, GS_Finished); + return; + } + } +} + +static void snake_update_frames(const GameData *game, SnakeState *state, TIME_MS cur_time) +{ + if (!game_do_object_state_update(game, cur_time, state->last_draw_update, SNAKE_FRAME_DRAW_SPEED)) { + return; + } + + state->last_draw_update = cur_time; + + if (state->has_powerup) { + const int time_left = SNAKE_POWERUP_TIMER - (get_unix_time() - state->powerup_timer); + + if (time_left <= 5 && time_left % 2 == 0) { + snake_set_body_attributes(state->snake, state->snake_length, SNAKE_BODY_COLOUR, A_BOLD); + } else { + snake_set_body_attributes(state->snake, state->snake_length, -1, A_BOLD); + } + } +} + +static void snake_draw_self(WINDOW *win, const SnakeState *state) +{ + for (size_t i = 0; i < state->snake_length; ++i) { + const Snake *body = &state->snake[i]; + + if (body->coords.x <= 0 || body->coords.y <= 0) { + continue; + } + + wattron(win, body->attributes | COLOR_PAIR(body->colour)); + mvwaddch(win, body->coords.y, body->coords.x, body->display_char); + wattroff(win, body->attributes | COLOR_PAIR(body->colour)); + } +} + +static void snake_draw_food(WINDOW *win, const SnakeState *state) +{ + wattron(win, A_BOLD | COLOR_PAIR(SNAKE_FOOD_COLOUR)); + mvwaddch(win, state->food.y, state->food.x, SNAKE_FOOD_CHAR); + wattroff(win, A_BOLD | COLOR_PAIR(SNAKE_FOOD_COLOUR)); +} + +static void snake_draw_agent(WINDOW *win, const SnakeState *state) +{ + for (size_t i = 0; i < state->agent_list_size; ++i) { + NasaAgent agent = state->agents[i]; + + if (agent.is_alive) { + Coords coords = agent.coords; + + wattron(win, agent.attributes | COLOR_PAIR(agent.colour)); + mvwaddch(win, coords.y, coords.x, agent.display_char); + wattroff(win, agent.attributes | COLOR_PAIR(agent.colour)); + } + } +} + +static void snake_draw_powerup(WINDOW *win, const SnakeState *state) +{ + Coords powerup = state->powerup; + + if (powerup.x != -1) { + int colour = game_util_random_colour(); + + wattron(win, A_BOLD | COLOR_PAIR(colour)); + mvwaddch(win, powerup.y, powerup.x, SNAKE_POWERUP_CHAR); + wattroff(win, A_BOLD | COLOR_PAIR(colour)); + } +} + +void snake_cb_update_game_state(GameData *game, void *cb_data) +{ + if (!cb_data) { + return; + } + + SnakeState *state = (SnakeState *)cb_data; + + TIME_MS cur_time = get_time_millis(); + + snake_do_powerup(game, state); + snake_agent_move(game, state, cur_time); + snake_move(game, state, cur_time); + snake_decay_points(game, state); + + if (!state->game_over) { + snake_update_frames(game, state, cur_time); + } +} + +void snake_cb_render_window(GameData *game, WINDOW *win, void *cb_data) +{ + if (!cb_data) { + return; + } + + SnakeState *state = (SnakeState *)cb_data; + + snake_draw_food(win, state); + snake_draw_powerup(win, state); + snake_draw_agent(win, state); + snake_draw_self(win, state); +} + +void snake_cb_kill(GameData *game, void *cb_data) +{ + if (!cb_data) { + return; + } + + SnakeState *state = (SnakeState *)cb_data; + + free(state->snake); + free(state->agents); + 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_pause(game, NULL, NULL); +} + +void snake_cb_on_keypress(GameData *game, int key, void *cb_data) +{ + if (!cb_data) { + return; + } + + SnakeState *state = (SnakeState *)cb_data; + + snake_set_key_press(state, key); +} + +void snake_cb_pause(GameData *game, bool is_paused, void *cb_data) +{ + UNUSED_VAR(game); + + if (!cb_data) { + return; + } + + SnakeState *state = (SnakeState *)cb_data; + + TIME_S t = get_unix_time(); + + if (is_paused) { + state->pause_time = t; + } else { + state->powerup_timer += (t - state->pause_time); + state->last_powerup_time += (t - state->pause_time); + } +} + +static void snake_initialize_snake_head(const GameData *game, Snake *snake) +{ + int max_x; + int max_y; + game_max_x_y(game, &max_x, &max_y); + + snake[0].coords.x = max_x / 2; + snake[0].coords.y = max_y / 2; + snake[0].colour = SNAKE_HEAD_COLOUR; + snake[0].attributes = A_BOLD; +} + +int snake_initialize(GameData *game) +{ + if (game_set_window_shape(game, GW_ShapeSquare) == -1) { + return -1; + } + + SnakeState *state = calloc(1, sizeof(SnakeState)); + + if (state == NULL) { + return -1; + } + + state->snake = calloc(1, SNAKE_MAX_SNAKE_LENGTH * sizeof(Snake)); + + if (state->snake == NULL) { + free(state); + return -1; + } + + state->agents = calloc(1, SNAKE_AGENT_MAX_LIST_SIZE * sizeof(NasaAgent)); + + if (state->agents == NULL) { + free(state->snake); + free(state); + return -1; + } + + snake_initialize_snake_head(game, state->snake); + + state->snake_speed = SNAKE_DEFAULT_SNAKE_SPEED; + state->snake_length = 1; + state->direction = NORTH; + snake_set_head_char(state); + + state->powerup.x = -1; + state->powerup.y = -1; + + state->last_powerup_time = get_unix_time(); + + game_update_lives(game, -1); + game_increment_level(game); + game_set_update_interval(game, SNAKE_DEFAULT_UPDATE_INTERVAL); + game_random_coords(game, &state->food); + + game_set_cb_update_state(game, snake_cb_update_game_state, state); + game_set_cb_render_window(game, snake_cb_render_window, state); + game_set_cb_on_keypress(game, snake_cb_on_keypress, state); + game_set_cb_kill(game, snake_cb_kill, state); + game_set_cb_on_pause(game, snake_cb_pause, state); + + return 0; +} diff --git a/src/game_snake.h b/src/game_snake.h new file mode 100644 index 0000000..0fe7f04 --- /dev/null +++ b/src/game_snake.h @@ -0,0 +1,30 @@ +/* game_snake.h + * + * + * Copyright (C) 2020 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_SNAKE +#define GAME_SNAKE + +#include "game_base.h" + +int snake_initialize(GameData *game); + +#endif // GAME_SNAKE diff --git a/src/game_util.c b/src/game_util.c new file mode 100644 index 0000000..045837f --- /dev/null +++ b/src/game_util.c @@ -0,0 +1,158 @@ +/* game_util.c + * + * + * Copyright (C) 2020 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 "game_util.h" +#include "toxic.h" +#include "windows.h" + +Direction game_util_get_direction(int key) +{ + switch (key) { + case KEY_UP: { + return NORTH; + } + + case KEY_DOWN: { + return SOUTH; + } + + case KEY_RIGHT: { + return EAST; + } + + case KEY_LEFT: { + return WEST; + } + + default: { + return INVALID_DIRECTION; + } + } +} + +Direction game_util_move_towards(const Coords *coords_a, const Coords *coords_b, bool inverse) +{ + const int x1 = coords_a->x; + const int y1 = coords_a->y; + const int x2 = coords_b->x; + const int y2 = coords_b->y; + + const int x_diff = abs(x1 - x2); + const int y_diff = abs(y1 - y2); + + if (inverse) { + if (x_diff > y_diff) { + return x2 >= x1 ? WEST : EAST; + } else { + return y2 >= y1 ? NORTH : SOUTH; + } + } else { + if (x_diff > y_diff) { + return x2 < x1 ? WEST : EAST; + } else { + return y2 < y1 ? NORTH : SOUTH; + } + } +} + +Direction game_util_random_direction(void) +{ + int r = rand() % 4; + + switch (r) { + case 0: + return NORTH; + + case 1: + return SOUTH; + + case 2: + return EAST; + + case 3: + return WEST; + + default: // impossible + return NORTH; + } +} + +void game_util_move_coords(Direction direction, Coords *coords) +{ + switch (direction) { + case NORTH: { + --(coords->y); + break; + } + + case SOUTH: { + ++(coords->y); + break; + } + + case EAST: { + ++(coords->x); + break; + } + + case WEST: { + --(coords->x); + break; + } + + default: { + fprintf(stderr, "Warning: tried to move in an invalid direction\n"); + return; + } + } +} + +int game_util_random_colour(void) +{ + int r = rand() % 6; + + switch (r) { + case 0: + return GREEN; + + case 1: + return CYAN; + + case 2: + return RED; + + case 3: + return BLUE; + + case 4: + return YELLOW; + + case 5: + return MAGENTA; + + default: // impossible + return RED; + } +} diff --git a/src/game_util.h b/src/game_util.h new file mode 100644 index 0000000..5ad3fc8 --- /dev/null +++ b/src/game_util.h @@ -0,0 +1,92 @@ +/* game_util.h + * + * + * Copyright (C) 2020 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_UTIL +#define GAME_UTIL + +#include + +typedef struct Coords { + int x; + int y; +} Coords; + +// don't change these +typedef enum Direction { + NORTH = 0, + SOUTH = 1, + EAST = 3, + WEST = 4, + INVALID_DIRECTION = 5 +} Direction; + + +/* + * Use these for ms and second timestamps respectively so we don't accidentally interchange them + */ +typedef int64_t TIME_MS; +typedef time_t TIME_S; + + +/* + * Return true if coordinates x1, y1 overlap with x2, y2. + */ +#define COORDINATES_OVERLAP(x1, y1, x2, y2)(((x1) == (x2)) && ((y1) == (y2))) + +/* + * Halves speed if moving north or south. This accounts for the fact that Y steps are twice as large as X steps. + */ +#define GAME_UTIL_REAL_SPEED(dir, speed)((((dir) == (NORTH)) || ((dir) == (SOUTH))) ? (MAX(1, ((speed) / 2))) : (speed)) + +/* + * Return true if dir is a valid Direction. + */ +#define GAME_UTIL_DIRECTION_VALID(dir)(((dir) >= 0) && ((dir) < (INVALID_DIRECTION))) + +/* + * Returns cardinal direction mapped to `key`. + */ +Direction game_util_get_direction(int key); + +/* + * Returns the direction that will move `coords_a` closest to `coords_b`. + * + * If `inverse` is true, returns the opposite result. + */ +Direction game_util_move_towards(const Coords *coords_a, const Coords *coords_b, bool inverse); + +/* + * Returns a random direction. + */ +Direction game_util_random_direction(void); + +/* + * Moves `coords` one square towards `direction`. + */ +void game_util_move_coords(Direction direction, Coords *coords); + +/* + * Returns a random colour. + */ +int game_util_random_colour(void); + +#endif // GAME_UTIL diff --git a/src/global_commands.c b/src/global_commands.c index 47e75cb..7622079 100644 --- a/src/global_commands.c +++ b/src/global_commands.c @@ -26,6 +26,7 @@ #include "avatars.h" #include "conference.h" #include "friendlist.h" +#include "game_base.h" #include "help.h" #include "line_info.h" #include "log.h" @@ -340,6 +341,58 @@ void cmd_decline(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv) --FrndRequests.num_requests; } +void cmd_game(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + UNUSED_VAR(window); + + if (argc < 1) { + game_list_print(self); + return; + } + + GameType type = game_get_type(argv[1]); + + if (type >= GT_Invalid) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Unknown game."); + return; + } + + if (get_num_active_windows() >= MAX_WINDOWS_NUM) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); + return; + } + + bool force_small = false; + + if (argc >= 2) { + force_small = strcasecmp(argv[2], "small") == 0; + + if (!force_small) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Unknown argument."); + return; + } + } + + int ret = game_initialize(self, m, type, force_small); + + switch (ret) { + case 0: { + break; + } + + case -1: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, + "Window is too small. Try enlarging your window or re-running the command with the 'small' argument."); + return; + } + + default: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize (error %d)", ret); + return; + } + } +} + void cmd_conference(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); diff --git a/src/global_commands.h b/src/global_commands.h index 33bec86..ca86a68 100644 --- a/src/global_commands.h +++ b/src/global_commands.h @@ -33,6 +33,7 @@ void cmd_clear(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE void cmd_connect(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_decline(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_game(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_log(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_myid(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #ifdef QRCODE diff --git a/src/prompt.c b/src/prompt.c index 6c3a151..67e3198 100644 --- a/src/prompt.c +++ b/src/prompt.c @@ -60,6 +60,7 @@ static const char *glob_cmd_list[] = { "/decline", "/exit", "/conference", + "/game", "/help", "/log", "/myid", diff --git a/src/toxic.c b/src/toxic.c index 3310fa7..f3963e0 100644 --- a/src/toxic.c +++ b/src/toxic.c @@ -256,7 +256,7 @@ static void init_term(void) keypad(stdscr, 1); noecho(); nonl(); - timeout(50); + timeout(30); if (has_colors()) { short bg_color = COLOR_BLACK; @@ -377,7 +377,9 @@ static void init_term(void) init_pair(MAGENTA, COLOR_MAGENTA, bg_color); init_pair(BLACK, COLOR_BLACK, COLOR_BLACK); init_pair(BLUE_BLACK, COLOR_BLUE, COLOR_BLACK); + init_pair(WHITE_BLUE, COLOR_WHITE, COLOR_BLUE); init_pair(BLACK_WHITE, COLOR_BLACK, COLOR_WHITE); + init_pair(WHITE_BLACK, COLOR_WHITE, COLOR_BLACK); init_pair(BLACK_BG, COLOR_BLACK, bar_bg_color); init_pair(PURPLE_BG, COLOR_MAGENTA, bar_bg_color); init_pair(BAR_TEXT, bar_fg_color, bar_bg_color); @@ -1461,6 +1463,8 @@ int main(int argc, char **argv) /* Make sure all written files are read/writeable only by the current user. */ umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); + srand(time(NULL)); // We use rand() for trivial/non-security related things + parse_args(argc, argv); /* Use the -b flag to enable stderr */ diff --git a/src/windows.c b/src/windows.c index 5f16fac..8104a33 100644 --- a/src/windows.c +++ b/src/windows.c @@ -30,6 +30,7 @@ #include "conference.h" #include "file_transfers.h" #include "friendlist.h" +#include "game_base.h" #include "line_info.h" #include "misc_tools.h" #include "prompt.h" @@ -440,6 +441,9 @@ void on_window_resize(void) refresh(); clear(); + int x2; + int y2; + for (uint8_t i = 0; i < MAX_WINDOWS_NUM; ++i) { ToxWindow *w = windows[i]; @@ -455,6 +459,19 @@ void on_window_resize(void) continue; } + if (w->type == WINDOW_TYPE_GAME) { + delwin(w->window_bar); + delwin(w->window); + delwin(w->game->window); + w->window = newwin(LINES, COLS, 0, 0); + + getmaxyx(w->window, y2, x2); + + w->window_bar = subwin(w->window, WINDOW_BAR_HEIGHT, COLS, LINES - 2, 0); + w->game->window = subwin(w->window, y2 - CHATBOX_HEIGHT - WINDOW_BAR_HEIGHT, x2, 0, 0); + continue; + } + if (w->help->active) { wclear(w->help->win); } @@ -473,8 +490,6 @@ void on_window_resize(void) w->window = newwin(LINES, COLS, 0, 0); - int x2; - int y2; getmaxyx(w->window, y2, x2); if (y2 <= 0 || x2 <= 0) { @@ -710,6 +725,23 @@ void draw_active_window(Tox *m) a->onDraw(a, m); wrefresh(a->window); + if (a->type == WINDOW_TYPE_GAME) { + int ch = getch(); + + if (ch == ERR) { + return; + } + + if (ch == user_settings->key_next_tab || ch == user_settings->key_prev_tab) { + set_next_window(ch); + ch = KEY_F(2); + } + + a->onKey(a, m, ch, false); + + return; + } + wint_t ch = 0; int printable = get_current_char(&ch); @@ -796,14 +828,18 @@ int get_num_active_windows(void) void kill_all_windows(Tox *m) { for (uint8_t i = 2; i < MAX_WINDOWS_NUM; ++i) { - if (windows[i] == NULL) { + ToxWindow *w = windows[i]; + + if (w == NULL) { continue; } - if (windows[i]->type == WINDOW_TYPE_CHAT) { - kill_chat_window(windows[i], m); - } else if (windows[i]->type == WINDOW_TYPE_CONFERENCE) { - free_conference(windows[i], windows[i]->num); + if (w->type == WINDOW_TYPE_CHAT) { + kill_chat_window(w, m); + } else if (w->type == WINDOW_TYPE_CONFERENCE) { + free_conference(w, w->num); + } else if (w->type == WINDOW_TYPE_GAME) { + game_kill(w); } } diff --git a/src/windows.h b/src/windows.h index f5beadd..7d8c765 100644 --- a/src/windows.h +++ b/src/windows.h @@ -55,6 +55,8 @@ typedef enum { BLACK, BLUE_BLACK, BLACK_WHITE, + WHITE_BLACK, + WHITE_BLUE, BAR_TEXT, STATUS_ONLINE, BAR_ACCENT, @@ -78,6 +80,7 @@ typedef enum { WINDOW_TYPE_CHAT, WINDOW_TYPE_CONFERENCE, WINDOW_TYPE_FRIEND_LIST, + WINDOW_TYPE_GAME, } WINDOW_TYPE; /* Fixes text color problem on some terminals. @@ -129,6 +132,7 @@ typedef struct StatusBar StatusBar; typedef struct PromptBuf PromptBuf; typedef struct ChatContext ChatContext; typedef struct Help Help; +typedef struct GameData GameData; struct ToxWindow { /* ncurses */ @@ -193,6 +197,8 @@ struct ToxWindow { StatusBar *stb; Help *help; + GameData *game; + WINDOW *window; WINDOW *window_bar; };