//! The game itself. //! //! The game is written the way the original is: as straight-line code that //! calls [`Game::wait_frame`] whenever it wants the next frame (to animate, to //! wait for a button, ...). It runs on its own thread; `wait_frame` hands the //! finished picture and sound to the host and blocks until the host sends the //! next frame's buttons. //! //! Modules: `menus` (title, how to play, password, puzzle list), `story` //! (story screens and the ending), `map` (the overworld walk between levels) //! and `puzzle` (the puzzle itself and the level-clear screen). mod map; mod menus; mod puzzle; mod story; use std::sync::mpsc::{Receiver, SyncSender}; use std::sync::Arc; use crate::apu::{Apu, CLOCK_HZ}; use crate::input::Joypad; use crate::rom::{GameData, Shape}; use crate::sound::SoundDriver; use crate::video::{Display, Sprite, Video, MAP0, SCREEN_H, SCREEN_W}; /// Clock cycles per frame (the Game Boy runs at ~59.73 frames per second). pub const FRAME_CYCLES: u64 = 70_224; pub const FRAME_HZ: f64 = CLOCK_HZ as f64 / FRAME_CYCLES as f64; /// The normal palette: shade n for color n, reversed (the game's art is /// stored with 0 = black). const PALETTE: u8 = 0x1B; // ---- actor slots (multi-tile sprites, drawn every frame) ---- /// puzzle cursor / held piece; also the piece that drops on the map const CURSOR: usize = 0; /// menu arrow on the title screen and puzzle list const MENU_CURSOR: usize = 1; /// the hero on the map, the storyteller, the password cursor const HERO: usize = 2; /// "something here" marker on the map; the second figure in story scenes const MARKER: usize = 3; /// the bird that brings the password on the map const MESSENGER: usize = 4; const ACTOR_SLOTS: usize = 12; /// A sprite made of one shape's cells, positioned at (y, x) in sprite /// coordinates (screen + 16, screen + 8). #[derive(Clone, Copy, Default)] pub struct Actor { pub visible: bool, pub y: u8, pub x: u8, pub shape: u8, } impl Actor { const fn at(y: u8, x: u8, shape: u8) -> Actor { Actor { visible: true, y, x, shape } } } /// Where a placed piece is and which board tiles it covers. #[derive(Clone, Copy, Default)] struct PieceSlot { y: u8, x: u8, /// the board tiles under the piece's cells under: [u8; 6], } /// The puzzle board: which piece id covers each background map cell. type Board = Box<[u8; 0x400]>; /// Scratch state that the original keeps in $C000-$CFFF and wipes at the /// start of each puzzle (and at the title screen). struct Scratch { actors: [Actor; ACTOR_SLOTS], /// id of the piece in hand (type << 4 | orientation), 0 = none held: u8, /// one slot per piece type slots: [PieceSlot; 16], slots_backup: [PieceSlot; 16], board: Board, board_backup: Board, bridge: map::Bridge, marker_blink: u8, story: u8, piece_drop_done: bool, timer: Timer, messenger: map::Messenger, /// how far the hero can walk on the map walk_left: u8, walk_right: u8, } impl Default for Scratch { fn default() -> Self { Scratch { actors: Default::default(), held: 0, slots: Default::default(), slots_backup: Default::default(), board: Box::new([0; 0x400]), board_backup: Box::new([0; 0x400]), bridge: Default::default(), marker_blink: 0, story: 0, piece_drop_done: false, timer: Timer::default(), messenger: Default::default(), walk_left: 0, walk_right: 0, } } } #[derive(Clone, Copy, Default, PartialEq)] enum TimerMode { #[default] Hidden, Running, Stopped, } /// The clear-time clock in the top right corner. #[derive(Clone, Copy, Default)] struct Timer { mode: TimerMode, frames: u8, /// seconds, tens of seconds, minutes, tens of minutes, hours digits: [u8; 5], } #[derive(Clone, Copy, Default)] struct Palettes { bg: u8, obj0: u8, obj1: u8, /// palette 1 inverts every 16 frames (makes the cursor blink) blink: u8, } /// What the game hands the host each frame. pub struct Frame { /// one shade (0 = white .. 3 = black) per pixel pub pixels: Vec, /// interleaved stereo samples pub audio: Vec, /// a peek at the game state, for testing pub probe: Probe, } #[derive(Clone, Copy, Debug, Default)] pub struct Probe { pub level: u8, pub cursor_y: u8, pub cursor_x: u8, pub held: u8, } /// The game thread's end of the connection to the host. pub struct Link { pub frames: SyncSender, pub buttons: Receiver, } /// Unwinds the game thread when the host goes away. struct Quit; pub struct Game { data: Arc, link: Link, video: Video, apu: Apu, sound: SoundDriver, joy: Joypad, level: u8, pieces_needed: u8, palettes: Palettes, /// sprites built this frame; the display picks them up at the next vblank oam: [Sprite; 40], s: Scratch, } /// Run the game on a new thread. pub fn spawn(data: Arc, sample_rate: u32, link: Link) -> std::thread::JoinHandle<()> { std::thread::spawn(move || { let mut game = Game::new(data, sample_rate, link); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| game.run())); if let Err(e) = result { if !e.is::() { std::panic::resume_unwind(e); } } }) } impl Game { fn new(data: Arc, sample_rate: u32, link: Link) -> Game { Game { sound: SoundDriver::new(data.clone()), data, link, video: Video::new(), apu: Apu::new(sample_rate), joy: Joypad::default(), level: 0, pieces_needed: 0, palettes: Palettes::default(), oam: [Sprite::default(); 40], s: Scratch::default(), } } fn run(&mut self) -> ! { loop { self.reset(); match self.title_screen() { menus::TitleChoice::HowToPlay => { self.how_to_play(); continue; } menus::TitleChoice::Password => { self.password_screen(); self.puzzle_list(); } menus::TitleChoice::Start => {} } self.play_levels(); self.ending(); } } /// Story, map, puzzle, repeat; returns after the last level. fn play_levels(&mut self) { loop { self.s.timer.mode = TimerMode::Hidden; self.story_screen(); self.story_text(); self.map_screen(); self.map_walk(); self.puzzle_screen(); self.s.timer.mode = TimerMode::Running; loop { self.wait_frame(); self.check_reset_puzzle(); self.move_cursor(); self.piece_action(); if self.pieces_in_place() == self.pieces_needed { break; } } self.s.timer.mode = TimerMode::Stopped; self.level_clear(); if self.level as usize + 1 >= crate::rom::LEVEL_COUNT { self.s.timer.mode = TimerMode::Hidden; return; } self.level += 1; } } /// Back to power-on state (the title screen clears all of RAM). fn reset(&mut self) { self.video.display = Display::Off; self.s = Scratch::default(); self.level = 0; self.pieces_needed = 0; self.palettes = Palettes::default(); self.joy = Joypad::default(); self.sound = SoundDriver::new(self.data.clone()); } // ---------------------------------------------------------------- frames /// Finish this frame and wait for the next one. fn wait_frame(&mut self) { let mut pixels = vec![0; SCREEN_W * SCREEN_H]; self.video.render(&mut pixels); let probe = Probe { level: self.level, cursor_y: self.s.actors[CURSOR].y, cursor_x: self.s.actors[CURSOR].x, held: self.s.held, }; let frame = Frame { pixels, audio: std::mem::take(&mut self.apu.out), probe }; if self.link.frames.send(frame).is_err() { std::panic::resume_unwind(Box::new(Quit)); } let buttons = match self.link.buttons.recv() { Ok(b) => b, Err(_) => std::panic::resume_unwind(Box::new(Quit)), }; // things the original does every vblank self.video.sprites = self.oam; self.update_palettes(); self.update_cursor_shape(); self.build_sprites(); self.joy.update(buttons); self.update_bridge(); self.update_timer(); self.draw_timer(); self.update_marker_blink(); self.update_messenger(); self.sound.update(&mut self.apu); self.apu.run(FRAME_CYCLES); } fn wait_frames(&mut self, n: u8) { for _ in 0..n { self.wait_frame(); } } fn update_palettes(&mut self) { let p = &mut self.palettes; self.video.bg_palette = p.bg; self.video.obj_palettes = [p.obj0, p.obj1]; let (blink, wrapped) = p.blink.overflowing_add(0x10); p.blink = blink; if wrapped { p.obj1 ^= 0xFF; } } fn set_palettes(&mut self, v: u8) { self.palettes.bg = v; self.palettes.obj0 = v; self.palettes.obj1 = v; } /// Turn the screen on showing the given background map. fn show(&mut self, map: u16) { self.video.display = Display::On(map); } fn play(&mut self, sound: u8) { self.sound.play(sound); } fn shape(&self, n: u8) -> &Shape { &self.data.shapes[n as usize] } fn clear_actors(&mut self) { self.s.actors = Default::default(); self.s.held = 0; } /// The cursor actor shows the held piece, or the cursor itself (which /// changes shape over the piece tray on the right). fn update_cursor_shape(&mut self) { let a = &mut self.s.actors[CURSOR]; a.shape = if self.s.held != 0 { self.data.piece_shape(self.s.held) } else if a.x >= 0xA4 { 0x5D } else { 0 }; } fn build_sprites(&mut self) { let mut sprites = [Sprite::default(); 40]; let mut n = 0; for a in self.s.actors.iter().filter(|a| a.visible) { let shape = &self.data.shapes[a.shape as usize]; for cell in &shape.cells { let (dy, dx) = cell.sprite_offset(); if let Some(s) = sprites.get_mut(n) { *s = Sprite { y: a.y.wrapping_add(dy), x: a.x.wrapping_add(dx), tile: cell.tile, attr: shape.attr }; } n += 1; } } self.oam = sprites; } // ----------------------------------------------------------------- timer fn update_timer(&mut self) { let t = &mut self.s.timer; if t.mode != TimerMode::Running { return; } t.frames += 1; if t.frames < 60 { return; } t.frames = 0; for (digit, limit) in t.digits.iter_mut().zip([10, 6, 10, 6, 10]) { *digit += 1; if *digit < limit { return; } *digit = 0; } } /// Two-tile-tall digits in the last 10 sprite slots: h mm ss. fn draw_timer(&mut self) { let t = self.s.timer; if t.mode == TimerMode::Hidden { return; } for (i, x) in [0x88, 0x80, 0x70, 0x68, 0x58].into_iter().enumerate() { let tile = t.digits[i] * 2 + 0x64; self.oam[30 + 2 * i] = Sprite { y: 0x10, x, tile, attr: 0 }; self.oam[31 + 2 * i] = Sprite { y: 0x18, x, tile: tile + 1, attr: 0 }; } } } /// Map cell under a point in sprite coordinates. fn cell_at(y: u8, x: u8) -> u16 { let row = (y.wrapping_sub(16) >> 3) as u16; let col = (x.wrapping_sub(8) >> 3) as u16; MAP0 + row * 32 + col }