Created
November 20, 2025 07:51
-
-
Save 7etsuo/f6e48b826ee17b62afe2345181bc3fab to your computer and use it in GitHub Desktop.
ASCII Marico
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
| /** | |
| * ASCII Mario Ultimate Scroller - A terminal-based side-scrolling platformer | |
| * Built by Tetsuo and Grok 4.1! | |
| */ | |
| #define _POSIX_C_SOURCE 200809L | |
| #include <ncurses.h> | |
| #include <signal.h> | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <time.h> | |
| #include <unistd.h> | |
| /* --- Constants & Configuration --- */ | |
| #define TARGET_FPS 60 | |
| #define FPS_DELAY_US (1000000 / TARGET_FPS) | |
| #define MAX_WIDTH 120 | |
| #define MAX_HEIGHT 30 | |
| #define MIN_WIDTH 60 | |
| #define MIN_HEIGHT 20 | |
| #define GROUND_OFFSET 4 | |
| #define WORLD_BUFFER_WIDTH 20 | |
| #define MARIO_RENDER_WIDTH 7 | |
| #define MARIO_RENDER_HEIGHT 3 | |
| /* Physics Constants */ | |
| static const double GRAVITY = 0.35; | |
| static const double JUMP_FORCE = -8.5; | |
| static const double STOMP_BOUNCE = -5.5; | |
| static const double MAX_VEL_Y = 1.8; | |
| static const double SCROLL_INIT_SPEED = 0.15; | |
| static const double SCROLL_ACCEL = 0.0001; | |
| /* Gameplay Constants */ | |
| #define MAX_LIVES 3 | |
| #define NUM_HIGH_SCORES 3 | |
| #define POINTS_COIN 100 | |
| #define POINTS_ENEMY 50 | |
| #define SCORE_PER_TICK 1 | |
| /* World Generation Constants */ | |
| #define PLATFORM_CHANCE 10 | |
| #define COIN_CHANCE 15 | |
| #define ENEMY_CHANCE 25 | |
| #define MIN_PLATFORM_LEN 3 | |
| #define MAX_PLATFORM_EXTRA 5 | |
| /* Colors */ | |
| enum ColorPair { | |
| PAIR_NONE = 0, | |
| PAIR_HAT = 1, | |
| PAIR_SHIRT, | |
| PAIR_PLATFORM, | |
| PAIR_COIN, | |
| PAIR_UI, | |
| PAIR_PAUSE, | |
| PAIR_SKY, | |
| PAIR_CLOUD, | |
| PAIR_DIRT, | |
| PAIR_BRICK, | |
| PAIR_SPARKLE | |
| }; | |
| /* --- Data Structures --- */ | |
| typedef struct { | |
| double x; | |
| double y; | |
| double vel_y; | |
| int lives; | |
| int score; | |
| int anim_frame; | |
| bool grounded; | |
| } Player; | |
| typedef struct { | |
| char tiles[MAX_HEIGHT][MAX_WIDTH + WORLD_BUFFER_WIDTH]; | |
| char bg_tiles[MAX_HEIGHT][MAX_WIDTH + WORLD_BUFFER_WIDTH]; | |
| double scroll_offset; | |
| double bg_offset; | |
| double scroll_speed; | |
| } World; | |
| typedef struct { | |
| volatile bool running; | |
| bool paused; | |
| int width; | |
| int height; | |
| long frame_count; | |
| int high_scores[NUM_HIGH_SCORES]; | |
| WINDOW *game_pad; | |
| } GameState; | |
| typedef struct { | |
| Player player; | |
| World world; | |
| GameState state; | |
| } GameContext; | |
| /* --- Globals (Static Context) --- */ | |
| static GameContext ctx; | |
| /* --- Assets --- */ | |
| static const char *MARIO_SPRITE[8][3] = { | |
| {" ^__^ ", " (oo)\\", " /||\\\\"}, /* Run 1 */ | |
| {" ^__^ ", " (oo)\\", " //\\\\ "}, /* Run 2 */ | |
| {" ^__^ ", " (oo)\\", " //||\\\\"}, /* Run 3 */ | |
| {" ^__^ ", " (--)\\", " //\\\\ "}, /* Blink */ | |
| {" ^__^ ", " (OO)|", " / \\ "}, /* Jump 1 */ | |
| {" ^__^ ", " (OO)|", " | | "}, /* Jump 2 */ | |
| {" ^__^ ", " (xx)\\", " //||\\\\"}, /* Dead */ | |
| {" ^__^ ", " (oo)\\", " < > "}, /* Crouch */ | |
| }; | |
| /* --- Prototypes --- */ | |
| static void init_graphics(void); | |
| static void cleanup_resources(void); | |
| static void handle_signal(int sig); | |
| static void load_data(void); | |
| static void save_data(void); | |
| static void world_init(World *w); | |
| static void world_scroll(World *w, int width); | |
| static void world_generate_column(World *w, int x, int height); | |
| static void world_update_parallax(World *w, int width); | |
| static void player_update_physics(Player *p, World *w, int height); | |
| static void player_resolve_collisions(Player *p, World *w); | |
| static void player_jump(Player *p, int height); | |
| static void player_move(Player *p, double dx, int max_width); | |
| static void render_world(WINDOW *pad, World *w, int h, int w_px); | |
| static void render_player(WINDOW *pad, Player *p); | |
| static void render_ui(GameContext *g); | |
| /* --- Helper Functions --- */ | |
| static int safe_clamp(int val, int min, int max) { | |
| if (val < min) | |
| return min; | |
| if (val > max) | |
| return max; | |
| return val; | |
| } | |
| static void add_sparkle(World *w, int cx, int cy) { | |
| /* 3x3 explosion effect */ | |
| for (int dy = -1; dy <= 1; dy++) { | |
| for (int dx = -1; dx <= 1; dx++) { | |
| int ny = cy + dy; | |
| int nx = cx + dx; | |
| if (ny >= 0 && ny < MAX_HEIGHT && nx >= 0 && | |
| nx < MAX_WIDTH + WORLD_BUFFER_WIDTH) { | |
| if (w->tiles[ny][nx] == ' ') { | |
| w->tiles[ny][nx] = '\''; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /* --- Initialization & Lifecycle --- */ | |
| static void init_graphics(void) { | |
| initscr(); | |
| if (has_colors()) { | |
| start_color(); | |
| use_default_colors(); | |
| init_pair(PAIR_HAT, COLOR_CYAN, -1); | |
| init_pair(PAIR_SHIRT, COLOR_RED, -1); | |
| init_pair(PAIR_PLATFORM, COLOR_GREEN, -1); | |
| init_pair(PAIR_COIN, COLOR_YELLOW, -1); | |
| init_pair(PAIR_UI, COLOR_WHITE, -1); | |
| init_pair(PAIR_PAUSE, COLOR_MAGENTA, -1); | |
| init_pair(PAIR_SKY, COLOR_BLUE, -1); | |
| init_pair(PAIR_CLOUD, COLOR_WHITE, -1); | |
| init_pair(PAIR_DIRT, COLOR_MAGENTA, -1); | |
| init_pair(PAIR_BRICK, COLOR_GREEN, -1); | |
| init_pair(PAIR_SPARKLE, COLOR_YELLOW, -1); | |
| } | |
| cbreak(); | |
| noecho(); | |
| keypad(stdscr, TRUE); | |
| nodelay(stdscr, TRUE); | |
| curs_set(0); | |
| timeout(0); | |
| } | |
| static void cleanup_resources(void) { | |
| if (ctx.state.game_pad) { | |
| delwin(ctx.state.game_pad); | |
| } | |
| endwin(); | |
| save_data(); | |
| } | |
| static void handle_signal(int sig) { | |
| (void)sig; | |
| ctx.state.running = false; | |
| } | |
| static void load_data(void) { | |
| FILE *f = fopen("mario_highs.txt", "r"); | |
| if (f) { | |
| for (int i = 0; i < NUM_HIGH_SCORES; i++) { | |
| if (fscanf(f, "%d", &ctx.state.high_scores[i]) != 1) { | |
| ctx.state.high_scores[i] = 0; | |
| } | |
| } | |
| fclose(f); | |
| } else { | |
| memset(ctx.state.high_scores, 0, sizeof(ctx.state.high_scores)); | |
| } | |
| } | |
| static void save_data(void) { | |
| FILE *f = fopen("mario_highs.txt", "w"); | |
| if (f) { | |
| for (int i = 0; i < NUM_HIGH_SCORES; i++) { | |
| fprintf(f, "%d\n", ctx.state.high_scores[i]); | |
| } | |
| fclose(f); | |
| } | |
| } | |
| static bool init_game(void) { | |
| signal(SIGINT, handle_signal); | |
| atexit(cleanup_resources); | |
| srand(time(NULL)); | |
| init_graphics(); | |
| getmaxyx(stdscr, ctx.state.height, ctx.state.width); | |
| /* Dimensions Security Check */ | |
| if (ctx.state.height > MAX_HEIGHT) | |
| ctx.state.height = MAX_HEIGHT; | |
| if (ctx.state.width > MAX_WIDTH) | |
| ctx.state.width = MAX_WIDTH; | |
| if (ctx.state.width < MIN_WIDTH || ctx.state.height < MIN_HEIGHT) { | |
| endwin(); | |
| fprintf(stderr, "Terminal too small. Minimum %dx%d required.\n", MIN_WIDTH, | |
| MIN_HEIGHT); | |
| return false; | |
| } | |
| /* Pad Allocation */ | |
| ctx.state.game_pad = | |
| newpad(MAX_HEIGHT + GROUND_OFFSET, MAX_WIDTH + WORLD_BUFFER_WIDTH); | |
| if (!ctx.state.game_pad) { | |
| endwin(); | |
| fprintf(stderr, "Failed to allocate memory for game pad.\n"); | |
| return false; | |
| } | |
| /* State Initialization */ | |
| ctx.state.running = true; | |
| ctx.state.paused = false; | |
| ctx.state.frame_count = 0; | |
| ctx.player.lives = MAX_LIVES; | |
| ctx.player.x = 15.0; | |
| ctx.player.y = ctx.state.height - GROUND_OFFSET - 3; | |
| ctx.player.vel_y = 0; | |
| ctx.player.score = 0; | |
| ctx.player.grounded = false; | |
| ctx.world.scroll_speed = SCROLL_INIT_SPEED; | |
| ctx.world.scroll_offset = 0.0; | |
| ctx.world.bg_offset = 0.0; | |
| world_init(&ctx.world); | |
| load_data(); | |
| return true; | |
| } | |
| /* --- World Logic --- */ | |
| static void world_init(World *w) { | |
| memset(w->tiles, ' ', sizeof(w->tiles)); | |
| memset(w->bg_tiles, ' ', sizeof(w->bg_tiles)); | |
| /* Initial Sky */ | |
| for (int y = 0; y < 5; y++) { | |
| for (int x = 0; x < MAX_WIDTH + WORLD_BUFFER_WIDTH; x++) { | |
| if (rand() % 30 == 0) | |
| w->bg_tiles[y][x] = '.'; | |
| } | |
| } | |
| /* Initial Hills */ | |
| for (int x = 0; x < MAX_WIDTH + WORLD_BUFFER_WIDTH; x++) { | |
| w->bg_tiles[MAX_HEIGHT - 5][x] = '~'; | |
| } | |
| } | |
| static void world_generate_column(World *w, int x, int height) { | |
| int ground_level = height - GROUND_OFFSET; | |
| /* Clear column */ | |
| for (int y = 0; y < MAX_HEIGHT; y++) { | |
| w->tiles[y][x] = ' '; | |
| } | |
| static int plat_run = 0; | |
| static int gap_run = 0; | |
| /* Platform Logic */ | |
| if (plat_run > 0) { | |
| w->tiles[ground_level - 5][x] = '_'; | |
| plat_run--; | |
| if (rand() % 20 == 0) { | |
| w->tiles[ground_level - 6][x] = 'e'; | |
| } | |
| } else if (gap_run > 0) { | |
| gap_run--; | |
| } else if (rand() % PLATFORM_CHANCE == 0) { | |
| plat_run = MIN_PLATFORM_LEN + rand() % MAX_PLATFORM_EXTRA; | |
| gap_run = 3; | |
| } | |
| /* Coin Logic */ | |
| if (rand() % COIN_CHANCE == 0) { | |
| int cy = ground_level - 4 - (rand() % 6); | |
| if (cy > 2) { | |
| w->tiles[cy][x] = '*'; | |
| } | |
| } | |
| /* Ground Enemy Logic */ | |
| if (rand() % ENEMY_CHANCE == 0 && plat_run == 0) { | |
| w->tiles[ground_level - 1][x] = 'e'; | |
| } | |
| } | |
| static void world_update_parallax(World *w, int width) { | |
| int right_edge = (width < MAX_WIDTH + WORLD_BUFFER_WIDTH) | |
| ? width | |
| : MAX_WIDTH + WORLD_BUFFER_WIDTH - 1; | |
| /* Cloud Generation */ | |
| if (rand() % 60 == 0) { | |
| int cy = 2 + rand() % 6; | |
| if (right_edge > 4) { | |
| w->bg_tiles[cy][right_edge - 3] = '('; | |
| w->bg_tiles[cy][right_edge - 2] = ' '; | |
| w->bg_tiles[cy][right_edge - 1] = ' '; | |
| w->bg_tiles[cy][right_edge] = ')'; | |
| } | |
| } | |
| /* Hill Generation */ | |
| static int hill_height = 0; | |
| if (rand() % 10 == 0) { | |
| hill_height = (rand() % 3) - 1; | |
| } | |
| int base_y = 20; | |
| int current_h = base_y + hill_height; | |
| for (int y = base_y - 5; y < MAX_HEIGHT; y++) { | |
| char c = ' '; | |
| if (y == current_h) | |
| c = '^'; | |
| else if (y > current_h) | |
| c = '|'; | |
| w->bg_tiles[y][right_edge] = c; | |
| } | |
| } | |
| static void world_scroll(World *w, int width) { | |
| int limit = (width < MAX_WIDTH + WORLD_BUFFER_WIDTH) | |
| ? width | |
| : MAX_WIDTH + WORLD_BUFFER_WIDTH - 1; | |
| /* Shift World Layers */ | |
| for (int y = 0; y < MAX_HEIGHT; y++) { | |
| memmove(&w->tiles[y][0], &w->tiles[y][1], limit); | |
| w->tiles[y][limit] = ' '; | |
| } | |
| world_generate_column(w, limit, ctx.state.height); | |
| } | |
| static void world_scroll_bg(World *w, int width) { | |
| int limit = (width < MAX_WIDTH + WORLD_BUFFER_WIDTH) | |
| ? width | |
| : MAX_WIDTH + WORLD_BUFFER_WIDTH - 1; | |
| for (int y = 0; y < MAX_HEIGHT; y++) { | |
| memmove(&w->bg_tiles[y][0], &w->bg_tiles[y][1], limit); | |
| w->bg_tiles[y][limit] = ' '; | |
| } | |
| world_update_parallax(w, width); | |
| } | |
| /* --- Player Logic --- */ | |
| static void player_jump(Player *p, int height) { | |
| int ground_threshold = height - GROUND_OFFSET - 5; | |
| /* Allow jumping if grounded or slightly in air (coyote time approximation) */ | |
| if (p->grounded || (p->y > ground_threshold && p->vel_y < 1.0)) { | |
| p->vel_y = JUMP_FORCE; | |
| p->grounded = false; | |
| } | |
| } | |
| static void player_move(Player *p, double dx, int max_width) { | |
| p->x += dx; | |
| if (p->x < 0) | |
| p->x = 0; | |
| if (p->x > max_width - MARIO_RENDER_WIDTH) | |
| p->x = max_width - MARIO_RENDER_WIDTH; | |
| } | |
| static void player_check_entity_collision(Player *p, World *w, int cx, int cy) { | |
| char tile = w->tiles[cy][cx]; | |
| if (tile == '*') { | |
| p->score += POINTS_COIN; | |
| w->tiles[cy][cx] = ' '; | |
| add_sparkle(w, cx, cy); | |
| flash(); | |
| } else if (tile == 'e') { | |
| /* Stomp detection: falling and feet above enemy center */ | |
| if (p->vel_y > 0 && (p->y + MARIO_RENDER_HEIGHT - 2) <= cy) { | |
| p->score += POINTS_ENEMY; | |
| w->tiles[cy][cx] = ' '; | |
| p->vel_y = STOMP_BOUNCE; | |
| add_sparkle(w, cx, cy); | |
| } else { | |
| p->lives--; | |
| flash(); | |
| /* Clear immediate area to prevent tick-lock death */ | |
| for (int ex = -2; ex <= 2; ex++) { | |
| if (cx + ex >= 0 && cx + ex < MAX_WIDTH + WORLD_BUFFER_WIDTH) { | |
| if (w->tiles[cy][cx + ex] == 'e') | |
| w->tiles[cy][cx + ex] = ' '; | |
| } | |
| } | |
| if (p->lives <= 0) { | |
| ctx.state.running = false; | |
| } | |
| } | |
| } | |
| } | |
| static void player_resolve_collisions(Player *p, World *w) { | |
| int px = (int)p->x; | |
| int py = (int)p->y; | |
| int feet_y = py + MARIO_RENDER_HEIGHT; | |
| p->grounded = false; | |
| /* 1. Entity Collisions */ | |
| for (int dy = 0; dy <= MARIO_RENDER_HEIGHT; dy++) { | |
| for (int dx = 0; dx < MARIO_RENDER_WIDTH; dx++) { | |
| int cx = px + dx; | |
| int cy = py + dy; | |
| if (cx >= MAX_WIDTH || cy >= MAX_HEIGHT || cy < 0) | |
| continue; | |
| player_check_entity_collision(p, w, cx, cy); | |
| } | |
| } | |
| /* 2. Environmental / Ground Collisions */ | |
| /* Check platforms */ | |
| if (feet_y < MAX_HEIGHT) { | |
| for (int dx = 1; dx < MARIO_RENDER_WIDTH - 1; dx++) { | |
| char under = w->tiles[feet_y][px + dx]; | |
| if (under == '_' || under == '=') { | |
| p->grounded = true; | |
| } | |
| } | |
| } | |
| /* Check hard floor */ | |
| int ground_floor = ctx.state.height - GROUND_OFFSET; | |
| if (feet_y >= ground_floor) { | |
| p->y = ground_floor - MARIO_RENDER_HEIGHT; | |
| p->grounded = true; | |
| } | |
| if (p->grounded) { | |
| if (p->vel_y > 0) | |
| p->vel_y = 0; | |
| p->y = (int)p->y; /* Snap to grid */ | |
| } | |
| } | |
| static void player_update_physics(Player *p, World *w, int height) { | |
| (void)height; | |
| /* Apply Gravity */ | |
| if (!p->grounded) { | |
| p->vel_y += GRAVITY; | |
| if (p->vel_y > MAX_VEL_Y) | |
| p->vel_y = MAX_VEL_Y; | |
| } | |
| /* Integrate Velocity */ | |
| p->y += p->vel_y; | |
| /* Resolve */ | |
| player_resolve_collisions(p, w); | |
| } | |
| /* --- Main Game Loop --- */ | |
| static void process_input(void) { | |
| int key = getch(); | |
| if (key == ERR) | |
| return; | |
| switch (key) { | |
| case 'q': | |
| case 'Q': | |
| ctx.state.running = false; | |
| break; | |
| case 'p': | |
| case 'P': | |
| ctx.state.paused = !ctx.state.paused; | |
| break; | |
| case ' ': | |
| if (!ctx.state.paused) | |
| player_jump(&ctx.player, ctx.state.height); | |
| break; | |
| case KEY_LEFT: | |
| if (!ctx.state.paused) { | |
| player_move(&ctx.player, -1.5, ctx.state.width); | |
| } | |
| break; | |
| case KEY_RIGHT: | |
| if (!ctx.state.paused) { | |
| player_move(&ctx.player, 1.5, ctx.state.width); | |
| } | |
| break; | |
| } | |
| } | |
| static void update_state(void) { | |
| if (ctx.state.paused) | |
| return; | |
| /* 1. Physics */ | |
| player_update_physics(&ctx.player, &ctx.world, ctx.state.height); | |
| /* 2. World Scroll */ | |
| ctx.world.scroll_offset += ctx.world.scroll_speed; | |
| if (ctx.world.scroll_offset >= 1.0) { | |
| world_scroll(&ctx.world, ctx.state.width); | |
| ctx.world.scroll_offset -= 1.0; | |
| ctx.player.score += SCORE_PER_TICK; | |
| } | |
| /* 3. BG Scroll */ | |
| ctx.world.bg_offset += ctx.world.scroll_speed * 0.5; | |
| if (ctx.world.bg_offset >= 1.0) { | |
| world_scroll_bg(&ctx.world, ctx.state.width); | |
| ctx.world.bg_offset -= 1.0; | |
| } | |
| /* 4. Difficulty */ | |
| ctx.world.scroll_speed += SCROLL_ACCEL; | |
| /* 5. Animation */ | |
| ctx.player.anim_frame = (ctx.state.frame_count / 5) % 8; | |
| /* 6. Effect Cleanup */ | |
| static int clear_tick = 0; | |
| if (++clear_tick > 5) { | |
| for (int y = 0; y < MAX_HEIGHT; y++) { | |
| for (int x = 0; x < MAX_WIDTH + WORLD_BUFFER_WIDTH; x++) { | |
| if (ctx.world.tiles[y][x] == '\'') | |
| ctx.world.tiles[y][x] = ' '; | |
| } | |
| } | |
| clear_tick = 0; | |
| } | |
| ctx.state.frame_count++; | |
| } | |
| /* --- Rendering --- */ | |
| static void render_world(WINDOW *pad, World *w, int h, int w_px) { | |
| int ground_y = h - GROUND_OFFSET; | |
| for (int y = 0; y < MAX_HEIGHT; y++) { | |
| if (y + GROUND_OFFSET >= MAX_HEIGHT + GROUND_OFFSET) | |
| break; | |
| for (int x = 0; x < w_px; x++) { | |
| char ch = w->tiles[y][x]; | |
| char bg = w->bg_tiles[y][x]; | |
| int color = PAIR_NONE; | |
| int attrs = A_NORMAL; | |
| if (y >= ground_y) { | |
| ch = '='; | |
| color = PAIR_DIRT; | |
| } else if (ch != ' ') { | |
| switch (ch) { | |
| case '_': | |
| color = PAIR_PLATFORM; | |
| break; | |
| case '*': | |
| color = PAIR_COIN; | |
| attrs = A_BOLD; | |
| break; | |
| case 'e': | |
| color = PAIR_SHIRT; | |
| attrs = A_BOLD; | |
| break; | |
| case '\'': | |
| color = PAIR_SPARKLE; | |
| attrs = A_BOLD | A_BLINK; | |
| break; | |
| } | |
| } else { | |
| ch = bg; | |
| switch (ch) { | |
| case '.': | |
| color = PAIR_SKY; | |
| break; | |
| case '(': | |
| case ')': | |
| case '_': | |
| color = PAIR_CLOUD; | |
| break; | |
| case '^': | |
| case '|': | |
| color = PAIR_DIRT; | |
| break; | |
| } | |
| } | |
| if (ch != ' ') { | |
| wattron(pad, COLOR_PAIR(color) | attrs); | |
| mvwaddch(pad, y + GROUND_OFFSET, x, ch); | |
| wattroff(pad, COLOR_PAIR(color) | attrs); | |
| } | |
| } | |
| } | |
| } | |
| static void render_player(WINDOW *pad, Player *p) { | |
| int py = (int)p->y + GROUND_OFFSET; | |
| int px = (int)p->x; | |
| int frame = p->anim_frame; | |
| if (py >= 0 && py + MARIO_RENDER_HEIGHT <= MAX_HEIGHT + GROUND_OFFSET) { | |
| for (int r = 0; r < MARIO_RENDER_HEIGHT; r++) { | |
| const char *row_str = MARIO_SPRITE[frame][r]; | |
| for (int c = 0; c < MARIO_RENDER_WIDTH; c++) { | |
| char pixel = row_str[c]; | |
| if (pixel != ' ') { | |
| int color = (r == 0) ? PAIR_HAT : PAIR_SHIRT; | |
| wattron(pad, COLOR_PAIR(color) | A_BOLD); | |
| mvwaddch(pad, py + r, px + c, pixel); | |
| wattroff(pad, COLOR_PAIR(color) | A_BOLD); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| static void render_ui(GameContext *g) { | |
| attron(COLOR_PAIR(PAIR_UI) | A_BOLD); | |
| mvprintw(0, 2, "MARIO CLI ULTIMATE"); | |
| mvprintw(0, g->state.width - 25, "SCORE: %06d", g->player.score); | |
| mvprintw(1, 2, "LIVES: "); | |
| for (int i = 0; i < g->player.lives; i++) | |
| addch('*' | COLOR_PAIR(PAIR_SHIRT)); | |
| mvprintw(1, g->state.width - 25, "HI: %06d", g->state.high_scores[0]); | |
| if (g->state.paused) { | |
| attron(COLOR_PAIR(PAIR_PAUSE) | A_BLINK); | |
| mvprintw(g->state.height / 2, (g->state.width / 2) - 3, "- PAUSED -"); | |
| attroff(COLOR_PAIR(PAIR_PAUSE) | A_BLINK); | |
| } | |
| attroff(COLOR_PAIR(PAIR_UI) | A_BOLD); | |
| } | |
| static void render_scene(void) { | |
| werase(ctx.state.game_pad); | |
| render_world(ctx.state.game_pad, &ctx.world, ctx.state.height, | |
| ctx.state.width); | |
| render_player(ctx.state.game_pad, &ctx.player); | |
| prefresh(ctx.state.game_pad, GROUND_OFFSET, 0, GROUND_OFFSET, 0, | |
| ctx.state.height - 1, ctx.state.width - 1); | |
| render_ui(&ctx); | |
| refresh(); | |
| } | |
| int main(void) { | |
| if (!init_game()) | |
| return 1; | |
| int initial_high = ctx.state.high_scores[0]; | |
| while (ctx.state.running) { | |
| clock_t start = clock(); | |
| process_input(); | |
| update_state(); | |
| render_scene(); | |
| clock_t end = clock(); | |
| double elapsed = (double)(end - start) / CLOCKS_PER_SEC * 1000000.0; | |
| if (elapsed < FPS_DELAY_US) { | |
| struct timespec ts; | |
| ts.tv_sec = 0; | |
| ts.tv_nsec = (long)((FPS_DELAY_US - elapsed) * 1000); | |
| nanosleep(&ts, NULL); | |
| } | |
| } | |
| /* Score Handling */ | |
| if (ctx.player.score > ctx.state.high_scores[0]) { | |
| ctx.state.high_scores[2] = ctx.state.high_scores[1]; | |
| ctx.state.high_scores[1] = ctx.state.high_scores[0]; | |
| ctx.state.high_scores[0] = ctx.player.score; | |
| } | |
| /* Game Over Screen */ | |
| nodelay(stdscr, FALSE); | |
| clear(); | |
| int cy = ctx.state.height / 2; | |
| int cx = ctx.state.width / 2; | |
| attron(COLOR_PAIR(PAIR_UI) | A_BOLD); | |
| mvprintw(cy - 2, cx - 10, "G A M E O V E R"); | |
| mvprintw(cy, cx - 12, "Final Score: %06d", ctx.player.score); | |
| if (ctx.player.score > initial_high) { | |
| attron(A_BLINK | COLOR_PAIR(PAIR_COIN)); | |
| mvprintw(cy + 2, cx - 10, "NEW HIGH SCORE!"); | |
| attroff(A_BLINK | COLOR_PAIR(PAIR_COIN)); | |
| } | |
| mvprintw(cy + 4, cx - 8, "Press Any Key..."); | |
| attroff(COLOR_PAIR(PAIR_UI) | A_BOLD); | |
| refresh(); | |
| getch(); | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment