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.
This commit is contained in:
claude 2026-09-22 03:32:35 +00:00
parent d75b7320ea
commit 68228ef2a1
9 changed files with 1051 additions and 17 deletions

View file

@ -32,12 +32,22 @@ pub const WALL_TILE: u8 = 0x60;
pub struct Wram {
bytes: Vec<u8>,
/// Fake cartridge banks, for the one read that needs one.
///
/// A bank nothing has written answers `None`, which is what a seam with no cartridge behind
/// it answers and what the whole-map grid has to narrow on
/// (`docs/design/macros.md` section 15).
rom: std::collections::HashMap<(u8, u16), u8>,
}
impl MemoryReader for Wram {
fn read8(&mut self, address: u16) -> u8 {
self.bytes[address as usize]
}
fn read_rom(&mut self, bank: u8, address: u16) -> Option<u8> {
self.rom.get(&(bank, address)).copied()
}
}
impl Default for Wram {
@ -49,7 +59,7 @@ impl Default for Wram {
impl Wram {
/// All zero: the title screen, since nothing has set the game-timer bit.
pub fn new() -> Self {
Self { bytes: vec![0; 0x1_0000] }
Self { bytes: vec![0; 0x1_0000], rom: std::collections::HashMap::new() }
}
pub fn set(&mut self, address: u16, value: u8) -> &mut Self {
@ -331,6 +341,76 @@ impl Wram {
self
}
/// The ROM bank and address this fake keeps a tileset's blockset at.
///
/// Any non-zero bank: the point of the number is that it is *not* bank 0, because a bank the
/// CPU bus does not have mapped is the whole reason the seam grew
/// [`MemoryReader::read_rom`] (`docs/design/macros.md` section 15).
pub const BLOCKSET_BANK: u8 = 0x11;
pub const BLOCKSET_BASE: u16 = 0x4000;
/// Which tileset the loaded map uses, for the tile-pair collision lists.
pub fn tileset(&mut self, id: u8) -> &mut Self {
self.set(ram::wCurMapTileset, id)
}
/// A tileset header's blockset, written where a cartridge keeps one: sixteen tile ids per
/// block, in block-id order, in a ROM bank that is not bank 0.
pub fn blockset(&mut self, blocks: &[[u8; 16]]) -> &mut Self {
for (id, block) in blocks.iter().enumerate() {
for (offset, tile) in block.iter().enumerate() {
let address = Self::BLOCKSET_BASE + (id * 16 + offset) as u16;
self.rom.insert((Self::BLOCKSET_BANK, address), *tile);
}
}
self.set(ram::wTilesetBank, Self::BLOCKSET_BANK)
.set(ram::wTilesetBlocksPtr, (Self::BLOCKSET_BASE & 0xff) as u8)
.set(ram::wTilesetBlocksPtr + 1, (Self::BLOCKSET_BASE >> 8) as u8)
}
/// The loaded map's block ids, as `LoadTileBlockMap` leaves them in `wOverworldMap`: rows of
/// `wCurMapWidth + MAP_BORDER * 2` bytes with the map itself three rows and three columns in.
///
/// `blocks` is row-major and `wCurMapWidth * wCurMapHeight` long; the border is left as
/// whatever it was, exactly as a map with no connections leaves it.
pub fn map_blocks(&mut self, blocks: &[u8]) -> &mut Self {
let width = u16::from(self.peek(ram::wCurMapWidth));
let height = u16::from(self.peek(ram::wCurMapHeight));
let border = crate::pokemon_red::mapgrid::MAP_BORDER as u16;
let stride = width + border * 2;
for row in 0..height {
for column in 0..width {
let index = usize::from(row * width + column);
let Some(block) = blocks.get(index) else { continue };
self.set(ram::wOverworldMap + (row + border) * stride + column + border, *block);
}
}
self
}
/// Write the screen buffer so that it agrees with the block data, tile for tile.
///
/// The grid reader cross-checks its decode against the window predicate before it trusts it
/// ([`crate::pokemon_red::state::map_grid`]), and on a cartridge the two agree because they
/// are two readings of one map. This is that agreement in a fake: every map tile inside the
/// ten-by-nine window gets the tile id the blockset gives it, and the tiles outside it keep
/// whatever the screen held, which is what makes them `Unknown` to the window and answerable
/// only by the grid.
pub fn screen_from_blocks(&mut self, blocks: &[u8], blockset: &[[u8; 16]]) -> &mut Self {
let width = usize::from(self.peek(ram::wCurMapWidth));
let height = usize::from(self.peek(ram::wCurMapHeight));
for y in 0..height * 2 {
for x in 0..width * 2 {
let Some(block) = blocks.get((y / 2) * width + (x / 2)) else { continue };
let Some(tiles) = blockset.get(usize::from(*block)) else { continue };
let tile = tiles[(y % 2) * 2 * 4 + (x % 2) * 2];
self.map_tile(x as u8, y as u8, tile);
}
}
self
}
/// A playable overworld frame: Red's ground floor, the fly standing where a cold boot's walk
/// out of the bedroom lands it, every tile a wall until a test opens one.
pub fn overworld() -> Self {

View file

@ -24,7 +24,7 @@
use crate::adapter::PlaceKind;
use super::geography::Amenity;
use super::state::{Facing, GameState};
use super::state::{Facing, GameState, MapGrid};
/// `constants/pokemon_data_constants.asm`: `PARTY_LENGTH`, how many Pokémon fit in the party.
///
@ -315,6 +315,23 @@ pub trait MacroState: GameState {
false
}
/// The whole loaded map's walkability, when the cartridge's tables can be decoded.
///
/// `docs/design/macros.md` section 15, the operator 2026-09-22: "the frontier and warp macros
/// need to be map aware: A* over walkable tiles." [`GameState::walkable`] answers for the
/// ten-by-nine window of the screen buffer and [`super::state::Walkable::Unknown`] for
/// everything else, so before this every walk planned through guesses, re-planned at every
/// window edge, and `GO FRONTIER` aimed at whatever unstood ground happened to be on screen.
///
/// The default is `None`, which is this trait's usual narrowing and here it is also the
/// documented fallback: [`super::path::route`] and [`super::path::frontier`] use the window
/// predicate when the grid is absent, exactly as they did before, and
/// [`crate::pokemon_red::state::GridRefusal`] is what says why it is absent on a frame the
/// reader could not decode.
fn map_grid(&mut self) -> Option<std::sync::Arc<MapGrid>> {
None
}
/// Whether this run has already been into `area`'s mart or Pokémon Center.
///
/// Section 13's `areaVisited(kind, area)`: **one visit per area per run**, so the errand is

View file

@ -16,6 +16,7 @@ use crate::macros::{
MacroPalette, Observed, Outcome, PaletteMode, RunLedger, SLOTS, SceneId, SlotBinding, Started,
};
use super::super::mapgrid::MapGrids;
use super::super::state::PokeState;
use super::cartridge::{Areas, MacroState, Pushed, Stood, Talked, Targets, Tile};
use super::geography;
@ -70,6 +71,14 @@ pub struct PokemonPalette {
/// and walks the fly down on every frame it stands there until the Pokédex exists, and a map
/// does not stop being like that ten brain minutes later.
pushed: Pushed,
/// The decoded walkability of the map the fly is on (`docs/design/macros.md` section 15).
///
/// Owned here beside the session ledgers because it is the same shape of thing: built from
/// the cartridge, valid for as long as the map is loaded, dropped on arrival somewhere else,
/// and never checkpointed -- a restored run decodes the map again on its first overworld
/// frame. It is a *cache* rather than a ledger: nothing about the run is in it, only what the
/// cartridge's own tables say about the ground.
grids: MapGrids,
/// The brain clock of the frame being decided, from [`MacroPalette::clock`].
///
/// The blocked ledger is a *window*, so it needs the same clock the loop publishes rather
@ -93,6 +102,7 @@ impl PokemonPalette {
stood: Stood::default(),
areas: Areas::default(),
pushed: Pushed::default(),
grids: MapGrids::default(),
now_ms: 0.0,
}
}
@ -158,11 +168,13 @@ impl MacroPalette for PokemonPalette {
fn observe(&mut self, memory: &mut dyn MemoryReader, ledger: &dyn RunLedger) -> Observed {
let (scene, bindings, standing) = {
let Self { machine, mode, palette: cached, talked, targets, stood, areas, pushed, .. } =
self;
let Self {
machine, mode, palette: cached, talked, targets, stood, areas, pushed, grids, ..
} = self;
let mut state = PokeState::with_ledgers(
memory, ledger, talked, targets, &*stood, &*areas, &*pushed,
);
)
.caching_grid(grids);
// `GameState::scene` is `pokemon_red::scene::detect` over the same reader, so the
// palette and the scene the feed reports cannot disagree about which frame they are
// for.
@ -227,7 +239,8 @@ impl MacroPalette for PokemonPalette {
&self.stood,
&self.areas,
&self.pushed,
);
)
.caching_grid(&mut self.grids);
self.machine.start(&palette, slot, &mut state)
};
let started = match begun {
@ -267,7 +280,8 @@ impl MacroPalette for PokemonPalette {
&self.stood,
&self.areas,
&self.pushed,
);
)
.caching_grid(&mut self.grids);
self.machine.step(&mut state)
};
// A macro that just finished may have been the press that talked to something; the ledger

View file

@ -392,6 +392,179 @@ impl Walkable {
}
}
/// Every tile of one map, answered: `docs/design/macros.md` section 15.
///
/// [`GameState::walkable`] answers about the ten-by-nine window of the screen buffer and
/// [`Walkable::Unknown`] elsewhere. This is the same predicate over the whole loaded map, decoded
/// from the block and collision tables the cartridge has loaded
/// ([`crate::pokemon_red::mapgrid`]), so a walk can be planned once instead of guessed at and
/// re-planned at every window edge.
///
/// It carries three things and no policy at all:
///
/// - the walkability of every tile, in map-tile coordinates -- the same unit the player's
/// coordinates, the warp table and the sign table are in;
/// - the tile id each answer came from, which is what the cross-check against the window
/// predicate compares and what the tile-pair rules are keyed on;
/// - **directed walls**: one step out of one tile in one direction that the cartridge refuses
/// although both tiles are passable. Pokered has two such rules and the tile-pair lists are the
/// one that can be read ahead of time; a ledge is already [`Walkable::No`] in the collision
/// list, and a person in the way is the sprite list's answer, not the ground's.
///
/// Nothing here is a fact about the run: no ledger, no visit, no target. Those stay where they
/// are, session state in the executor layer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MapGrid {
map: u8,
width: u8,
height: u8,
/// Row-major, `width * height`, [`Walkable::Unknown`] until [`MapGrid::set`] says otherwise.
tiles: Vec<Walkable>,
/// Row-major screen tile id per map tile, `None` where the tile was never decoded.
ids: Vec<Option<u8>>,
/// Row-major bitmask of the directions a step out of this tile is refused in
/// ([`MapGrid::wall`]).
walls: Vec<u8>,
}
impl MapGrid {
/// An all-[`Walkable::Unknown`] grid of this size, for a decoder to fill in.
pub fn new(map: u8, width: u8, height: u8) -> Self {
let cells = usize::from(width) * usize::from(height);
Self {
map,
width,
height,
tiles: vec![Walkable::Unknown; cells],
ids: vec![None; cells],
walls: vec![0; cells],
}
}
/// Which map this grid is of. A grid is only ever valid for the loaded map.
pub fn map(&self) -> u8 {
self.map
}
pub fn width(&self) -> u8 {
self.width
}
pub fn height(&self) -> u8 {
self.height
}
fn index(&self, x: u8, y: u8) -> Option<usize> {
(x < self.width && y < self.height)
.then(|| usize::from(y) * usize::from(self.width) + usize::from(x))
}
/// Record a decoded tile: its screen tile id and what the collision list makes of it.
pub fn set(&mut self, x: u8, y: u8, tile: u8, walkable: Walkable) {
if let Some(index) = self.index(x, y) {
self.tiles[index] = walkable;
self.ids[index] = Some(tile);
}
}
/// Record that the cartridge refuses the step out of `(x, y)` in `facing`.
pub fn wall(&mut self, x: u8, y: u8, facing: Facing) {
if let Some(index) = self.index(x, y) {
self.walls[index] |= wall_bit(facing);
}
}
/// Whether the player could stand on this tile. Off the map is [`Walkable::No`], which is the
/// window predicate's own answer for it.
pub fn walkable(&self, x: u8, y: u8) -> Walkable {
match self.index(x, y) {
None => Walkable::No,
Some(index) => self.tiles[index],
}
}
/// The screen tile id this tile's answer came from, or `None` for a tile never decoded.
pub fn tile_id(&self, x: u8, y: u8) -> Option<u8> {
self.index(x, y).and_then(|index| self.ids[index])
}
/// Whether the step out of `(x, y)` in `facing` is one the cartridge refuses.
pub fn walled(&self, x: u8, y: u8, facing: Facing) -> bool {
self.index(x, y).is_some_and(|index| self.walls[index] & wall_bit(facing) != 0)
}
/// How many tiles of the map the player could stand on.
pub fn walkable_count(&self) -> usize {
self.tiles.iter().filter(|tile| tile.is_walkable()).count()
}
/// How many tiles the map has that were never decoded, i.e. [`Walkable::Unknown`].
pub fn unknown_count(&self) -> usize {
self.tiles.iter().filter(|tile| matches!(tile, Walkable::Unknown)).count()
}
/// Every walkable tile, in row-major order, as `(x, y)`.
pub fn walkable_tiles(&self) -> Vec<(u8, u8)> {
let mut out = Vec::with_capacity(self.walkable_count());
for y in 0..self.height {
for x in 0..self.width {
if self.walkable(x, y).is_walkable() {
out.push((x, y));
}
}
}
out
}
/// How many walkable tiles a walk from `(x, y)` could reach, the directed walls respected.
///
/// The starting tile counts whether or not it is walkable, for the same reason the route
/// search treats it as passable: the fly is standing on it. What this number is *for* is
/// reading a stalled walk at a glance -- a fly on a route with 600 walkable tiles and 4
/// reachable ones is fenced in, and no amount of re-planning is going to help it
/// (`docs/design/macros.md` section 15, `examples/scene_probe.rs`).
pub fn reachable_from(&self, x: u8, y: u8) -> usize {
if self.index(x, y).is_none() {
return 0;
}
let mut seen = vec![false; self.tiles.len()];
let mut queue = std::collections::VecDeque::new();
if let Some(index) = self.index(x, y) {
seen[index] = true;
}
queue.push_back((x, y));
let mut count = 0;
while let Some((tx, ty)) = queue.pop_front() {
count += 1;
for facing in [Facing::Up, Facing::Down, Facing::Left, Facing::Right] {
if self.walled(tx, ty, facing) {
continue;
}
let (dx, dy) = facing.delta();
let Ok(nx) = u8::try_from(i16::from(tx) + dx) else { continue };
let Ok(ny) = u8::try_from(i16::from(ty) + dy) else { continue };
let Some(index) = self.index(nx, ny) else { continue };
if seen[index] || !self.tiles[index].is_walkable() {
continue;
}
seen[index] = true;
queue.push_back((nx, ny));
}
}
count
}
}
/// Which bit of a [`MapGrid`] wall mask a direction is.
fn wall_bit(facing: Facing) -> u8 {
match facing {
Facing::Up => 1,
Facing::Down => 2,
Facing::Left => 4,
Facing::Right => 8,
}
}
/// One entry of the current map's warp table: a door, a staircase, a cave mouth.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Warp {

View file

@ -0,0 +1,246 @@
//! The whole current map's walkability, decoded from the tables the cartridge has loaded.
//!
//! `docs/design/macros.md` section 15, the operator 2026-09-22: "the frontier and warp macros need
//! to be map aware: A* over walkable tiles." [`super::state::walkable`] answers for the ten-by-nine
//! window of the screen buffer and [`super::macros::state::Walkable::Unknown`] for everything
//! else, which is honest and is also why every walk planned through guesses, re-planned at each
//! window edge, and called "frontier" whatever unstood ground happened to be on screen.
//!
//! This module is the same predicate over the whole map. Nothing about the *rule* changes -- a tile
//! is walkable when the current tileset's collision list holds its tile id, which is
//! `CheckTilePassable` -- what changes is where the tile id comes from:
//!
//! | what | where the cartridge keeps it | how it is read |
//! | --- | --- | --- |
//! | the map's blocks | `wOverworldMap`, one byte per 4x4-tile block, rows of `width + MAP_BORDER * 2` with the map three rows and three columns in (`LoadTileBlockMap`) | WRAM, through the ordinary reader |
//! | a block's tiles | the tileset header's blockset, 16 bytes per block id, four rows of four tile ids (`DrawTileBlock`) | ROM, through [`crate::adapter::MemoryReader::read_rom`], because the blockset is not in bank 0 |
//! | which tiles are passable | `wTilesetCollisionPtr`, a `$ff`-terminated list in bank 0 | WRAM pointer, ROM bank 0 read, exactly as the window predicate already did |
//! | which steps are refused between two passable tiles | `TilePairCollisionsLand`, keyed by `wCurMapTileset` | the table's values, quoted below |
//!
//! Two coordinate systems meet here and keeping them apart is the whole of the arithmetic. The
//! cartridge's *blocks* are 4x4 screen tiles; the player moves in *map tiles* of 2x2 screen tiles,
//! which is the unit `wXCoord`, the warp table and everything in `macros/` is in. So one block is
//! [`TILES_PER_BLOCK`] map tiles each way, and the screen tile a map tile's walkability is read
//! from is the top left of its 2x2 quadrant -- the same corner `_GetTileAndCoordsInFrontOfPlayer`
//! reads at screen `(8, 9)` for the tile the player stands on, which is what
//! [`super::state::map_grid`]'s cross-check against the window predicate proves on a cartridge.
//!
//! What it does **not** model is what it did not model before: sprites standing on ground (the
//! sprite list answers that, and [`super::macros::path::frontier`] reads it), warps that fire on
//! the step onto them, and scripts that push the fly off a tile (a session ledger answers that).
use std::sync::Arc;
use super::macros::state::{Facing, MapGrid, Walkable};
/// Screen tiles across and down one block: `BLOCK_WIDTH` and `BLOCK_HEIGHT`
/// (`constants/gfx_constants.asm`).
pub const BLOCK_TILES: usize = 4;
/// Bytes one block takes in a tileset's blockset: its sixteen tile ids, four rows of four.
pub const BLOCK_BYTES: usize = BLOCK_TILES * BLOCK_TILES;
/// Blocks of border `wOverworldMap` keeps on every side: `MAP_BORDER`
/// (`constants/map_data_constants.asm`), which is what lets the view centre on a map smaller than
/// the screen and what a map's own blocks are offset by.
pub const MAP_BORDER: usize = 3;
/// Map tiles across one block. A block is four screen tiles and the player walks two at a time,
/// which is why `wCurMapWidth` in blocks is `map_size().width` in tiles divided by this.
pub const TILES_PER_BLOCK: u8 = 2;
/// Tileset ids the tile-pair lists name (`constants/tileset_constants.asm`, counted in the order
/// that file declares them: OVERWORLD 0 … FOREST 3 … CAVERN 17).
pub mod tileset {
pub const FOREST: u8 = 3;
pub const CAVERN: u8 = 17;
}
/// `TilePairCollisionsLand` at the pinned pokered commit, as `(tileset, one tile, the other)`.
///
/// `data/tilesets/pair_collision_tile_ids.asm`. Values rather than an address, like the item
/// prices in [`super::macros::cartridge`]: what the cartridge keeps here is eleven triples, and
/// the file they came from is quoted so a wrong one is a review comment rather than a mystery.
///
/// `CheckForTilePairCollisions` walks the list comparing the tile the player stands on against
/// *either* member of the pair and the tile in front against the other, so the refusal is
/// symmetric -- a forest's tree line refuses the step in both directions -- and each triple
/// becomes two directed walls. Both tiles are passable on their own, which is why nothing in the
/// collision list can predict it and why the walk used to learn it one refusal at a time
/// ([`super::macros::path::Refusal`]).
///
/// `TilePairCollisionsWater` is deliberately absent: it is the list
/// `CheckForJumpingAndTilePairCollisions` uses while surfing, the fly has no Surf, and a rule for
/// a movement mode the palette cannot enter is not knowledge this grid should carry.
pub const TILE_PAIRS_LAND: [(u8, u8, u8); 11] = [
(tileset::CAVERN, 0x20, 0x05),
(tileset::CAVERN, 0x41, 0x05),
(tileset::FOREST, 0x30, 0x2e),
(tileset::CAVERN, 0x2a, 0x05),
(tileset::CAVERN, 0x05, 0x21),
(tileset::FOREST, 0x52, 0x2e),
(tileset::FOREST, 0x55, 0x2e),
(tileset::FOREST, 0x56, 0x2e),
(tileset::FOREST, 0x20, 0x2e),
(tileset::FOREST, 0x5e, 0x2e),
(tileset::FOREST, 0x5f, 0x2e),
];
/// The tileset's two tables, as the decoder needs them.
///
/// Gathered by [`super::state::map_grid`] from WRAM and ROM; separate from the decoding so that
/// the decoding is a pure function of bytes and can be tested against a made-up tileset.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tileset {
/// `wCurMapTileset`: which tileset, for the tile-pair lists.
pub id: u8,
/// The blockset: sixteen tile ids per block id, in block-id order from zero.
pub blocks: Vec<u8>,
/// The `$ff`-terminated passable-tile list, terminator included or not.
pub passable: Vec<u8>,
}
impl Tileset {
/// The screen tile id at `(column, row)` of block `block`, or `None` past the blockset.
fn tile(&self, block: u8, column: usize, row: usize) -> Option<u8> {
let offset = usize::from(block) * BLOCK_BYTES + row * BLOCK_TILES + column;
self.blocks.get(offset).copied()
}
/// Whether the collision list holds `tile`, which is `CheckTilePassable` and nothing else.
fn passable(&self, tile: u8) -> bool {
for candidate in &self.passable {
if *candidate == TERMINATOR {
return false;
}
if *candidate == tile {
return true;
}
}
false
}
}
/// The `$ff` that ends a collision list.
pub const TERMINATOR: u8 = 0xff;
/// Decode the whole map into a [`MapGrid`].
///
/// `blocks` is the map's block ids, row-major, `width_blocks * height_blocks` of them, already
/// lifted out of `wOverworldMap`'s bordered rows. Every map tile gets one answer:
/// [`Walkable::Yes`] or [`Walkable::No`] from the collision list, and [`Walkable::Unknown`] only
/// for a tile whose block id is past the end of the blockset -- which is a table that was read
/// short rather than a tile the game is unsure about.
pub fn decode(map: u8, width_blocks: u8, height_blocks: u8, blocks: &[u8], tiles: &Tileset) -> MapGrid {
let width = width_blocks.saturating_mul(TILES_PER_BLOCK);
let height = height_blocks.saturating_mul(TILES_PER_BLOCK);
let mut grid = MapGrid::new(map, width, height);
for y in 0..height {
for x in 0..width {
let block_index =
usize::from(y / TILES_PER_BLOCK) * usize::from(width_blocks)
+ usize::from(x / TILES_PER_BLOCK);
let Some(block) = blocks.get(block_index).copied() else {
continue;
};
// The top left screen tile of the map tile's own 2x2 quadrant of the block, which is
// the corner every collision read in the game uses.
let column = usize::from(x % TILES_PER_BLOCK) * usize::from(TILES_PER_BLOCK);
let row = usize::from(y % TILES_PER_BLOCK) * usize::from(TILES_PER_BLOCK);
let Some(tile) = tiles.tile(block, column, row) else {
continue;
};
grid.set(x, y, tile, if tiles.passable(tile) { Walkable::Yes } else { Walkable::No });
}
}
add_pair_walls(&mut grid, tiles.id);
grid
}
/// Turn every tile-pair collision the loaded tileset has into two directed walls.
fn add_pair_walls(grid: &mut MapGrid, tileset: u8) {
let pairs: Vec<(u8, u8)> = TILE_PAIRS_LAND
.iter()
.filter(|(id, _, _)| *id == tileset)
.map(|(_, one, other)| (*one, *other))
.collect();
if pairs.is_empty() {
return;
}
for y in 0..grid.height() {
for x in 0..grid.width() {
let Some(here) = grid.tile_id(x, y) else { continue };
for facing in [Facing::Down, Facing::Right] {
let (dx, dy) = facing.delta();
let Some(nx) = checked_step(x, dx) else { continue };
let Some(ny) = checked_step(y, dy) else { continue };
if nx >= grid.width() || ny >= grid.height() {
continue;
}
let Some(there) = grid.tile_id(nx, ny) else { continue };
if pairs.iter().any(|(one, other)| {
(*one == here && *other == there) || (*one == there && *other == here)
}) {
grid.wall(x, y, facing);
grid.wall(nx, ny, opposite(facing));
}
}
}
}
}
fn checked_step(value: u8, delta: i16) -> Option<u8> {
u8::try_from(i16::from(value) + delta).ok()
}
fn opposite(facing: Facing) -> Facing {
match facing {
Facing::Down => Facing::Up,
Facing::Up => Facing::Down,
Facing::Left => Facing::Right,
Facing::Right => Facing::Left,
}
}
/// The decoded grid of the map that is loaded, kept for as long as it is the loaded one.
///
/// One slot, keyed by map id and size: arriving on another map drops it, and so does a map whose
/// header reads a different size, because both mean the block data under it has been replaced.
/// A decode is a few thousand WRAM reads and a walk of the blockset, so it happens once per
/// arrival rather than once per plan -- and never per frame, which is what a precondition asking
/// for the frontier would otherwise cost.
///
/// Session state like the other ledgers of `docs/design/macros.md` section 12: owned by
/// [`super::macros::driver::PokemonPalette`], never checkpointed, and rebuilt from the cartridge
/// on the first frame after a restore.
#[derive(Debug, Clone, Default)]
pub struct MapGrids {
current: Option<Arc<MapGrid>>,
}
impl MapGrids {
/// The cached grid for `map` at this size, or `None` when the cache is for somewhere else.
///
/// Handed out behind an [`Arc`] so that a caller that asks once a frame -- a precondition
/// wanting to know whether the frontier is empty -- pays a refcount rather than a copy of the
/// map.
pub fn get(&self, map: u8, width: u8, height: u8) -> Option<Arc<MapGrid>> {
self.current
.as_ref()
.filter(|grid| grid.map() == map && grid.width() == width && grid.height() == height)
.map(Arc::clone)
}
/// Keep `grid`, dropping whatever map the cache held before.
pub fn store(&mut self, grid: MapGrid) -> Arc<MapGrid> {
Arc::clone(self.current.insert(Arc::new(grid)))
}
/// Which map the cache is holding, for a log line and the tests.
pub fn held(&self) -> Option<u8> {
self.current.as_ref().map(|grid| grid.map())
}
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,161 @@
//! 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());
}

View file

@ -11,6 +11,7 @@ pub mod catalog;
#[cfg(test)]
pub(crate) mod fake_wram;
pub mod macros;
pub mod mapgrid;
pub mod maps;
pub mod scene;
pub mod state;
@ -343,6 +344,13 @@ impl MemoryReader for SampleCache<'_> {
self.bytes.insert(address, value);
value
}
/// Straight through, uncached: a ROM byte cannot change, so there is nothing
/// for a per-sample cache to save, and the caller that reads a blockset
/// (`docs/design/macros.md` section 15) caches the decoded map instead.
fn read_rom(&mut self, bank: u8, address: u16) -> Option<u8> {
self.source.read_rom(bank, address)
}
}
fn word(memory: &mut impl MemoryReader, address: u16) -> u32 {

View file

@ -29,9 +29,10 @@ use super::macros::cartridge::{
Objective, PushedLedger, StoodLedger, TalkLedger, TalkTarget, TargetKey, TargetLedger, Tile,
};
use super::macros::geography::Amenity;
use super::mapgrid::{self, MapGrids};
use super::macros::state::{
BagItem, Battle, BattleKind, BattleMenu, Connections, Cursor, EnemyMon, Facing, GameState,
MapSize, Mon, Move, Npc, Party, Pc, Player, Scene, Shop, ShopScreen, Sign, StartMenu,
MapGrid, MapSize, Mon, Move, Npc, Party, Pc, Player, Scene, Shop, ShopScreen, Sign, StartMenu,
Status, TextBox, Walkable, Warp,
};
use super::symbols::ram;
@ -775,12 +776,16 @@ pub fn npcs(memory: &mut dyn MemoryReader) -> Vec<Npc> {
npcs
}
/// Whether the current tileset calls this tile id passable.
/// The current tileset's list of passable tile ids, terminator included.
///
/// `CheckTilePassable` walks the list at `wTilesetCollisionPtr` — a little-endian pointer into the
/// collision tables, which all live in ROM bank 0 at this commit and so are always mapped — until
/// it matches or hits `$ff`. `None` means the pointer is not one this module will follow.
fn passable(memory: &mut dyn MemoryReader, tile: u8) -> Option<bool> {
/// it matches or hits `$ff`. `None` means the pointer is not one this module will follow, or the
/// list is not terminated inside the bound below.
///
/// One read of the list serves both callers: [`walkable`] asks about one tile of the window, and
/// [`map_grid`] asks about every tile of the map, and neither is allowed its own copy of the rule.
fn collision_list(memory: &mut dyn MemoryReader) -> Option<Vec<u8>> {
let low = u16::from(read(memory, ram::wTilesetCollisionPtr));
let high = u16::from(read(memory, ram::wTilesetCollisionPtr + 1));
let base = high * 256 + low;
@ -792,16 +797,162 @@ fn passable(memory: &mut dyn MemoryReader, tile: u8) -> Option<bool> {
}
// No collision list in the game is longer than this; the bound is what stops a bad pointer
// from walking the cartridge.
let mut list = Vec::new();
for offset in 0..64u16 {
match read(memory, base + offset) {
0xff => return Some(false),
found if found == tile => return Some(true),
_ => {}
let byte = read(memory, base + offset);
list.push(byte);
if byte == mapgrid::TERMINATOR {
return Some(list);
}
}
None
}
/// Whether the current tileset calls this tile id passable.
///
/// [`collision_list`]'s own walk, and `CheckTilePassable`'s: match or `$ff`, whichever comes
/// first. `None` means the list could not be read at all.
fn passable(memory: &mut dyn MemoryReader, tile: u8) -> Option<bool> {
let list = collision_list(memory)?;
for candidate in list {
if candidate == mapgrid::TERMINATOR {
return Some(false);
}
if candidate == tile {
return Some(true);
}
}
Some(false)
}
/// Why a whole-map grid could not be decoded on this frame.
///
/// `docs/design/macros.md` section 15 asks the fallback to *say when*, so every way out of
/// [`map_grid`] is named rather than being one `None`. Each one leaves the window predicate in
/// charge, which is what the walks did before the grid existed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridRefusal {
/// The map header is not loaded, or its size is out of range (`map_size` said `None`).
NoHeader,
/// The player's coordinates are not readable, so nothing can be cross-checked.
NoPlayer,
/// The tileset's collision list could not be followed ([`collision_list`]).
NoCollisionList,
/// The blockset could not be read: the seam has no cartridge behind it
/// ([`MemoryReader::read_rom`] answered `None`), or the header's pointer runs off the image.
NoBlockset,
/// The screen is not showing the map — a battle, a text box, a frame mid-warp — so there is
/// nothing to check the decode against, and `wOverworldMap` shares its bytes with the picture
/// buffer (`ram/wram.asm`'s own union), which is exactly when it must not be trusted.
NoScreen,
/// The decode and the screen buffer disagree about a tile the window can answer for. A wrong
/// stride, a wrong quadrant or a half-loaded map all land here, and all of them answer
/// plausibly, which is why this check is not optional.
ScreenDisagrees,
}
impl GridRefusal {
/// A short label for a log line and the probes.
pub fn label(self) -> &'static str {
match self {
GridRefusal::NoHeader => "no map header",
GridRefusal::NoPlayer => "no player",
GridRefusal::NoCollisionList => "no collision list",
GridRefusal::NoBlockset => "no blockset",
GridRefusal::NoScreen => "map not on screen",
GridRefusal::ScreenDisagrees => "screen disagrees",
}
}
}
/// The whole loaded map's walkability, decoded from the tables the cartridge has loaded.
///
/// `docs/design/macros.md` section 15. The rule is [`walkable`]'s rule — the tileset's collision
/// list — and what this adds is the tile id of every tile of the map rather than of the ten-by-nine
/// window:
///
/// - the map's **blocks** come from `wOverworldMap`, which `LoadTileBlockMap` fills from the map's
/// own ROM bank as rows of `wCurMapWidth + MAP_BORDER * 2` bytes with the map itself three rows
/// and three columns in. That is WRAM, so it needs no bank at all.
/// - a block's **tiles** come from the tileset header's blockset, sixteen bytes per block id
/// (`DrawTileBlock`). That is ROM, and not bank 0, so it is the one read that goes through
/// [`MemoryReader::read_rom`] — the cartridge image as the process already holds it, because the
/// alternative would be *writing* the mapper's bank register and the joypad is the only write
/// this workspace makes into a running game.
/// - the **tile-pair** refusals come from the values of `TilePairCollisionsLand`, keyed by
/// `wCurMapTileset`, and become directed walls ([`mapgrid::TILE_PAIRS_LAND`]).
///
/// The last thing it does is check itself: the decoded tile ids are compared against
/// [`map_tile_id`] for the player's own tile and its four neighbours, every one the window can
/// answer for. A frame where the window can answer for none of them is refused
/// ([`GridRefusal::NoScreen`]) rather than trusted, because `wOverworldMap` shares its bytes with
/// the picture buffer and a battle is exactly when the blocks under it are somebody else's.
pub fn map_grid(memory: &mut dyn MemoryReader) -> Result<MapGrid, GridRefusal> {
let size = map_size(memory).ok_or(GridRefusal::NoHeader)?;
let player = player(memory).ok_or(GridRefusal::NoPlayer)?;
let passable = collision_list(memory).ok_or(GridRefusal::NoCollisionList)?;
let width_blocks = read(memory, ram::wCurMapWidth);
let height_blocks = read(memory, ram::wCurMapHeight);
let stride = u16::from(width_blocks) + (mapgrid::MAP_BORDER as u16) * 2;
let border = mapgrid::MAP_BORDER as u16;
let mut blocks = Vec::with_capacity(usize::from(width_blocks) * usize::from(height_blocks));
for row in 0..u16::from(height_blocks) {
for column in 0..u16::from(width_blocks) {
blocks.push(read(memory, ram::wOverworldMap + (row + border) * stride + column + border));
}
}
// Only as much of the blockset as this map's blocks index into: a tileset has up to 256 of
// them and a room uses a dozen, and a read that stops at the highest block id used is a read
// that cannot run off the end of a bank for tiles nothing asks about.
let highest = blocks.iter().copied().max().unwrap_or(0);
let bank = read(memory, ram::wTilesetBank);
let base = u16::from(read(memory, ram::wTilesetBlocksPtr))
+ u16::from(read(memory, ram::wTilesetBlocksPtr + 1)) * 256;
let wanted = (usize::from(highest) + 1) * mapgrid::BLOCK_BYTES;
let mut blockset = Vec::with_capacity(wanted);
for offset in 0..wanted {
let address = base.checked_add(u16::try_from(offset).map_err(|_| GridRefusal::NoBlockset)?);
let byte = address
.and_then(|address| memory.read_rom(bank, address))
.ok_or(GridRefusal::NoBlockset)?;
blockset.push(byte);
}
let tiles = mapgrid::Tileset { id: read(memory, ram::wCurMapTileset), blocks: blockset, passable };
let grid = mapgrid::decode(player.map, width_blocks, height_blocks, &blocks, &tiles);
if grid.width() != size.width || grid.height() != size.height {
return Err(GridRefusal::NoHeader);
}
// The cross-check. `map_tile_id` reads the screen buffer at the offset
// `_GetTileAndCoordsInFrontOfPlayer` uses, so agreeing with it on the tiles it can answer for
// is agreeing with the cartridge's own reading of the same ground.
let mut checked = 0;
for (x, y) in neighbourhood(player.x, player.y) {
let Some(screen) = map_tile_id(memory, x, y) else { continue };
if grid.tile_id(x, y) != Some(screen) {
return Err(GridRefusal::ScreenDisagrees);
}
checked += 1;
}
if checked == 0 {
return Err(GridRefusal::NoScreen);
}
Ok(grid)
}
/// The player's own tile and its four neighbours, which is every tile the window is certain to be
/// able to answer for from where the fly is standing.
fn neighbourhood(x: u8, y: u8) -> Vec<(u8, u8)> {
let mut out = vec![(x, y)];
for (dx, dy) in [(0i16, 1i16), (0, -1), (-1, 0), (1, 0)] {
if let (Ok(nx), Ok(ny)) =
(u8::try_from(i16::from(x) + dx), u8::try_from(i16::from(y) + dy))
{
out.push((nx, ny));
}
}
out
}
/// Whether the player could stand on this tile of the current map.
///
/// Mirrors `CheckTilePassable`: the tile id comes out of the screen buffer at the offset
@ -918,6 +1069,16 @@ pub struct PokeState<'a> {
/// Tiles the cartridge has pushed the fly off ([`MacroState::pushed_tile`]). Session state
/// beside the four above, owned by the same type (`infra/docs/macros-traps.md` row 37).
pushed: &'a dyn PushedLedger,
/// Where the decoded map grid is kept between frames ([`MacroState::map_grid`],
/// `docs/design/macros.md` section 15).
///
/// Mutable, unlike every ledger above, because this is the one thing the state *computes*
/// rather than looks up: a decode is a few thousand reads and a walk of the blockset, and it
/// is valid for as long as the map is loaded. Without a cache every caller decodes again,
/// which is correct and is what the tests do; the sim loop passes one
/// ([`PokeState::caching_grid`]) so that a precondition asking for the frontier costs a
/// refcount instead of a map.
grids: Option<&'a mut MapGrids>,
}
impl<'a> PokeState<'a> {
@ -935,6 +1096,7 @@ impl<'a> PokeState<'a> {
stood: &NoStood,
areas: &NoAreas,
pushed: &NoPushed,
grids: None,
}
}
@ -948,6 +1110,7 @@ impl<'a> PokeState<'a> {
stood: &NoStood,
areas: &NoAreas,
pushed: &NoPushed,
grids: None,
}
}
@ -965,7 +1128,18 @@ impl<'a> PokeState<'a> {
areas: &'a dyn AreaLedger,
pushed: &'a dyn PushedLedger,
) -> Self {
Self { memory, ledger, talk, targets, stood, areas, pushed }
Self { memory, ledger, talk, targets, stood, areas, pushed, grids: None }
}
/// Keep the decoded map grid in `grids` instead of decoding it per question.
///
/// The cache is keyed by map id and size and holds one map, so arriving somewhere else drops
/// it (`docs/design/macros.md` section 15). Session state: it is owned by
/// [`super::macros::driver::PokemonPalette`], never checkpointed, and rebuilt from the
/// cartridge on the first overworld frame after a restore.
pub fn caching_grid(mut self, grids: &'a mut MapGrids) -> Self {
self.grids = Some(grids);
self
}
}
@ -1051,6 +1225,28 @@ impl MacroState for PokeState<'_> {
!controllable(self.memory)
}
/// The whole loaded map's walkability, from the cache when it is for this map
/// (`docs/design/macros.md` section 15).
///
/// `None` is the honest answer on every frame [`map_grid`] refuses — no cartridge behind the
/// seam, a battle or a text box over the map, a header that is not loaded — and every caller
/// falls back to the ten-by-nine window predicate then, which is what all of them did before
/// this existed. [`GridRefusal`] names which, for the probes.
fn map_grid(&mut self) -> Option<std::sync::Arc<MapGrid>> {
let size = map_size(self.memory)?;
let map = player(self.memory)?.map;
if let Some(grids) = self.grids.as_deref()
&& let Some(grid) = grids.get(map, size.width, size.height)
{
return Some(grid);
}
let grid = map_grid(self.memory).ok()?;
match self.grids.as_deref_mut() {
Some(grids) => Some(grids.store(grid)),
None => Some(std::sync::Arc::new(grid)),
}
}
/// What the open mart sells, in menu order (`docs/design/macros.md` section 13).
///
/// Gated on the mart scene being up, and that gate is the whole of the accuracy here:

