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:
commit
702b4258e0
5 changed files with 111 additions and 14 deletions
|
|
@ -173,6 +173,8 @@ typedef struct {
|
|||
int potions_used;
|
||||
int floors_reached;
|
||||
int final_score;
|
||||
// Seed for this run
|
||||
unsigned int run_seed;
|
||||
} GameState;
|
||||
|
||||
|
||||
|
|
|
|||
87
src/main.c
87
src/main.c
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
26
src/render.c
26
src/render.c
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue