Files
daedalian/src/game/mod.rs
T

423 lines
12 KiB
Rust
Raw Normal View History

2026-10-31 18:12:15 -05:00
//! 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<u8>,
/// interleaved stereo samples
pub audio: Vec<f32>,
/// 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<Frame>,
pub buttons: Receiver<u8>,
}
/// Unwinds the game thread when the host goes away.
struct Quit;
pub struct Game {
data: Arc<GameData>,
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<GameData>, 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::<Quit>() {
std::panic::resume_unwind(e);
}
}
})
}
impl Game {
fn new(data: Arc<GameData>, 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
}