1
0
mirror of https://github.com/Tha14/toxic.git synced 2024-11-22 09:23:02 +01:00

Add game module (WIP)

This commit is contained in:
jfreegman 2020-12-11 01:46:01 -05:00
parent 556a522637
commit a623976a0e
No known key found for this signature in database
GPG Key ID: 3627F3144076AE63
18 changed files with 4273 additions and 11 deletions

View File

@ -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)

View File

@ -76,6 +76,7 @@ static const char *chat_cmd_list[] = {
"/connect",
"/exit",
"/conference",
"/game",
"/help",
"/invite",
"/join",

View File

@ -88,6 +88,7 @@ static const char *conference_cmd_list[] = {
"/decline",
"/exit",
"/conference",
"/game",
"/help",
"/log",
#ifdef AUDIO

View File

@ -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 },

914
src/game_base.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#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;
}

295
src/game_base.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef GAME_BASE
#define GAME_BASE
#include <ncurses.h>
#include <time.h>
#include <tox/tox.h>
#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

1738
src/game_centipede.c Normal file

File diff suppressed because it is too large Load Diff

30
src/game_centipede.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef GAME_CENTIPEDE
#define GAME_CENTIPEDE
#include "game_base.h"
int centipede_initialize(GameData *game);
#endif // GAME_CENTIPEDE

901
src/game_snake.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

30
src/game_snake.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef GAME_SNAKE
#define GAME_SNAKE
#include "game_base.h"
int snake_initialize(GameData *game);
#endif // GAME_SNAKE

158
src/game_util.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#include <stdlib.h>
#include <stdio.h>
#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;
}
}

92
src/game_util.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef GAME_UTIL
#define GAME_UTIL
#include <stdbool.h>
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

View File

@ -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);

View File

@ -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

View File

@ -60,6 +60,7 @@ static const char *glob_cmd_list[] = {
"/decline",
"/exit",
"/conference",
"/game",
"/help",
"/log",
"/myid",

View File

@ -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 */

View File

@ -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);
}
}

View File

@ -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;
};