Merge pull request 'map: implement seeded generation; allow passing custom seed' (#17) from notashelf/push-svxosluqnsnp into main

Reviewed-on: #17
This commit is contained in:
raf 2026-04-10 11:15:44 +00:00
commit 702b4258e0
5 changed files with 111 additions and 14 deletions

View file

@ -173,6 +173,8 @@ typedef struct {
int potions_used;
int floors_reached;
int final_score;
// Seed for this run
unsigned int run_seed;
} GameState;

View file

@ -10,9 +10,12 @@
#include "render.h"
#include "rng.h"
#include "settings.h"
#include <ctype.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Add message to action log
static void add_log(GameState *gs, const char *msg) {
@ -102,11 +105,14 @@ static void update_effects(GameState *gs) {
// Initialize a new floor
static void init_floor(GameState *gs, int floor_num) {
// Seed RNG with run seed combined with floor number for deterministic generation
rng_seed(gs->run_seed + floor_num * 54321);
// Generate dungeon
dungeon_generate(&gs->dungeon, &gs->map, floor_num);
// Seed rng for this floor's content
rng_seed(floor_num * 54321);
rng_seed(gs->run_seed + floor_num * 98765);
// Find spawn position
int start_x, start_y;
@ -478,7 +484,13 @@ static int handle_input(GameState *gs) {
// Check for restart (works during game over)
if (IsKeyPressed(KEY_R) && gs->game_over) {
memset(gs, 0, sizeof(GameState));
// Generate a new random seed for the new run
gs->run_seed = (unsigned int)time(NULL);
init_floor(gs, 1);
// Update window title with new seed
char title[128];
snprintf(title, sizeof(title), "Roguelike - Seed: %u", gs->run_seed);
SetWindowTitle(title);
return 0;
}
@ -508,12 +520,12 @@ void load_audio_assets(GameState *gs) {
}
// Main game loop
static void game_loop(void) {
static void game_loop(unsigned int run_seed) {
GameState gs;
memset(&gs, 0, sizeof(GameState));
gs.run_seed = run_seed;
load_audio_assets(&gs);
// Initialize first floor
rng_seed(12345);
init_floor(&gs, 1);
// Disable esc to exit
@ -533,10 +545,16 @@ static void game_loop(void) {
break;
if (IsKeyPressed(KEY_R)) {
memset(&gs, 0, sizeof(GameState));
// Generate a new random seed for the new run
gs.run_seed = (unsigned int)time(NULL);
gs.game_over = 0;
gs.game_won = 0;
load_audio_assets(&gs);
init_floor(&gs, 1);
// Update window title with new seed
char title[128];
snprintf(title, sizeof(title), "Roguelike - Seed: %u", gs.run_seed);
SetWindowTitle(title);
}
}
@ -579,6 +597,9 @@ static void game_loop(void) {
render_message(gs.last_message);
}
// Draw persistent seed display in top right
render_seed_display(gs.run_seed);
// Draw game over screen
if (gs.game_over) {
// Compute final score
@ -589,7 +610,7 @@ static void game_loop(void) {
}
render_end_screen(gs.game_won, gs.total_kills, gs.items_collected, gs.damage_dealt, gs.damage_taken,
gs.crits_landed, gs.times_hit, gs.potions_used, gs.floors_reached, gs.turn_count,
gs.final_score);
gs.final_score, gs.run_seed);
}
EndDrawing();
@ -599,17 +620,67 @@ static void game_loop(void) {
}
}
int main(void) {
// Check if a string is a valid unsigned integer
static int is_valid_uint(const char *str) {
if (str == NULL || *str == '\0')
return 0;
// Check for optional leading +
if (*str == '+')
str++;
// Must have at least one digit
if (*str == '\0')
return 0;
// All characters must be digits
for (const char *p = str; *p != '\0'; p++) {
if (!isdigit((unsigned char)*p))
return 0;
}
return 1;
}
int main(int argc, char **argv) {
// Parse command-line arguments
unsigned int run_seed = 0;
int seed_provided = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--seed") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "Error: --seed requires a value\n");
fprintf(stderr, "Usage: %s [--seed <number>]\n", argv[0]);
return 1;
}
const char *seed_str = argv[i + 1];
if (!is_valid_uint(seed_str)) {
fprintf(stderr, "Error: Invalid seed value: %s\n", seed_str);
fprintf(stderr, "Seed must be a non-negative integer\n");
return 1;
}
run_seed = (unsigned int)strtoul(seed_str, NULL, 10);
seed_provided = 1;
i++; // Skip the value
}
}
// If no seed provided, generate random seed from time
if (!seed_provided) {
run_seed = (unsigned int)time(NULL);
}
printf("Starting game with seed: %u\n", run_seed);
// Initialize audio
audio_init();
// Initialize random number generator
SetRandomSeed(88435);
// Initialize window
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, "Roguelike");
// Initialize window with seed in title
char title[128];
snprintf(title, sizeof(title), "Roguelike - Seed: %u", run_seed);
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, title);
SetTargetFPS(60);
// Run game
game_loop();
game_loop(run_seed);
// Cleanup
CloseWindow();

View file

@ -170,9 +170,6 @@ void get_random_floor_tile(Map *map, int *x, int *y, int attempts) {
}
void dungeon_generate(Dungeon *d, Map *map, int floor_num) {
// Seed RNG with floor number for deterministic generation
rng_seed(floor_num * 12345);
// Initialize map to all walls
map_init(map);

View file

@ -507,7 +507,7 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak
}
void render_end_screen(int is_victory, int kills, int items, int damage_dealt, int damage_taken, int crits,
int times_hit, int potions, int floors, int turns, int score) {
int times_hit, int potions, int floors, int turns, int score, unsigned int seed) {
// Semi-transparent overlay
Rectangle overlay = {0, 0, (float)SCREEN_WIDTH, (float)SCREEN_HEIGHT};
DrawRectangleRec(overlay, (Color){0, 0, 0, 210});
@ -523,7 +523,7 @@ void render_end_screen(int is_victory, int kills, int items, int damage_dealt, i
int box_x = SCREEN_WIDTH / 2 - 200;
int box_y = 110;
int box_w = 400;
int box_h = 320;
int box_h = 350;
DrawRectangle(box_x, box_y, box_w, box_h, (Color){20, 20, 20, 240});
DrawRectangleLines(box_x, box_y, box_w, box_h, (Color){100, 100, 100, 255});
@ -589,7 +589,14 @@ void render_end_screen(int is_victory, int kills, int items, int damage_dealt, i
DrawText("SCORE:", col1_x, row_y, 22, GOLD);
snprintf(line, sizeof(line), "%d", score);
DrawText(line, col1_x + 90, row_y, 22, GOLD);
row_y += 35;
// Seed display
DrawText("SEED:", col1_x, row_y, 18, label_color);
snprintf(line, sizeof(line), "%u", seed);
DrawText(line, col1_x + 60, row_y, 18, END_SEED);
// Instructions
if (is_victory) {
const char *subtitle = "Press R to play again or Q to quit";
int sub_width = MeasureText(subtitle, 20);
@ -672,3 +679,18 @@ void render_message(const char *message) {
DrawText(message, text_x, text_y, font_size, WHITE);
}
void render_seed_display(unsigned int seed) {
char seed_text[64];
snprintf(seed_text, sizeof(seed_text), "Seed: %u", seed);
const int font_size = 14;
int text_width = MeasureText(seed_text, font_size);
// Position at top right with padding
int x = SCREEN_WIDTH - text_width - 10;
int y = 5;
// Draw with non-obstructive dim text color
DrawText(seed_text, x, y, font_size, TEXT_DIM);
}

View file

@ -1,3 +1,4 @@
#ifndef RENDER_H
#define RENDER_H
@ -68,6 +69,7 @@
#define END_OVERLAY (Color){0, 0, 0, 210}
#define END_BOX_BG (Color){20, 20, 20, 240}
#define END_BOX_BORDER (Color){100, 100, 100, 255}
#define END_SEED (Color){150, 200, 255, 255}
// Portrait placeholder
// FIXME: remove when player sprites are available
@ -99,9 +101,12 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak
// Render end screen (victory or death) with stats breakdown
void render_end_screen(int is_victory, int kills, int items, int damage_dealt, int damage_taken, int crits,
int times_hit, int potions, int floors, int turns, int score);
int times_hit, int potions, int floors, int turns, int score, unsigned int seed);
// Render a message popup
void render_message(const char *message);
// Render seed display at top right of screen
void render_seed_display(unsigned int seed);
#endif // RENDER_H