perf(citygen): bound each lane search to its endpoints' box; document cyberspace
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
16
README.md
16
README.md
@@ -10,6 +10,7 @@ The generator builds a city as walkable tile maps on four strata:
|
|||||||
|
|
||||||
| Stratum | What lives there |
|
| Stratum | What lives there |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| Net | cyberspace: a dotted grid, a construct on every building, district hubs, link lanes, ICE at the gates |
|
||||||
| Heights | rooftops of the tall towers, helipads, bridges between them |
|
| Heights | rooftops of the tall towers, helipads, bridges between them |
|
||||||
| Skyways | elevated walkways, mid-rise lobbies, platforms over intersections |
|
| Skyways | elevated walkways, mid-rise lobbies, platforms over intersections |
|
||||||
| Street | districts, arterials, blocks, buildings, markets, neon |
|
| Street | districts, arterials, blocks, buildings, markets, neon |
|
||||||
@@ -19,8 +20,12 @@ Every building you see at street level is the same building you stand on at
|
|||||||
Heights. Districts have themes (corp core, night market, megablocks, combat
|
Heights. Districts have themes (corp core, night market, megablocks, combat
|
||||||
zone, docks, industrial, neon strip, old town) that drive street layout,
|
zone, docks, industrial, neon strip, old town) that drive street layout,
|
||||||
building mix, palette and signage. A Net graph of subnets, nodes and links is
|
building mix, palette and signage. A Net graph of subnets, nodes and links is
|
||||||
anchored to the physical tiles and can be shown as a digital overlay. That
|
anchored to the physical tiles and can be shown as a digital overlay.
|
||||||
graph is the seed of the cyberspace side of the game.
|
|
||||||
|
The same graph is cyberspace. Jack in at any street terminal (walk up to it and
|
||||||
|
press `enter`): your body stays at the port and you emerge in the Net at the
|
||||||
|
same spot, in a wireframe world laid out one to one with the city. `enter`
|
||||||
|
again jacks out. In Explore mode `]` past Heights shows the Net.
|
||||||
|
|
||||||
Same seed and parameters always produce a byte-identical city.
|
Same seed and parameters always produce a byte-identical city.
|
||||||
|
|
||||||
@@ -54,18 +59,19 @@ on `RANDOM SEED` / `GENERATE` · `esc` back
|
|||||||
**Generating** `space` pause · `+` `-` steps per frame · `enter` skip to the
|
**Generating** `space` pause · `+` `-` steps per frame · `enter` skip to the
|
||||||
end · `r` reseed after an error · `esc` back
|
end · `r` reseed after an error · `esc` back
|
||||||
|
|
||||||
**Explore** `hjkl` / arrows pan (shift: fast) · `[` `]` stratum down / up ·
|
**Explore** `hjkl` / arrows pan the cursor (shift: fast) · `[` `]` stratum down / up (Net on top) ·
|
||||||
`w` walk · `i` inspector · `m` minimap · `e` legend · `d` digital overlay ·
|
`w` walk · `i` inspector · `m` minimap · `e` legend · `d` digital overlay ·
|
||||||
`s` save · `r` regenerate · `g` configure · `?` keys · `q` title
|
`s` save · `r` regenerate · `g` configure · `?` keys · `q` title
|
||||||
|
|
||||||
**Walk** `hjkl` `yubn` / arrows move · `<` `>` take stairs, lift, ladder or
|
**Walk** `hjkl` `yubn` / arrows move · `<` `>` take stairs, lift, ladder or
|
||||||
manhole · `.` wait · `tab` back to the free camera · panels as above
|
manhole · `enter` jack in at an adjacent terminal / jack out · `.` wait ·
|
||||||
|
`tab` back to the free camera · panels as above
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/citygen world model + city generator (no terminal dependencies)
|
crates/citygen world model + city generator (no terminal dependencies)
|
||||||
crates/cyberpunk the game: ratatui app, screens, renderer, and the `gen` CLI
|
crates/cyberpunk the game: ratatui app, screens, renderer, walking and jacking in, the `gen` CLI
|
||||||
flavor/ names.ron (names, signage glyphs) · style.ron (palettes, glyph sets)
|
flavor/ names.ron (names, signage glyphs) · style.ron (palettes, glyph sets)
|
||||||
docs/superpowers/specs design specs
|
docs/superpowers/specs design specs
|
||||||
docs/superpowers/plans implementation plans
|
docs/superpowers/plans implementation plans
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Stage 12: cyberspace — the Net stratum as a grid with constructs, hubs, lanes and ports.
|
//! Stage 12: cyberspace — the Net stratum as a grid with constructs, hubs, lanes and ports.
|
||||||
|
|
||||||
use super::{GenCtx, StageRunner, StepOut};
|
use super::{GenCtx, StageRunner, StepOut};
|
||||||
use crate::geom::{Pos, Rect};
|
use crate::geom::{Pos, Rect, Size};
|
||||||
use crate::ids::{BuildingId, DistrictId};
|
use crate::ids::{BuildingId, DistrictId};
|
||||||
use crate::net::{Node, NodeKind, SubnetOwner};
|
use crate::net::{Node, NodeKind, SubnetOwner};
|
||||||
use crate::params::GenError;
|
use crate::params::GenError;
|
||||||
@@ -16,6 +16,8 @@ const GRID_LIGHT: u8 = 200;
|
|||||||
const HUB_RADIUS: i32 = 2;
|
const HUB_RADIUS: i32 = 2;
|
||||||
const NODE_SEARCH: i32 = 6;
|
const NODE_SEARCH: i32 = 6;
|
||||||
const LANE_LIGHT: u8 = 255;
|
const LANE_LIGHT: u8 = 255;
|
||||||
|
/// How far a lane may stray from the box spanned by its endpoints.
|
||||||
|
const LANE_MARGIN: i32 = 16;
|
||||||
|
|
||||||
enum Work {
|
enum Work {
|
||||||
Begin,
|
Begin,
|
||||||
@@ -65,6 +67,35 @@ fn outward(r: Rect, p: Pos) -> Pos {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A* confined to the bounding box of `start` and `goal` grown by `margin`, so each
|
||||||
|
/// search allocates a small grid instead of the whole map. A detour that leaves the
|
||||||
|
/// box is not found.
|
||||||
|
fn bounded_astar(
|
||||||
|
size: Size,
|
||||||
|
start: Pos,
|
||||||
|
goal: Pos,
|
||||||
|
margin: i32,
|
||||||
|
cost: impl Fn(Pos, Pos) -> Option<u32>,
|
||||||
|
) -> Option<Vec<Pos>> {
|
||||||
|
let x0 = (start.x.min(goal.x) - margin).max(0);
|
||||||
|
let y0 = (start.y.min(goal.y) - margin).max(0);
|
||||||
|
let x1 = (start.x.max(goal.x) + margin + 1).min(size.w as i32);
|
||||||
|
let y1 = (start.y.max(goal.y) + margin + 1).min(size.h as i32);
|
||||||
|
if x1 <= x0 || y1 <= y0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sub = Size {
|
||||||
|
w: (x1 - x0) as u32,
|
||||||
|
h: (y1 - y0) as u32,
|
||||||
|
};
|
||||||
|
let to_local = |p: Pos| Pos::new(p.x - x0, p.y - y0);
|
||||||
|
let to_world = |p: Pos| Pos::new(p.x + x0, p.y + y0);
|
||||||
|
astar(sub, to_local(start), to_local(goal), |a, b| {
|
||||||
|
cost(to_world(a), to_world(b))
|
||||||
|
})
|
||||||
|
.map(|path| path.into_iter().map(to_world).collect())
|
||||||
|
}
|
||||||
|
|
||||||
fn backbone_anchor(ctx: &GenCtx, d: DistrictId) -> Option<Pos> {
|
fn backbone_anchor(ctx: &GenCtx, d: DistrictId) -> Option<Pos> {
|
||||||
ctx.city
|
ctx.city
|
||||||
.net
|
.net
|
||||||
@@ -252,7 +283,7 @@ impl CyberspaceStage {
|
|||||||
let size = ctx.size();
|
let size = ctx.size();
|
||||||
let path = {
|
let path = {
|
||||||
let city = &ctx.city;
|
let city = &ctx.city;
|
||||||
astar(size, start, goal, |_, q| {
|
bounded_astar(size, start, goal, LANE_MARGIN, |_, q| {
|
||||||
let t = city.tile(K, q)?;
|
let t = city.tile(K, q)?;
|
||||||
if q == start || q == goal {
|
if q == start || q == goal {
|
||||||
return Some(1);
|
return Some(1);
|
||||||
@@ -599,4 +630,30 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lane_search_is_bounded_to_the_endpoints_box() {
|
||||||
|
use crate::geom::Size;
|
||||||
|
let size = Size { w: 200, h: 60 };
|
||||||
|
// A wall across x = 50 whose only opening, y = 58, lies far outside the margin.
|
||||||
|
let walled = |_: Pos, q: Pos| {
|
||||||
|
if q.x == 50 && q.y != 58 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(1u32)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (a, b) = (Pos::new(40, 10), Pos::new(60, 10));
|
||||||
|
assert!(
|
||||||
|
crate::pathing::astar(size, a, b, walled).is_some(),
|
||||||
|
"unbounded search finds the detour"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
super::bounded_astar(size, a, b, 16, walled).is_none(),
|
||||||
|
"bounded search gives up"
|
||||||
|
);
|
||||||
|
let free = |_: Pos, _: Pos| Some(1u32);
|
||||||
|
let path = super::bounded_astar(size, a, b, 16, free).unwrap();
|
||||||
|
assert_eq!((path[0], *path.last().unwrap()), (a, b));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user