toxic/src/game_centipede.c

1751 lines
46 KiB
C

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#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_DEFAULT * GAME_MAX_SQUARE_X_DEFAULT)
/* 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 7000
/* 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
/* Determines the overall speed of the game per game_set_update_interval() */
#define CENT_GAME_UPDATE_INTERVAL 14
/* How often a head that reaches the 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 300
#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
static const 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;
centipedes->heads[head_idx] = new_head;
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;
}
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) on a timer.
*/
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;
if (state->game_over) {
return;
}
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_keypress(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)
{
// note: If this changes we must update CENT_MUSHROOMS_LENGTH
if (game_set_window_shape(game, GW_ShapeSquare) == -1) {
return -1;
}
CentState *state = calloc(1, sizeof(CentState));
if (state == NULL) {
return -1;
}
game_show_level(game, true);
game_show_score(game, true);
game_show_lives(game, true);
game_show_high_score(game, true);
game_increment_level(game);
game_set_update_interval(game, CENT_GAME_UPDATE_INTERVAL);
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;
}