Initial commit
This commit is contained in:
+422
@@ -0,0 +1,422 @@
|
||||
//! 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
|
||||
}
|
||||
Reference in New Issue
Block a user