Last active
April 26, 2018 09:48
-
-
Save chebert/7e1255304c2fb79c6636 to your computer and use it in GitHub Desktop.
pacman ncurses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| map ,g :!gcc % -o %:r -lncurses && ./%:r <CR> | |
| */ | |
| #include <assert.h> | |
| #include <stdint.h> | |
| #include <ncurses.h> | |
| #include <sys/time.h> | |
| #include <time.h> | |
| #include <stdlib.h> | |
| #include <unistd.h> | |
| void* memset(void* s, int c, size_t n); | |
| char* strchr(const char* s, int c); | |
| #define internal static | |
| #define global_variable static | |
| #define local_persist static | |
| #define UNUSED(x) (void)(x); | |
| #define MIN(x,y) ((x) < (y) ? (x) : (y)) | |
| #define MAX(x,y) ((x) < (y) ? (y) : (x)) | |
| #define ARRAY_LENGTH(arr)\ | |
| (sizeof(arr) / sizeof(arr[0])) | |
| #define for_array(index, arr)\ | |
| for (index = 0; index < ARRAY_LENGTH(arr); ++index) | |
| #define MS(x) (1000*(x)) | |
| #define SECONDS(x) (MS(1000*(x))) | |
| typedef int8_t int8; | |
| typedef uint8_t uint8; | |
| typedef int16_t int16; | |
| typedef uint16_t uint16; | |
| typedef int32_t int32; | |
| typedef uint32_t uint32; | |
| typedef int64_t int64; | |
| typedef uint64_t uint64; | |
| typedef float real32; | |
| typedef double real64; | |
| typedef int32_t bool32; | |
| typedef char* c_str; | |
| struct v2 { | |
| int32 x, y; | |
| }; | |
| inline internal struct v2 | |
| add(struct v2 a, struct v2 b) { | |
| a.x += b.x; | |
| a.y += b.y; | |
| return a; | |
| } | |
| inline internal struct v2 | |
| sub(struct v2 a, struct v2 b) { | |
| a.x -= b.x; | |
| a.y -= b.y; | |
| return a; | |
| } | |
| inline internal bool32 | |
| eql(struct v2* a, struct v2* b) { | |
| return a->x == b->x && a->y == b->y; | |
| } | |
| inline internal struct v2 | |
| scale(struct v2 a, int32 amt) { | |
| a.x *= amt; | |
| a.y *= amt; | |
| return a; | |
| } | |
| inline internal int32 | |
| floor_div(int32 num, int32 den) { | |
| double fresult = (double)num / (double)den; | |
| int32 result = (int32)fresult; | |
| if (fresult < (double)result) { | |
| --result; | |
| } | |
| return result; | |
| } | |
| inline internal struct v2 | |
| div_scale(struct v2 a, int32 amt) { | |
| a.x = floor_div(a.x, amt); | |
| a.y = floor_div(a.y, amt); | |
| return a; | |
| } | |
| uint32 | |
| time_in_us() { | |
| struct timeval t; | |
| gettimeofday(&t, NULL); | |
| return t.tv_sec * 1000000 + t.tv_usec; | |
| } | |
| #define SQUARE(x) ((x)*(x)) | |
| #define DIST_SQUARE(x, y) (SQUARE(x) + SQUARE(y)) | |
| #define PIXEL_SIZE 20 | |
| #define TILE_SIZE_IN_PIXELS 8 | |
| #define TILE_SIZE (PIXEL_SIZE * TILE_SIZE_IN_PIXELS) | |
| #define LIFE_LOST_PAUSE_TIME 30 | |
| #define LIFE_LOST_TURN_TIME 12 | |
| struct v2 TILE_CENTER_IN_PIXELS = { 3, 4 }; | |
| struct v2 TILE_CENTER = { 3*PIXEL_SIZE, 4*PIXEL_SIZE }; | |
| #define NUM_GHOSTS 4 | |
| #define NUM_DIRS 4 | |
| struct view { | |
| int top, left; | |
| struct v2 camera_target_tile; | |
| bool32 zoom_view; | |
| }; | |
| #define ARENA_WIDTH_IN_TILES 28 | |
| #define ARENA_HEIGHT_IN_TILES 31 | |
| #define HOUSE_CENTER (ARENA_WIDTH_IN_TILES / 2) | |
| #define HOUSE_LEFT (HOUSE_CENTER - 3) | |
| #define HOUSE_RIGHT (HOUSE_CENTER + 2) | |
| #define HOUSE_TOP 13 | |
| #define HOUSE_BOTTOM 15 | |
| #define ARENA_HEIGHT (ARENA_HEIGHT_IN_TILES*TILE_SIZE) | |
| #define ARENA_WIDTH (ARENA_WIDTH_IN_TILES*TILE_SIZE) | |
| global_variable | |
| struct v2 top_house_targets[] = { | |
| { 0 }, | |
| { HOUSE_CENTER, HOUSE_TOP }, | |
| { HOUSE_LEFT, HOUSE_TOP }, | |
| { HOUSE_RIGHT, HOUSE_TOP }, | |
| }; | |
| global_variable | |
| struct v2 bottom_house_targets[] = { | |
| { 0 }, | |
| { HOUSE_CENTER, HOUSE_BOTTOM }, | |
| { HOUSE_LEFT, HOUSE_BOTTOM }, | |
| { HOUSE_RIGHT, HOUSE_BOTTOM }, | |
| }; | |
| global_variable | |
| struct v2 forbidden_upward_tiles[] = { | |
| { 12, 10 }, | |
| { 15, 10 }, | |
| { 12, 22 }, | |
| { 15, 22 }, | |
| }; | |
| global_variable | |
| struct v2 fruit_tile = { HOUSE_CENTER, HOUSE_BOTTOM + 2 }; | |
| /* Corners: | |
| * top-left: / | |
| * top-right: ` | |
| * bottom-left: [ | |
| * bottom-right: ] | |
| */ | |
| const | |
| char arena[ARENA_HEIGHT_IN_TILES*ARENA_WIDTH_IN_TILES] = | |
| "/------------`/------------`" | |
| "| || |" | |
| "| /--` /---` || /---` /--` |" | |
| "| |xx| |xxx| || |xxx| |xx| |" | |
| "| [--] [---] [] [---] [--] |" | |
| "| |" | |
| "| /--` /` /------` /` /--` |" | |
| "| [--] || [--`/--] || [--] |" | |
| "| || || || |" | |
| "[----` |[--` || /--]| /----]" | |
| "xxxxx| |/--] [] [--`| |xxxxx" | |
| "xxxxx| || || |xxxxx" | |
| "xxxxx| || +--__--+ || |xxxxx" | |
| "-----] [] |xxxxxx| [] [-----" | |
| " |xxxxxx| " | |
| "-----` /` |xxxxxx| /` /-----" | |
| "xxxxx| || +------+ || |xxxxx" | |
| "xxxxx| || || |xxxxx" | |
| "xxxxx| || /------` || |xxxxx" | |
| "/----] [] [--`/--] [] [----`" | |
| "| || |" | |
| "| /--` /---` || /---` /--` |" | |
| "| [-`| [---] [] [---] |/-] |" | |
| "| || || |" | |
| "[-` || /` /------` /` || /-]" | |
| "/-] [] || [--`/--] || [] [-`" | |
| "| || || || |" | |
| "| /----][--` || /--][----` |" | |
| "| [--------] [] [--------] |" | |
| "| |" | |
| "[--------------------------]" | |
| ; | |
| const | |
| char dot_placement_map[ARENA_HEIGHT_IN_TILES*ARENA_WIDTH_IN_TILES] = | |
| "+------------++------------+" | |
| "|............||............|" | |
| "|.+--+.+---+.||.+---+.+--+.|" | |
| "|*| |.| |.||.| |.| |*|" | |
| "|.+--+.+---+.++.+---+.+--+.|" | |
| "|..........................|" | |
| "|.+--+.++.+------+.++.+--+.|" | |
| "|.+--+.||.+--++--+.||.+--+.|" | |
| "|......||....||....||......|" | |
| "+----+.|+--+ || +--+|.+----+" | |
| " |.|+--+ ++ +--+|.| " | |
| " |.|| ||.| " | |
| " |.|| +--__--+ ||.| " | |
| "-----+.++ | | ++.+-----" | |
| " . | | . " | |
| "-----+.++ | | ++.+-----" | |
| " |.|| +------+ ||.| " | |
| " |.|| ||.| " | |
| " |.|| +------+ ||.| " | |
| "+----+.++ +--++--+ ++.+----+" | |
| "|............||............|" | |
| "|.+--+.+---+.||.+---+.+--+.|" | |
| "|.+-+|.+---+.++.+---+.|+-+.|" | |
| "|*..||....... .......||..*|" | |
| "+-+.||.++.+------+.++.||.+-+" | |
| "+-+.++.||.+--++--+.||.++.+-+" | |
| "|......||....||....||......|" | |
| "|.+----++--+.||.+--++----+.|" | |
| "|.+--------+.++.+--------+.|" | |
| "|..........................|" | |
| "+--------------------------+" | |
| ; | |
| char dot_map[ARENA_HEIGHT_IN_TILES][ARENA_WIDTH_IN_TILES]; | |
| enum { | |
| BG_PAIR = 1, | |
| PACMAN_PAIR, | |
| DOT_PAIR, | |
| GAME_OVER_TEXT_PAIR, | |
| BLINKY_PAIR, | |
| INKY_PAIR, | |
| PINKY_PAIR, | |
| CLYDE_PAIR, | |
| FRIGHT_PAIR, | |
| FRIGHT_FLASH_PAIR, | |
| EYES_PAIR, | |
| EMPTY_PAIR, | |
| WALL_PAIR, | |
| DOOR_PAIR, | |
| CHERRIES_PAIR, | |
| STRAWBERRY_PAIR, | |
| PEACH_PAIR, | |
| APPLE_PAIR, | |
| GRAPES_PAIR, | |
| GALAXIAN_PAIR, | |
| BELL_PAIR, | |
| KEY_PAIR, | |
| }; | |
| internal char | |
| arena_get(int row, int col) { | |
| assert(row >= 0 && row < ARENA_HEIGHT_IN_TILES); | |
| if (col < 0) { | |
| col += ARENA_WIDTH_IN_TILES; | |
| } else if (col >= ARENA_WIDTH_IN_TILES) { | |
| col -= ARENA_WIDTH_IN_TILES; | |
| } | |
| return arena[row*ARENA_WIDTH_IN_TILES + col]; | |
| } | |
| enum dir { | |
| UP, | |
| LEFT, | |
| DOWN, | |
| RIGHT | |
| }; | |
| #define DRAW_SIZE 8 | |
| internal struct v2 | |
| draw_pos(int row, int col, struct view* view) { | |
| struct v2 ret = { | |
| 3 * (col - view->camera_target_tile.x) + view->left, | |
| (row - view->camera_target_tile.y) + view->top, | |
| }; | |
| return ret; | |
| } | |
| internal struct v2 | |
| draw_pos_v2(struct v2 p, struct view* view) { | |
| return draw_pos(p.y, p.x, view); | |
| } | |
| internal void | |
| draw_tile(int row, int col, char ch, struct view* view, char fill_ch) { | |
| if (view->zoom_view) { | |
| int tile_size = DRAW_SIZE; | |
| int i; | |
| for (i = 0; i < tile_size; ++i) { | |
| int j; | |
| for (j = 0; j < tile_size; ++j) { | |
| mvaddch(tile_size * (row - view->camera_target_tile.y) + view->top + j, tile_size * (col - view->camera_target_tile.x) + view->left + i, fill_ch); | |
| } | |
| } | |
| } else { | |
| struct v2 pos = draw_pos(row, col, view); | |
| mvaddch(pos.y, pos.x, fill_ch); | |
| mvaddch(pos.y, pos.x + 1, ch); | |
| mvaddch(pos.y, pos.x + 2, fill_ch); | |
| } | |
| } | |
| internal void | |
| draw_tile_v2(struct v2 tile_pos, char ch, struct view* view, char fill_ch) { | |
| draw_tile(tile_pos.y, tile_pos.x, ch, view, fill_ch); | |
| } | |
| enum { | |
| COLOR_LIGHT_BLUE=51, | |
| COLOR_PINK=197, | |
| COLOR_ORANGE=172, | |
| COLOR_DARK_RED=88, | |
| COLOR_BRIGHT_RED=196, | |
| COLOR_PURPLE=91, | |
| COLOR_DOOR_RED=124, | |
| COLOR_PACMAN_YELLOW=226, | |
| COLOR_WALL_BLUE=21, | |
| COLOR_DARK_BACKGROUND=232, | |
| COLOR_GREY=233, | |
| }; | |
| const char* DIR_NAMES[] = { | |
| "Up", "Down", "Left", "Right" | |
| }; | |
| internal bool32 | |
| is_h_dir(enum dir dir) { | |
| return dir == LEFT || dir == RIGHT; | |
| } | |
| internal void | |
| move_in_dir(enum dir dir, struct v2* pos, int amt) { | |
| switch (dir) { | |
| case RIGHT: | |
| pos->x += amt; | |
| break; | |
| case LEFT: | |
| pos->x -= amt; | |
| break; | |
| case UP: | |
| pos->y -= amt; | |
| break; | |
| case DOWN: | |
| pos->y += amt; | |
| break; | |
| } | |
| } | |
| internal struct v2 | |
| pos_to_tile(struct v2* p) { | |
| return div_scale(*p, TILE_SIZE); | |
| } | |
| int num_blocked_tiles = 0; | |
| struct v2 blocked_tiles[20]; | |
| enum ghost_mode { | |
| NORMAL, | |
| FRIGHTENED, | |
| EYES, | |
| }; | |
| internal bool32 | |
| test_pacman_dir_blocked(enum dir dir, struct v2* next_pos) { | |
| int mv_amt = TILE_SIZE / 2; | |
| if (dir == RIGHT || dir == UP) { | |
| mv_amt += PIXEL_SIZE; | |
| } | |
| move_in_dir(dir, next_pos, mv_amt); | |
| struct v2 next_tile = pos_to_tile(next_pos); | |
| return !!strchr("/`[]-|+_", arena_get(next_tile.y, next_tile.x)); | |
| } | |
| inline internal uint32 | |
| get_speed(int percentage) { | |
| return percentage / (100 / PIXEL_SIZE); | |
| } | |
| /* TODO | |
| * | |
| * BUG: Ghosts get stuck! | |
| * Try: | |
| * start from every position, and make sure inky can get out. | |
| * | |
| * onomatopoeic sound effect? | |
| * intro screen? | |
| * | |
| */ | |
| enum bonus_symbol { | |
| CHERRIES, | |
| STRAWBERRY, | |
| PEACH, | |
| APPLE, | |
| GRAPES, | |
| GALAXIAN, | |
| BELL, | |
| KEY | |
| }; | |
| struct level_constants { | |
| enum bonus_symbol bonus_symbol; | |
| uint32 pacman_speed, pacman_powerup_speed; | |
| uint32 ghost_speed, ghost_tunnel_speed, ghost_frightened_speed; | |
| uint32 num_ghost_flashes; | |
| uint32 pacman_powerup_time; | |
| uint32 elroy_v1_dots_left, elroy_v1_speed; | |
| uint32 scatter_times[4]; | |
| uint32 chase_times[3]; | |
| uint32 ghost_dot_limits[NUM_GHOSTS]; | |
| }; | |
| inline internal uint32 | |
| seconds_to_frames(uint32 seconds) { | |
| return SECONDS(seconds) / MS(16); | |
| } | |
| enum ghost_name { | |
| BLINKY, | |
| PINKY, | |
| INKY, | |
| CLYDE | |
| }; | |
| uint32 symbol_points[] = { | |
| 100, 300, 500, 700, 1000, 2000, 3000, 5000 | |
| }; | |
| internal enum bonus_symbol | |
| get_symbol_for_level(uint32 level) { | |
| enum bonus_symbol level_symbols[] = { | |
| CHERRIES, STRAWBERRY, PEACH, PEACH, APPLE, APPLE, GRAPES, GRAPES, | |
| GALAXIAN, GALAXIAN, BELL, BELL | |
| }; | |
| if (level < ARRAY_LENGTH(level_symbols)) { | |
| return level_symbols[level]; | |
| } else { | |
| return KEY; | |
| } | |
| } | |
| internal void | |
| set_level_constants(struct level_constants* level_constants, uint32 level) { | |
| level_constants->bonus_symbol = get_symbol_for_level(level); | |
| if (level == 0) { | |
| level_constants->pacman_speed = get_speed(80); | |
| level_constants->pacman_powerup_speed = get_speed(90); | |
| } else if (level < 4 || level >= 20) { | |
| level_constants->pacman_speed = get_speed(90); | |
| level_constants->pacman_powerup_speed = get_speed(95); | |
| } else { | |
| level_constants->pacman_powerup_speed = level_constants->pacman_speed = get_speed(100); | |
| } | |
| if (level == 0) { | |
| level_constants->ghost_speed = get_speed(75); | |
| level_constants->ghost_tunnel_speed = get_speed(40); | |
| level_constants->ghost_frightened_speed = get_speed(50); | |
| } else if (level < 4) { | |
| level_constants->ghost_speed = get_speed(85); | |
| level_constants->ghost_tunnel_speed = get_speed(45); | |
| level_constants->ghost_frightened_speed = get_speed(55); | |
| } else { | |
| level_constants->ghost_speed = get_speed(95); | |
| level_constants->ghost_tunnel_speed = get_speed(50); | |
| level_constants->ghost_frightened_speed = get_speed(60); | |
| } | |
| if ((level != 8 && level <= 10) || level == 14) { | |
| level_constants->num_ghost_flashes = 5; | |
| } else { | |
| level_constants->num_ghost_flashes = 3; | |
| } | |
| uint32 pacman_powerup_times[] = { | |
| 6, 5, 4, 3, 2, 5, 2, 2, 1, 5, 2, 1, 1, 3, 1, 1, 0, 1 | |
| }; | |
| if (level == 16 || level >= ARRAY_LENGTH(pacman_powerup_times)) { | |
| level_constants->pacman_powerup_time = 1; | |
| } else { | |
| level_constants->pacman_powerup_time = seconds_to_frames(pacman_powerup_times[level]); | |
| } | |
| level_constants->elroy_v1_speed = get_speed(100); | |
| if (level == 0) { | |
| level_constants->elroy_v1_dots_left = 20; | |
| level_constants->elroy_v1_speed = get_speed(80); | |
| } else if (level == 1) { | |
| level_constants->elroy_v1_dots_left = 30; | |
| level_constants->elroy_v1_speed = get_speed(90); | |
| } else if (level < 5) { | |
| level_constants->elroy_v1_dots_left = 40; | |
| } else if (level < 8) { | |
| level_constants->elroy_v1_dots_left = 50; | |
| } else if (level < 11) { | |
| level_constants->elroy_v1_dots_left = 60; | |
| } else if (level < 14) { | |
| level_constants->elroy_v1_dots_left = 80; | |
| } else if (level < 18) { | |
| level_constants->elroy_v1_dots_left = 100; | |
| } else { | |
| level_constants->elroy_v1_dots_left = 120; | |
| } | |
| level_constants->chase_times[0] = level_constants->chase_times[1] = seconds_to_frames(20); | |
| if (level == 0) { | |
| level_constants->scatter_times[0] = level_constants->scatter_times[1] = seconds_to_frames(7); | |
| level_constants->scatter_times[2] = level_constants->scatter_times[3] = seconds_to_frames(5); | |
| level_constants->chase_times[2] = seconds_to_frames(20); | |
| } else if (level < 4) { | |
| level_constants->scatter_times[0] = level_constants->scatter_times[1] = seconds_to_frames(7); | |
| level_constants->scatter_times[2] = seconds_to_frames(5); | |
| level_constants->scatter_times[3] = 1; | |
| level_constants->chase_times[2] = seconds_to_frames(1033); | |
| } else { | |
| level_constants->scatter_times[0] = level_constants->scatter_times[1] = level_constants->scatter_times[2] = seconds_to_frames(5); | |
| level_constants->scatter_times[3] = 1; | |
| level_constants->chase_times[2] = seconds_to_frames(1037); | |
| } | |
| level_constants->ghost_dot_limits[BLINKY] = 0; | |
| level_constants->ghost_dot_limits[PINKY] = 0; | |
| if (level == 0) { | |
| level_constants->ghost_dot_limits[INKY] = 30; | |
| level_constants->ghost_dot_limits[CLYDE] = 60; | |
| } else if (level == 1) { | |
| level_constants->ghost_dot_limits[INKY] = 0; | |
| level_constants->ghost_dot_limits[CLYDE] = 50; | |
| } else { | |
| level_constants->ghost_dot_limits[INKY] = 0; | |
| level_constants->ghost_dot_limits[CLYDE] = 0; | |
| } | |
| } | |
| enum ghost_house_state { | |
| EXITING_GHOST_HOUSE, | |
| OUTSIDE_GHOST_HOUSE, | |
| ENTERING_GHOST_HOUSE, | |
| IN_GHOST_HOUSE, | |
| }; | |
| struct ghost { | |
| struct v2 pos; | |
| enum dir dir; | |
| struct v2 last_tile; | |
| bool32 is_path_chosen; | |
| enum dir chosen_dir; | |
| struct v2 target_tile; | |
| enum ghost_mode mode; | |
| enum ghost_name name; | |
| char nickname; | |
| int curses_color_pair; | |
| enum ghost_house_state ghost_house_state; | |
| uint32 dot_counter; | |
| bool32 bounce_target_bottom; | |
| }; | |
| enum game_mode { | |
| PAUSED_BEFORE_PLAYING, | |
| PLAYING, | |
| LOSING_A_LIFE, | |
| GAME_OVER, | |
| LEVEL_TRANSITION, | |
| }; | |
| #define TUNNEL_WIDTH (4*TILE_SIZE) | |
| #define GHOST_FLASH_TIME 10 | |
| internal void | |
| init_ghost(struct ghost* ghost_in, char nickname, enum ghost_name name, int curses_color_pair) { | |
| struct ghost ghost; | |
| memset(&ghost, 0, sizeof(ghost)); | |
| ghost.nickname = nickname; | |
| ghost.name = name; | |
| ghost.curses_color_pair = curses_color_pair; | |
| *ghost_in = ghost; | |
| } | |
| internal void | |
| reverse_ghosts(struct ghost* ghosts) { | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| ghosts[i].is_path_chosen = TRUE; | |
| if (ghosts[i].mode == NORMAL && ghosts[i].ghost_house_state == OUTSIDE_GHOST_HOUSE) { | |
| switch (ghosts[i].dir) { | |
| case UP: | |
| ghosts[i].chosen_dir = DOWN; | |
| break; | |
| case DOWN: | |
| ghosts[i].chosen_dir = UP; | |
| break; | |
| case LEFT: | |
| ghosts[i].chosen_dir = RIGHT; | |
| break; | |
| case RIGHT: | |
| ghosts[i].chosen_dir = LEFT; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| const | |
| enum dir ghost_start_dirs[NUM_GHOSTS] = { | |
| LEFT, UP, UP, UP | |
| }; | |
| #define NUM_DOTS 244 | |
| struct level_data { | |
| struct ghost* ghost_to_be_eaten; | |
| uint32 pacman_chomp_timer, pacman_eat_timer, | |
| ghost_eat_timer, pacman_powerup_timer, | |
| ghost_mode_timer, fruit_timer, | |
| ghost_flash_timer, num_ghost_mode_cycles, | |
| fruit_score_timer, consecutive_ghosts_eaten, | |
| extra_life_timer; | |
| uint32 fright_ghost_seed; | |
| uint32 global_ghost_house_dot_counter; | |
| bool32 fruit_is_visible, pacman_blocked, pacman_turning, is_chasing, use_global_ghost_house_dot_counter; | |
| uint32 dots_eaten; | |
| struct v2 pacman_pos; | |
| enum dir pacman_dir, next_dir; | |
| uint32 dot_timer; | |
| }; | |
| internal void | |
| get_normal_target_tile( | |
| struct level_data* level_data, | |
| struct v2 scatter_targets[NUM_GHOSTS], | |
| struct v2* blinky_pos, struct ghost* ghost, struct v2 ghost_tile) { | |
| struct v2 pacman_tile = pos_to_tile(&level_data->pacman_pos); | |
| /* Chase after a target */ | |
| if (level_data->is_chasing) { | |
| switch (ghost->name) { | |
| case BLINKY: { | |
| /* Follow pacman */ | |
| ghost->target_tile = pacman_tile; | |
| } break; | |
| case PINKY: { | |
| /* Follow pacmans direction of motion. */ | |
| struct v2 target = pacman_tile; | |
| move_in_dir(level_data->pacman_dir, &target, 4); | |
| ghost->target_tile = target; | |
| } break; | |
| case INKY: { | |
| /* This gets close sometimes, but usually runs away. */ | |
| struct v2 blinky_tile = pos_to_tile(blinky_pos); | |
| struct v2 target = pacman_tile; | |
| move_in_dir(level_data->pacman_dir, &target, 2); | |
| struct v2 delta = sub(target, blinky_tile); | |
| target = add(target, delta); | |
| ghost->target_tile = target; | |
| } break; | |
| case CLYDE: { | |
| struct v2 target = pacman_tile; | |
| struct v2 delta = sub(ghost_tile, target); | |
| int dist_square = DIST_SQUARE(delta.x, delta.y); | |
| /* Follow pacman from a distance */ | |
| if (dist_square >= SQUARE(8)) { | |
| ghost->target_tile = target; | |
| } else { | |
| /* Scatter if too close. */ | |
| ghost->target_tile = scatter_targets[CLYDE]; | |
| } | |
| } break; | |
| } | |
| } else { | |
| ghost->target_tile = scatter_targets[ghost->name]; | |
| } | |
| } | |
| internal void | |
| return_ghosts_and_pacman_to_start_position(struct ghost ghosts[NUM_GHOSTS], struct level_data* level_data) { | |
| struct v2 pacman_start_pos = { ARENA_WIDTH / 2, (ARENA_HEIGHT_IN_TILES - 8)*TILE_SIZE + TILE_CENTER.y }; | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| struct v2 start_position; | |
| switch (i) { | |
| case BLINKY: { | |
| struct v2 blinky_start_pos_s = { ARENA_WIDTH / 2 - 2*PIXEL_SIZE, 11*TILE_SIZE + TILE_CENTER.y }; | |
| start_position = blinky_start_pos_s; | |
| } break; | |
| case INKY: | |
| start_position = add(TILE_CENTER, scale(bottom_house_targets[INKY], TILE_SIZE)); | |
| break; | |
| case PINKY: | |
| start_position = add(TILE_CENTER, scale(bottom_house_targets[PINKY], TILE_SIZE)); | |
| break; | |
| case CLYDE: | |
| start_position = add(TILE_CENTER, scale(bottom_house_targets[CLYDE], TILE_SIZE)); | |
| break; | |
| } | |
| ghosts[i].pos = start_position; | |
| ghosts[i].last_tile = pos_to_tile(&ghosts[i].pos); | |
| ghosts[i].dir = ghost_start_dirs[i]; | |
| ghosts[i].is_path_chosen = FALSE; | |
| ghosts[i].mode = NORMAL; | |
| ghosts[i].bounce_target_bottom = FALSE; | |
| if (i == BLINKY) { | |
| ghosts[i].ghost_house_state = OUTSIDE_GHOST_HOUSE; | |
| } else { | |
| ghosts[i].ghost_house_state = IN_GHOST_HOUSE; | |
| } | |
| } | |
| level_data->pacman_pos = pacman_start_pos; | |
| level_data->pacman_dir = LEFT; | |
| level_data->pacman_blocked = FALSE; | |
| level_data->pacman_turning = FALSE; | |
| level_data->next_dir = level_data->pacman_dir; | |
| level_data->dot_timer = 0; | |
| } | |
| internal void | |
| start_new_level(struct ghost ghosts[NUM_GHOSTS], struct level_data* level_data, uint32 original_fright_ghost_seed) { | |
| int ghost; | |
| for (ghost = 0; ghost < NUM_GHOSTS; ++ghost) { | |
| ghosts[ghost].dot_counter = 0; | |
| } | |
| int row, col; | |
| for (row = 0; row < ARENA_HEIGHT_IN_TILES; ++ row) { | |
| for (col = 0; col < ARENA_WIDTH_IN_TILES; ++ col) { | |
| char ch = dot_placement_map[row*ARENA_WIDTH_IN_TILES + col]; | |
| if (ch == '.' || ch == '*') { | |
| dot_map[row][col] = ch; | |
| } else { | |
| dot_map[row][col] = '\0'; | |
| } | |
| } | |
| } | |
| memset(level_data, 0, sizeof(struct level_data)); | |
| level_data->fright_ghost_seed = original_fright_ghost_seed; | |
| level_data->ghost_mode_timer = 0; | |
| return_ghosts_and_pacman_to_start_position(ghosts, level_data); | |
| } | |
| struct game_data { | |
| uint32 num_extra_lives; | |
| uint32 current_level; | |
| uint32 score; | |
| uint32 original_fright_ghost_seed; | |
| }; | |
| internal void | |
| start_new_game(struct game_data* game_data, struct ghost ghosts[NUM_GHOSTS], struct level_data* level_data, struct level_constants* level_constants) { | |
| game_data->num_extra_lives = 2; | |
| game_data->current_level = 0; | |
| game_data->score = 0; | |
| game_data->original_fright_ghost_seed = time(NULL); | |
| set_level_constants(level_constants, game_data->current_level); | |
| init_ghost(&ghosts[BLINKY], 'B', BLINKY, BLINKY_PAIR); | |
| init_ghost(&ghosts[INKY], 'I', INKY, INKY_PAIR); | |
| init_ghost(&ghosts[PINKY], 'P', PINKY, PINKY_PAIR); | |
| init_ghost(&ghosts[CLYDE], 'C', CLYDE, CLYDE_PAIR); | |
| start_new_level(ghosts, level_data, game_data->original_fright_ghost_seed); | |
| } | |
| internal void | |
| enable_bonus_symbol_color(enum bonus_symbol bonus_symbol) { | |
| attron(COLOR_PAIR(bonus_symbol + CHERRIES_PAIR)); | |
| } | |
| internal void | |
| draw_fruit(struct v2 left_tile, enum bonus_symbol bonus_symbol, struct view* view) { | |
| const char* bonus_strs[] = { | |
| "^00", | |
| "<0'", | |
| "(0)", | |
| "`0)", | |
| "`88", | |
| "|^|", | |
| "3>-", | |
| "3-^", | |
| }; | |
| enable_bonus_symbol_color(bonus_symbol); | |
| struct v2 pos = draw_pos_v2(left_tile, view); | |
| mvprintw(pos.y, pos.x, bonus_strs[bonus_symbol]); | |
| } | |
| internal uint32 | |
| get_ghost_score(uint32 ghosts_eaten) { | |
| return (2 << ghosts_eaten) * 100; | |
| } | |
| internal void | |
| add_extra_life(struct game_data* game_data, struct level_data* level_data) { | |
| level_data->extra_life_timer = seconds_to_frames(1); | |
| if (game_data->num_extra_lives < 5) { | |
| ++game_data->num_extra_lives; | |
| } | |
| } | |
| int main(int argc, char** argv) { | |
| UNUSED(argc); | |
| UNUSED(argv); | |
| /* Initialize curses */ | |
| initscr(); | |
| cbreak(); | |
| keypad(stdscr, TRUE); | |
| noecho(); | |
| curs_set(FALSE); | |
| timeout(0); | |
| enum game_mode game_mode = GAME_OVER; | |
| uint32 transition_timer = 0; | |
| bool32 running = TRUE; | |
| uint32 last_update = time_in_us(); | |
| uint32 frame_timer = 0; | |
| uint32 high_score = 0; | |
| bool32 has_color_terminal = has_colors() && can_change_color(); | |
| start_color(); | |
| if (!has_color_terminal || COLORS != 256) { | |
| mvprintw(0, 0, "Warning: Colors not enabled for this terminal.\nAuthors suggestion: try running in the unicode-rxvt terminal.\nPress any key to continue."); | |
| mvprintw(0, 0, "%u colors available in this terminal. This game currently supports 256.", COLORS); | |
| timeout(-1); | |
| getch(); | |
| timeout(0); | |
| } else { | |
| init_pair(BG_PAIR, COLOR_WHITE, COLOR_DARK_BACKGROUND); | |
| int bkgd_color = COLOR_GREY; | |
| init_pair(EMPTY_PAIR, COLOR_WHITE, bkgd_color); | |
| init_pair(WALL_PAIR, COLOR_WALL_BLUE, COLOR_DARK_BACKGROUND); | |
| init_pair(DOOR_PAIR, COLOR_DOOR_RED, COLOR_DARK_BACKGROUND); | |
| init_pair(PACMAN_PAIR, COLOR_PACMAN_YELLOW, bkgd_color); | |
| init_pair(DOT_PAIR, COLOR_WHITE, bkgd_color); | |
| init_pair(GAME_OVER_TEXT_PAIR, COLOR_DOOR_RED, bkgd_color); | |
| init_pair(BLINKY_PAIR, COLOR_WHITE, COLOR_DOOR_RED); | |
| init_pair(INKY_PAIR, COLOR_WHITE, COLOR_LIGHT_BLUE); | |
| init_pair(PINKY_PAIR, COLOR_WHITE, COLOR_PINK); | |
| init_pair(CLYDE_PAIR, COLOR_WHITE, COLOR_ORANGE); | |
| init_pair(FRIGHT_PAIR, COLOR_WHITE, COLOR_WALL_BLUE); | |
| init_pair(FRIGHT_FLASH_PAIR, COLOR_WALL_BLUE, COLOR_WHITE); | |
| init_pair(EYES_PAIR, COLOR_WHITE, bkgd_color); | |
| init_pair(CHERRIES_PAIR, COLOR_DARK_RED, bkgd_color); | |
| init_pair(STRAWBERRY_PAIR, COLOR_BRIGHT_RED, bkgd_color); | |
| init_pair(PEACH_PAIR, COLOR_ORANGE, bkgd_color); | |
| init_pair(APPLE_PAIR, COLOR_DOOR_RED, bkgd_color); | |
| init_pair(GRAPES_PAIR, COLOR_PURPLE, bkgd_color); | |
| init_pair(GALAXIAN_PAIR, COLOR_ORANGE, bkgd_color); | |
| init_pair(BELL_PAIR, COLOR_PACMAN_YELLOW, bkgd_color); | |
| init_pair(KEY_PAIR, COLOR_LIGHT_BLUE, bkgd_color); | |
| bkgd(COLOR_PAIR(BG_PAIR)); | |
| } | |
| bool32 debug_ghost_mode_overridden = FALSE, debug_no_death_mode = FALSE; | |
| struct view view = { 0 }; | |
| struct ghost ghosts[NUM_GHOSTS]; | |
| struct level_data level_data; | |
| struct level_constants level_constants; | |
| struct game_data game_data; | |
| start_new_game(&game_data, ghosts, &level_data, &level_constants); | |
| struct v2 pacman_turn_tile; | |
| uint32 frame_times[] = { 8, 16, 50, 100, 150 }; | |
| int frame_time_index = 1; | |
| while (running) { | |
| /* Get Input */ | |
| int ch; | |
| while ((ch = getch()) != -1) { | |
| switch (ch) { | |
| case 'q': | |
| case 'Q': | |
| running = FALSE; | |
| continue; | |
| #ifdef DEBUG | |
| case 'j': | |
| case 'J': | |
| --frame_time_index; | |
| if (frame_time_index < 0) { | |
| frame_time_index = ARRAY_LENGTH(frame_times) - 1; | |
| } | |
| break; | |
| case 'k': | |
| case 'K': | |
| ++frame_time_index; | |
| if (frame_time_index == ARRAY_LENGTH(frame_times)) { | |
| frame_time_index = 0; | |
| } | |
| break; | |
| case 'z': | |
| case 'Z': | |
| view.zoom_view = !view.zoom_view; | |
| break; | |
| case 'e': | |
| case 'E': { | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| if (ghosts[i].ghost_house_state == OUTSIDE_GHOST_HOUSE) { | |
| ghosts[i].mode = EYES; | |
| } | |
| } | |
| } break; | |
| case 'f': | |
| case 'F': | |
| game_mode = LEVEL_TRANSITION; | |
| transition_timer = 0; | |
| break; | |
| case 'l': | |
| case 'L': | |
| game_mode = LOSING_A_LIFE; | |
| transition_timer = 0; | |
| break; | |
| case '+': | |
| add_extra_life(&game_data, &level_data); | |
| break; | |
| case '-': | |
| if (game_data.num_extra_lives > 0) { | |
| --game_data.num_extra_lives; | |
| } | |
| break; | |
| case 'c': | |
| case 'C': | |
| debug_ghost_mode_overridden = TRUE; | |
| level_data.is_chasing = !level_data.is_chasing; | |
| break; | |
| case 'd': | |
| case 'D': | |
| debug_no_death_mode = !debug_no_death_mode; | |
| break; | |
| case 'r': | |
| case 'R': | |
| reverse_ghosts(ghosts); | |
| break; | |
| case 'p': | |
| case 'P': | |
| start_new_game(&game_data, ghosts, &level_data, &level_constants); | |
| transition_timer = 0; | |
| game_mode = PAUSED_BEFORE_PLAYING; | |
| break; | |
| #endif | |
| } | |
| if (game_mode == PLAYING && !level_data.pacman_turning) { | |
| switch (ch) { | |
| case KEY_LEFT: | |
| level_data.next_dir = LEFT; | |
| break; | |
| case KEY_RIGHT: | |
| level_data.next_dir = RIGHT; | |
| break; | |
| case KEY_UP: | |
| level_data.next_dir = UP; | |
| break; | |
| case KEY_DOWN: | |
| level_data.next_dir = DOWN; | |
| break; | |
| } | |
| } else if (game_mode == GAME_OVER) { | |
| start_new_game(&game_data, ghosts, &level_data, &level_constants); | |
| transition_timer = 0; | |
| game_mode = PAUSED_BEFORE_PLAYING; | |
| } | |
| } | |
| frame_timer += time_in_us() - last_update; | |
| last_update = time_in_us(); | |
| if (frame_timer > MS(frame_times[frame_time_index])) { | |
| if (!level_data.ghost_eat_timer) { | |
| ++level_data.pacman_chomp_timer; | |
| if (level_data.fruit_is_visible) { | |
| ++level_data.fruit_timer; | |
| if (level_data.fruit_timer > seconds_to_frames(9)) { | |
| level_data.fruit_is_visible = FALSE; | |
| } | |
| } | |
| } | |
| switch (game_mode) { | |
| case PAUSED_BEFORE_PLAYING: { | |
| transition_timer++; | |
| if (transition_timer >= seconds_to_frames(2)) { | |
| transition_timer = 0; | |
| game_mode = PLAYING; | |
| } | |
| } break; | |
| case PLAYING: { | |
| uint32 pacman_speed; | |
| uint32 old_score = game_data.score; | |
| num_blocked_tiles = 0; | |
| if (level_data.ghost_eat_timer) { | |
| --level_data.ghost_eat_timer; | |
| if (!level_data.ghost_eat_timer) { | |
| level_data.ghost_to_be_eaten->mode = EYES; | |
| level_data.ghost_to_be_eaten = NULL; | |
| } | |
| } else if (level_data.pacman_powerup_timer) { | |
| --level_data.pacman_powerup_timer; | |
| pacman_speed = level_constants.pacman_powerup_speed; | |
| if (!level_data.pacman_powerup_timer) { | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| if (ghosts[i].mode != EYES) { | |
| ghosts[i].mode = NORMAL; | |
| } | |
| } | |
| } else if (level_data.pacman_powerup_timer <= level_constants.num_ghost_flashes * 2 * GHOST_FLASH_TIME) { | |
| ++level_data.ghost_flash_timer; | |
| } | |
| } else { | |
| pacman_speed = level_constants.pacman_speed; | |
| } | |
| if (level_data.pacman_eat_timer) { | |
| --level_data.pacman_eat_timer; | |
| } else if (!level_data.ghost_eat_timer) { | |
| if (level_data.pacman_dir != level_data.next_dir) { | |
| if (is_h_dir(level_data.pacman_dir) == is_h_dir(level_data.next_dir)) { | |
| level_data.pacman_dir = level_data.next_dir; | |
| } else { | |
| struct v2 blocked_pos_s = level_data.pacman_pos; | |
| bool32 blocked = test_pacman_dir_blocked(level_data.next_dir, &blocked_pos_s); | |
| if (blocked) { | |
| blocked_tiles[num_blocked_tiles++] = pos_to_tile(&blocked_pos_s); | |
| } else { | |
| level_data.pacman_turning = TRUE; | |
| pacman_turn_tile = pos_to_tile(&level_data.pacman_pos); | |
| } | |
| } | |
| } | |
| if (level_data.pacman_turning) { | |
| move_in_dir(level_data.next_dir, &level_data.pacman_pos, pacman_speed); | |
| /* Move toward the center of the turn_tile */ | |
| struct v2 tile_center = add(scale(pacman_turn_tile, TILE_SIZE), TILE_CENTER); | |
| bool32 reached_centerline = FALSE; | |
| if (is_h_dir(level_data.pacman_dir)) { | |
| int mv_amt = tile_center.x - level_data.pacman_pos.x; | |
| if (mv_amt < 0) { | |
| level_data.pacman_pos.x -= MIN(-mv_amt, pacman_speed); | |
| reached_centerline = -mv_amt < pacman_speed; | |
| } else { | |
| level_data.pacman_pos.x += MIN(mv_amt, pacman_speed); | |
| reached_centerline = mv_amt < pacman_speed; | |
| } | |
| } else { | |
| int mv_amt = tile_center.y - level_data.pacman_pos.y; | |
| if (mv_amt < 0) { | |
| level_data.pacman_pos.y -= MIN(-mv_amt, pacman_speed); | |
| reached_centerline = -mv_amt < pacman_speed; | |
| } else { | |
| level_data.pacman_pos.y += MIN(mv_amt, pacman_speed); | |
| reached_centerline = mv_amt < pacman_speed; | |
| } | |
| } | |
| if (reached_centerline) { | |
| level_data.pacman_turning = FALSE; | |
| level_data.pacman_dir = level_data.next_dir; | |
| } | |
| } else { | |
| struct v2 blocked_pos_s = level_data.pacman_pos; | |
| level_data.pacman_blocked = test_pacman_dir_blocked(level_data.pacman_dir, &blocked_pos_s); | |
| if (level_data.pacman_blocked) { | |
| blocked_tiles[num_blocked_tiles++] = pos_to_tile(&blocked_pos_s); | |
| } else { | |
| move_in_dir(level_data.pacman_dir, &level_data.pacman_pos, pacman_speed); | |
| if (level_data.pacman_pos.x < -TUNNEL_WIDTH / 2) { | |
| level_data.pacman_pos.x += ARENA_WIDTH + TUNNEL_WIDTH; | |
| } else if (level_data.pacman_pos.x >= ARENA_WIDTH + TUNNEL_WIDTH / 2) { | |
| level_data.pacman_pos.x -= ARENA_WIDTH + TUNNEL_WIDTH; | |
| } | |
| } | |
| } | |
| } | |
| { | |
| struct v2 pacman_tile = pos_to_tile(&level_data.pacman_pos); | |
| char ch = dot_map[pacman_tile.y][pacman_tile.x]; | |
| if (ch == '.') { | |
| level_data.pacman_eat_timer = 1; | |
| game_data.score += 10; | |
| } else if (ch == '*') { | |
| level_data.pacman_eat_timer = 3; | |
| level_data.pacman_powerup_timer = level_constants.pacman_powerup_time; | |
| level_data.consecutive_ghosts_eaten = 0; | |
| level_data.ghost_flash_timer = 0; | |
| reverse_ghosts(ghosts); | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| if (ghosts[i].mode != EYES) { | |
| ghosts[i].mode = FRIGHTENED; | |
| } | |
| } | |
| game_data.score += 50; | |
| } | |
| if (ch == '.' || ch == '*') { | |
| uint32 fruit_dot_counter = 70; | |
| level_data.dot_timer = 0; | |
| ++level_data.dots_eaten; | |
| if (level_data.dots_eaten == fruit_dot_counter) { | |
| level_data.fruit_is_visible = TRUE; | |
| } else if (level_data.dots_eaten == NUM_DOTS) { | |
| game_mode = LEVEL_TRANSITION; | |
| transition_timer = 0; | |
| } | |
| if (level_data.use_global_ghost_house_dot_counter) { | |
| ++level_data.global_ghost_house_dot_counter; | |
| } else { | |
| int i; | |
| for (i = PINKY; i < NUM_GHOSTS; ++i) { | |
| if (ghosts[i].ghost_house_state == IN_GHOST_HOUSE) { | |
| ++ghosts[i].dot_counter; | |
| } | |
| } | |
| } | |
| } else { | |
| ++level_data.dot_timer; | |
| } | |
| dot_map[pacman_tile.y][pacman_tile.x] = 0; | |
| } | |
| if (level_data.num_ghost_mode_cycles < 4 && !level_data.pacman_powerup_timer && !debug_ghost_mode_overridden) { | |
| ++level_data.ghost_mode_timer; | |
| if (!level_data.is_chasing && level_data.ghost_mode_timer > level_constants.scatter_times[level_data.num_ghost_mode_cycles]) { | |
| /* Switch to chase mode */ | |
| level_data.is_chasing = TRUE; | |
| level_data.ghost_mode_timer = 0; | |
| ++level_data.num_ghost_mode_cycles; | |
| reverse_ghosts(ghosts); | |
| } else if (level_data.is_chasing && level_data.ghost_mode_timer > level_constants.chase_times[level_data.num_ghost_mode_cycles]) { | |
| /* Switch to scatter mode */ | |
| level_data.is_chasing = FALSE; | |
| level_data.ghost_mode_timer = 0; | |
| reverse_ghosts(ghosts); | |
| } | |
| } | |
| { /* Update all ghost positions */ | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| struct ghost* ghost = &ghosts[i]; | |
| if (ghost->mode != EYES && level_data.ghost_eat_timer) { | |
| continue; | |
| } | |
| uint32 speed; | |
| struct v2 eyes_target_tile = { ARENA_WIDTH_IN_TILES / 2 - 1, 11 }; | |
| struct v2 eyes_target_pos = add(scale(eyes_target_tile, TILE_SIZE), TILE_CENTER); | |
| if (ghost->ghost_house_state == EXITING_GHOST_HOUSE) { | |
| if (ghost->pos.x != eyes_target_pos.x) { | |
| ghost->dir = ghost->pos.x < eyes_target_pos.x ? RIGHT : LEFT; | |
| } else { | |
| ghost->dir = UP; | |
| } | |
| } | |
| { | |
| uint32 ghost_eyes_speed = get_speed(175); | |
| uint32 elroy_v2_speed = level_constants.elroy_v1_speed + get_speed(5); | |
| uint32 elroy_v2_dots_left = level_constants.elroy_v1_dots_left / 2; | |
| struct v2 ghost_tile = pos_to_tile(&ghost->pos); | |
| /* If the ghost is inside the tunnel, slow down. */ | |
| bool32 is_in_tunnel = ghost_tile.y == 14 && (ghost_tile.x <= 5 || ghost_tile.x >= ARENA_WIDTH_IN_TILES - 5); | |
| if (ghost->mode == EYES) { | |
| speed = ghost_eyes_speed; | |
| } else if (ghost->ghost_house_state != OUTSIDE_GHOST_HOUSE) { | |
| speed = level_constants.ghost_tunnel_speed; | |
| } else if (is_in_tunnel) { | |
| speed = level_constants.ghost_tunnel_speed; | |
| } else if (ghost->mode == FRIGHTENED) { | |
| speed = level_constants.ghost_frightened_speed; | |
| } else if (i == BLINKY && NUM_DOTS - level_data.dots_eaten <= level_constants.elroy_v1_dots_left) { | |
| /* Oh he mad now beotch. */ | |
| speed = level_constants.elroy_v1_speed; | |
| } else if (i == BLINKY && NUM_DOTS - level_data.dots_eaten <= elroy_v2_dots_left) { | |
| /* Cruise Elroy */ | |
| speed = elroy_v2_speed; | |
| } else { | |
| speed = level_constants.ghost_speed; | |
| } | |
| if (ghost->ghost_house_state == EXITING_GHOST_HOUSE) { | |
| int offset = is_h_dir(ghost->dir) ? | |
| eyes_target_pos.x - ghost->pos.x : | |
| eyes_target_pos.y - ghost->pos.y | |
| ; | |
| if (offset < 0) { | |
| offset = -offset; | |
| } | |
| speed = MIN(offset, speed); | |
| } else if (!eql(&ghost_tile, &ghost->last_tile)) { | |
| struct v2 tile_center = add(scale(ghost_tile, TILE_SIZE), TILE_CENTER); | |
| bool32 making_a_90_degree_turn = ghost->is_path_chosen && is_h_dir(ghost->chosen_dir) != is_h_dir(ghost->dir); | |
| if (making_a_90_degree_turn) { | |
| int offset = is_h_dir(ghost->dir) ? | |
| tile_center.x - ghost->pos.x : | |
| tile_center.y - ghost->pos.y | |
| ; | |
| if (offset < 0) { | |
| offset = -offset; | |
| } | |
| speed = MIN(offset, speed); | |
| } | |
| } | |
| } | |
| move_in_dir(ghost->dir, &ghost->pos, speed); | |
| if (ghost->pos.x < -TUNNEL_WIDTH / 2) { | |
| ghost->pos.x += ARENA_WIDTH + TUNNEL_WIDTH; | |
| } else if (ghost->pos.x >= ARENA_WIDTH + TUNNEL_WIDTH / 2) { | |
| ghost->pos.x -= ARENA_WIDTH + TUNNEL_WIDTH; | |
| } | |
| struct v2 ghost_tile = pos_to_tile(&ghost->pos); | |
| if (ghost->ghost_house_state == EXITING_GHOST_HOUSE) { | |
| struct v2 just_below_eyes_tile = eyes_target_tile; | |
| --just_below_eyes_tile.y; | |
| if (eql(&eyes_target_pos, &ghost->pos)) { | |
| ghost->ghost_house_state = OUTSIDE_GHOST_HOUSE; | |
| } else if (!eql(&ghost_tile, &just_below_eyes_tile)) { | |
| continue; | |
| } | |
| } | |
| if (!eql(&ghost_tile, &ghost->last_tile)) { | |
| /* CLEANUP: merge with other movement to center */ | |
| struct v2 tile_center = add(scale(ghost_tile, TILE_SIZE), TILE_CENTER); | |
| bool32 made_it_to_center; | |
| switch (ghost->dir) { | |
| case UP: | |
| made_it_to_center = ghost->pos.y <= tile_center.y; | |
| break; | |
| case DOWN: | |
| made_it_to_center = ghost->pos.y >= tile_center.y; | |
| break; | |
| case LEFT: | |
| made_it_to_center = ghost->pos.x <= tile_center.x; | |
| break; | |
| case RIGHT: | |
| made_it_to_center = ghost->pos.x >= tile_center.x; | |
| break; | |
| } | |
| if (!made_it_to_center) { | |
| continue; | |
| } | |
| ghost->last_tile = ghost_tile; | |
| if (ghost->is_path_chosen) { | |
| ghost->dir = ghost->chosen_dir; | |
| ghost->is_path_chosen = FALSE; | |
| } | |
| move_in_dir(ghost->dir, &ghost_tile, 1); | |
| enum dir paths[NUM_DIRS]; | |
| int num_paths = 0; | |
| /* Get all paths from the next tile. */ | |
| { | |
| int dir; | |
| for (dir = 0; dir < NUM_DIRS; ++dir) { | |
| /* Prevent reversal */ | |
| if ((is_h_dir(dir) == is_h_dir(ghost->dir) && dir != ghost->dir)) { | |
| continue; | |
| } | |
| /* Only horizontal dirs when leaving the ghost house */ | |
| if (ghost->ghost_house_state == EXITING_GHOST_HOUSE && !is_h_dir(dir)) { | |
| continue; | |
| } | |
| struct v2 tile = ghost_tile; | |
| move_in_dir(dir, &tile, 1); | |
| bool32 ghost_is_blocked = !!strchr( | |
| ghost->mode == EYES ? | |
| "/`[]-|+" : "/`[]-|+_", | |
| arena_get(tile.y, tile.x)); | |
| if (ghost_is_blocked) { | |
| blocked_tiles[num_blocked_tiles++] = tile; | |
| } else { | |
| /* Don't go through any of the 'forbidden tiles'. */ | |
| if (dir == UP && ghost->mode == NORMAL) { | |
| int i; | |
| bool32 found = FALSE; | |
| for_array(i, forbidden_upward_tiles) { | |
| if (eql(&forbidden_upward_tiles[i], &tile)) { | |
| found = TRUE; | |
| break; | |
| } | |
| } | |
| if (found) { | |
| continue; | |
| } | |
| } | |
| paths[num_paths++] = dir; | |
| } | |
| } | |
| } | |
| assert(num_paths >= 1); | |
| int best_choice; | |
| struct v2 scatter_targets[NUM_GHOSTS] = { | |
| { ARENA_WIDTH_IN_TILES - 3, -3 }, | |
| { 2, -3 }, | |
| { ARENA_WIDTH_IN_TILES, ARENA_HEIGHT_IN_TILES }, | |
| { 0, ARENA_HEIGHT_IN_TILES }, | |
| }; | |
| if (ghost->mode == FRIGHTENED && ghost->ghost_house_state == OUTSIDE_GHOST_HOUSE) { | |
| /* Take pseudo-random turns. */ | |
| int choice = rand_r(&level_data.fright_ghost_seed) % NUM_DIRS; | |
| best_choice = -1; | |
| while (best_choice == -1) { | |
| int i; | |
| for (i = 0; i < num_paths; ++i) { | |
| if (paths[i] == choice) { | |
| best_choice = choice; | |
| break; | |
| } | |
| } | |
| if (best_choice == -1) { | |
| --choice; | |
| if (choice < 0) { | |
| choice = NUM_DIRS - 1; | |
| } | |
| } | |
| } | |
| } else { | |
| /* Chase after a target */ | |
| if (ghost->ghost_house_state == IN_GHOST_HOUSE) { | |
| /* BOUNCING, alternate targets. */ | |
| uint32 ghost_global_dot_limits[] = { 0, 7, 17, 32 }; | |
| if (ghost_tile.y >= 13 && ghost->mode == EYES) { | |
| ghost->mode = NORMAL; | |
| } | |
| enum ghost_name next_ghost_released; | |
| for (next_ghost_released = PINKY; next_ghost_released < NUM_GHOSTS; ++next_ghost_released) { | |
| if (ghosts[next_ghost_released].ghost_house_state == IN_GHOST_HOUSE) { | |
| break; | |
| } | |
| } | |
| if (ghost->name == BLINKY || | |
| (ghost->name == next_ghost_released && ( | |
| level_data.dot_timer >= seconds_to_frames(4) || | |
| (level_data.use_global_ghost_house_dot_counter && level_data.global_ghost_house_dot_counter >= ghost_global_dot_limits[i]) || | |
| (!level_data.use_global_ghost_house_dot_counter && ghost->dot_counter >= level_constants.ghost_dot_limits[i])))) { | |
| if (ghost->name != BLINKY) { | |
| level_data.dot_timer = 0; | |
| } | |
| ghost->target_tile = top_house_targets[PINKY]; | |
| ghost->ghost_house_state = EXITING_GHOST_HOUSE; | |
| } else if (ghost->bounce_target_bottom) { | |
| if (eql(&ghost_tile, &bottom_house_targets[i])) { | |
| ghost->bounce_target_bottom = !ghost->bounce_target_bottom; | |
| ghost->target_tile = top_house_targets[i]; | |
| } else { | |
| ghost->target_tile = bottom_house_targets[i]; | |
| } | |
| } else { | |
| if (eql(&ghost_tile, &top_house_targets[i])) { | |
| ghost->bounce_target_bottom = !ghost->bounce_target_bottom; | |
| ghost->target_tile = bottom_house_targets[i]; | |
| } else { | |
| ghost->target_tile = top_house_targets[i]; | |
| } | |
| } | |
| } else if (ghost->mode == NORMAL) { | |
| get_normal_target_tile(&level_data, scatter_targets, &ghosts[BLINKY].pos, ghost, ghost_tile); | |
| } else if (ghost->mode == EYES) { | |
| /* Target the door. */ | |
| struct v2 home_target = { ARENA_WIDTH_IN_TILES / 2 - 1, 13 }; | |
| if (eql(&ghost_tile, &eyes_target_tile)) { | |
| ghost->ghost_house_state = ENTERING_GHOST_HOUSE; | |
| ghost->target_tile = home_target; | |
| } else if (ghost->ghost_house_state == ENTERING_GHOST_HOUSE) { | |
| if (!eql(&ghost_tile, &home_target)) { | |
| ghost->target_tile = home_target; | |
| } else { | |
| ghost->ghost_house_state = IN_GHOST_HOUSE; | |
| ghost->target_tile = home_target; | |
| ++ghost->target_tile.y; | |
| } | |
| } else { | |
| ghost->target_tile = eyes_target_tile; | |
| } | |
| } | |
| int i, best_path = 0, best_distance = -1; | |
| for (i = 0; i < num_paths; ++i) { | |
| struct v2 tile = ghost_tile; | |
| move_in_dir(paths[i], &tile, 1); | |
| int distance = DIST_SQUARE(tile.x - ghost->target_tile.x, tile.y - ghost->target_tile.y); | |
| if (best_distance == -1 || distance < best_distance) { | |
| best_distance = distance; | |
| best_path = i; | |
| } | |
| } | |
| best_choice = paths[best_path]; | |
| } | |
| ghost->is_path_chosen = TRUE; | |
| ghost->chosen_dir = best_choice; | |
| } | |
| } | |
| } | |
| { | |
| struct v2 pacman_tile = pos_to_tile(&level_data.pacman_pos); | |
| if (level_data.fruit_is_visible && (eql(&pacman_tile, &fruit_tile))) { | |
| level_data.fruit_is_visible = FALSE; | |
| level_data.fruit_score_timer = seconds_to_frames(2); | |
| game_data.score += symbol_points[level_constants.bonus_symbol]; | |
| } | |
| /* Check for pacman/ghost collisions */ | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| struct v2 ghost_tile = pos_to_tile(&ghosts[i].pos); | |
| if (eql(&ghost_tile, &pacman_tile)) { | |
| if (ghosts[i].mode == FRIGHTENED && !level_data.ghost_to_be_eaten) { | |
| uint32 ghost_eat_time = seconds_to_frames(1); | |
| level_data.ghost_eat_timer = ghost_eat_time; | |
| game_data.score += get_ghost_score(level_data.consecutive_ghosts_eaten++); | |
| level_data.ghost_to_be_eaten = &ghosts[i]; | |
| } else if (ghosts[i].mode == NORMAL && !debug_no_death_mode) { | |
| game_mode = LOSING_A_LIFE; | |
| level_data.extra_life_timer = 0; | |
| level_data.ghost_eat_timer = 0; | |
| transition_timer = 0; | |
| } | |
| } | |
| } | |
| } | |
| high_score = MAX(game_data.score, high_score); | |
| if (old_score / 10000 != game_data.score / 10000) { | |
| add_extra_life(&game_data, &level_data); | |
| } | |
| if (level_data.extra_life_timer) { | |
| --level_data.extra_life_timer; | |
| } | |
| } break; | |
| case GAME_OVER: { | |
| } break; | |
| case LEVEL_TRANSITION: { | |
| ++transition_timer; | |
| if (transition_timer > 30) { | |
| set_level_constants(&level_constants, ++game_data.current_level); | |
| start_new_level(ghosts, &level_data, game_data.original_fright_ghost_seed); | |
| transition_timer = 0; | |
| game_mode = PAUSED_BEFORE_PLAYING; | |
| } | |
| } break; | |
| case LOSING_A_LIFE: { | |
| ++transition_timer; | |
| switch (transition_timer) { | |
| /* Spin in a circle */ | |
| case LIFE_LOST_PAUSE_TIME + LIFE_LOST_TURN_TIME*1: | |
| case LIFE_LOST_PAUSE_TIME + LIFE_LOST_TURN_TIME*2: | |
| case LIFE_LOST_PAUSE_TIME + LIFE_LOST_TURN_TIME*3: | |
| case LIFE_LOST_PAUSE_TIME + LIFE_LOST_TURN_TIME*4: | |
| --level_data.pacman_dir; | |
| if (level_data.pacman_dir == -1) { | |
| level_data.pacman_dir = NUM_DIRS - 1; | |
| } | |
| break; | |
| /* Restart the level */ | |
| case LIFE_LOST_PAUSE_TIME + LIFE_LOST_TURN_TIME*8: | |
| if (game_data.num_extra_lives) { | |
| --game_data.num_extra_lives; | |
| transition_timer = 0; | |
| game_mode = PAUSED_BEFORE_PLAYING; | |
| return_ghosts_and_pacman_to_start_position(ghosts, &level_data); | |
| level_data.use_global_ghost_house_dot_counter = TRUE; | |
| level_data.global_ghost_house_dot_counter = 0; | |
| level_data.ghost_mode_timer = 0; | |
| level_data.num_ghost_mode_cycles = 0; | |
| level_data.is_chasing = FALSE; | |
| } else { | |
| game_mode = GAME_OVER; | |
| } | |
| break; | |
| } | |
| } break; | |
| } | |
| erase(); | |
| view.left = 2; | |
| view.top = 3; | |
| if (view.zoom_view) { | |
| struct v2 offset = { -8, -2 }; | |
| view.camera_target_tile = add(offset, pos_to_tile(&ghosts[INKY].pos)); | |
| } else { | |
| struct v2 zero = { 0 }; | |
| view.camera_target_tile = zero; | |
| } | |
| if (level_data.extra_life_timer && level_data.extra_life_timer / 10 % 2 == 0) { | |
| init_pair(WALL_PAIR, COLOR_LIGHT_BLUE, COLOR_DARK_BACKGROUND); | |
| } else { | |
| init_pair(WALL_PAIR, COLOR_WALL_BLUE, COLOR_DARK_BACKGROUND); | |
| } | |
| { /* Draw Arena */ | |
| int row, col; | |
| for (row = 0; row < ARENA_HEIGHT_IN_TILES; ++row) { | |
| for (col = 0; col < ARENA_WIDTH_IN_TILES; ++col) { | |
| char ch = arena_get(row, col), draw_ch = ch, fill_ch = ' '; | |
| switch (ch) { | |
| case '`': | |
| case '[': | |
| case ']': | |
| case '/': | |
| draw_ch = '+'; | |
| attron(COLOR_PAIR(WALL_PAIR)); | |
| break; | |
| case 'x': | |
| draw_ch = ' '; | |
| attron(COLOR_PAIR(WALL_PAIR)); | |
| break; | |
| case ' ': | |
| attron(COLOR_PAIR(EMPTY_PAIR)); | |
| break; | |
| case '_': | |
| attron(COLOR_PAIR(DOOR_PAIR)); | |
| fill_ch = ch; | |
| break; | |
| case '-': | |
| case '|': | |
| case '+': | |
| attron(COLOR_PAIR(WALL_PAIR)); | |
| break; | |
| } | |
| if (ch == '-') { | |
| fill_ch = ch; | |
| } | |
| draw_tile(row, col, draw_ch, &view, fill_ch); | |
| } | |
| } | |
| } | |
| if (level_data.fruit_is_visible) { | |
| draw_fruit(fruit_tile, level_constants.bonus_symbol, &view); | |
| } else if (level_data.fruit_score_timer) { | |
| enable_bonus_symbol_color(level_constants.bonus_symbol); | |
| --level_data.fruit_score_timer; | |
| struct v2 pos = draw_pos_v2(fruit_tile, &view); | |
| mvprintw(pos.y, pos.x, "%u", symbol_points[level_constants.bonus_symbol]); | |
| } | |
| { | |
| int i; | |
| int start_level = game_data.current_level - 6; | |
| start_level = MAX(0, start_level); | |
| for (i = start_level; i < game_data.current_level + 1; ++i) { | |
| struct v2 tile = { ARENA_WIDTH_IN_TILES - 2*(i+1 - start_level), ARENA_HEIGHT_IN_TILES }; | |
| draw_fruit(tile, get_symbol_for_level(i), &view); | |
| } | |
| } | |
| { | |
| attron(COLOR_PAIR(DOT_PAIR)); | |
| int row, col; | |
| for (row = 0; row < ARENA_HEIGHT_IN_TILES; ++row) { | |
| for (col = 0; col < ARENA_WIDTH_IN_TILES; ++col) { | |
| char ch = dot_map[row][col]; | |
| if (ch == '.' || ch == '*') { | |
| draw_tile(row, col, ch, &view, ' '); | |
| } | |
| } | |
| } | |
| } | |
| attron(COLOR_PAIR(PACMAN_PAIR)); | |
| char pacman_char = 'O'; | |
| if (game_mode != PLAYING || level_data.pacman_blocked || level_data.pacman_chomp_timer / 3 % 3 != 0) { | |
| switch (level_data.pacman_dir) { | |
| case RIGHT: | |
| pacman_char = '<'; | |
| break; | |
| case LEFT: | |
| pacman_char = '>'; | |
| break; | |
| case UP: | |
| pacman_char = 'Y'; | |
| break; | |
| case DOWN: | |
| pacman_char = '^'; | |
| break; | |
| } | |
| } | |
| { | |
| int i; | |
| for (i = 0; i < game_data.num_extra_lives; ++i) { | |
| draw_tile(ARENA_HEIGHT_IN_TILES, i + 1, '<', &view, ' '); | |
| } | |
| } | |
| struct v2 pacman_tile = pos_to_tile(&level_data.pacman_pos); | |
| if (!level_data.ghost_eat_timer) { | |
| if (pacman_tile.x >= 0 && pacman_tile.x < ARENA_WIDTH_IN_TILES) { | |
| draw_tile_v2(pacman_tile, pacman_char, &view, ' '); | |
| } | |
| } | |
| #if 0 | |
| { | |
| attron(COLOR_PAIR(DOOR_PAIR)); | |
| int i; | |
| for (i = 0; i < num_blocked_tiles; ++i) { | |
| draw_tile_v2(blocked_tiles[i], 'X', &view); | |
| } | |
| for_array(i, forbidden_upward_tiles) { | |
| draw_tile_v2(forbidden_upward_tiles[i], 'X', &view); | |
| } | |
| } | |
| #endif | |
| if (game_mode != LOSING_A_LIFE && game_mode != GAME_OVER) { | |
| int i; | |
| for (i = 0; i < NUM_GHOSTS; ++i) { | |
| if (level_data.ghost_eat_timer && &ghosts[i] == level_data.ghost_to_be_eaten) { | |
| continue; | |
| } | |
| if (ghosts[i].mode == FRIGHTENED) { | |
| if (level_data.ghost_flash_timer / GHOST_FLASH_TIME % 2 == 0) { | |
| attron(COLOR_PAIR(FRIGHT_PAIR)); | |
| } else { | |
| attron(COLOR_PAIR(FRIGHT_FLASH_PAIR)); | |
| } | |
| } else if (ghosts[i].mode == EYES) { | |
| attron(COLOR_PAIR(EYES_PAIR)); | |
| } else { | |
| attron(COLOR_PAIR(ghosts[i].curses_color_pair)); | |
| } | |
| struct v2 ghost_tile = pos_to_tile(&ghosts[i].pos); | |
| if (ghost_tile.x >= 0 && ghost_tile.x < ARENA_WIDTH_IN_TILES) { | |
| draw_tile_v2(ghost_tile, 'm', &view, ' '); | |
| } | |
| #if 0 | |
| /* Draw Target Tile */ | |
| attron(COLOR_PAIR(DOT_PAIR)); | |
| draw_tile_v2(ghosts[i].target_tile, ghosts[i].nickname, &view); | |
| #endif | |
| } | |
| } | |
| if (level_data.ghost_eat_timer) { | |
| attron(COLOR_PAIR(KEY_PAIR)); | |
| struct v2 pos = draw_pos_v2(pacman_tile, &view); | |
| mvprintw(pos.y, pos.x, "%d", get_ghost_score(level_data.consecutive_ghosts_eaten - 1)); | |
| } | |
| if (game_mode == GAME_OVER) { | |
| attron(COLOR_PAIR(GAME_OVER_TEXT_PAIR)); | |
| struct v2 pos = draw_pos(HOUSE_BOTTOM + 2, HOUSE_LEFT, &view); | |
| mvprintw(pos.y, pos.x, "G A M E O V E R"); | |
| } | |
| if (view.zoom_view) { | |
| attron(COLOR_PAIR(PACMAN_PAIR)); | |
| draw_tile_v2(pacman_tile, 'X', &view, ' '); | |
| if (level_data.pacman_turning) { | |
| attron(COLOR_PAIR(WALL_PAIR)); | |
| draw_tile_v2(pacman_turn_tile, 'O', &view, ' '); | |
| } | |
| attron(COLOR_PAIR(BG_PAIR)); | |
| mvaddch( | |
| ghosts[INKY].pos.y / PIXEL_SIZE + view.top - view.camera_target_tile.y * TILE_SIZE_IN_PIXELS, | |
| ghosts[INKY].pos.x / PIXEL_SIZE + view.left - view.camera_target_tile.x * TILE_SIZE_IN_PIXELS, 'O'); | |
| } | |
| attron(COLOR_PAIR(BG_PAIR)); | |
| mvprintw(view.top - 2, view.left + 2*3, "1 U P"); | |
| mvprintw(view.top - 1, view.left + 2*3, "%7d", game_data.score); | |
| mvprintw(view.top - 2, (HOUSE_CENTER - 2)*3, "H I G H S C O R E"); | |
| mvprintw(view.top - 1, (HOUSE_CENTER - 1)*3, "%7d", game_data.score); | |
| #if 0 | |
| attron(COLOR_PAIR(BG_PAIR)); | |
| int diag_row = 0; | |
| mvprintw(diag_row++, 0, "Score: %06d, High Score: %06d", game_data.score, high_score); | |
| mvprintw(diag_row++, 0, "Dots Eaten: %d", level_data.dots_eaten); | |
| mvprintw(diag_row++, 0, "Current Level %u%s", game_data.current_level, debug_no_death_mode ? ", GOD-MODE" : ""); | |
| #endif | |
| refresh(); | |
| frame_timer = 0; | |
| } | |
| usleep(1); | |
| } | |
| endwin(); | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment