use crossterm::{
ExecutableCommand, QueueableCommand,
cursor::{Hide, MoveTo, Show},
event::{poll, read},
style::Print,
terminal::{
Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
enable_raw_mode, size,
},
};
use std::collections::VecDeque;
use std::env;
use std::io::{self, StdoutLock, Write, stdout};
use std::time::Duration;
mod d51 {
pub const HEIGHT: i32 = 10;
pub const FUNNEL: i32 = 7;
pub const LENGTH: i32 = 83;
pub const PATTERNS: usize = 6;
pub const STR: [&str; 7] = [
" ==== ________ ___________ ",
" _D _| |_______/ \\__I_I_____===__|_________| ",
" |(_)--- | H\\________/ | | =|___ ___| ",
" / | | H | | | | ||_| |_|| ",
" | | | H |__--------------------| [___] | ",
" | ________|___H__/__|_____/[][]~\\_______| | ",
" |/ | |-----------I_____I [][] [] D |=======|__ ",
];
pub const WHL: [[&str; 3]; PATTERNS] = [
[
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\O=====O=====O=====O_/ \\_/ ",
],
[
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|=O=====O=====O=====O |_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
],
[
"__/ =| o |=-O=====O=====O=====O \\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
],
[
"__/ =| o |=-~O=====O=====O=====O\\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
],
[
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|= O=====O=====O=====O|_____/~\\___/ ",
" \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ",
],
[
"__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ",
" |/-=|___|= || || || |_____/~\\___/ ",
" \\_/ \\_O=====O=====O=====O/ \\_/ ",
],
];
pub const DEL: &str = " ";
}
mod c51 {
pub const HEIGHT: i32 = 11;
pub const FUNNEL: i32 = 7;
pub const LENGTH: i32 = 87;
pub const PATTERNS: usize = 6;
pub const STR: [&str; 7] = [
" ___ ",
" _|_|_ _ __ __ ___________",
" D__/ \\_(_)___| |__H__| |_____I_Ii_()|_________|",
" | `---' |:: `--' H `--' | |___ ___| ",
" +|~~~~~~~~++::~~~~~~~H~~+=====+~~~~~~|~~||_| |_|| ",
" || | :: H +=====+ | |:: ...| ",
"| | _______|_::-----------------[][]-----| | ",
];
pub const WHL: [[&str; 4]; PATTERNS] = [
[
"| /~~ || |-----/~~~~\\ /[I_____I][][] --|||_______|__",
"------'|oOo|=[]=- || || | ||=======_|__",
"/~\\____|___|/~\\_| O=======O=======O |__|+-/~\\_| ",
"\\_/ \\_/ \\____/ \\____/ \\____/ \\_/ ",
],
[
"| /~~ || |-----/~~~~\\ /[I_____I][][] --|||_______|__",
"------'|oOo|=[]=- O=======O=======O | ||=======_|__",
"/~\\____|___|/~\\_| || || |__|+-/~\\_| ",
"\\_/ \\_/ \\____/ \\____/ \\____/ \\_/ ",
],
[
"| /~~ || |-----/~~~~\\ /[I_____I][][] --|||_______|__",
"------'|oOo|==[]=- O=======O=======O | ||=======_|__",
"/~\\____|___|/~\\_| || || |__|+-/~\\_| ",
"\\_/ \\_/ \\____/ \\____/ \\____/ \\_/ ",
],
[
"| /~~ || |-----/~~~~\\ /[I_____I][][] --|||_______|__",
"------'|oOo|===[]=- O=======O=======O | ||=======_|__",
"/~\\____|___|/~\\_| || || |__|+-/~\\_| ",
"\\_/ \\_/ \\____/ \\____/ \\____/ \\_/ ",
],
[
"| /~~ || |-----/~~~~\\ /[I_____I][][] --|||_______|__",
"------'|oOo|===[]=- || || | ||=======_|__",
"/~\\____|___|/~\\_| O=======O=======O |__|+-/~\\_| ",
"\\_/ \\_/ \\____/ \\____/ \\____/ \\_/ ",
],
[
"| /~~ || |-----/~~~~\\ /[I_____I][][] --|||_______|__",
"------'|oOo|==[]=- || || | ||=======_|__",
"/~\\____|___|/~\\_| O=======O=======O |__|+-/~\\_| ",
"\\_/ \\_/ \\____/ \\____/ \\____/ \\_/ ",
],
];
pub const DEL: &str = " ";
}
mod logo {
pub const HEIGHT: i32 = 6;
pub const FUNNEL: i32 = 4;
pub const LENGTH: i32 = 84;
pub const PATTERNS: usize = 6;
pub const STR: [&str; 4] = [
" ++ +------ ",
" || |+-+ | ",
" /---------|| | | ",
" + ======== +-+ | ",
];
pub const WHL: [[&str; 2]; PATTERNS] = [
[" _|--O========O~\\-+ ", "//// \\_/ \\_/ "],
[" _|--/O========O\\-+ ", "//// \\_/ \\_/ "],
[" _|--/~O========O-+ ", "//// \\_/ \\_/ "],
[" _|--/~\\------/~\\-+ ", "//// \\_O========O "],
[" _|--/~\\------/~\\-+ ", "//// \\O========O/ "],
[" _|--/~\\------/~\\-+ ", "//// O========O_/ "],
];
pub const DEL: &str = " ";
}
const COAL: [&str; 12] = [
" ",
" ",
" ",
" _________________ ",
" _| \\_____A ",
" =| | ",
" -| | ",
"__|________________________|_ ",
"|__________________________|_ ",
" |_D__D__D_| |_D__D__D_| ",
" \\_/ \\_/ \\_/ \\_/ ",
" ",
];
const LCOAL: [&str; 7] = [
"____ ",
"| \\@@@@@@@@@@@ ",
"| \\@@@@@@@@@@@@@_ ",
"| | ",
"|__________________| ",
" (O) (O) ",
" ",
];
const LCAR: [&str; 7] = [
"____________________ ",
"| ___ ___ ___ ___ | ",
"| |_| |_| |_| |_| | ",
"|__________________| ",
"|__________________| ",
" (O) (O) ",
" ",
];
#[derive(Default)]
struct Config {
accident: bool,
fly: bool,
logo: bool,
c51: bool,
}
impl Config {
fn parse() -> Self {
let mut config = Self::default();
for arg in env::args().skip(1).filter(|a| a.starts_with('-')) {
for ch in arg.chars().skip(1) {
match ch {
'a' => config.accident = true,
'F' => config.fly = true,
'l' => config.logo = true,
'c' => config.c51 = true,
_ => {}
}
}
}
config
}
}
struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
let mut out = stdout();
let _ = out.execute(Show);
let _ = out.execute(LeaveAlternateScreen);
let _ = disable_raw_mode();
}
}
struct Renderer<'a> {
stdout: StdoutLock<'a>,
cols: i32,
rows: i32,
}
impl<'a> Renderer<'a> {
fn new(stdout: StdoutLock<'a>) -> io::Result<Self> {
let (cols, rows) = size()?;
Ok(Self {
stdout,
cols: cols as i32,
rows: rows as i32,
})
}
fn draw_str(&mut self, y: i32, x: i32, s: &str) -> io::Result<()> {
if y < 0 || y >= self.rows {
return Ok(());
}
let skip_bytes = x.min(0).unsigned_abs() as usize;
let display_x = x.max(0) as u16;
if display_x >= self.cols as u16 || skip_bytes >= s.len() {
return Ok(());
}
let max_len = (self.cols as u16 - display_x) as usize;
let available_bytes = s.len() - skip_bytes;
let take_bytes = max_len.min(available_bytes);
let slice = &s[skip_bytes..skip_bytes + take_bytes];
if !slice.is_empty() {
self.stdout.queue(MoveTo(display_x, y as u16))?;
self.stdout.queue(Print(slice))?;
}
Ok(())
}
fn flush(&mut self) -> io::Result<()> {
self.stdout.flush()
}
}
struct SmokeParticle {
y: i32,
x: i32,
ptrn: usize,
kind: usize,
}
#[derive(Default)]
struct SmokeManager {
particles: VecDeque<SmokeParticle>,
sum: usize,
}
impl SmokeManager {
fn update_and_draw(&mut self, renderer: &mut Renderer, y: i32, x: i32) -> io::Result<()> {
const SMOKEPTNS: usize = 16;
const SMOKE_STR: [[&str; SMOKEPTNS]; 2] = [
[
"( )", "( )", "( )", "( )", "( )", "( )", "( )", "( )", "()", "()",
"O", "O", "O", "O", "O", " ",
],
[
"(@@@)", "(@@@@)", "(@@@@)", "(@@@)", "(@@)", "(@@)", "(@)", "(@)", "@@", "@@",
"@", "@", "@", "@", "@", " ",
],
];
const ERASER: [&str; SMOKEPTNS] = [
" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ",
" ", " ", " ", " ", " ",
];
const DY: [i32; SMOKEPTNS] = [2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const DX: [i32; SMOKEPTNS] = [-2, -1, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3];
if x % 4 != 0 {
return Ok(());
}
for p in self.particles.iter_mut() {
renderer.draw_str(p.y, p.x, ERASER[p.ptrn])?;
p.y -= DY[p.ptrn];
p.x += DX[p.ptrn];
if p.ptrn < SMOKEPTNS - 1 {
p.ptrn += 1;
}
renderer.draw_str(p.y, p.x, SMOKE_STR[p.kind][p.ptrn])?;
}
while let Some(p) = self.particles.front() {
if p.ptrn >= SMOKEPTNS - 1 {
self.particles.pop_front();
} else {
break;
}
}
renderer.draw_str(y, x, SMOKE_STR[self.sum % 2][0])?;
self.particles.push_back(SmokeParticle {
y,
x,
ptrn: 0,
kind: self.sum % 2,
});
self.sum += 1;
Ok(())
}
}
enum TrainModel {
D51,
C51,
Logo,
}
struct TrainSpec {
height: i32,
length: i32,
funnel: i32,
base_y_offset: i32,
patterns: usize,
pattern_divisor: i32,
fly_divisor: i32,
str_lines: &'static [&'static str],
whl_height: usize,
del: &'static str,
}
impl TrainModel {
fn from_config(config: &Config) -> Self {
if config.logo {
Self::Logo
} else if config.c51 {
Self::C51
} else {
Self::D51
}
}
fn spec(&self) -> TrainSpec {
match self {
Self::D51 => TrainSpec {
height: d51::HEIGHT,
length: d51::LENGTH,
funnel: d51::FUNNEL,
base_y_offset: 5,
patterns: d51::PATTERNS,
pattern_divisor: 1,
fly_divisor: 7,
str_lines: &d51::STR,
whl_height: 3,
del: d51::DEL,
},
Self::C51 => TrainSpec {
height: c51::HEIGHT,
length: c51::LENGTH,
funnel: c51::FUNNEL,
base_y_offset: 5,
patterns: c51::PATTERNS,
pattern_divisor: 1,
fly_divisor: 7,
str_lines: &c51::STR,
whl_height: 4,
del: c51::DEL,
},
Self::Logo => TrainSpec {
height: logo::HEIGHT,
length: logo::LENGTH,
funnel: logo::FUNNEL,
base_y_offset: 3,
patterns: logo::PATTERNS,
pattern_divisor: 3,
fly_divisor: 6,
str_lines: &logo::STR,
whl_height: 2,
del: logo::DEL,
},
}
}
fn get_whl(&self, pattern: usize, line: usize) -> &'static str {
match self {
Self::D51 => d51::WHL[pattern][line],
Self::C51 => c51::WHL[pattern][line],
Self::Logo => logo::WHL[pattern][line],
}
}
fn draw_man(&self, renderer: &mut Renderer, y: i32, x: i32, base_len: i32) -> io::Result<()> {
let man = [["", "(O)"], ["Help!", "\\O/"]];
let ptn = (((base_len + x) / 12) % 2).unsigned_abs() as usize;
for i in 0..2 {
renderer.draw_str(y + i, x, man[ptn][i as usize])?;
}
Ok(())
}
fn draw_cars(
&self,
renderer: &mut Renderer,
y: i32,
x: i32,
idx: usize,
dy: i32,
config: &Config,
) -> io::Result<()> {
match self {
Self::D51 => {
if idx < 11 {
renderer.draw_str(y + idx as i32 + dy, x + 53, COAL[idx + 1])?;
}
}
Self::C51 => {
renderer.draw_str(y + idx as i32 + dy, x + 55, COAL[idx])?;
}
Self::Logo => {
let (py1, py2, py3) = if config.fly { (2, 4, 6) } else { (0, 0, 0) };
renderer.draw_str(y + idx as i32 + py1, x + 21, LCOAL[idx])?;
renderer.draw_str(y + idx as i32 + py2, x + 42, LCAR[idx])?;
renderer.draw_str(y + idx as i32 + py3, x + 63, LCAR[idx])?;
}
}
Ok(())
}
fn draw_accidents(
&self,
renderer: &mut Renderer,
y: i32,
x: i32,
config: &Config,
) -> io::Result<()> {
if !config.accident {
return Ok(());
}
let spec = self.spec();
match self {
Self::D51 => {
self.draw_man(renderer, y + 2, x + 43, spec.length)?;
self.draw_man(renderer, y + 2, x + 47, spec.length)?;
}
Self::C51 => {
self.draw_man(renderer, y + 3, x + 45, spec.length)?;
self.draw_man(renderer, y + 3, x + 49, spec.length)?;
}
Self::Logo => {
let py2 = if config.fly { 4 } else { 0 };
let py3 = if config.fly { 6 } else { 0 };
self.draw_man(renderer, y + 1, x + 14, spec.length)?;
self.draw_man(renderer, y + 1 + py2, x + 45, spec.length)?;
self.draw_man(renderer, y + 1 + py2, x + 53, spec.length)?;
self.draw_man(renderer, y + 1 + py3, x + 66, spec.length)?;
self.draw_man(renderer, y + 1 + py3, x + 74, spec.length)?;
}
}
Ok(())
}
fn draw(
&self,
renderer: &mut Renderer,
x: i32,
config: &Config,
smoke_mgr: &mut SmokeManager,
) -> io::Result<()> {
let spec = self.spec();
let (y, dy) = if config.fly {
let fly_y = (x / spec.fly_divisor) + renderer.rows
- (renderer.cols / spec.fly_divisor)
- spec.height;
(fly_y, 1)
} else {
(renderer.rows / 2 - spec.base_y_offset, 0)
};
let pattern = (((spec.length + x) / spec.pattern_divisor) % spec.patterns as i32)
.unsigned_abs() as usize;
let str_count = spec.str_lines.len();
for i in 0..=spec.height {
let idx = i as usize;
let line = if idx < str_count {
spec.str_lines[idx]
} else if idx < str_count + spec.whl_height {
self.get_whl(pattern, idx - str_count)
} else {
spec.del
};
renderer.draw_str(y + i, x, line)?;
self.draw_cars(renderer, y, x, idx, dy, config)?;
}
self.draw_accidents(renderer, y, x, config)?;
smoke_mgr.update_and_draw(renderer, y - 1, x + spec.funnel)?;
Ok(())
}
}
fn main() -> io::Result<()> {
let config = Config::parse();
enable_raw_mode()?;
let _guard = TerminalGuard;
let stdout_handle = stdout();
let mut stdout_lock = stdout_handle.lock();
stdout_lock.execute(EnterAlternateScreen)?;
stdout_lock.execute(Hide)?;
stdout_lock.execute(Clear(ClearType::All))?;
let mut smoke_mgr = SmokeManager::default();
let train = TrainModel::from_config(&config);
let mut renderer = Renderer::new(stdout_lock)?;
let start_x = renderer.cols;
let end_x = -(train.spec().length + 20);
for x in (end_x..=start_x).rev() {
while poll(Duration::from_millis(0))? {
let _ = read()?;
}
train.draw(&mut renderer, x, &config, &mut smoke_mgr)?;
renderer.flush()?;
std::thread::sleep(Duration::from_millis(40));
}
Ok(())
}