pub(super) const WIDTH: usize = 18;
pub(super) const HEIGHT: usize = 9;
const SIZE: usize = 18;
pub(super) type Rgb = (u8, u8, u8);
type Canvas = [[u8; SIZE]; SIZE];
fn color(key: u8) -> Option<Rgb> {
Some(match key {
b'A' => (230, 173, 84), b'B' => (242, 196, 121), b'D' => (207, 143, 53), b'S' => (189, 125, 35), b'K' => (32, 25, 14), b'W' => (245, 234, 214), b'L' => (214, 232, 232), b'G' => (255, 255, 255), b'T' => (247, 228, 184), b'H' => (84, 56, 22), b'1' => (230, 150, 92), b'2' => (229, 134, 99), b'3' => (228, 122, 104), b'R' => (214, 62, 70), b'Y' => (247, 216, 138), b'V' => (106, 168, 79), _ => return None,
})
}
#[derive(Clone, Copy)]
struct Layer {
x: usize,
y: usize,
rows: &'static [&'static str],
}
impl Layer {
const fn at(x: usize, y: usize, rows: &'static [&'static str]) -> Self {
Self { x, y, rows }
}
fn pixels(self) -> impl Iterator<Item = (usize, usize, u8)> {
self.rows
.iter()
.enumerate()
.flat_map(move |(j, row)| {
row.bytes()
.enumerate()
.filter(|&(_, key)| key != b'.')
.map(move |(i, key)| (self.x + i, self.y + j, key))
})
.filter(|&(x, y, _)| x < SIZE && y < SIZE)
}
}
#[rustfmt::skip]
const BODY: Layer = Layer::at(1, 6, &[
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"AAAAAAAAAAAAAAAA",
"DDDDDDDDDDDDDDDD",
]);
const OPENING: Layer = Layer::at(1, 5, &["DHHHHHHHHHHHHHHD"]);
#[rustfmt::skip]
const LID: Layer = Layer::at(0, 4, &[
"..BBBBBBBBBBBBBB..",
"DDDDDDDDDDDDDDDDDD",
]);
#[rustfmt::skip]
const TAPE: Layer = Layer::at(8, 4, &[
"TT",
"TT",
"TT",
]);
#[rustfmt::skip]
const KNOCKED_LID: Layer = Layer::at(0, 3, &[
"..........BBBB....",
"....BBBBBBDDDDDD..",
"BBBBDDDDDD........",
"DDDD..............",
]);
#[rustfmt::skip]
const EYE_WHITE: Layer = Layer::at(3, 9, &[
"WWWW",
"WWWW",
".WW.",
]);
const EYELID: Layer = Layer::at(3, 8, &["KKKK"]);
const EYELID_SQUINT: Layer = Layer::at(3, 9, &["KKKK"]);
const EYE_SHUT: Layer = Layer::at(3, 9, &["KKKK"]);
#[rustfmt::skip]
const GLUM_LEFT: Layer = Layer::at(2, 8, &[
"...KK",
".KKWW",
".WWWW",
".WKKW",
"..KK.",
]);
#[rustfmt::skip]
const GLUM_RIGHT: Layer = Layer::at(10, 8, &[
"KK...",
"WWKK.",
"WWWW.",
"WKKW.",
".KK..",
]);
#[rustfmt::skip]
const PUPIL: &[&str] = &[
"KK",
"KK",
];
const PUPIL_DOWN: &[&str] = &["KK"];
#[rustfmt::skip]
const RING: Layer = Layer::at(8, 6, &[
"..KKKK..",
".KLLLLK.",
"KLLLLLLK",
"KLLLLLLK",
"KLLLLLLK",
".KLLLLK.",
"..KKKK..",
]);
const PARKED_GLINT: Layer = Layer::at(10, 8, &["G"]);
const PARKED_GLINT_RIGHT: Layer = Layer::at(13, 8, &["G"]);
const MONOCLE_SHUT: Layer = Layer::at(9, 9, &["KKKKKK"]);
#[rustfmt::skip]
const DROPPED: Layer = Layer::at(13, 7, &[
"..S.",
"....",
"..S.",
"....",
"..S.",
".KK.",
"KGLK",
"KLLK",
".KK.",
]);
const CHEEKS: [Layer; 2] = [Layer::at(1, 13, &["cc"]), Layer::at(15, 13, &["cc"])];
#[rustfmt::skip]
const MUSTACHE: Layer = Layer::at(3, 14, &[
"K...KKKK...K",
"KKKKK..KKKKK",
".KKK....KKK.",
]);
#[rustfmt::skip]
const MUSTACHE_DROOP: Layer = Layer::at(3, 14, &[
"...KKKKK...",
".KKKK.KKKK.",
"KK.......KK",
]);
#[rustfmt::skip]
const STRAWBERRY: Layer = Layer::at(4, 0, &[
"..V.",
"VVVV",
"RYRR",
"RRYR",
".RR.",
]);
pub(super) const LID_MAX: u8 = 4;
const LID_STEP_PX: u8 = 1;
const GAZE_LOOP_MS: u128 = 7000;
const GAZE_LIST_UNTIL_MS: u128 = 4200;
const GAZE_BAR_UNTIL_MS: u128 = 5400;
const BLINK_WINDOW_MS: u128 = 4300;
const BLINK_EARLIEST_MS: u128 = 500;
const BLINK_SPREAD_MS: u128 = 2800;
const BLINK_MS: u128 = 160;
const GLINT_CYCLE_MS: u128 = 640;
const GLINT_STEP_MS: u128 = 100;
const GLINT_POSITIONS: u128 = 4;
const GLINT_SWEEP_MS: u128 = GLINT_STEP_MS * GLINT_POSITIONS;
const GLINT_REST_MS: u128 = GLINT_CYCLE_MS - GLINT_SWEEP_MS;
fn hash32(n: u32) -> u32 {
let mut v = n.wrapping_mul(2_654_435_761).wrapping_add(12_345);
v ^= v >> 15;
v = v.wrapping_mul(2_246_822_519);
v ^ (v >> 13)
}
fn blinking(ms: u128) -> bool {
let start =
BLINK_EARLIEST_MS + u128::from(hash32((ms / BLINK_WINDOW_MS) as u32)) % BLINK_SPREAD_MS;
(start..start + BLINK_MS).contains(&(ms % BLINK_WINDOW_MS))
}
fn gaze_at(ms: u128) -> Gaze {
match ms % GAZE_LOOP_MS {
cycle if cycle < GAZE_LIST_UNTIL_MS => Gaze::List,
cycle if cycle < GAZE_BAR_UNTIL_MS => Gaze::Bar,
_ => Gaze::You,
}
}
fn lid_offset(done: usize, total: Option<usize>) -> u8 {
let Some(total) = total.filter(|total| *total > 0) else {
return LID_MAX;
};
let n = 9 * total as u128;
let d = (10 * done.min(total) as u128).min(n);
(u128::from(LID_MAX) * (n - d)).div_ceil(n) as u8
}
fn step_lid(shown: u8, target: u8) -> u8 {
if target < shown {
shown - LID_STEP_PX
} else {
target
}
}
fn glint_at(ms: u128, since_hit_ms: Option<u128>) -> Option<u8> {
let since_hit_ms = since_hit_ms?;
let phase = ms % GLINT_CYCLE_MS;
if phase >= GLINT_SWEEP_MS || since_hit_ms > phase + GLINT_REST_MS {
return None;
}
Some((phase / GLINT_STEP_MS) as u8)
}
fn cheek_level(hits: u64, misses: u64) -> u8 {
if hits == 0 {
return 0;
}
let share = 3 * u128::from(hits) / (u128::from(hits) + u128::from(misses));
1 + share.min(2) as u8
}
#[derive(Clone, Copy, Debug)]
pub(super) struct Inputs {
pub ms: u128,
pub since_hit_ms: Option<u128>,
pub done: usize,
pub total: Option<usize>,
pub lid_shown: u8,
pub hits: u64,
pub misses: u64,
pub testing: bool,
pub ok: Option<bool>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum Eye {
#[default]
Skeptic,
Squint,
Shut,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum Gaze {
List,
Bar,
#[default]
You,
}
impl Gaze {
fn pupils(self) -> [Layer; 2] {
match self {
Self::List => [Layer::at(3, 9, PUPIL), Layer::at(10, 9, PUPIL)],
Self::Bar => [Layer::at(3, 10, PUPIL_DOWN), Layer::at(10, 10, PUPIL_DOWN)],
Self::You => [Layer::at(4, 9, PUPIL), Layer::at(11, 9, PUPIL)],
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct Pose {
pub lid: u8,
pub taped: bool,
pub failed: bool,
pub eye: Eye,
pub gaze: Gaze,
pub glint: Option<u8>,
pub cheeks: u8,
pub strawberry: bool,
}
pub(super) fn pose_at(inputs: Inputs) -> Pose {
let cheeks = cheek_level(inputs.hits, inputs.misses);
match inputs.ok {
Some(false) => Pose {
failed: true,
..Pose::default()
},
Some(true) => Pose {
taped: true,
cheeks,
strawberry: inputs.hits > 0,
..Pose::default()
},
None => Pose {
lid: step_lid(inputs.lid_shown, lid_offset(inputs.done, inputs.total)),
eye: if blinking(inputs.ms) {
Eye::Shut
} else if inputs.testing {
Eye::Squint
} else {
Eye::Skeptic
},
gaze: gaze_at(inputs.ms),
glint: glint_at(inputs.ms, inputs.since_hit_ms),
cheeks,
..Pose::default()
},
}
}
fn stamp(canvas: &mut Canvas, layer: Layer) {
for (x, y, key) in layer.pixels() {
canvas[y][x] = key;
}
}
fn stamp_lid(canvas: &mut Canvas, rise: u8) {
if rise == 0 {
stamp(canvas, LID);
return;
}
let mut previous_y = LID.y + 1;
for (x, _) in LID.rows[1].bytes().enumerate() {
let front_y = LID.y + 1 - usize::from(rise) * x / (SIZE - 1);
canvas[front_y][x] = b'D';
if front_y < previous_y {
canvas[previous_y][x] = b'D';
}
if (2..SIZE - 2).contains(&x) {
canvas[front_y - 1][x] = b'B';
}
previous_y = front_y;
}
}
fn light_band(canvas: &mut Canvas, position: u8) {
let band = [2 * position + 1, 2 * position + 2].map(usize::from);
for (x, y, key) in RING.pixels() {
if key != b'L' || canvas[y][x] != b'L' {
continue;
}
if band.contains(&(x + y - RING.x - RING.y - 2)) {
canvas[y][x] = b'G';
}
}
}
fn sprite(pose: Pose) -> Canvas {
let mut canvas = [[b'.'; SIZE]; SIZE];
stamp(&mut canvas, BODY);
if pose.failed {
for layer in [
OPENING,
KNOCKED_LID,
GLUM_LEFT,
GLUM_RIGHT,
MUSTACHE_DROOP,
DROPPED,
] {
stamp(&mut canvas, layer);
}
return canvas;
}
if pose.lid > 0 {
stamp(&mut canvas, OPENING);
}
stamp_lid(&mut canvas, pose.lid);
if pose.taped {
stamp(&mut canvas, TAPE);
}
if pose.cheeks > 0 {
for (x, y, _) in CHEEKS.into_iter().flat_map(Layer::pixels) {
canvas[y][x] = b'0' + pose.cheeks;
}
}
let [eye_pupil, lens_pupil] = pose.gaze.pupils();
let (eyelid, monocle) = match pose.eye {
Eye::Skeptic => (EYELID, lens_pupil),
Eye::Squint => (EYELID_SQUINT, lens_pupil),
Eye::Shut => (EYE_SHUT, MONOCLE_SHUT),
};
if pose.eye != Eye::Shut {
stamp(&mut canvas, EYE_WHITE);
stamp(&mut canvas, eye_pupil);
}
stamp(&mut canvas, eyelid);
stamp(&mut canvas, RING);
stamp(&mut canvas, monocle);
stamp(
&mut canvas,
if matches!(pose.gaze, Gaze::List | Gaze::Bar) {
PARKED_GLINT_RIGHT
} else {
PARKED_GLINT
},
);
if let Some(position) = pose.glint {
light_band(&mut canvas, position);
}
stamp(&mut canvas, MUSTACHE);
if pose.strawberry {
stamp(&mut canvas, STRAWBERRY);
}
canvas
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Cell {
pub glyph: char,
pub fg: Option<Rgb>,
pub bg: Option<Rgb>,
}
fn cells(canvas: &Canvas) -> [[Cell; WIDTH]; HEIGHT] {
std::array::from_fn(|r| {
std::array::from_fn(|x| {
let (top, bottom) = (canvas[2 * r][x], canvas[2 * r + 1][x]);
let (glyph, fg, bg) = match (color(top), color(bottom)) {
(None, None) => (' ', None, None),
(Some(both), _) if top == bottom => (' ', None, Some(both)),
(Some(upper), None) => ('▀', Some(upper), None),
(None, Some(lower)) => ('▄', Some(lower), None),
(Some(upper), Some(lower)) => ('▀', Some(upper), Some(lower)),
};
Cell { glyph, fg, bg }
})
})
}
pub(super) fn draw(pose: Pose) -> [[Cell; WIDTH]; HEIGHT] {
cells(&sprite(pose))
}
#[cfg(test)]
#[path = "mascot_tests.rs"]
mod tests;