Rust Implementation of terminal Text game - Trainspotting II

Started by ron77, Dec 27, 2024, 01:59 PM

Previous topic - Next topic

ron77

https://retrocoders.phatcode.net/index.php?topic=868.0

the rust implementation:
main.rs:
use std::fs;
use std::io::{self, Write};
use std::thread;
use std::time::Duration;
use std::ffi::CString;
use std::env;
use rand::Rng;
use anyhow::{Result, Context};
use bass_sys::*;

// File paths
const DESC1: &str = "data/des1.txt";
const OPENING_BANNER: &str = "banners/opening1.txt";
const OPENING_POINT: &str = "data/opening1.txt";
const MENU: &str = "data/mainmenu.txt";
const MONEY_MENU: &str = "data/moneyMenu.txt";
const FRIENDS_MENU: &str = "data/friendsmenu.txt";
const OVER: &str = "data/suicide.txt";
const GAME_END: &str = "data/theend.txt";
const FAMILY1: &str = "data/family1.txt";
const SCHOOL1: &str = "data/school1.txt";

// Arrays of file paths
const MONTHS: [&str; 12] = [
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"
];

const SONGS_RAND: [&str; 12] = [
    "sound/walrus1.ogg", "sound/walrus2.ogg", "sound/walrus3.ogg",
    "sound/walrus4.ogg", "sound/walrus5.ogg", "sound/walrus6.ogg",
    "sound/walrus7.ogg", "sound/walrus8.ogg", "sound/walrus9.ogg",
    "sound/walrus10.ogg", "sound/walrus11.ogg", "sound/walrus12.ogg"
];

const MARY: [&str; 5] = [
    "data/mary1.txt", "data/mary2rehab.txt", "data/mary3pregnant.txt",
    "data/mary4jail.txt", "data/mary5dead.txt"
];

const JOE: [&str; 5] = [
    "data/joe1.txt", "data/joe2rehab.txt", "data/joe3jail.txt",
    "data/joe4.txt", "data/joe5prison.txt"
];

const FELIX: [&str; 5] = [
    "data/felix1.txt", "data/felix2.txt", "data/felix3jail.txt",
    "data/felix4rehab.txt", "data/felix5crazy.txt"
];

struct GameState {
    health: i32,
    money: i32,
    turns: usize,
    year: i32,
    is_over: bool,
    is_mary: bool,
    is_joe: bool,
    is_felix: bool,
    is_suicide: bool,
    stream: u32,
}

impl GameState {
    fn new() -> Self {
        Self {
            health: 100,
            money: 5000,
            turns: 0,
            year: 1991,
            is_over: false,
            is_mary: true,
            is_joe: true,
            is_felix: true,
            is_suicide: false,
            stream: 0,
        }
    }

    fn play_music(&mut self, path: &str) -> Result<()> {
        let mut full_path = env::current_dir()?;
        full_path.push(path);
        let path_str = full_path.to_str().unwrap();
        
        println!("Attempting to play: {}", path_str);

        if self.stream != 0 {
            BASS_StreamFree(self.stream);
            self.stream = 0;
        }

        let path_c = CString::new(path_str)?;
        self.stream = BASS_StreamCreateFile(
            0,
            path_c.as_ptr() as *const _,
            0,
            0,
            BASS_SAMPLE_LOOP
        );

        if self.stream == 0 {
            println!("BASS Error Code: {}", BASS_ErrorGetCode());
            return Ok(());
        }

        if BASS_ChannelPlay(self.stream, 0) == 0 {
            println!("Failed to play: Error code {}", BASS_ErrorGetCode());
            return Ok(());
        }
        
        Ok(())
    }

    fn stop_music(&mut self) {
        if self.stream != 0 {
            BASS_ChannelStop(self.stream);
            BASS_StreamFree(self.stream);
            self.stream = 0;
        }
    }

    fn read_file(&self, path: &str) -> Result<String> {
        let mut full_path = env::current_dir()?;
        full_path.push(path);
        fs::read_to_string(&full_path)
            .with_context(|| format!("Failed to read file: {}", full_path.display()))
    }

    fn clear_screen(&self) {
        print!("\x1B[2J\x1B[1;1H");
        io::stdout().flush().unwrap();
    }

    fn get_input(&self) -> Result<i32> {
        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        Ok(input.trim().parse()?)
    }

    fn wait_for_enter(&self) -> Result<()> {
        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        Ok(())
    }