View file

@ -5,7 +5,8 @@
//! passable-tile list walked out of ROM bank 0.
use super::*;
use crate::pokemon_red::fake_wram::{REDS_HOUSE_1F, WALL_TILE, Wram};
use crate::pokemon_red::fake_wram::{self, REDS_HOUSE_1F, WALL_TILE, Wram};
use crate::pokemon_red::macros::cartridge::MacroState;
use crate::pokemon_red::macros::state::{BattleKind, BattleMenu, ShopScreen, Sign};
/// A walkable tile id from `RedsHouse1_Coll`.
@ -603,3 +604,141 @@ fn the_live_implementation_answers_the_whole_trait() {
assert_eq!(state.warps().len(), 1);
assert!(state.connections().south);
}
/// A map ten blocks by nine — twenty tiles by eighteen, wider than the ten-by-nine window — with
/// a wall down one column of blocks, and a screen buffer that agrees with it.
///
/// The three tables the grid is decoded from, all synthetic: block ids in `wOverworldMap`, a
/// blockset in a ROM bank that is not bank 0, and a collision list in bank 0 where
/// `wTilesetCollisionPtr` points.
fn town() -> (Wram, Vec<u8>, Vec<[u8; 16]>) {
const FLOOR: u8 = 0x01;
let blockset = vec![[FLOOR; 16], [WALL_TILE; 16]];
let (wide, high) = (10usize, 9usize);
let mut blocks = vec![0u8; wide * high];
for row in 0..high {
blocks[row * wide + 5] = 1;
}
let mut wram = Wram::new();
wram.started()
.map(fake_wram::PALLET_TOWN, wide as u8, high as u8, 3, 4)
.facing(0)
.house_collision()
.tileset(0)
.blockset(&blockset)
.map_blocks(&blocks)
.fill_screen(WALL_TILE)
.screen_from_blocks(&blocks, &blockset);
(wram, blocks, blockset)
}
#[test]
fn the_whole_map_decodes_from_the_block_and_collision_tables() {
let (mut wram, _, _) = town();
let grid = map_grid(&mut wram).expect("a decodable map");
assert_eq!((grid.width(), grid.height()), (20, 18));
assert_eq!(grid.unknown_count(), 0);
// The far corner of the map, which the window predicate cannot answer for at all: the grid
// does, and that is the whole of section 15.
assert_eq!(walkable(&mut wram, 19, 17), Walkable::Unknown);
assert_eq!(grid.walkable(19, 17), Walkable::Yes);
// The wall column, again outside the window.
assert_eq!(grid.walkable(10, 17), Walkable::No);
assert_eq!(grid.walkable(11, 17), Walkable::No);
// Inside the window the two readings agree tile for tile, which is what the reader checks
// itself with before it trusts a decode.
for y in 0..18u8 {
for x in 0..20u8 {
if let Some(tile) = map_tile_id(&mut wram, x, y) {
assert_eq!(grid.tile_id(x, y), Some(tile), "({x}, {y})");
assert_eq!(grid.walkable(x, y), walkable(&mut wram, x, y), "({x}, {y})");
}
}
}
}
#[test]
fn a_decode_the_screen_disagrees_with_is_refused() {
let (mut wram, _, _) = town();
// One screen tile the block data does not account for: a wrong stride, a wrong quadrant or a
// half-loaded map all look like this, and all of them answer plausibly.
wram.map_tile(3, 4, 0x77);
assert_eq!(map_grid(&mut wram), Err(GridRefusal::ScreenDisagrees));
}
#[test]
fn without_a_cartridge_behind_the_seam_there_is_no_grid() {
let (mut wram, blocks, _) = town();
// The same WRAM with no blockset in any bank: `read_rom` answers `None`, which is what every
// reader that is not the emulator answers, and the grid narrows to nothing rather than
// decoding the map out of whatever bytes were to hand.
let mut bare = Wram::new();
bare.started()
.map(fake_wram::PALLET_TOWN, 10, 9, 3, 4)
.facing(0)
.house_collision()
.map_blocks(&blocks)
.fill_screen(WALL_TILE);
assert_eq!(map_grid(&mut bare), Err(GridRefusal::NoBlockset));
// And the window predicate still answers, which is the fallback the whole thing rests on.
assert_eq!(walkable(&mut wram, 3, 4), Walkable::Yes);
}
#[test]
fn a_frame_that_is_not_showing_the_map_has_no_grid_to_check() {
let (mut wram, _, _) = town();
// `wOverworldMap` shares its bytes with the picture buffer (`ram/wram.asm`'s own union), so a
// battle is exactly when the blocks under it belong to somebody else. Nothing can be
// cross-checked then, and a decode nothing can check is refused.
wram.battle(1);
assert_eq!(map_grid(&mut wram), Err(GridRefusal::NoScreen));
let (mut wram, _, _) = town();
wram.dialogue_box();
assert_eq!(map_grid(&mut wram), Err(GridRefusal::NoScreen));
}
#[test]
fn a_frame_with_no_map_header_has_no_grid() {
let mut wram = Wram::new();
wram.started();
assert_eq!(map_grid(&mut wram), Err(GridRefusal::NoHeader));
assert_eq!(GridRefusal::NoHeader.label(), "no map header");
}
#[test]
fn the_grid_is_decoded_once_per_map_and_dropped_on_arrival_somewhere_else() {
let (mut wram, blocks, blockset) = town();
let mut grids = MapGrids::default();
{
let mut state = PokeState::new(&mut wram).caching_grid(&mut grids);
let first = state.map_grid().expect("a decodable map");
let again = state.map_grid().expect("the cached map");
assert!(std::sync::Arc::ptr_eq(&first, &again), "the second question is the same grid");
}
assert_eq!(grids.held(), Some(fake_wram::PALLET_TOWN));
// Walking through a door: another map id, so the cache is somebody else's and is dropped.
wram.map(fake_wram::OAKS_LAB, 10, 9, 3, 4)
.map_blocks(&blocks)
.screen_from_blocks(&blocks, &blockset);
{
let mut state = PokeState::new(&mut wram).caching_grid(&mut grids);
let grid = state.map_grid().expect("a decodable map");
assert_eq!(grid.map(), fake_wram::OAKS_LAB);
}
assert_eq!(grids.held(), Some(fake_wram::OAKS_LAB));
}
#[test]
fn a_state_with_no_cache_still_answers_and_a_state_with_no_cartridge_answers_none() {
let (mut wram, _, _) = town();
let mut state = PokeState::new(&mut wram);
assert!(state.map_grid().is_some(), "no cache is slower, not blinder");
let mut bare = Wram::overworld();
let mut state = PokeState::new(&mut bare);
assert!(state.map_grid().is_none(), "no blockset, no grid");
// Which is the frame the window predicate is for.
assert_eq!(state.walkable(3, 6), Walkable::No);
}