flybrain/services/flysim/crates/flybrain-gb/src/pokemon_red/mapgrid/tests.rs
claude 68228ef2a1 gb: decode the whole current map into a walkability grid
The walkable predicate answered for the ten-by-nine screen window and Unknown
everywhere else. MapGrid is the same rule over every tile of the loaded map:
block ids out of wOverworldMap, a blocks-to-tiles read of the tileset header s
blockset through the new bank-aware ROM read, the tileset s collision list as
before, and the TilePairCollisionsLand values as directed walls both ways.

The reader checks itself before it answers: the decode is compared against the
window predicate over the player s own neighbourhood, and a frame where the
window can answer for none of it -- a battle, a text box, a frame mid-warp -- is
refused, because wOverworldMap shares its bytes with the picture buffer. Every
refusal is named (GridRefusal) and leaves the window predicate in charge.

Cached per map id and size, so a map is decoded once on arrival rather than once
per question, and owned beside the session ledgers: never checkpointed.
2026-09-22 03:32:35 +00:00

161 lines
6.8 KiB
Rust

//! The decoder, against a made-up tileset.
//!
//! Every byte here is synthetic on purpose: a block table, a blockset and a collision list of this
//! module's own making, so a failure says which of the three readings is wrong rather than "the
//! cartridge disagrees". The cartridge half — that the decode matches real presses on Pallet Town
//! and Viridian Forest — is `tests/rom_map_grid.rs`.
use super::*;
use crate::pokemon_red::macros::state::{Facing, Walkable};
/// A blockset with three blocks: all floor, all wall, and one whose four map-tile quadrants are
/// four different tile ids.
///
/// A block is four screen tiles each way and a map tile is two, so the quadrants are the four
/// corners and the tile a map tile's walkability comes from is the top left of its own quadrant.
fn blockset() -> Vec<u8> {
let floor = [FLOOR; 16];
let wall = [WALL; 16];
let quadrants = [
NORTH_WEST, 0x90, NORTH_EAST, 0x91, //
0x92, 0x93, 0x94, 0x95, //
SOUTH_WEST, 0x96, SOUTH_EAST, 0x97, //
0x98, 0x99, 0x9a, 0x9b,
];
let mut out = Vec::new();
out.extend_from_slice(&floor);
out.extend_from_slice(&wall);
out.extend_from_slice(&quadrants);
out
}
const FLOOR: u8 = 0x01;
const WALL: u8 = 0x60;
const NORTH_WEST: u8 = 0x11;
const NORTH_EAST: u8 = 0x12;
const SOUTH_WEST: u8 = 0x13;
const SOUTH_EAST: u8 = 0x14;
/// Floor and the four quadrant tiles are passable; the wall tile is in no list.
fn tileset(id: u8) -> Tileset {
Tileset {
id,
blocks: blockset(),
passable: vec![FLOOR, NORTH_WEST, NORTH_EAST, SOUTH_WEST, SOUTH_EAST, TERMINATOR],
}
}
#[test]
fn one_block_becomes_four_map_tiles_from_its_four_quadrants() {
let grid = decode(7, 1, 1, &[2], &tileset(0));
assert_eq!((grid.map(), grid.width(), grid.height()), (7, 2, 2));
assert_eq!(grid.tile_id(0, 0), Some(NORTH_WEST));
assert_eq!(grid.tile_id(1, 0), Some(NORTH_EAST));
assert_eq!(grid.tile_id(0, 1), Some(SOUTH_WEST));
assert_eq!(grid.tile_id(1, 1), Some(SOUTH_EAST));
}
#[test]
fn the_collision_list_answers_every_tile_of_a_map_larger_than_the_window() {
// Ten blocks by nine is twenty tiles by eighteen: wider than the ten-by-nine window the
// screen buffer can answer for, which is the point of the whole grid.
let (wide, high) = (10u8, 9u8);
let mut blocks = vec![0u8; usize::from(wide) * usize::from(high)];
// A wall down the middle column of blocks.
for row in 0..usize::from(high) {
blocks[row * usize::from(wide) + 5] = 1;
}
let grid = decode(0, wide, high, &blocks, &tileset(0));
assert_eq!((grid.width(), grid.height()), (20, 18));
assert_eq!(grid.walkable(0, 0), Walkable::Yes);
assert_eq!(grid.walkable(19, 17), Walkable::Yes, "the far corner, which no window reaches");
assert_eq!(grid.walkable(10, 9), Walkable::No);
assert_eq!(grid.walkable(11, 9), Walkable::No);
assert_eq!(grid.unknown_count(), 0);
assert_eq!(grid.walkable_count(), 20 * 18 - 18 * 2);
// Off the map is not a tile to stand on, which is the window predicate's own answer for it.
assert_eq!(grid.walkable(20, 0), Walkable::No);
assert_eq!(grid.walkable(0, 18), Walkable::No);
}
#[test]
fn a_tile_pair_collision_is_a_wall_in_both_directions_and_only_in_its_own_tileset() {
// `FOREST, $30, $2E`: two tiles that are each passable on their own and that the cartridge
// refuses the step between (`data/tilesets/pair_collision_tile_ids.asm`).
let (one, other) = (0x30, 0x2e);
let tiles = Tileset {
id: tileset::FOREST,
blocks: [[one; 16], [other; 16]].concat(),
passable: vec![one, other, TERMINATOR],
};
let grid = decode(0, 2, 1, &[0, 1], &tiles);
assert_eq!(grid.walkable(1, 0), Walkable::Yes);
assert_eq!(grid.walkable(2, 0), Walkable::Yes);
assert!(grid.walled(1, 0, Facing::Right), "the step onto the other tile");
assert!(grid.walled(2, 0, Facing::Left), "and the step back, which is the same rule");
assert!(!grid.walled(0, 0, Facing::Right), "two tiles of the same id are not a pair");
// The same two tile ids in a tileset the list does not name are ordinary ground.
let elsewhere = Tileset { id: tileset::CAVERN, ..tiles };
let grid = decode(0, 2, 1, &[0, 1], &elsewhere);
assert!(!grid.walled(1, 0, Facing::Right));
assert!(!grid.walled(2, 0, Facing::Left));
}
#[test]
fn a_block_id_the_blockset_does_not_reach_stays_unknown() {
// A blockset read short — a header pointer near the end of its bank — is not a tile the game
// is unsure about, and the search prices `Unknown` as plausible ground rather than refusing
// it, so saying so is the honest answer.
let grid = decode(0, 2, 1, &[0, 9], &tileset(0));
assert_eq!(grid.walkable(0, 0), Walkable::Yes);
assert_eq!(grid.walkable(2, 0), Walkable::Unknown);
assert_eq!(grid.tile_id(2, 0), None);
// Block 9 is one block, which is four map tiles: two rows of two.
assert_eq!(grid.unknown_count(), 4);
}
#[test]
fn reachable_from_counts_what_a_walk_can_get_to_and_not_what_it_can_see() {
// Two floor blocks with a wall block between them: eight walkable tiles, four of them fenced
// off from the fly. This is the number that tells a stalled walk from a long one.
let grid = decode(0, 3, 1, &[0, 1, 0], &tileset(0));
assert_eq!(grid.walkable_count(), 8);
assert_eq!(grid.reachable_from(0, 0), 4);
assert_eq!(grid.reachable_from(4, 0), 4);
// With the wall gone, the whole row is one region.
let grid = decode(0, 3, 1, &[0, 0, 0], &tileset(0));
assert_eq!(grid.reachable_from(0, 0), 12);
}
#[test]
fn a_directed_wall_fences_a_region_off_for_the_reachable_count_too() {
let (one, other) = (0x30, 0x2e);
let tiles = Tileset {
id: tileset::FOREST,
blocks: [[one; 16], [other; 16]].concat(),
passable: vec![one, other, TERMINATOR],
};
// A column of `$30` and a column of `$2e`, four tiles each, with the tile-pair rule between
// every pair of them: every tile is walkable and half of them are unreachable.
let grid = decode(0, 2, 1, &[0, 1], &tiles);
assert_eq!(grid.walkable_count(), 8);
assert_eq!(grid.reachable_from(0, 0), 4);
}
#[test]
fn the_cache_holds_one_map_and_drops_it_on_arrival_somewhere_else() {
let mut grids = MapGrids::default();
assert_eq!(grids.held(), None);
let stored = grids.store(decode(3, 2, 2, &[0, 0, 0, 0], &tileset(0)));
assert_eq!(grids.held(), Some(3));
assert_eq!(grids.get(3, 4, 4).as_deref(), Some(&*stored));
// The same map at another size is another map's block data under the same id, which is what a
// half-loaded header looks like.
assert!(grids.get(3, 8, 8).is_none());
assert!(grids.get(4, 4, 4).is_none());
grids.store(decode(4, 1, 1, &[0], &tileset(0)));
assert_eq!(grids.held(), Some(4));
assert!(grids.get(3, 4, 4).is_none());
}