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}