⚠️ Warning: This is a draft ⚠️

This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.

If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.

{{collection|RCRPG}} [[Rust]] version of [[:Category:RCRPG|RCRPG]].

==Code==


//! Implementation of the simple text-based game [RCRPG](https://web.archive.org/web/20080212201605/http://shortcircuit.us/muddy-kinda-like-a-mud-but-single-player/)
//! in Rust

use rand::prelude::*;
use std::borrow::BorrowMut;
use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display};
use std::iter::FromIterator;
use std::ops::Add;
use std::{fmt, io};

/// Maps each Locations to a direction
const DIRECTION_MAPPING: [(Location, Direction); 6] = [
    (Location(0, -1, 0), Direction::North),
    (Location(0, 1, 0), Direction::South),
    (Location(-1, 0, 0), Direction::West),
    (Location(1, 0, 0), Direction::East),
    (Location(0, 0, 1), Direction::Down),
    (Location(0, 0, -1), Direction::Up),
];

/// Objects possessed by the player
type Inventory = HashSet<Object>;
/// Maps the (possibly user-defined) aliases to their actual action, so that for instance a player
/// can input either `n` or `north` to go North, and can also define new aliases
type CommandAliases = Vec<(HashSet<String>, Command)>;

/// 3D coordinates of objects in the dungeon
#[derive(Hash, Eq, PartialEq, Copy, Clone)]
struct Location(i32, i32, i32);

impl Add for Location {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Location(self.0 + rhs.0, self.1 + rhs.1, self.2 + rhs.2)
    }
}

impl Debug for Location {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "({}, {}, {})", self.0, self.1, self.2)
    }
}

/// Objects that can be found in the dungon rooms
#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)]
enum Object {
    Ladder,
    Sledge,
    Gold,
}

impl Display for Object {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match *self {
            Object::Ladder => write!(f, "a ladder"),
            Object::Sledge => write!(f, "a sledge"),
            Object::Gold => write!(f, "some gold"),
        }
    }
}

impl Object {
    /// Tries to parse a string to an object, like `"gold"` to `Object::Gold`
    fn from_string(s: &str) -> Option<Object> {
        match s {
            "ladder" => Some(Object::Ladder),
            "sledge" => Some(Object::Sledge),
            "gold" => Some(Object::Gold),
            _ => None,
        }
    }
}

/// Player information
struct Player {
    /// Room where the player currently is
    location: Location,
    /// The objects carried by the player
    inventory: Inventory,
    /// The object wieled by the player, if any
    equipped: Option<Object>,
}

/// Information about each room of the dungeon
struct Room {
    /// Fixed description for special rooms (like the first one or the prize room)
    description: Option<String>,
    /// Objects currently in the room
    objects: Inventory,
}

impl Room {
    fn new() -> Self {
        Room {
            description: None,
            objects: HashSet::new(),
        }
    }

    /// Sets the room description
    fn with_description(mut self, description: &str) -> Self {
        self.description = Some(description.to_string());
        self
    }

    /// Sets the objects in the room
    fn with_objects(mut self, objects: Vec<Object>) -> Self {
        self.objects.extend(objects);
        self
    }

    /// Adds some randoms objects to the room
    fn with_random_objects(mut self, rng: &mut ThreadRng) -> Self {
        let objects: Vec<_> = vec![
            if rng.gen::<f32>() < 0.33 {
                Some(Object::Sledge)
            } else {
                None
            },
            if rng.gen::<f32>() < 0.33 {
                Some(Object::Ladder)
            } else {
                None
            },
            if rng.gen::<f32>() < 0.33 {
                Some(Object::Gold)
            } else {
                None
            },
        ]
        .iter()
        .filter_map(|o| *o)
        .collect();

        self.objects.extend(objects);
        self
    }
}

/// Cardinat directions
#[derive(Copy, Clone, Eq, PartialEq)]
enum Direction {
    North,
    South,
    West,
    East,
    Down,
    Up,
}

impl Display for Direction {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match *self {
            Direction::North => write!(f, "north"),
            Direction::South => write!(f, "south"),
            Direction::West => write!(f, "west"),
            Direction::East => write!(f, "east"),
            Direction::Down => write!(f, "down"),
            Direction::Up => write!(f, "up"),
        }
    }
}

