flybrain/services/flysim/crates/flybrain-gb/src/pokemon_red/fake_wram.rs
dev e2bf4c0305 macros: the move list is the box on screen, not the cursor bytes it left behind
Row 50: MOVE n reported blocked 890 times in 1,431 macros on the cartridge,
every one of them on a frame the seam read as an open move list with a
placeable cursor.

MoveSelectionMenu writes wTopMenuItemY 12 and wTopMenuItemX 5 and nothing in
the game clears them, exactly as the two-option box's geometry outlives its
box. SelectMenuItem then decrements wCurrentMenuItem back to the 0-based slot
on its way out, which lands straight back inside the one-based range the
accessor reads. So the whole of a turn -- the text, the animation, the
enemy's reply -- read as the fly's own turn on an open list, the pad dealt
the four move buttons on it, and the cursor step pressed at a list nobody was
reading until its budget ran out.

The reading is the figure the menu draws, the same construction text_box's
waiting and yes_no_prompt already make: a box at (4, 12) fourteen wide with a
horizontal run over its top-left corner and the junction tile at (10, 12).
Surveyed with one rollback pulse per battle frame over 3,102 frames at the
rung-9 forest checkpoint: by the cursor bytes alone a real directional press
was honoured on 264 of them, and by the cursor bytes and the box on 231 of
231.

A frame whose list is not on screen is between turns, whose pad is the one
NEXT that advances text.
2026-09-22 18:37:36 +00:00

510 lines
22 KiB
Rust

