use std::fmt;
use std::time::Duration;
use crate::paint::{Ink, Motion, Palette, Row};
use crate::recipe::{Glyphs, Recipe, Unfit};
use crate::stage::{Canvas, Shader, Stage, TICK_SECS};
use crate::{STALE, TICK};
pub(crate) const QUANTA: f64 = 100.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Spell {
#[default]
Idle,
Busy,
Wait,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
pub struct Intensity(f64);
impl Intensity {
pub const BRISK: Self = Self(1.0);
pub fn new(level: f64) -> Self {
Self(if level > 0.0 {
level.min(f64::MAX)
} else {
0.0
})
}
pub fn level(self) -> f64 {
self.0
}
fn pace(self) -> f64 {
QUANTA
* if self.0 <= 1.0 {
self.0
} else {
1.0 + self.0.ln()
}
}
}
pub struct Shimmer {
recipe: &'static Recipe,
shader: Box<dyn Shader>,
stage: Stage,
clock: Clock,
}
impl Shimmer {
pub fn new(recipe: &'static Recipe, seed: u64, glyphs: Glyphs) -> Result<Self, Unfit> {
recipe.fits(glyphs)?;
Ok(Self {
recipe,
shader: (recipe.summon)(seed),
stage: Stage::default(),
clock: Clock::default(),
})
}
pub fn recipe(&self) -> &'static Recipe {
self.recipe
}
pub fn show(&mut self, caption: &str) {
self.stage.show(caption);
}
pub fn set_spell(&mut self, spell: Spell) {
if self.stage.cast(spell) && spell == Spell::Idle {
self.shader.idle(&self.stage);
}
}
pub fn set_intensity(&mut self, intensity: Intensity) {
self.stage.intensity = intensity;
}
pub fn advance(&mut self, dt: Duration) {
for _ in 0..self.clock.due(dt, self.stage.spell) {
let quanta = self.clock.quanta(self.stage.intensity);
self.stage.advance(quanta);
self.shader.tick(&self.stage);
}
}
pub fn status(&self, palette: Palette, motion: Motion) -> Row {
let mut canvas = Canvas {
letters: vec![Ink::default(); self.stage.caption.len()],
..Canvas::default()
};
self.shader.status(&self.stage, palette, &mut canvas);
crate::rest::settle(&mut canvas, &self.stage, palette);
Row::paint(&self.stage.caption, canvas, motion)
}
pub fn bullet(&self, palette: Palette) -> Ink {
self.shader.bullet(&self.stage, palette)
}
}
impl fmt::Debug for Shimmer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shimmer")
.field("recipe", &self.recipe.name)
.field("spell", &self.stage.spell)
.field("t", &self.stage.t)
.finish_non_exhaustive()
}
}
#[derive(Default)]
struct Clock {
owed: u128,
carry: f64,
}
impl Clock {
fn due(&mut self, dt: Duration, spell: Spell) -> u32 {
if dt > STALE {
*self = Self::default();
return 1;
}
let drag = u128::from(crate::rest::DRAG);
let tick = TICK.as_nanos() * drag;
self.owed += dt.as_nanos() * if spell == Spell::Idle { 1 } else { drag };
let mut due = 0;
while self.owed >= tick {
self.owed -= tick;
due += 1;
}
due
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "The carry is finite and nonnegative, and logarithmic pacing holds one tick's quanta below 3000."
)]
fn quanta(&mut self, intensity: Intensity) -> u32 {
self.carry += intensity.pace() * TICK_SECS;
let whole = self.carry.floor();
self.carry -= whole;
whole as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Spell::{Busy, Idle, Wait};
use crate::paint::Rgb;
const PALETTE: Palette = Palette {
fg: Rgb(208.0, 208.0, 208.0),
bg: Rgb(21.0, 21.0, 21.0),
};
#[derive(Default)]
struct Recorder {
ended: u32,
}
impl Shader for Recorder {
fn idle(&mut self, _stage: &Stage) {
self.ended += 1;
}
fn status(&self, _stage: &Stage, _palette: Palette, _canvas: &mut Canvas) {}
fn bullet(&self, _stage: &Stage, _palette: Palette) -> Ink {
Ink::of(char::from_digit(self.ended, 10).unwrap())
}
}
static RECORDER: Recipe = Recipe {
name: "Recorder",
blurb: "",
credit: "",
needs: Glyphs::BASE,
summon: |_| Box::<Recorder>::default(),
};
#[test]
fn each_entry_into_idle_ends_one_bout() {
let mut shimmer: Shimmer = Shimmer::new(&RECORDER, 0, Glyphs::BASE).unwrap();
let script = [
(Idle, '0'),
(Busy, '0'),
(Wait, '0'),
(Idle, '1'),
(Idle, '1'),
(Wait, '1'),
(Busy, '1'),
(Idle, '2'),
];
for (spell, ended) in script {
shimmer.set_spell(spell);
shimmer.advance(STALE);
assert_eq!(shimmer.bullet(PALETTE).glyph, Some(ended), "{spell:?}");
}
}
#[test]
fn a_stale_gap_runs_one_tick() {
let mut clock = Clock::default();
assert_eq!(clock.due(TICK / 2, Busy), 0);
assert_eq!(clock.due(STALE + Duration::from_nanos(1), Busy), 1);
assert_eq!(
clock.due(TICK / 2, Busy),
0,
"a stale gap drops the time owed"
);
assert_eq!(clock.due(STALE, Busy), 31);
}
#[test]
fn rest_keeps_every_nanosecond() {
let mut clock = Clock::default();
let ns = Duration::from_nanos;
assert_eq!(clock.due(ns(1), Idle), 0);
assert_eq!(
clock.due((TICK * crate::rest::DRAG).checked_sub(ns(2)).unwrap(), Idle),
0
);
assert_eq!(clock.due(ns(1), Idle), 1);
}
}