fix: review findings
- screen transitions snapshot the previous frame (ratatui swaps buffers after draw, so the old snapshot was always blank); frame loop extracted into a testable Runner - .city loading rejects structurally inconsistent files: Grid deserialize checks size vs data, load_city checks header vs body and runs City::validate (now also checks Net link endpoints) - saving from the map hands the live map screen back instead of a fresh explore screen - signage glyphs covered by the unicode-width test Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -189,13 +189,42 @@ impl Dir {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dense row-major grid of `T`.
|
||||
/// Dense row-major grid of `T`. Deserialization rejects data that does not match `size`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "RawGrid<T>")]
|
||||
#[serde(bound(deserialize = "T: Deserialize<'de>"))]
|
||||
pub struct Grid<T> {
|
||||
size: Size,
|
||||
data: Vec<T>,
|
||||
}
|
||||
|
||||
/// The on-disk shape of a grid, before the size/data consistency check.
|
||||
#[derive(Deserialize)]
|
||||
struct RawGrid<T> {
|
||||
size: Size,
|
||||
data: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> TryFrom<RawGrid<T>> for Grid<T> {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(raw: RawGrid<T>) -> Result<Self, Self::Error> {
|
||||
if raw.data.len() != raw.size.area() {
|
||||
return Err(format!(
|
||||
"grid of {}x{} needs {} cells, found {}",
|
||||
raw.size.w,
|
||||
raw.size.h,
|
||||
raw.size.area(),
|
||||
raw.data.len()
|
||||
));
|
||||
}
|
||||
Ok(Grid {
|
||||
size: raw.size,
|
||||
data: raw.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Grid<T> {
|
||||
pub fn filled(size: Size, v: T) -> Self {
|
||||
Self {
|
||||
@@ -308,6 +337,24 @@ mod tests {
|
||||
assert_eq!(g.count(|&v| v == 9), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_rejects_inconsistent_serialized_data() {
|
||||
#[derive(serde::Serialize)]
|
||||
struct Fake {
|
||||
size: Size,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
let bad = ron::to_string(&Fake {
|
||||
size: Size { w: 4, h: 3 },
|
||||
data: vec![0; 5],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(ron::from_str::<Grid<u8>>(&bad).is_err());
|
||||
let good = ron::to_string(&Grid::filled(Size { w: 4, h: 3 }, 7u8)).unwrap();
|
||||
let g: Grid<u8> = ron::from_str(&good).unwrap();
|
||||
assert_eq!(g.count(|&v| v == 7), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dir_delta_opposite() {
|
||||
assert_eq!(Dir::N.delta(), (0, -1));
|
||||
|
||||
@@ -152,6 +152,14 @@ impl City {
|
||||
}
|
||||
}
|
||||
|
||||
for link in &self.net.links {
|
||||
for id in [link.0, link.1] {
|
||||
if id.index() >= self.net.nodes.len() {
|
||||
out.push(Violation::NetAnchor(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for d in &self.districts {
|
||||
let has_building = self.buildings.iter().any(|b| b.district == d.id);
|
||||
let has_public = self.connectors.iter().any(|c| {
|
||||
|
||||
Reference in New Issue
Block a user