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;
// ==========================================
// Constant group (namespace separated by a module)
// ==========================================
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) ",
" ",
];
// ==========================================
// Structs and Implementation (Idiomatic Rust)
// ==========================================
#[derive(Default)]
struct Config {
accident: bool,
fly: bool,
logo: bool,
c51: bool,
}
impl Config {
/// Argument parsing using iterators
fn parse() -> Self {
let mut config = Self::default();
// Skip the initial program name and extract only the arguments starting with `-`.
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
}
}
/// RAII guard ensuring terminal restoration
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();
}
}
/// Drawing context (encapsulates writing to the terminal and clipping operations)
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,
})
}
/// Safe string rendering that accounts for off-screen clipping.
fn draw_str(&mut self, y: i32, x: i32, s: &str) -> io::Result<()> {
if y < 0 || y >= self.rows {
return Ok(());
}
// If x is negative, skip that many items; if positive, skip zeros.
let skip_chars = x.min(0).unsigned_abs() as usize;
let display_x = x.max(0) as u16;
if display_x >= self.cols as u16 {
return Ok(());
}
let max_len = (self.cols as u16 - display_x) as usize;
let final_str: String = s.chars().skip(skip_chars).take(max_len).collect();
if !final_str.is_empty() {
self.stdout.queue(MoveTo(display_x, y as u16))?;
self.stdout.queue(Print(final_str))?;
}
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(());
}
// Updating and rendering existing smoke
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])?;
}
// Discarding smoke thet has reached the end of its lifespan.
while let Some(p) = self.particles.front() {
if p.ptrn >= SMOKEPTNS - 1 {
self.particles.pop_front();
} else {
break;
}
}
// Generating new smoke
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(())
}
}
/// Train Polymophism using Enums(Streamlining C-Language brancheing)
enum TrainModel {
D51,
C51,
Logo,
}
impl TrainModel {
fn from_config(config: &Config) -> Self {
if config.logo {
Self::Logo
} else if config.c51 {
Self::C51
} else {
Self::D51
}
}
fn length(&self) -> i32 {
match self {
Self::D51 => d51::LENGTH,
Self::C51 => c51::LENGTH,
Self::Logo => logo::LENGTH,
}
}
/// Helper: Draw passengers (common to all models)
fn draw_accident(
&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(())
}
/// Drawing method for each train
fn draw(
&self,
renderer: &mut Renderer,
x: i32,
config: &Config,
smoke_mgr: &mut SmokeManager,
) -> io::Result<()> {
match self {
Self::D51 => {
let mut y = renderer.rows / 2 - 5;
let mut dy = 0;
if config.fly {
y = (x / 7) + renderer.rows - (renderer.cols / 7) - d51::HEIGHT;
dy = 1;
}
let pattern = ((d51::LENGTH + x) % d51::PATTERNS as i32).unsigned_abs() as usize;
for i in 0..=d51::HEIGHT {
let idx = i as usize;
let line = if idx < 7 {
d51::STR[idx]
} else if idx < 10 {
d51::WHL[pattern][idx - 7]
} else {
d51::DEL
};
renderer.draw_str(y + i, x, line)?;
if idx < 11 {
renderer.draw_str(y + i + dy, x + 53, COAL[idx + 1])?;
}
}
if config.accident {
self.draw_accident(renderer, y + 2, x + 43, d51::LENGTH)?;
self.draw_accident(renderer, y + 2, x + 47, d51::LENGTH)?;
}
smoke_mgr.update_and_draw(renderer, y - 1, x + d51::FUNNEL)?;
}
Self::C51 => {
let mut y = renderer.rows / 2 - 5;
let mut dy = 0;
if config.fly {
y = (x / 7) + renderer.rows - (renderer.cols / 7) - c51::HEIGHT;
dy = 1;
}
let pattern = ((c51::LENGTH + x) % c51::PATTERNS as i32).unsigned_abs() as usize;
for i in 0..=c51::HEIGHT {
let idx = i as usize;
let line = if idx < 7 {
c51::STR[idx]
} else if idx < 11 {
c51::WHL[pattern][idx - 7]
} else {
c51::DEL
};
renderer.draw_str(y + i, x, line)?;
renderer.draw_str(y + i + dy, x + 55, COAL[idx])?;
}
if config.accident {
self.draw_accident(renderer, y + 3, x + 45, c51::LENGTH)?;
self.draw_accident(renderer, y + 3, x + 49, c51::LENGTH)?;
}
smoke_mgr.update_and_draw(renderer, y - 1, x + c51::FUNNEL)?;
}
Self::Logo => {
let mut y = renderer.rows / 2 - 3;
let mut py1 = 0;
let mut py2 = 0;
let mut py3 = 0;
if config.fly {
y = (x / 6) + renderer.rows - (renderer.cols / 6) - logo::HEIGHT;
py1 = 2;
py2 = 4;
py3 = 6;
}
let pattern =
(((logo::LENGTH + x) / 3) % logo::PATTERNS as i32).unsigned_abs() as usize;
for i in 0..=logo::HEIGHT {
let idx = i as usize;
let line = if idx < 4 {
logo::STR[idx]
} else if idx < 6 {
logo::WHL[pattern][idx - 4]
} else {
logo::DEL
};
renderer.draw_str(y + i, x, line)?;
renderer.draw_str(y + i + py1, x + 21, LCOAL[idx])?;
renderer.draw_str(y + i + py2, x + 42, LCAR[idx])?;
renderer.draw_str(y + i + py3, x + 63, LCAR[idx])?;
}
if config.accident {
self.draw_accident(renderer, y + 1, x + 14, logo::LENGTH)?;
self.draw_accident(renderer, y + 1 + py2, x + 45, logo::LENGTH)?;
self.draw_accident(renderer, y + 1 + py2, x + 53, logo::LENGTH)?;
self.draw_accident(renderer, y + 1 + py3, x + 66, logo::LENGTH)?;
self.draw_accident(renderer, y + 1 + py3, x + 74, logo::LENGTH)?;
}
smoke_mgr.update_and_draw(renderer, y - 1, x + logo::FUNNEL)?;
}
}
Ok(())
}
}
// ==========================================
// main method
// ==========================================
fn main() -> io::Result<()> {
let config = Config::parse();
// Acquire StdoutLock to accelerate I/O operations.
let stdout_handle = stdout();
let mut stdout_lock = stdout_handle.lock();
enable_raw_mode()?;
stdout_lock.execute(EnterAlternateScreen)?;
stdout_lock.execute(Hide)?;
stdout_lock.execute(Clear(ClearType::All))?;
let _guard = TerminalGuard;
let mut smoke_mgr = SmokeManager::default();
let train = TrainModel::from_config(&config);
// Initialie before the loop (can be updated here if resizing within th loop needs to be
// supported).
let mut renderer = Renderer::new(stdout_lock)?;
let start_x = renderer.cols;
let end_x = -(train.length() + 20);
for x in (end_x..=start_x).rev() {
// Discard all bufferd inputs(penalty feature)
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(())
}