    fn friends(&mut self) -> Result<()> {
        self.clear_screen();
        println!(
            "date: {}, {} health: {} money: {}\n\n{}",
            MONTHS[self.turns], self.year, self.health, self.money,
            self.read_file(FRIENDS_MENU)?
        );

        match self.get_input()? {
            1 => self.visit_mary()?,
            2 => self.visit_joe()?,
            3 => self.visit_felix()?,
            4 => self.visit_school()?,
            5 => self.visit_family()?,
            _ => {
                println!("INVALID INPUT!");
                thread::sleep(Duration::from_secs(5));
            }
        }
        Ok(())
    }

    fn visit_mary(&mut self) -> Result<()> {
        self.clear_screen();
        if !self.is_mary {
            println!("{}", self.read_file(MARY[4])?);
        } else {
            let mut rng = rand::thread_rng();
            let r = rng.gen_range(0..5);
            if r == 4 {
                self.is_mary = false;
            }
            println!("{}", self.read_file(MARY[r])?);
        }
        self.wait_for_enter()?;
        Ok(())
    }

    fn visit_joe(&mut self) -> Result<()> {
        self.clear_screen();
        if !self.is_joe {
            println!("{}", self.read_file(JOE[4])?);
        } else {
            let mut rng = rand::thread_rng();
            let r = rng.gen_range(0..5);
            if r == 4 {
                self.is_joe = false;
            }
            println!("{}", self.read_file(JOE[r])?);
        }
        self.wait_for_enter()?;
        Ok(())
    }

    fn visit_felix(&mut self) -> Result<()> {
        self.clear_screen();
        if !self.is_felix {
            println!("{}", self.read_file(FELIX[4])?);
        } else {
            let mut rng = rand::thread_rng();
            let r = rng.gen_range(0..5);
            if r == 4 {
                self.is_felix = false;
            }
            println!("{}", self.read_file(FELIX[r])?);
        }
        self.wait_for_enter()?;
        Ok(())
    }

    fn visit_school(&mut self) -> Result<()> {
        self.clear_screen();
        println!("{}", self.read_file(SCHOOL1)?);
        self.wait_for_enter()?;
        Ok(())
    }

    fn visit_family(&mut self) -> Result<()> {
        self.clear_screen();
        println!("{}", self.read_file(FAMILY1)?);
        self.wait_for_enter()?;
        Ok(())
    }

    fn drugs(&mut self) -> Result<()> {
        self.clear_screen();
        let mut rng = rand::thread_rng();
        let index = rng.gen_range(0..5);
        
        println!("{}", self.read_file(&format!("data/drug{}.txt", index + 1))?);
        
        match index {
            0 => self.money -= 170,
            3 => {
                self.health -= 5;
                self.money -= 350;
            }
            4 => {
                self.health -= 5;
                self.money -= 250;
            }
            _ => {}
        }
        
        self.wait_for_enter()?;
        Ok(())
    }

    fn shoplist(&mut self) -> Result<()> {
        self.clear_screen();
        let mut rng = rand::thread_rng();
        let index = rng.gen_range(0..5);
        
        println!("{}", self.read_file(&format!("data/shop{}.txt", index + 1))?);
        
        match index {
            0 => self.money += 200,
            1 => self.money += 150,
            2 => self.health -= 3,
            3 => self.money += 200,
            4 => self.health -= 3,
            _ => {}
        }
        
        self.wait_for_enter()?;
        Ok(())
    }

    fn car(&mut self) -> Result<()> {
        self.clear_screen();
        let mut rng = rand::thread_rng();
        let index = rng.gen_range(0..5);
        
        println!("{}", self.read_file(&format!("data/car{}.txt", index + 1))?);
        
        match index {
            0 => self.money += 1000,
            1 => self.money += 2000,
            2 => self.health -= 5,
            3 => self.money += 1500,
            4 => self.health -= 5,
            _ => {}
        }
        
        self.wait_for_enter()?;
        Ok(())
    }

    fn burglary(&mut self) -> Result<()> {
        self.clear_screen();
        let mut rng = rand::thread_rng();
        let index = rng.gen_range(0..5);
        
        println!("{}", self.read_file(&format!("data/burglur{}.txt", index + 1))?);
        
        match index {
            0 => self.money += 2500,
            1 => self.health -= 7,
            2 => self.health -= 7,
            3 => self.money += 2500,
            4 => self.health -= 7,
            _ => {}
        }
        
        self.wait_for_enter()?;
        Ok(())
    }