impl Direction {
    /// Tries to parse a string to a direction, like `"north"` to `Direction::North`
    fn from_string(s: &str) -> Option<Direction> {
        match s {
            "north" => Some(Direction::North),
            "south" => Some(Direction::South),
            "west" => Some(Direction::West),
            "east" => Some(Direction::East),
            "down" => Some(Direction::Down),
            "up" => Some(Direction::Up),
            _ => None,
        }
    }

    /// Returns the normalized 3D point of the location, for instance `Direction::North` is
    /// `(0, -1, 0)` where `(x, y, z)`
    fn to_location(self) -> Location {
        DIRECTION_MAPPING.iter().find(|d| d.1 == self).unwrap().0
    }
}

/// Collection of rooms
struct Dungeon {
    /// The rooms that make up the dungeon
    rooms: HashMap<Location, Room>,
}

impl Dungeon {
    fn new() -> Self {
        Dungeon {
            rooms: HashMap::from_iter(vec![
                (
                    Location(0, 0, 0),
                    Room::new()
                        .with_description("The room where it all started...")
                        .with_objects(vec![Object::Ladder, Object::Sledge]),
                ),
                (
                    Location(1, 1, 5),
                    Room::new().with_description("You found it! Lots of gold!"),
                ),
            ]),
        }
    }

    /// Given a room location, returns the list of `Direction`s that lead to other rooms
    fn exits_for_room(&self, location: Location) -> Vec<Direction> {
        DIRECTION_MAPPING
            .iter()
            .filter_map(|d| {
                let location_to_test = location + d.0;

                if self.rooms.contains_key(&location_to_test) {
                    return Some(d.1);
                }
                None
            })
            .collect()
    }
}

/// Collection of all the available commands to interact to the dungeon world
#[derive(Debug, Copy, Clone)]
enum Command {
    North,
    South,
    West,
    East,
    Down,
    Up,
    Help,
    Dig,
    Look,
    Inventory,
    Take,
    Drop,
    Equip,
    Unequip,
    Alias,
}

/// Returns the list of all the default command aliases
fn default_aliases() -> CommandAliases {
    vec![
        (
            vec!["n".to_string(), "north".to_string()]
                .into_iter()
                .collect(),
            Command::North,
        ),
        (
            vec!["s".to_string(), "south".to_string()]
                .into_iter()
                .collect(),
            Command::South,
        ),
        (
            vec!["w".to_string(), "west".to_string()]
                .into_iter()
                .collect(),
            Command::West,
        ),
        (
            vec!["e".to_string(), "east".to_string()]
                .into_iter()
                .collect(),
            Command::East,
        ),
        (
            vec!["d".to_string(), "down".to_string()]
                .into_iter()
                .collect(),
            Command::Down,
        ),
        (
            vec!["u".to_string(), "up".to_string()]
                .into_iter()
                .collect(),
            Command::Up,
        ),
        (
            vec!["help".to_string()].into_iter().collect(),
            Command::Help,
        ),
        (vec!["dig".to_string()].into_iter().collect(), Command::Dig),
        (
            vec!["l".to_string(), "look".to_string()]
                .into_iter()
                .collect(),
            Command::Look,
        ),
        (
            vec!["i".to_string(), "inventory".to_string()]
                .into_iter()
                .collect(),
            Command::Inventory,
        ),
        (
            vec!["take".to_string()].into_iter().collect(),
            Command::Take,
        ),
        (
            vec!["drop".to_string()].into_iter().collect(),
            Command::Drop,
        ),
        (
            vec!["equip".to_string()].into_iter().collect(),
            Command::Equip,
        ),
        (
            vec!["unequip".to_string()].into_iter().collect(),
            Command::Unequip,
        ),
        (
            vec!["alias".to_string()].into_iter().collect(),
            Command::Alias,
        ),
    ]
}

/// Tries to parse a string to a command also taking into account the aliases
fn find_command(command: &str, aliases: &[(HashSet<String>, Command)]) -> Option<Command> {
    let command = command.to_lowercase();

    aliases.iter().find(|a| a.0.contains(&command)).map(|a| a.1)
}

