Previously unknown flags were silently ignored; now they print usage to stderr and exit 2, matching the genesis3d behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
241 lines
8.6 KiB
C
241 lines
8.6 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "gen/grid.h"
|
|
#include "gen/rng.h"
|
|
#include "gen/room_types.h"
|
|
#include "gen/events.h"
|
|
#include "gen/accretion.h"
|
|
#include "gen/loops.h"
|
|
#include "gen/lakes.h"
|
|
#include "gen/walls.h"
|
|
#include "gen/stairs.h"
|
|
#include "gen/chokepoints.h"
|
|
#include "gen/machines.h"
|
|
#include "gen/dijkstra.h"
|
|
#include "json_emit.h"
|
|
|
|
static int g_verbose = 0;
|
|
static int g_quiet = 0;
|
|
static int g_mark_unreached = 0;
|
|
static int g_emit_json = 0;
|
|
|
|
static void print_ascii(grid_t g) {
|
|
dist_map_t dist;
|
|
if (g_mark_unreached) {
|
|
int sx = -1, sy = -1;
|
|
for (int y = 0; y < DROWS && sx < 0; y++)
|
|
for (int x = 0; x < DCOLS && sx < 0; x++) {
|
|
uint8_t t = g[y][x].terrain;
|
|
if (t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR || t == T_BRIDGE) {
|
|
sx = x; sy = y;
|
|
}
|
|
}
|
|
if (sx >= 0) bfs_from(g, sx, sy, dist, -1, -1);
|
|
}
|
|
for (int y = 0; y < DROWS; y++) {
|
|
for (int x = 0; x < DCOLS; x++) {
|
|
char ch;
|
|
uint8_t t = g[y][x].terrain;
|
|
int passable = (t == T_FLOOR || t == T_CORRIDOR || t == T_DOOR || t == T_BRIDGE);
|
|
if (g_mark_unreached && passable && dist[y][x] == DIST_UNREACHABLE) {
|
|
ch = '!';
|
|
} else switch (t) {
|
|
case T_FLOOR: ch = (g[y][x].flags & F_WREATH) ? ';' : '.'; break;
|
|
case T_CORRIDOR: ch = ','; break;
|
|
case T_DOOR: ch = '+'; break;
|
|
case T_WALL: ch = (g[y][x].flags & F_WREATH) ? ':' : '#'; break;
|
|
case T_LIQUID:
|
|
switch (g[y][x].liquid) {
|
|
case L_LAVA: ch = '^'; break;
|
|
case L_CHASM: ch = 'v'; break;
|
|
case L_BRIMSTONE: ch = '%'; break;
|
|
case L_WATER:
|
|
default: ch = '~'; break;
|
|
}
|
|
break;
|
|
case T_BRIDGE: ch = '='; break;
|
|
case T_STAIRS_UP: ch = '<'; break;
|
|
case T_STAIRS_DOWN: ch = '>'; break;
|
|
default: ch = ' '; break;
|
|
}
|
|
putchar(ch);
|
|
}
|
|
putchar('\n');
|
|
}
|
|
}
|
|
|
|
static void stderr_sink(void *ctx, const gen_event_t *ev) {
|
|
(void)ctx;
|
|
if (!g_verbose) return;
|
|
fprintf(stderr, "[ev %4u] %s\n",
|
|
ev->step, event_kind_name((event_kind_t)ev->kind));
|
|
}
|
|
|
|
typedef struct {
|
|
int counts[EV_KIND_COUNT];
|
|
} sink_ctx_t;
|
|
|
|
static void counting_sink(void *ctx, const gen_event_t *ev) {
|
|
sink_ctx_t *s = (sink_ctx_t *)ctx;
|
|
s->counts[ev->kind]++;
|
|
if (g_verbose) stderr_sink(NULL, ev);
|
|
}
|
|
|
|
typedef struct {
|
|
int rooms, loops, lakes_proposed, lakes_rejected, lakes_placed, bridges;
|
|
int machines_proposed, machines_placed;
|
|
int floor_cells;
|
|
int connected;
|
|
int unreached;
|
|
} run_stats_t;
|
|
|
|
static void run_once(uint64_t seed, int depth, grid_t *gp, run_stats_t *out, int verify) {
|
|
cell_t (*g)[DCOLS] = *gp;
|
|
rng_t rng;
|
|
rng_seed(&rng, seed);
|
|
grid_fill(g, T_NOTHING);
|
|
|
|
sink_ctx_t sctx = {0};
|
|
event_sink_t sink = { .fn = counting_sink, .ctx = &sctx, .step = 0 };
|
|
|
|
event_emit(&sink, EV_GEN_BEGIN, NULL, 0);
|
|
|
|
accrete_rooms(g, &rng, &sink);
|
|
if (verify && !is_connected(g, NULL))
|
|
fprintf(stderr, "!! seed %llu: disconnected after accretion\n", (unsigned long long)seed);
|
|
add_loops(g, &sink);
|
|
if (verify && !is_connected(g, NULL))
|
|
fprintf(stderr, "!! seed %llu: disconnected after loops\n", (unsigned long long)seed);
|
|
place_lakes(g, &rng, &sink, depth);
|
|
if (verify && !is_connected(g, NULL))
|
|
fprintf(stderr, "!! seed %llu: disconnected after lakes\n", (unsigned long long)seed);
|
|
apply_wreaths(g, &sink);
|
|
place_bridges(g, &sink);
|
|
finish_walls(g, &sink);
|
|
place_stairs(g, &sink);
|
|
|
|
/* Chokepoint analysis + machine placement. */
|
|
static choke_map_t choke;
|
|
static choke_flag_t is_choke, is_gate;
|
|
compute_chokepoints(g, &sink, choke, is_choke, is_gate);
|
|
place_machines(g, &rng, depth, &sink, choke, is_gate);
|
|
|
|
event_emit(&sink, EV_GEN_END, NULL, 0);
|
|
|
|
out->rooms = sctx.counts[EV_ROOM_PLACED];
|
|
out->loops = sctx.counts[EV_LOOP_ACCEPTED];
|
|
out->lakes_proposed = sctx.counts[EV_LAKE_PROPOSED];
|
|
out->lakes_rejected = sctx.counts[EV_LAKE_REJECTED_IMPASSABLE];
|
|
out->lakes_placed = sctx.counts[EV_LAKE_PLACED];
|
|
out->bridges = sctx.counts[EV_BRIDGE_PLACED];
|
|
out->machines_proposed = sctx.counts[EV_MACHINE_PROPOSED];
|
|
out->machines_placed = sctx.counts[EV_MACHINE_ACCEPTED];
|
|
out->floor_cells = grid_count_floor(g);
|
|
out->connected = is_connected(g, &out->unreached);
|
|
}
|
|
|
|
static void print_summary(uint64_t seed, int depth, const run_stats_t *s) {
|
|
fprintf(stderr,
|
|
"seed=%llu depth=%d rooms=%d loops=%d "
|
|
"lakes_proposed=%d lakes_rejected=%d lakes_placed=%d "
|
|
"bridges=%d machines=%d/%d floor=%d connected=%d unreached=%d\n",
|
|
(unsigned long long)seed, depth,
|
|
s->rooms, s->loops,
|
|
s->lakes_proposed, s->lakes_rejected, s->lakes_placed,
|
|
s->bridges, s->machines_placed, s->machines_proposed,
|
|
s->floor_cells, s->connected, s->unreached);
|
|
}
|
|
|
|
static void usage(FILE *f, const char *prog) {
|
|
fprintf(f,
|
|
"usage: %s [options]\n"
|
|
"\n"
|
|
"Generates a single Brogue-style 2D level and prints an ASCII map.\n"
|
|
"\n"
|
|
"options:\n"
|
|
" --seed N RNG seed (default 1234)\n"
|
|
" --depth N dungeon depth 1..26, biases lakes/machines (default 1)\n"
|
|
" --verify assert connectivity after each pipeline stage\n"
|
|
" --mark-unreached print '!' on passable cells BFS cannot reach from start\n"
|
|
" --verbose stream generator events to stderr\n"
|
|
" --quiet suppress ASCII map (summary only)\n"
|
|
" --emit=json, --json emit grid JSON to stdout instead of ASCII\n"
|
|
" --stress N run N seeds (from --stress-base) and report failures\n"
|
|
" --stress-base N starting seed for --stress (default 1)\n"
|
|
" -h, --help show this help and exit\n",
|
|
prog);
|
|
}
|
|
|
|
static int cmd_stress(int n, uint64_t base_seed, int depth) {
|
|
grid_t g;
|
|
int failures = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
uint64_t seed = base_seed + (uint64_t)i;
|
|
run_stats_t s = {0};
|
|
run_once(seed, depth, &g, &s, 0);
|
|
if (!s.connected) {
|
|
fprintf(stderr, "FAIL seed=%llu unreached=%d\n",
|
|
(unsigned long long)seed, s.unreached);
|
|
failures++;
|
|
}
|
|
}
|
|
fprintf(stderr, "stress: %d seeds, %d failures\n", n, failures);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
uint64_t seed = 1234;
|
|
int depth = 1;
|
|
int verify = 0;
|
|
int stress_n = 0;
|
|
uint64_t stress_base = 1;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (!strcmp(argv[i], "--seed") && i + 1 < argc) {
|
|
seed = strtoull(argv[++i], NULL, 10);
|
|
} else if (!strcmp(argv[i], "--depth") && i + 1 < argc) {
|
|
depth = atoi(argv[++i]);
|
|
if (depth < 1) depth = 1;
|
|
if (depth > 26) depth = 26;
|
|
} else if (!strcmp(argv[i], "--verbose")) {
|
|
g_verbose = 1;
|
|
} else if (!strcmp(argv[i], "--quiet")) {
|
|
g_quiet = 1;
|
|
} else if (!strcmp(argv[i], "--verify")) {
|
|
verify = 1;
|
|
} else if (!strcmp(argv[i], "--stress") && i + 1 < argc) {
|
|
stress_n = atoi(argv[++i]);
|
|
} else if (!strcmp(argv[i], "--stress-base") && i + 1 < argc) {
|
|
stress_base = strtoull(argv[++i], NULL, 10);
|
|
} else if (!strcmp(argv[i], "--mark-unreached")) {
|
|
g_mark_unreached = 1;
|
|
} else if (!strcmp(argv[i], "--emit=json") || !strcmp(argv[i], "--json")) {
|
|
g_emit_json = 1;
|
|
} else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
|
usage(stdout, argv[0]);
|
|
return 0;
|
|
} else {
|
|
fprintf(stderr, "unknown arg: %s\n", argv[i]);
|
|
usage(stderr, argv[0]);
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
if (stress_n > 0) return cmd_stress(stress_n, stress_base, depth);
|
|
|
|
grid_t g;
|
|
run_stats_t s = {0};
|
|
run_once(seed, depth, &g, &s, verify);
|
|
if (g_emit_json) {
|
|
emit_grid_json(g, seed, depth);
|
|
if (verify && !s.connected) return 1;
|
|
return 0;
|
|
}
|
|
print_summary(seed, depth, &s);
|
|
if (!g_quiet) print_ascii(g);
|
|
|
|
if (verify && !s.connected) return 1;
|
|
return 0;
|
|
}
|