    fn get_money(&mut self) -> Result<()> {
        self.clear_screen();
        println!(
            "date: {}, {} health: {} money: {}\n\n{}",
            MONTHS[self.turns], self.year, self.health, self.money,
            self.read_file(MONEY_MENU)?
        );

        match self.get_input()? {
            1 => self.shoplist()?,
            2 => self.car()?,
            3 => self.burglary()?,
            _ => {}
        }
        
        Ok(())
    }

    fn rehab(&mut self) -> Result<()> {
        if self.money < 2800 {
            self.clear_screen();
            println!(
                "\nYOU DON'T HAVE ENOUGH MONEY FOR REHAB...\nIT COSTS 2800 AND YOU ONLY HAVE {}\n",
                self.money
            );
            self.wait_for_enter()?;
            return Ok(());
        }

        self.money -= 2800;

        for i in 0..6 {
            self.clear_screen();
            println!("{}", self.read_file(&format!("data/rehab{}.txt", i + 1))?);
            self.wait_for_enter()?;
        }

        self.health += 65;
        if self.health > 100 {
            self.health = 100;
        }

        Ok(())
    }

    fn mirror(&mut self) -> Result<()> {
        self.clear_screen();
        
        let mirror_index = match self.health {
            h if h >= 90 => 0,
            h if h >= 80 => 1,
            h if h >= 70 => 2,
            h if h >= 60 => 3,
            h if h >= 50 => 4,
            h if h >= 40 => 5,
            h if h >= 30 => 6,
            h if h >= 20 => 7,
            h if h >= 10 => 8,
            _ => 9,
        };

        println!("{}", self.read_file(&format!("data/mirror{}.txt", mirror_index + 1))?);

        if self.health < 35 {
            self.is_suicide = true;
        }

        self.wait_for_enter()?;
        Ok(())
    }

    fn suicide(&mut self) -> Result<()> {
        self.clear_screen();
        println!("{}", self.read_file(OVER)?);
        self.wait_for_enter()?;
        self.is_over = true;
        Ok(())
    }
}

fn main() -> Result<()> {
    // Initialize BASS
    if BASS_Init(-1, 44100, 0, std::ptr::null_mut(), std::ptr::null_mut()) == 0 {
        let error_code = BASS_ErrorGetCode();
        eprintln!("Error initializing BASS library: {}", error_code);
        return Ok(());
    }

    let mut game = GameState::new();
    let mut rng = rand::thread_rng();

    // Show intro
    println!("{}", game.read_file(DESC1)?);
    game.wait_for_enter()?;
    game.clear_screen();

    // Start background music
    let song_index = rng.gen_range(0..12);
    if let Err(e) = game.play_music(SONGS_RAND[song_index]) {
        eprintln!("Failed to play music: {}", e);
    }

    println!("{}", game.read_file(OPENING_BANNER)?);
    game.wait_for_enter()?;
    game.clear_screen();

    println!("{}", game.read_file(OPENING_POINT)?);
    game.wait_for_enter()?;
    game.clear_screen();

    // Main game loop
    while game.health > 0 && !game.is_over {
        game.clear_screen();

        if game.turns > 11 {
            game.turns = 0;
            game.year += 1;
        }

        if game.money < 0 {
            game.money = 0;
        }

        println!(
            "date: {}, {} health: {} money: {}\n\n{}",
            MONTHS[game.turns], game.year, game.health, game.money,
            game.read_file(MENU)?
        );

        if game.is_suicide {
            println!("PRESS 8 TO TRY AND SUICIDE");
        }

        match game.get_input()? {
            1 => {
                game.stop_music();
                let song_index = rng.gen_range(0..12);
                if let Err(e) = game.play_music(SONGS_RAND[song_index]) {
                    eprintln!("Failed to change music: {}", e);
                }
            },
            2 => game.friends()?,
            3 => game.drugs()?,
            4 => game.get_money()?,
            5 => game.rehab()?,
            6 => game.mirror()?,
            7 => game.is_over = true,
            8 if game.is_suicide => game.suicide()?,
            _ => {}
        }

        game.turns += 1;
    }

    // Show ending
    game.clear_screen();
    println!("{}", game.read_file(GAME_END)?);
    game.wait_for_enter()?;

    // Cleanup
    game.stop_music();
    BASS_Free();

    Ok(())
}

Cargo.toml:
[package]
name = "rust_trainspotting_II_game"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8"
bass-sys = "2.1"
anyhow = "1.0"