RCRPG/Rust: Difference between revisions
Content added Content deleted
Pistacchio (talk | contribs) (Created page with "{{collection|RCRPG}} Rust version of RCRPG. ==Code== <lang rust> use std::collections::{HashMap, HashSet}; use std::iter::FromIterator; use std::{io,...") |
Pistacchio (talk | contribs) (Refactoring) |
||
Line 4: | Line 4: | ||
==Code== |
==Code== |
||
<lang rust> |
<lang rust> |
||
//! 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::collections::{HashMap, HashSet}; |
||
use std::fmt::{Debug, Display}; |
|||
use std::iter::FromIterator; |
use std::iter::FromIterator; |
||
use std::{io, fmt}; |
|||
use std::fmt::{Display, Debug}; |
|||
use std::borrow::BorrowMut; |
|||
use std::ops::Add; |
use std::ops::Add; |
||
use |
use std::{fmt, io}; |
||
/////////////// |
|||
// CONSTANTS // |
|||
/////////////// |
|||
/// Maps each Locations to a direction |
|||
const DIRECTION_MAPPING: [(Location, Direction); 6] = [ |
const DIRECTION_MAPPING: [(Location, Direction); 6] = [ |
||
(Location(0, -1, 0), Direction::North), |
(Location(0, -1, 0), Direction::North), |
||
Line 25: | Line 25: | ||
]; |
]; |
||
/// Objects possessed by the player |
|||
/////////// |
|||
type Inventory = HashSet<Object>; |
|||
// TYPES // |
|||
/// 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 Invetory = HashSet<Object>; |
|||
type CommandAliases = Vec<(HashSet<String>, Command)>; |
type CommandAliases = Vec<(HashSet<String>, Command)>; |
||
/// 3D coordinates of objects in the dungeon |
|||
////////////// |
|||
// LOCATION // |
|||
////////////// |
|||
#[derive(Hash, Eq, PartialEq, Copy, Clone)] |
#[derive(Hash, Eq, PartialEq, Copy, Clone)] |
||
struct Location(i32, i32, i32); |
struct Location(i32, i32, i32); |
||
Line 53: | Line 49: | ||
} |
} |
||
/// Objects that can be found in the dungon rooms |
|||
//////////// |
|||
// OBJECT // |
|||
//////////// |
|||
#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)] |
#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)] |
||
enum Object { |
enum Object { |
||
Line 75: | Line 68: | ||
impl Object { |
impl Object { |
||
/// Tries to parse a string to an object, like `"gold"` to `Object::Gold` |
|||
fn from_string(s: &str) -> Option<Object> { |
fn from_string(s: &str) -> Option<Object> { |
||
match s { |
match s { |
||
Line 80: | Line 74: | ||
"sledge" => Some(Object::Sledge), |
"sledge" => Some(Object::Sledge), |
||
"gold" => Some(Object::Gold), |
"gold" => Some(Object::Gold), |
||
_ => None |
_ => None, |
||
} |
} |
||
} |
} |
||
} |
} |
||
/// Player information |
|||
//////////// |
|||
// PLAYER // |
|||
//////////// |
|||
struct Player { |
struct Player { |
||
/// Room where the player currently is |
|||
location: Location, |
location: Location, |
||
/// The objects carried by the player |
|||
inventory: Invetory, |
|||
inventory: Inventory, |
|||
/// The object wieled by the player, if any |
|||
equipped: Option<Object>, |
equipped: Option<Object>, |
||
} |
} |
||
/// Information about each room of the dungeon |
|||
////////// |
|||
// ROOM // |
|||
////////// |
|||
struct Room { |
struct Room { |
||
/// Fixed description for special rooms (like the first one or the prize room) |
|||
description: Option<String>, |
description: Option<String>, |
||
/// Objects currently in the room |
|||
objects: Invetory, |
|||
objects: Inventory, |
|||
} |
} |
||
Line 112: | Line 105: | ||
} |
} |
||
/// Sets the room description |
|||
fn with_description(mut self, descrition: &str) -> Self { |
|||
fn with_description(mut self, description: &str) -> Self { |
|||
self.description = Some(description.to_string()); |
|||
self |
self |
||
} |
} |
||
/// Sets the objects in the room |
|||
fn with_objects(mut self, objects: Vec<Object>) -> Self { |
fn with_objects(mut self, objects: Vec<Object>) -> Self { |
||
self.objects.extend(objects); |
self.objects.extend(objects); |
||
Line 122: | Line 117: | ||
} |
} |
||
/// Adds some randoms objects to the room |
|||
fn with_random_objects(mut self, rng: &mut ThreadRng ) -> Self { |
|||
fn with_random_objects(mut self, rng: &mut ThreadRng) -> Self { |
|||
let objects: Vec<_> = vec![ |
let objects: Vec<_> = vec![ |
||
if rng.gen::<f32>() < 0.33 { |
if rng.gen::<f32>() < 0.33 { |
||
Some(Object::Sledge) |
|||
} else { |
|||
None |
|||
].iter().filter_map(|o| *o).collect(); |
|||
}, |
|||
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.objects.extend(objects); |
||
Line 134: | Line 145: | ||
} |
} |
||
/// Cardinat directions |
|||
///////////// |
|||
// DUNGEON // |
|||
///////////// |
|||
#[derive(Copy, Clone, Eq, PartialEq)] |
#[derive(Copy, Clone, Eq, PartialEq)] |
||
enum Direction { |
enum Direction { |
||
Line 162: | Line 170: | ||
impl Direction { |
impl Direction { |
||
/// Tries to parse a string to a direction, like `"north"` to `Direction::North` |
|||
fn from_string(s: &str) -> Option<Direction> { |
fn from_string(s: &str) -> Option<Direction> { |
||
match s { |
match s { |
||
Line 170: | Line 179: | ||
"down" => Some(Direction::Down), |
"down" => Some(Direction::Down), |
||
"up" => Some(Direction::Up), |
"up" => Some(Direction::Up), |
||
_ => None |
_ => None, |
||
} |
} |
||
} |
} |
||
/// Returns the normalized 3D point of the location, for instance `Direction::North` is |
|||
fn to_location(&self) -> Location { |
|||
/// `(0, -1, 0)` where `(x, y, z)` |
|||
DIRECTION_MAPPING.iter() |
|||
fn to_location(self) -> Location { |
|||
.find(|d| d.1 == *self) |
|||
DIRECTION_MAPPING.iter().find(|d| d.1 == self).unwrap().0 |
|||
.0 |
|||
} |
} |
||
} |
} |
||
/// Collection of rooms |
|||
struct Dungeon { |
struct Dungeon { |
||
rooms |
/// The rooms that make up the dungeon |
||
rooms: HashMap<Location, Room>, |
|||
} |
} |
||
Line 190: | Line 200: | ||
Dungeon { |
Dungeon { |
||
rooms: HashMap::from_iter(vec![ |
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> { |
fn exits_for_room(&self, location: Location) -> Vec<Direction> { |
||
DIRECTION_MAPPING |
DIRECTION_MAPPING |
||
.iter() |
|||
.filter_map(|d| { |
|||
let location_to_test = location + d.0; |
|||
if self.rooms.contains_key(&location_to_test) { |
if self.rooms.contains_key(&location_to_test) { |
||
return Some(d.1); |
return Some(d.1); |
||
} |
} |
||
None |
None |
||
} |
}) |
||
.collect() |
|||
} |
} |
||
} |
} |
||
/// Collection of all the available commands to interact to the dungeon world |
|||
////////////// |
|||
// COMMANDS // |
|||
////////////// |
|||
#[derive(Debug, Copy, Clone)] |
#[derive(Debug, Copy, Clone)] |
||
enum Command { |
enum Command { |
||
Line 234: | Line 250: | ||
} |
} |
||
/// Returns the list of all the default command aliases |
|||
fn default_aliases() -> CommandAliases { |
fn default_aliases() -> CommandAliases { |
||
vec![ |
vec![ |
||
( |
|||
(vec!["n".to_string(), "north".to_string()].into_iter().collect(), Command::North), |
|||
vec!["n".to_string(), "north".to_string()] |
|||
.into_iter() |
|||
.collect(), |
|||
Command::North, |
|||
), |
|||
(vec!["u".to_string(), "up".to_string()].into_iter().collect(), Command::Up), |
|||
( |
|||
(vec!["help".to_string()].into_iter().collect(), Command::Help), |
|||
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!["dig".to_string()].into_iter().collect(), Command::Dig), |
||
( |
|||
(vec!["l".to_string(), "look".to_string()].into_iter().collect(), Command::Look), |
|||
vec!["l".to_string(), "look".to_string()] |
|||
.into_iter() |
|||
.collect(), |
|||
Command::Look, |
|||
), |
|||
(vec!["unequip".to_string()].into_iter().collect(), Command::Unequip), |
|||
( |
|||
(vec!["alias".to_string()].into_iter().collect(), Command::Alias), |
|||
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: &CommandAliases) -> Option<Command> { |
|||
fn find_command(command: &str, aliases: &[(HashSet<String>, Command)]) -> Option<Command> { |
|||
let command = command.to_lowercase(); |
let command = command.to_lowercase(); |
||
aliases.iter() |
aliases.iter().find(|a| a.0.contains(&command)).map(|a| a.1) |
||
.find(|a| a.0.contains(&command)) |
|||
.map(|a| a.1) |
|||
} |
} |
||
/// Prints the help string |
|||
fn help() { |
fn help() { |
||
println!( |
|||
println!("You need a sledge to dig rooms and ladders to go upwards. |
|||
"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. |
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'. |
Additionally you can tag rooms with the 'name' command and alias commands with 'alias'. |
||
Have fun!" |
Have fun!" |
||
) |
|||
} |
} |
||
/// Defines a new alias for a command |
|||
fn alias(command_aliases: &mut CommandAliases, args: &[&str]) { |
fn alias(command_aliases: &mut CommandAliases, args: &[&str]) { |
||
if args.len() < 2 { |
if args.len() < 2 { |
||
Line 292: | Line 370: | ||
} |
} |
||
/// Describes the current rooom |
|||
fn look(player: &Player, dungeon: &Dungeon) { |
fn look(player: &Player, dungeon: &Dungeon) { |
||
let room = &dungeon.rooms[&player.location]; |
let room = &dungeon.rooms[&player.location]; |
||
Line 300: | Line 379: | ||
print!("Room at {:?}.", player.location); |
print!("Room at {:?}.", player.location); |
||
} |
} |
||
if !room.objects.is_empty() { |
if !room.objects.is_empty() { |
||
print!( |
print!( |
||
. |
" On the floor you can see: {}.", |
||
. |
room.objects |
||
. |
.iter() |
||
. |
.map(|o| o.to_string()) |
||
.collect::<Vec<String>>() |
|||
.join(", ") |
|||
); |
|||
} |
} |
||
Line 314: | Line 395: | ||
0 => println!(" There are no exits in this room."), |
0 => println!(" There are no exits in this room."), |
||
1 => println!(" There is one exit: {}.", room_exits[0].to_string()), |
1 => println!(" There is one exit: {}.", room_exits[0].to_string()), |
||
_ => println!( |
_ => 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]) { |
fn take(player: &mut Player, dungeon: &mut Dungeon, args: &[&str]) { |
||
if args.is_empty() { |
if args.is_empty() { |
||
Line 328: | Line 413: | ||
println!("There is nothing to take here") |
println!("There is nothing to take here") |
||
} else if args[0] == "all" { |
} else if args[0] == "all" { |
||
let room_objects = dungeon |
let room_objects = dungeon |
||
.rooms |
|||
.get_mut(&player.location) |
|||
.expect("The player is in a room that should not exist!") |
.expect("The player is in a room that should not exist!") |
||
.objects |
.objects |
||
Line 338: | Line 425: | ||
println!("All items taken"); |
println!("All items taken"); |
||
} else if let Some(object) = Object::from_string(args[0]) { |
} else if let Some(object) = Object::from_string(args[0]) { |
||
let room_objects = dungeon |
let room_objects = dungeon |
||
.rooms |
|||
.get_mut(&player.location) |
|||
.expect("The player is in a room that should not exist!") |
.expect("The player is in a room that should not exist!") |
||
.objects |
.objects |
||
Line 353: | Line 442: | ||
} |
} |
||
/// 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]) { |
fn drop(player: &mut Player, dungeon: &mut Dungeon, args: &[&str]) { |
||
if args.is_empty() { |
if args.is_empty() { |
||
Line 359: | Line 449: | ||
println!("You are not carrying anything") |
println!("You are not carrying anything") |
||
} else if args[0] == "all" { |
} else if args[0] == "all" { |
||
let room_objects = dungeon |
let room_objects = dungeon |
||
.rooms |
|||
.get_mut(&player.location) |
|||
.expect("The player is in a room that should not exist!") |
.expect("The player is in a room that should not exist!") |
||
.objects |
.objects |
||
Line 369: | Line 461: | ||
println!("All items dropped"); |
println!("All items dropped"); |
||
} else if let Some(object) = Object::from_string(args[0]) { |
} else if let Some(object) = Object::from_string(args[0]) { |
||
let room_objects = dungeon |
let room_objects = dungeon |
||
.rooms |
|||
.get_mut(&player.location) |
|||
.expect("The player is in a room that should not exist!") |
.expect("The player is in a room that should not exist!") |
||
.objects |
.objects |
||
Line 384: | Line 478: | ||
} |
} |
||
/// Prints the list of object currently carries by the player |
|||
fn inventory(player: &Player) { |
fn inventory(player: &Player) { |
||
if player.inventory.is_empty() { |
if player.inventory.is_empty() { |
||
println!("You are not carrying anything") |
println!("You are not carrying anything") |
||
} else { |
} else { |
||
println!( |
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)] |
#[allow(clippy::map_entry)] |
||
fn dig(player: &Player, dungeon: &mut Dungeon, rng: &mut ThreadRng, args: &[&str]) { |
fn dig(player: &Player, dungeon: &mut Dungeon, rng: &mut ThreadRng, args: &[&str]) { |
||
Line 425: | Line 525: | ||
} |
} |
||
/// Moves the player to an adjacent room |
|||
fn goto(player: &mut Player, dungeon: &Dungeon, direction: &Direction) { |
|||
fn goto(player: &mut Player, dungeon: &Dungeon, direction: Direction) { |
|||
if direction == &Direction::North && !dungeon.rooms[&player.location].objects.contains(&Object::Ladder) { |
|||
if direction == Direction::North |
|||
&& !dungeon.rooms[&player.location] |
|||
.objects |
|||
.contains(&Object::Ladder) |
|||
{ |
|||
println!("You can't go upwards without a ladder!"); |
println!("You can't go upwards without a ladder!"); |
||
} else { |
} else { |
||
Line 439: | Line 544: | ||
} |
} |
||
/// Equips an object |
|||
fn equip(player: &mut Player, args: &[&str]) { |
fn equip(player: &mut Player, args: &[&str]) { |
||
if args.is_empty() { |
if args.is_empty() { |
||
Line 454: | Line 560: | ||
} |
} |
||
/// Unequips an object |
|||
fn unequip(player: &mut Player) { |
fn unequip(player: &mut Player) { |
||
if player.equipped.is_some() { |
if player.equipped.is_some() { |
||
Line 463: | Line 570: | ||
} |
} |
||
/// Main game loop |
|||
////////// |
|||
// MAIN // |
|||
////////// |
|||
fn main() { |
fn main() { |
||
let mut command_aliases = default_aliases(); |
let mut command_aliases = default_aliases(); |
||
Line 483: | Line 587: | ||
loop { |
loop { |
||
let mut input = String::new(); |
let mut input = String::new(); |
||
io::stdin() |
io::stdin() |
||
.read_line(&mut input) |
|||
.expect("Cannot read from stdin"); |
|||
let input: &str = &input.trim().to_lowercase(); |
let input: &str = &input.trim().to_lowercase(); |
||
Line 499: | Line 605: | ||
Some(Command::Equip) => equip(&mut player, &splitted[1..]), |
Some(Command::Equip) => equip(&mut player, &splitted[1..]), |
||
Some(Command::Unequip) => unequip(&mut player), |
Some(Command::Unequip) => unequip(&mut player), |
||
Some(Command::North) => goto(&mut player, &dungeon, |
Some(Command::North) => goto(&mut player, &dungeon, Direction::North), |
||
Some(Command::South) => goto(&mut player, &dungeon, |
Some(Command::South) => goto(&mut player, &dungeon, Direction::South), |
||
Some(Command::West) => goto(&mut player, &dungeon, |
Some(Command::West) => goto(&mut player, &dungeon, Direction::West), |
||
Some(Command::East) => goto(&mut player, &dungeon, |
Some(Command::East) => goto(&mut player, &dungeon, Direction::East), |
||
Some(Command::Down) => goto(&mut player, &dungeon, |
Some(Command::Down) => goto(&mut player, &dungeon, Direction::Down), |
||
Some(Command::Up) => goto(&mut player, &dungeon, |
Some(Command::Up) => goto(&mut player, &dungeon, Direction::Up), |
||
_ => println!("I don't know what you mean.") |
_ => println!("I don't know what you mean."), |
||
} |
} |
||
} |
} |
||
} |
} |
||
}</lang> |
|||
} |
|||
</lang> |