//! Synthetic WRAM for the scene and accessor tests.
//!
//! A flat 64 KiB address space with a [`MemoryReader`] over it, plus builders that write the byte
//! patterns the real cartridge produces. The patterns are the interesting part: each one is
//! assembled from the same disassembly evidence as the accessor it exercises, so a test that
//! passes here is a test against what `docs/design/macros-wram.md` claims, not against the
//! implementation's own opinion. The ROM-gated tests in `tests/rom_scene.rs` are what check the
//! claims against the cartridge.
//!
//! The space covers ROM bank 0 as well as WRAM, because one accessor reads it: the tileset
//! collision lists live at `00:17xx` and [`Wram::house_collision`] puts the real `RedsHouse1_Coll`
//! bytes there.
use crate::adapter::MemoryReader;
use super::state::poke;
use super::symbols::ram;
/// `constants/map_constants.asm`.
pub const REDS_HOUSE_1F: u8 = 0x25;
pub const PALLET_TOWN: u8 = 0x00;
pub const OAKS_LAB: u8 = 0x28;
/// `data/tilesets/collision_tile_ids.asm`: `RedsHouse1_Coll` and `RedsHouse2_Coll` share a list.
pub const REDS_HOUSE_COLL: [u8; 9] =
[0x01, 0x02, 0x03, 0x11, 0x12, 0x13, 0x14, 0x1c, 0x1a];
/// Where that list sits in the cartridge (`pokered.sym`: `00:1749 RedsHouse1_Coll`).
pub const REDS_HOUSE_COLL_ADDRESS: u16 = 0x1749;
/// A tile id that is in no collision list in the game, for "this tile is a wall".
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 {
fn default() -> Self {
Self::new()
}
}
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], rom: std::collections::HashMap::new() }
}
pub fn set(&mut self, address: u16, value: u8) -> &mut Self {
self.bytes[address as usize] = value;
self
}
/// A big-endian 16-bit quantity, which is how the cartridge stores HP.
pub fn set_word_be(&mut self, address: u16, value: u16) -> &mut Self {
self.set(address, (value >> 8) as u8).set(address + 1, (value & 0xff) as u8)
}
pub fn peek(&self, address: u16) -> u8 {
self.bytes[address as usize]
}
/// One byte of the screen's tile buffer.
pub fn screen_tile(&mut self, x: u16, y: u16, tile: u8) -> &mut Self {
self.set(ram::wTileMap + y * poke::SCREEN_WIDTH + x, tile)
}
/// Fill the whole screen buffer with one tile id.
pub fn fill_screen(&mut self, tile: u8) -> &mut Self {
for index in 0..poke::SCREEN_WIDTH * poke::SCREEN_HEIGHT {
self.set(ram::wTileMap + index, tile);
}
self
}
/// Draw a `TextBoxBorder` box: the four corners are what the detector looks at, and the edges
/// are drawn too so the pattern is the one the game leaves behind.
pub fn draw_box(&mut self, left: u16, top: u16, right: u16, bottom: u16) -> &mut Self {
for x in left..=right {
self.screen_tile(x, top, poke::frame::HORIZONTAL);
self.screen_tile(x, bottom, poke::frame::HORIZONTAL);
}
for y in top..=bottom {
self.screen_tile(left, y, poke::frame::VERTICAL);
self.screen_tile(right, y, poke::frame::VERTICAL);
}
self.screen_tile(left, top, poke::frame::TOP_LEFT);
self.screen_tile(right, top, poke::frame::TOP_RIGHT);
self.screen_tile(left, bottom, poke::frame::BOTTOM_LEFT);
self.screen_tile(right, bottom, poke::frame::BOTTOM_RIGHT);
self
}
/// The game has started: `MainMenu`'s game-timer bit.
pub fn started(&mut self) -> &mut Self {
self.set(ram::wStatusFlags6, poke::BIT_GAME_TIMER_COUNTING)
}
/// A loaded map: id, size in blocks, and the player's coordinates in tiles.
pub fn map(&mut self, id: u8, blocks_wide: u8, blocks_high: u8, x: u8, y: u8) -> &mut Self {
self.set(ram::wCurMap, id)
.set(ram::wCurMapWidth, blocks_wide)
.set(ram::wCurMapHeight, blocks_high)
.set(ram::wXCoord, x)
.set(ram::wYCoord, y)
}
/// Point `wTilesetCollisionPtr` at a real collision list, written where the cartridge keeps it.
pub fn house_collision(&mut self) -> &mut Self {
for (offset, tile) in REDS_HOUSE_COLL.iter().enumerate() {
self.set(REDS_HOUSE_COLL_ADDRESS + offset as u16, *tile);
}
self.set(REDS_HOUSE_COLL_ADDRESS + REDS_HOUSE_COLL.len() as u16, 0xff);
self.set(ram::wTilesetCollisionPtr, (REDS_HOUSE_COLL_ADDRESS & 0xff) as u8)
.set(ram::wTilesetCollisionPtr + 1, (REDS_HOUSE_COLL_ADDRESS >> 8) as u8)
}
/// Write a map tile's id into the screen buffer at the position the game would hold it, given
/// where the player is. Silently does nothing for a tile outside the screen's window, which is
/// exactly the tile the accessor must report as unknown.
pub fn map_tile(&mut self, x: u8, y: u8, tile: u8) -> &mut Self {
let player_x = self.peek(ram::wXCoord);
let player_y = self.peek(ram::wYCoord);
let screen_x = poke::PLAYER_SCREEN_X + 2 * (i32::from(x) - i32::from(player_x));
let screen_y = poke::PLAYER_SCREEN_Y + 2 * (i32::from(y) - i32::from(player_y));
if (0..poke::SCREEN_WIDTH as i32).contains(&screen_x)
&& (0..poke::SCREEN_HEIGHT as i32).contains(&screen_y)
{
self.screen_tile(screen_x as u16, screen_y as u16, tile);
}
self
}
/// The player's facing, in sprite slot 0.
pub fn facing(&mut self, sprite_facing: u8) -> &mut Self {
self.set(ram::wSpriteStateData1 + 9, sprite_facing)
}
/// One party member, written into its 44-byte `party_struct`.
#[allow(clippy::too_many_arguments)]
pub fn party_mon(
&mut self,
slot: u8,
species: u8,
level: u8,
hp: u16,
max_hp: u16,
status: u8,
moves: &[(u8, u8)],
) -> &mut Self {
let base = ram::wPartyMon1 + u16::from(slot) * poke::PARTY_MON_BYTES;
self.set(ram::wPartySpecies + u16::from(slot), species);
self.set(base, species)
.set_word_be(base + 1, hp)
.set(base + 4, status)
.set(base + 33, level)
.set_word_be(base + 34, max_hp);
for (index, (id, pp)) in moves.iter().enumerate().take(4) {
self.set(base + 8 + index as u16, *id).set(base + 29 + index as u16, *pp);
}
let count = self.peek(ram::wPartyCount).max(slot + 1);
self.set(ram::wPartyCount, count)
}
/// The active battler's copy of a party entry.
#[allow(clippy::too_many_arguments)]
pub fn battle_mon(
&mut self,
slot: u8,
species: u8,
level: u8,
hp: u16,
max_hp: u16,
status: u8,
moves: &[(u8, u8)],
) -> &mut Self {
self.set(ram::wPlayerMonNumber, slot)
.set(ram::wBattleMonSpecies, species)
.set(ram::wBattleMonLevel, level)
.set_word_be(ram::wBattleMonHP, hp)
.set_word_be(ram::wBattleMonMaxHP, max_hp)
.set(ram::wBattleMonStatus, status);
for (index, (id, pp)) in moves.iter().enumerate().take(4) {
self.set(ram::wBattleMonMoves + index as u16, *id)
.set(ram::wBattleMonPP + index as u16, *pp);
}
self.set(ram::wNumMovesMinusOne, moves.len().clamp(1, 4) as u8 - 1)
}
pub fn enemy_mon(&mut self, species: u8, level: u8, hp: u16, max_hp: u16) -> &mut Self {
self.set(ram::wEnemyMonSpecies, species)
.set(ram::wEnemyMonLevel, level)
.set_word_be(ram::wEnemyMonHP, hp)
.set_word_be(ram::wEnemyMonMaxHP, max_hp)
}
/// `HandleMenuInput`'s state.
pub fn cursor(&mut self, top_y: u8, top_x: u8, current: u8, max: u8, keys: u8) -> &mut Self {
self.set(ram::wTopMenuItemY, top_y)
.set(ram::wTopMenuItemX, top_x)
.set(ram::wCurrentMenuItem, current)
.set(ram::wMaxMenuItem, max)
.set(ram::wMenuWatchedKeys, keys)
}
/// The four corner tiles of a box and nothing else, which is what a map can look like.
///
/// The overworld tilesets use the frame's own tile ids for ordinary ground, so these four
/// screen positions hold them from time to time — measured at 315 frames of 43,004 on the
/// cartridge (`infra/docs/macros-traps.md`, 2026-09-17). This is that pattern, for a test that
/// the detector is not fooled by it.
pub fn draw_box_corners(&mut self, left: u16, top: u16, right: u16, bottom: u16) -> &mut Self {
self.screen_tile(left, top, poke::frame::TOP_LEFT);
self.screen_tile(right, top, poke::frame::TOP_RIGHT);
self.screen_tile(left, bottom, poke::frame::BOTTOM_LEFT);
self.screen_tile(right, bottom, poke::frame::BOTTOM_RIGHT);
self
}
/// A text display is open, with the bottom-of-screen dialogue box drawn.
pub fn dialogue_box(&mut self) -> &mut Self {
self.set(ram::wFontLoaded, poke::BIT_FONT_LOADED).draw_box(0, 12, 19, 17)
}
/// The start menu, Pokédex entry included.
pub fn start_menu(&mut self) -> &mut Self {
self.set(ram::wFontLoaded, poke::BIT_FONT_LOADED)
.draw_box(10, 0, 19, 15)
.cursor(2, 11, 0, 7, poke::pad::DOWN | poke::pad::UP | poke::pad::START | poke::pad::B | poke::pad::A)
}
/// A wild or trainer battle, with no menu up yet.
pub fn battle(&mut self, is_in_battle: u8) -> &mut Self {
self.set(ram::wIsInBattle, is_in_battle)
.set(ram::wFontLoaded, poke::BIT_FONT_LOADED)
}
/// The top-level battle menu, in the left column (FIGHT / PKMN) or the right (ITEM / RUN).
pub fn battle_menu(&mut self, right_column: bool, current: u8) -> &mut Self {
let (x, keys) = if right_column {
(15, poke::pad::LEFT | poke::pad::A)
} else {
(9, poke::pad::RIGHT | poke::pad::A)
};
self.set(ram::wTextBoxID, poke::BATTLE_MENU_TEMPLATE).cursor(14, x, current, 1, keys)
}
/// The move list, `MoveSelectionMenu`'s regular menu, open and accepting input. `slot` is the
/// 0-based move.
///
/// Both halves, because since row 50 the seam reads both: the cursor bytes *and* the box the
/// menu draws. Use [`Self::move_menu_stale`] for the state a turn spends its text and animation
/// in, which is these bytes with no box on screen.
pub fn move_menu(&mut self, slot: u8, moves: u8) -> &mut Self {
self.move_menu_stale(slot, moves).draw_move_list()
}
/// The bytes `MoveSelectionMenu` wrote, with its box no longer on screen.
///
/// Nothing in the game clears `wTopMenuItemY` / `wTopMenuItemX` / `wCurrentMenuItem`, so this
/// is what every frame of a turn's text, animation and reply reads back once a move has been
/// chosen (`infra/docs/macros-traps.md` row 50). Note that `SelectMenuItem` decrements
/// `wCurrentMenuItem` back to the 0-based slot on its way out, so `slot` here is one lower than
/// the slot the fly chose.
pub fn move_menu_stale(&mut self, slot: u8, moves: u8) -> &mut Self {
self.set(ram::wNumMovesMinusOne, moves.saturating_sub(1)).cursor(
poke::MOVE_LIST_CURSOR_Y,
poke::MOVE_LIST_CURSOR_X,
slot + 1,
moves + 1,
poke::pad::UP | poke::pad::DOWN | poke::pad::A,
)
}
/// The figure `MoveSelectionMenu` draws: a box at (4, 12) with a horizontal run over its
/// top-left corner and the `┘` junction at (10, 12).
pub fn draw_move_list(&mut self) -> &mut Self {
let (left, top, right, bottom) = poke::MOVE_LIST_BOX;
self.draw_box(left, top, right, bottom)
.screen_tile(left, top, poke::frame::HORIZONTAL)
.screen_tile(poke::MOVE_LIST_JOIN, top, poke::frame::BOTTOM_RIGHT)
}
/// The party list. `forced` is the state `ChooseNextMon` leaves: A only, no way out.
pub fn party_list(&mut self, current: u8, forced: bool) -> &mut Self {
let count = self.peek(ram::wPartyCount).max(1);
let keys = if forced { poke::pad::A } else { poke::pad::A | poke::pad::B };
self.set(
ram::wPartyMenuTypeOrMessageID,
if forced { poke::BATTLE_PARTY_MENU } else { 0 },
)
.set(ram::wFontLoaded, poke::BIT_FONT_LOADED)
.cursor(1, 0, current, count - 1, keys)
}
/// Money, as three bytes of big-endian BCD.
pub fn money(&mut self, amount: u32) -> &mut Self {
let digits = amount.min(999_999);
let bcd = |value: u32| ((value / 10) << 4 | (value % 10)) as u8;
self.set(ram::wPlayerMoney, bcd(digits / 10_000))
.set(ram::wPlayerMoney + 1, bcd(digits / 100 % 100))
.set(ram::wPlayerMoney + 2, bcd(digits % 100))
}
/// The bag, as `(id, quantity)` pairs and a `$ff` terminator.
pub fn bag(&mut self, items: &[(u8, u8)]) -> &mut Self {
self.set(ram::wNumBagItems, items.len() as u8);
for (index, (id, count)) in items.iter().enumerate() {
self.set(ram::wBagItems + index as u16 * 2, *id)
.set(ram::wBagItems + index as u16 * 2 + 1, *count);
}
self.set(ram::wBagItems + items.len() as u16 * 2, 0xff)
}
/// One NPC sprite, in the slots the game keeps it: picture id and facing in
/// `wSpriteStateData1`, map coordinates plus four in `wSpriteStateData2`.
pub fn npc(&mut self, slot: u8, picture: u8, x: u8, y: u8, sprite_facing: u8) -> &mut Self {
let data1 = ram::wSpriteStateData1 + u16::from(slot) * poke::SPRITE_BYTES;
let data2 = ram::wSpriteStateData2 + u16::from(slot) * poke::SPRITE_BYTES;
self.set(data1, picture)
.set(data1 + 2, 0)
.set(data1 + 9, sprite_facing)
.set(data2 + 4, y + poke::SPRITE_COORD_BIAS)
.set(data2 + 5, x + poke::SPRITE_COORD_BIAS);
let count = self.peek(ram::wNumSprites).max(slot);
self.set(ram::wNumSprites, count)
}
/// The current map's sign table: `bg_event`s, `Y, X` per entry with no bias, and a text id
/// each.
pub fn signs(&mut self, signs: &[(u8, u8, u8)]) -> &mut Self {
self.set(ram::wNumSigns, signs.len() as u8);
for (index, (x, y, text_id)) in signs.iter().enumerate() {
let coords = ram::wSignCoords + index as u16 * 2;
self.set(coords, *y).set(coords + 1, *x).set(ram::wSignTextIDs + index as u16, *text_id);
}
self
}
/// The current map's warp table.
pub fn warps(&mut self, warps: &[(u8, u8, u8, u8)]) -> &mut Self {
self.set(ram::wNumberOfWarps, warps.len() as u8);
for (index, (x, y, destination_warp, destination_map)) in warps.iter().enumerate() {
let entry = ram::wWarpEntries + index as u16 * 4;
self.set(entry, *y)
.set(entry + 1, *x)
.set(entry + 2, *destination_warp)
.set(entry + 3, *destination_map);
}
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 };
// The lower-left tile of the map tile's own quadrant, which is the one the
// cartridge's collision read uses (`mapgrid::ANCHOR_ROW`).
let tile = tiles[((y % 2) * 2 + 1) * 4 + (x % 2) * 2];
self.map_tile(x as u8, y as u8, tile);
}
}
self
}
/// 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. The blocks and the blockset come back so that a caller can
/// redraw the screen ([`Wram::mid_step`]).
pub fn town() -> (Self, 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;
}
// Two landmarks beside the fly's own tile, one on each axis. A map whose neighbourhood is
// the same tile id in every direction cannot tell a view centred on the fly from a view
// centred one tile away, which is exactly what a mid-step frame is ([`Wram::mid_step`]).
// The fly stands on (3, 4) of the decoded map: these make (2..3, 2..3) and (0..1, 4..5)
// wall, leaving (3, 4) and every tile it can step to walkable.
blocks[wide + 1] = 1;
blocks[2 * wide] = 1;
let mut wram = Self::new();
wram.started()
.map(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)
}
/// The screen buffer centred one tile away from `wXCoord` / `wYCoord`, which is what a frame
/// **mid-step** looks like on the cartridge.
///
/// Measured 2026-09-22 (`infra/docs/macros-traps.md` row 54): the coordinates change at the
/// *end* of a sixteen-frame step and the background scrolls throughout it, so for fifteen
/// frames of every sixteen the two readings are one tile apart in the direction of travel.
/// This draws exactly that: the view is rendered from `(x + dx, y + dy)` and the coordinates
/// are put back.
pub fn mid_step(
&mut self,
dx: i16,
dy: i16,
blocks: &[u8],
blockset: &[[u8; 16]],
) -> &mut Self {
let (x, y) = (self.peek(ram::wXCoord), self.peek(ram::wYCoord));
self.set(ram::wXCoord, (i16::from(x) + dx) as u8);
self.set(ram::wYCoord, (i16::from(y) + dy) as u8);
self.fill_screen(WALL_TILE).screen_from_blocks(blocks, blockset);
self.set(ram::wXCoord, x).set(ram::wYCoord, y);
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 {
let mut wram = Self::new();
wram.started()
.map(REDS_HOUSE_1F, 4, 4, 3, 6)
.facing(0)
.house_collision()
.fill_screen(WALL_TILE);
wram
}
}