/// Prints the help string
fn help() {
    println!(
        "You need a sledge to dig rooms and ladders to go upwards.
Valid commands are: directions (north, south...), dig, take, drop, equip, inventory and look.
Additionally you can tag rooms with the 'name' command and alias commands with 'alias'.
Have fun!"
    )
}

/// Defines a new alias for a command
fn alias(command_aliases: &mut CommandAliases, args: &[&str]) {
    if args.len() < 2 {
        println!("To assign an alias: alias CMQ NEW_ALIAS");
    } else {
        let command = args[0].to_lowercase();
        let new_alias = args[1].to_lowercase();

        let mut found = false;
        for ca in command_aliases {
            if ca.0.contains(&command) {
                ca.0.insert(new_alias.clone());
                found = true;
            }
        }

        if found {
            println!("You can use \"{}\" in lieu of \"{}\"", new_alias, command);
        } else {
            println!("The commands \"{}\" does not exist", command);
        }
    }
}

/// Describes the current rooom
fn look(player: &Player, dungeon: &Dungeon) {
    let room = &dungeon.rooms[&player.location];

    if let Some(description) = &room.description {
        print!("{}", description);
    } else {
        print!("Room at {:?}.", player.location);
    }

    if !room.objects.is_empty() {
        print!(
            " On the floor you can see: {}.",
            room.objects
                .iter()
                .map(|o| o.to_string())
                .collect::<Vec<String>>()
                .join(", ")
        );
    }

    let room_exits = dungeon.exits_for_room(player.location);
    match room_exits.len() {
        0 => println!(" There are no exits in this room."),
        1 => println!(" There is one exit: {}.", room_exits[0].to_string()),
        _ => println!(
            " Exits: {}.",
            room_exits
                .iter()
                .map(|o| o.to_string())
                .collect::<Vec<String>>()
                .join(", ")
        ),
    }
}

/// Grabs an object lying on the floor of a room and puts it into the player's inventory
fn take(player: &mut Player, dungeon: &mut Dungeon, args: &[&str]) {
    if args.is_empty() {
        println!("To take something: take OBJECT|all")
    } else if dungeon.rooms[&player.location].objects.is_empty() {
        println!("There is nothing to take here")
    } else if args[0] == "all" {
        let room_objects = dungeon
            .rooms
            .get_mut(&player.location)
            .expect("The player is in a room that should not exist!")
            .objects
            .borrow_mut();

        player.inventory.extend(room_objects.iter());
        room_objects.clear();

        println!("All items taken");
    } else if let Some(object) = Object::from_string(args[0]) {
        let room_objects = dungeon
            .rooms
            .get_mut(&player.location)
            .expect("The player is in a room that should not exist!")
            .objects
            .borrow_mut();

        if room_objects.contains(&object) {
            player.inventory.insert(object);
            room_objects.remove(&object);
            println!("Taken");
        }
    } else {
        println!("You can't see anything like that here")
    }
}

/// Removes an object from the player's inventory and leaves it lying on the current room's floor
fn drop(player: &mut Player, dungeon: &mut Dungeon, args: &[&str]) {
    if args.is_empty() {
        println!("To drop something: drop OBJECT|all")
    } else if player.inventory.is_empty() {
        println!("You are not carrying anything")
    } else if args[0] == "all" {
        let room_objects = dungeon
            .rooms
            .get_mut(&player.location)
            .expect("The player is in a room that should not exist!")
            .objects
            .borrow_mut();

        room_objects.extend(player.inventory.iter());
        player.inventory.clear();

        println!("All items dropped");
    } else if let Some(object) = Object::from_string(args[0]) {
        let room_objects = dungeon
            .rooms
            .get_mut(&player.location)
            .expect("The player is in a room that should not exist!")
            .objects
            .borrow_mut();

        if player.inventory.contains(&object) {
            player.inventory.remove(&object);
            room_objects.insert(object);
            println!("Dropped");
        }
    } else {
        println!("You don't have anything like that")
    }
}

/// Prints the list of object currently carries by the player
fn inventory(player: &Player) {
    if player.inventory.is_empty() {
        println!("You are not carrying anything")
    } else {
        println!(
            "You are carrying: {}",
            player
                .inventory
                .iter()
                .map(|o| o.to_string())
                .collect::<Vec<String>>()
                .join(", ")
        );
    }
}

