Skip to main content
Verticopolis engineGitHub

verticopolis_engine/view/
structure.rs

1//! The structural snapshot: what a view rebuilds when `tower.revision`
2//! moves. Rooms go one record each, in the order of the rooms index
3//! (`Tower::room_index`), which is the slot order of the dynamic frame's
4//! room records. Floor and lobby tiles,
5//! most of a large tower's units, go as runs per floor, so the snapshot
6//! grows with the rooms and the floors rather than with every tile.
7use super::layout::{ROOM_RECORD, RUN_RECORD, SHAFT_FIXED, STRUCTURE_HEADER};
8use crate::clock::{CalendarKind, GameMode};
9use crate::facilities::{LOT_WIDTH, MAX_FLOOR, MIN_FLOOR};
10use crate::frame::unit_fields;
11use crate::sim::Simulation;
12use crate::tower::{Tower, Unit};
13
14/// A run of floor or lobby tiles being collected.
15struct Run {
16    kind: usize,
17    floor: i64,
18    x: i64,
19    width: i64,
20    count: i64,
21    fields: [f64; 5],
22}
23
24impl Run {
25    fn start(u: &Unit) -> Run {
26        Run {
27            kind: u.kind as usize,
28            floor: u.floor,
29            x: u.x,
30            width: u.width,
31            count: 1,
32            fields: unit_fields(u),
33        }
34    }
35
36    /// Whether `u` has this run's kind, floor, width and frame fields.
37    fn matches(&self, u: &Unit) -> bool {
38        u.kind as usize == self.kind
39            && u.floor == self.floor
40            && u.width == self.width
41            && unit_fields(u) == self.fields
42    }
43
44    /// Add `u` to this run when it sits right after its last tile or right
45    /// before its first one, with the same kind, floor, width and fields.
46    /// Whether it did.
47    fn take(&mut self, u: &Unit) -> bool {
48        if !self.matches(u) {
49            return false;
50        }
51        if u.x == self.x + self.count * self.width {
52            self.count += 1;
53        } else if u.x + self.width == self.x {
54            self.x = u.x;
55            self.count += 1;
56        } else {
57            return false;
58        }
59        true
60    }
61
62    /// Whether `next` starts where this run ends, with the same kind, floor,
63    /// width and frame fields.
64    fn continues_with(&self, next: &Run) -> bool {
65        next.kind == self.kind
66            && next.floor == self.floor
67            && next.width == self.width
68            && next.x == self.x + self.count * self.width
69            && next.fields == self.fields
70    }
71
72    fn push(&self, v: &mut Vec<f64>) {
73        v.extend_from_slice(&[
74            self.kind as f64,
75            self.floor as f64,
76            self.x as f64,
77            self.width as f64,
78            self.count as f64,
79        ]);
80        v.extend_from_slice(&self.fields);
81    }
82}
83
84/// Floors a tower can have, `MIN_FLOOR` to `MAX_FLOOR`.
85const FLOORS: usize = (MAX_FLOOR - MIN_FLOOR + 1) as usize;
86
87/// The fewest raw runs past which the walk stops and the runs are read off
88/// the grid instead; `run_limit` raises it with the tower. The limit bounds
89/// the sort, which grows with the scatter while the grid does not: the grid
90/// is one more pass over the units and its `FLOORS * LOT_WIDTH` cells
91/// (41,250). The round 3 review measured where the grid starts to win, on
92/// a list scattered on purpose: at about 4,000 to 5,500 raw runs on medium
93/// (12,062 units), and only past about 29,000 on large (43,358), where the
94/// pass over the units is the larger cost.
95const RUN_LIMIT: usize = 4096;
96
97/// The raw runs past which `tower`'s walk gives way to the grid: two
98/// fifths of its units, or `RUN_LIMIT` on a small tower. No share of the
99/// units meets both measured crossovers (about 4,000 to 5,500 raw runs on
100/// medium, about 29,000 on large), so this one is a compromise: it gives
101/// 4,824 on medium, inside that crossover, and 17,343 on large, where a
102/// list with 17,343 to about 29,000 raw runs takes the grid before it pays.
103/// No reference save comes near it (110 raw runs on large, 639 on medium,
104/// 1,132 on the owner's).
105fn run_limit(tower: &Tower) -> usize {
106    RUN_LIMIT.max(tower.units.len() * 2 / 5)
107}
108
109/// The runs of the floor and lobby tiles, by floor and then x, and
110/// maximal: no run continues the one before it. One walk in unit-list
111/// order makes them (`walk_runs`) unless the list is so scattered that it
112/// makes more than `limit`; then the runs come off a grid of the tiles
113/// (`grid_runs`), or, should a tile stand outside the grid or two share a
114/// cell (no save the loader accepts holds either), from a walk with no
115/// limit. The runs are sorted by where they start when they come out of
116/// order, and the ones that continue each other joined. The join
117/// runs whatever the order: a run that grew left can end where the run
118/// before it starts with the two still in order (a drag over 0 to 4, then
119/// one from 9 down to 5), and a floor outside the grid gets one run per
120/// tile. The sort and the join are over the runs, at most `limit` of them
121/// from the walk, and leave the tiles alone.
122fn tile_runs(tower: &Tower, limit: usize) -> Vec<Run> {
123    let mut runs = walk_runs(tower, limit)
124        .or_else(|| grid_runs(tower))
125        .unwrap_or_else(|| walk_runs(tower, usize::MAX).unwrap_or_default());
126    let ordered = runs
127        .windows(2)
128        .all(|w| (w[0].floor, w[0].x) < (w[1].floor, w[1].x));
129    if !ordered {
130        // A stable sort, so runs that start on the same tile (stacked
131        // tiles, which no accepted save holds) would keep their list order.
132        runs.sort_by_key(|r| (r.floor, r.x));
133    }
134    // `dedup_by` hands the later run first: fold it into the earlier one
135    // when it continues it.
136    runs.dedup_by(|next, last| {
137        let joins = last.continues_with(next);
138        if joins {
139            last.count += next.count;
140        }
141        joins
142    });
143    runs
144}
145
146/// The runs from one walk in unit-list order, or `None` once there are more
147/// than `limit`. Each floor keeps the run its last tile went into, and a
148/// tile joins that run when it sits right after or right before it, so a
149/// floor built in one drag in either direction is one run however the list
150/// interleaves floors. A floor built in separate drags gives one run per
151/// drag.
152fn walk_runs(tower: &Tower, limit: usize) -> Option<Vec<Run>> {
153    let mut runs: Vec<Run> = Vec::new();
154    let mut open: [Option<usize>; FLOORS] = [None; FLOORS];
155    for u in &tower.units {
156        if !u.kind.is_structural() {
157            continue;
158        }
159        // A floor outside the grid, which no save the loader accepts holds,
160        // still gets its runs, without the per-floor memory.
161        let slot = usize::try_from(u.floor - MIN_FLOOR)
162            .ok()
163            .filter(|&f| f < FLOORS);
164        let open_run = slot.and_then(|f| open[f]);
165        if let Some(r) = open_run {
166            if runs[r].take(u) {
167                continue;
168            }
169        }
170        if runs.len() == limit {
171            return None;
172        }
173        if let Some(f) = slot {
174            open[f] = Some(runs.len());
175        }
176        runs.push(Run::start(u));
177    }
178    Some(runs)
179}
180
181/// The runs read off a dense grid of the tiles by floor and x: one more pass
182/// over the units and one over the grid's cells, whatever the list's order.
183/// `None` when a tile stands outside the grid or two share a cell, which no
184/// save the loader accepts holds; `tile_runs` then walks with no limit.
185fn grid_runs(tower: &Tower) -> Option<Vec<Run>> {
186    let width = LOT_WIDTH as usize;
187    let mut grid = vec![u32::MAX; FLOORS * width];
188    for (i, u) in tower.units.iter().enumerate() {
189        if !u.kind.is_structural() {
190            continue;
191        }
192        let f = usize::try_from(u.floor - MIN_FLOOR)
193            .ok()
194            .filter(|&f| f < FLOORS)?;
195        let x = usize::try_from(u.x).ok().filter(|&x| x < width)?;
196        let cell = &mut grid[f * width + x];
197        if *cell != u32::MAX {
198            return None;
199        }
200        *cell = u32::try_from(i).ok()?;
201    }
202    let mut runs: Vec<Run> = Vec::new();
203    for &i in grid.iter().filter(|&&i| i != u32::MAX) {
204        let u = &tower.units[i as usize];
205        if runs.last_mut().is_some_and(|r| r.take(u)) {
206            continue;
207        }
208        runs.push(Run::start(u));
209    }
210    Some(runs)
211}
212
213/// Whether the grid reads `tower`'s tiles, so a test can tell the grid
214/// gave way.
215#[cfg(test)]
216pub(super) fn grid_reads(tower: &Tower) -> bool {
217    grid_runs(tower).is_some()
218}
219
220/// The runs as `structure_view` writes them, from the walk with `limit` (so
221/// a test reads the walk's path with `usize::MAX` and the grid's with 0).
222#[cfg(test)]
223pub(super) fn run_records(tower: &Tower, limit: usize) -> Vec<f64> {
224    let mut v = Vec::new();
225    for run in tile_runs(tower, limit) {
226        run.push(&mut v);
227    }
228    v
229}
230
231/// The structural snapshot as one flat number array.
232///
233/// Header (`STRUCTURE_HEADER` slots): the tower revision, the room count, the
234/// run count, the shaft count, the highest and the lowest floor any unit
235/// stands on with floor 1 always counted (so 1 on an empty tower), read from
236/// the tower's cached floor bounds (`Tower::floor_bounds`, which
237/// `highest_floor` and `lowest_floor` answer from), then the game mode and
238/// the Modern calendar as codes (`layout::MODES`, `layout::CALENDARS`) and
239/// the seed the tower was founded with (`rng.initial_seed`, a `u32`, whole in
240/// an `f64`), all three fixed when the tower was founded and carried here so
241/// a host reads them with the structure it reads at load. Then the rooms
242/// (`ROOM_RECORD` each: id, kind code, floor, x, width), in the order of the
243/// rooms index: the room in slot k is the one a dynamic frame's record with
244/// slot k describes. Then the floor and lobby tiles as runs (`RUN_RECORD`
245/// each: kind code, floor, x, width, count, then the five frame fields every
246/// tile of the run holds), ordered by floor and then x, so every tile is in
247/// exactly one run, and maximal: no run continues the one before it. A run
248/// carries no ids: a host that needs a tile's id asks for the unit at its
249/// floor and x. Then every transport (`SHAFT_FIXED` plus its skipped floors:
250/// id, kind code, x, width, bottom, top, cars, the skip count, then the
251/// skipped floors as the save carries them).
252///
253/// A field belongs here only if every write to it moves the revision. The
254/// tiles' frame fields are here on the strength of the view freshness test
255/// (`oracle_tests.rs`), which fails if one changes with the revision still.
256///
257/// A host reads it when it starts on an engine (a load or an attach builds a
258/// new tower, whose revision starts over) and whenever the frame header's
259/// tower revision moves. The unit count, which the dynamic frame's reader
260/// also keys on, moves only with the revision.
261///
262/// One pass over the units (the runs) and one over the rooms index, plus a
263/// sort of the runs when a floor was built in several drags (at most
264/// `run_limit` of them; a list more scattered than that is read off the
265/// grid in one more pass); the floor bounds and the rooms index are the
266/// tower's per-revision caches, shared with every other reader. The output
267/// is allocated once, after the walk, at its size. A read for each
268/// revision, too slow for each frame.
269pub fn structure_view(sim: &Simulation) -> Vec<f64> {
270    let tower = &sim.tower;
271    let rooms = tower.room_index();
272    let skips: usize = tower
273        .transports
274        .iter()
275        .map(|t| t.skip_floors.as_ref().map_or(0, Vec::len))
276        .sum();
277    let runs = tile_runs(tower, run_limit(tower));
278    let (low, high) = tower.floor_bounds();
279    let mut v = Vec::with_capacity(
280        STRUCTURE_HEADER
281            + rooms.len() * ROOM_RECORD
282            + runs.len() * RUN_RECORD
283            + tower.transports.len() * SHAFT_FIXED
284            + skips,
285    );
286    v.extend_from_slice(&[
287        tower.revision as f64,
288        rooms.len() as f64,
289        runs.len() as f64,
290        tower.transports.len() as f64,
291        high as f64,
292        low as f64,
293        mode_code(sim.mode),
294        calendar_code(sim.modern_calendar),
295        sim.rng.initial_seed as f64,
296    ]);
297    for &i in rooms.iter() {
298        let u = &tower.units[i];
299        v.extend_from_slice(&[
300            u.id as f64,
301            u.kind as usize as f64,
302            u.floor as f64,
303            u.x as f64,
304            u.width as f64,
305        ]);
306    }
307    for run in &runs {
308        run.push(&mut v);
309    }
310    for t in &tower.transports {
311        let skip = t.skip_floors.as_deref().unwrap_or(&[]);
312        v.extend_from_slice(&[
313            t.id as f64,
314            t.kind as usize as f64,
315            t.x as f64,
316            t.width as f64,
317            t.bottom as f64,
318            t.top as f64,
319            t.cars as f64,
320            skip.len() as f64,
321        ]);
322        v.extend_from_slice(skip);
323    }
324    v
325}
326
327/// A game mode's code: its index in `layout::MODES`.
328pub(crate) fn mode_code(mode: GameMode) -> f64 {
329    match mode {
330        GameMode::Classic => 0.0,
331        GameMode::Modern => 1.0,
332    }
333}
334
335/// A Modern calendar's code: its index in `layout::CALENDARS`.
336pub(crate) fn calendar_code(calendar: CalendarKind) -> f64 {
337    match calendar {
338        CalendarKind::Canon => 0.0,
339        CalendarKind::RealWorld => 1.0,
340    }
341}