use std::time::Duration;
pub use crate::canvas::{present_diff, screen, Canvas, Cell, Sprite};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
Up,
Down,
Left,
Right,
Enter,
Esc,
Space,
Tab,
Backspace,
Char(char),
F(u8),
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Input {
Key(Key),
Resize(u16, u16),
Quit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Camera {
pub x: i32,
pub y: i32,
}
impl Camera {
pub const fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
pub fn center_on(&mut self, wx: i32, wy: i32, view_w: i32, view_h: i32) {
self.x = wx - view_w / 2;
self.y = wy - view_h / 2;
}
pub fn screen_x(&self, world_x: i32) -> i32 {
world_x - self.x
}
pub fn screen_y(&self, world_y: i32) -> i32 {
world_y - self.y
}
}
#[allow(clippy::too_many_arguments)] pub fn rects_overlap(
ax: i32,
ay: i32,
aw: u32,
ah: u32,
bx: i32,
by: i32,
bw: u32,
bh: u32,
) -> bool {
ax < bx + bw as i32 && bx < ax + aw as i32 && ay < by + bh as i32 && by < ay + ah as i32
}
pub fn jump_arc(t: f32, duration: f32, height: f32) -> f32 {
if duration <= 0.0 || height <= 0.0 {
return 0.0;
}
let u = (t / duration).clamp(0.0, 1.0);
4.0 * height * u * (1.0 - u)
}
pub fn perspective_scale(distance: f32, focal: f32) -> f32 {
if focal <= 0.0 {
return 1.0;
}
(focal / (focal + distance.max(0.0))).clamp(0.0, 1.0)
}
pub fn shake_offset(trauma: f32, tick: u64, magnitude: i32) -> (i32, i32) {
if trauma <= 0.0 || magnitude <= 0 {
return (0, 0);
}
let amp = magnitude as f32 * trauma * trauma;
let noise = |seed: u64| {
let mut h = seed.wrapping_mul(0x9E3779B97F4A7C15);
h ^= h >> 30;
h = h.wrapping_mul(0xBF58476D1CE4E5B9);
h ^= h >> 27;
h = h.wrapping_mul(0x94D049BB133111EB);
h ^= h >> 31;
(h as f64 / u64::MAX as f64 * 2.0 - 1.0) as f32
};
(
(noise(tick.wrapping_mul(2).wrapping_add(1)) * amp).round() as i32,
(noise(tick.wrapping_mul(2).wrapping_add(0x9E37)) * amp).round() as i32,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Control {
Continue,
Quit,
}
pub trait Game {
fn update(&mut self, inputs: &[Input], dt: Duration) -> Control;
fn render(&self, canvas: &mut Canvas);
}
#[derive(Debug, Clone)]
pub struct TickClock {
step: Duration,
acc: Duration,
}
impl TickClock {
pub fn new(ticks_per_second: u32) -> Self {
Self {
step: Duration::from_secs_f64(1.0 / ticks_per_second.max(1) as f64),
acc: Duration::ZERO,
}
}
pub fn accumulate(&mut self, elapsed: Duration) -> u32 {
self.acc += elapsed;
let mut n = 0;
while self.acc >= self.step {
self.acc -= self.step;
n += 1;
}
n
}
pub fn accumulate_capped(&mut self, elapsed: Duration, max: u32) -> u32 {
self.accumulate(elapsed).min(max)
}
pub fn step(&self) -> Duration {
self.step
}
}
pub fn run_headless<G: Game>(
game: &mut G,
width: u32,
height: u32,
dt: Duration,
script: &[Vec<Input>],
) -> Vec<String> {
let mut canvas = Canvas::new(width, height);
let mut frames = Vec::with_capacity(script.len());
for inputs in script {
if game.update(inputs, dt) == Control::Quit {
break;
}
game.render(&mut canvas);
frames.push(canvas.to_string());
}
frames
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jump_arc_peaks_midway_and_lands_at_zero() {
assert_eq!(jump_arc(0.0, 10.0, 8.0), 0.0);
assert_eq!(jump_arc(10.0, 10.0, 8.0), 0.0);
assert_eq!(jump_arc(5.0, 10.0, 8.0), 8.0);
assert!((jump_arc(2.5, 10.0, 8.0) - 6.0).abs() < 1e-5);
assert_eq!(jump_arc(5.0, 0.0, 8.0), 0.0);
assert_eq!(jump_arc(-3.0, 10.0, 8.0), 0.0);
}
#[test]
fn perspective_scale_is_one_up_close_and_fades_with_distance() {
assert_eq!(perspective_scale(0.0, 10.0), 1.0);
let near = perspective_scale(5.0, 10.0);
let far = perspective_scale(50.0, 10.0);
assert!(near > far && far > 0.0);
assert_eq!(perspective_scale(-4.0, 10.0), 1.0);
assert_eq!(perspective_scale(5.0, 0.0), 1.0);
}
#[test]
fn shake_offset_is_zero_without_trauma_and_deterministic() {
assert_eq!(shake_offset(0.0, 99, 4), (0, 0));
assert_eq!(shake_offset(1.0, 7, 0), (0, 0));
let a = shake_offset(1.0, 42, 4);
let b = shake_offset(1.0, 42, 4);
assert_eq!(a, b, "same (trauma, tick) must shake identically");
for tick in 0..200u64 {
let (dx, dy) = shake_offset(1.0, tick, 4);
assert!(dx.abs() <= 4 && dy.abs() <= 4);
}
for tick in 0..200u64 {
let (dx, dy) = shake_offset(0.5, tick, 8);
assert!(dx.abs() <= 2 && dy.abs() <= 2);
}
}
#[test]
fn camera_centering_math() {
let mut cam = Camera::new(0, 0);
cam.center_on(100, 50, 40, 20);
assert_eq!((cam.screen_x(100), cam.screen_y(50)), (20, 10));
}
#[test]
fn rects_overlap_cases() {
assert!(rects_overlap(0, 0, 4, 4, 2, 2, 4, 4));
assert!(!rects_overlap(0, 0, 2, 2, 2, 0, 2, 2)); assert!(!rects_overlap(0, 0, 2, 2, 5, 5, 2, 2));
assert!(rects_overlap(-3, -3, 5, 5, 0, 0, 1, 1));
}
#[test]
fn tick_clock_accumulates_whole_steps() {
let mut clock = TickClock::new(10); assert_eq!(clock.accumulate(Duration::from_millis(30)), 0);
assert_eq!(clock.accumulate(Duration::from_millis(80)), 1); assert_eq!(clock.accumulate(Duration::from_millis(250)), 2);
assert_eq!(clock.step(), Duration::from_millis(100));
assert_eq!(clock.accumulate_capped(Duration::from_secs(10), 3), 3);
}
#[test]
fn run_headless_drives_a_toy_game() {
struct Right {
x: i32,
}
impl Game for Right {
fn update(&mut self, inputs: &[Input], _dt: Duration) -> Control {
if inputs.contains(&Input::Quit) {
return Control::Quit;
}
self.x += 1;
Control::Continue
}
fn render(&self, canvas: &mut Canvas) {
canvas.clear();
canvas.put(self.x, 0, '@', None);
}
}
let mut game = Right { x: 0 };
let script = vec![vec![], vec![Input::Key(Key::Right)], vec![Input::Quit]];
let frames = run_headless(&mut game, 6, 2, Duration::from_millis(16), &script);
assert_eq!(frames.len(), 2);
assert!(frames[0].starts_with(" @"));
assert!(frames[1].starts_with(" @"));
}
}