/// Digs a tunnel to a new room connected to the current one
#[allow(clippy::map_entry)]
fn dig(player: &Player, dungeon: &mut Dungeon, rng: &mut ThreadRng, args: &[&str]) {
    if args.is_empty() {
        println!("To dig a tunnel: dig DIRECTION");
    } else if let Some(direction) = Direction::from_string(args[0]) {
        if let Some(equipped) = player.equipped {
            if equipped == Object::Sledge {
                let target_location = player.location + direction.to_location();

                if dungeon.rooms.contains_key(&target_location) {
                    println!("There is already an exit, there!");
                }

                dungeon.rooms.entry(target_location).or_insert_with(|| {
                    println!("There is now an exit {}ward", direction);

                    Room::new().with_random_objects(rng)
                });
            } else {
                println!("You cannot dig with {}", equipped);
            }
        } else {
            println!("With your bare hands?");
        }
    } else {
        println!("That is not a direction I recognize");
    }
}

/// Moves the player to an adjacent room
fn goto(player: &mut Player, dungeon: &Dungeon, direction: Direction) {
    if direction == Direction::North
        && !dungeon.rooms[&player.location]
            .objects
            .contains(&Object::Ladder)
    {
        println!("You can't go upwards without a ladder!");
    } else {
        let target_location = player.location + direction.to_location();
        if !dungeon.rooms.contains_key(&target_location) {
            println!("There's no exit in that direction!");
        } else {
            player.location = target_location;
            look(player, dungeon);
        }
    }
}

/// Equips an object
fn equip(player: &mut Player, args: &[&str]) {
    if args.is_empty() {
        println!("To equip something: equip OBJECT");
    } else if let Some(object) = Object::from_string(args[0]) {
        if player.inventory.contains(&object) {
            player.equipped = Some(object);
            println!("Item equipped");
        } else {
            println!("You don't have such object");
        }
    } else {
        println!("You don't have such object");
    }
}

/// Unequips an object
fn unequip(player: &mut Player) {
    if player.equipped.is_some() {
        player.equipped = None;
        println!("Unequipped");
    } else {
        println!("You are already not using anything");
    }
}

/// Main game loop
fn main() {
    let mut command_aliases = default_aliases();
    let mut dungeon = Dungeon::new();
    let mut player = Player {
        location: Location(0, 0, 0),
        inventory: HashSet::from_iter(vec![Object::Sledge]),
        equipped: None,
    };
    let mut rng = rand::thread_rng();

    // init
    println!("Grab the sledge and make your way to room 1,1,5 for a non-existant prize!\n");
    help();

    loop {
        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("Cannot read from stdin");
        let input: &str = &input.trim().to_lowercase();

        let splitted = input.split_whitespace().collect::<Vec<&str>>();

        if !splitted.is_empty() {
            match find_command(splitted[0], &command_aliases) {
                Some(Command::Help) => help(),
                Some(Command::Alias) => alias(&mut command_aliases, &splitted[1..]),
                Some(Command::Look) => look(&player, &dungeon),
                Some(Command::Take) => take(&mut player, &mut dungeon, &splitted[1..]),
                Some(Command::Drop) => drop(&mut player, &mut dungeon, &splitted[1..]),
                Some(Command::Inventory) => inventory(&player),
                Some(Command::Dig) => dig(&player, &mut dungeon, &mut rng, &splitted[1..]),
                Some(Command::Equip) => equip(&mut player, &splitted[1..]),
                Some(Command::Unequip) => unequip(&mut player),
                Some(Command::North) => goto(&mut player, &dungeon, Direction::North),
                Some(Command::South) => goto(&mut player, &dungeon, Direction::South),
                Some(Command::West) => goto(&mut player, &dungeon, Direction::West),
                Some(Command::East) => goto(&mut player, &dungeon, Direction::East),
                Some(Command::Down) => goto(&mut player, &dungeon, Direction::Down),
                Some(Command::Up) => goto(&mut player, &dungeon, Direction::Up),
                _ => println!("I don't know what you mean."),
            }
        }
    }
}