Skip to content

Instantly share code, notes, and snippets.

@7etsuo
Created November 20, 2025 07:51
Show Gist options
  • Select an option

  • Save 7etsuo/f6e48b826ee17b62afe2345181bc3fab to your computer and use it in GitHub Desktop.

Select an option

Save 7etsuo/f6e48b826ee17b62afe2345181bc3fab to your computer and use it in GitHub Desktop.
ASCII Marico
/**
* 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