Hunt The Wumpus/Rust: Difference between revisions

From Rosetta Code
Content added Content deleted
(Created page with "{{collection|Hunt_The_Wumpus}} Rust version of Hunt The Wumpus . ==Code== <lang rust> use std::cell::{Cell, RefCell}; use std::io; use std::...")
 
m (Fixed syntax highlighting.)
 
(One intermediate revision by one other user not shown)
Line 2: Line 2:
[[Rust]] version of [[:Category:Hunt_The_Wumpus|Hunt The Wumpus]] .
[[Rust]] version of [[:Category:Hunt_The_Wumpus|Hunt The Wumpus]] .


==Code==
===Code===
<lang rust>
<syntaxhighlight lang="rust">
//! *Hunt the Wumpus* reimplementation in Rust.
use std::cell::{Cell, RefCell};

use rand;
use rand::prelude::SliceRandom;
use std::io;
use std::io;
use std::io::Write;
use std::io::Write;
use std::process::exit;
use std::process::exit;
use std::rc::Rc;
use std::ops::{DerefMut, Deref};
use rand::prelude::ThreadRng;
use rand::prelude::*;


///////////////
// CONSTANTS //
///////////////


/// Help message.
const HELP: &str = "\
const HELP: &str = "\
Welcome to \"Hunt the Wumpus\"
Welcome to \"Hunt the Wumpus\"

The wumpus lives in a cave of 20 rooms. Each room has 3 tunnels to
The wumpus lives in a cave of 20 rooms. Each room has 3
other rooms. (Look at a dodecahedron to see how this works. If you
tunnels to other rooms. (The tunnels form a dodecahedron:
dont know what a dodecahedron is, ask someone.)
http://en.wikipedia.org/wiki/dodecahedron)

Hazards:
Hazards:

Bottomless pits - Two rooms have bottomless pits in them. If you go
Bottomless pits: Two rooms have bottomless pits in them. If you go
there, you fall into the pit (& lose)!
there, you fall into the pit (& lose)!

Super bats - Two other rooms have super bats. If you go there, a
Super bats: Two other rooms have super bats. If you go
bat grabs you and takes you to some other room at random (which
there, a bat grabs you and takes you to some other room
may be troublesome).
at random (which may be troublesome).

Wumpus:
Wumpus:

The wumpus is not bothered by hazards. (He has sucker feet and is
The wumpus is not bothered by hazards. (He has sucker
too big for a bat to lift.) Usually he is asleep. Two things
feet and is too big for a bat to lift.) Usually he is
wake him up: your shooting an arrow, or your entering his room.
asleep. Two things wake him up: your shooting an arrow,
If the wumpus wakes, he moves one room or stays still.
or your entering his room. If the wumpus wakes, he moves
After that, if he is where you are, he eats you up and you lose!
one room or stays still. After that, if he is where you
are, he eats you up and you lose!

You:
You:

Each turn you may move or shoot a crooked arrow.
Each turn you may move or shoot a crooked arrow.

Moving: You can move one room (through one tunnel).
Arrows: You have 5 arrows. You lose when you run out.
Moving: You can move one room (through one tunnel).

You can only shoot to nearby rooms.
If the arrow hits the wumpus, you win.
Arrows: You have 5 arrows. You lose when you run out.
You can only shoot to nearby rooms. If the arrow hits
the wumpus, you win.

Warnings:
Warnings:

When you are one room away from a wumpus or hazard, the computer
When you are one room away from a wumpus or hazard, the
says:
computer says:

Wumpus: \"You smell something terrible nearby.\"
Wumpus: \"You smell something terrible nearby.\"
Bat : \"You hear a rustling.\"
Bat: \"You hear a rustling.\"
Pit : \"You feel a cold wind blowing from a nearby cavern.\"
Pit: \"You feel a cold wind blowing from a nearby cavern.\"
";
";


/// The maze is an dodecahedron.
const MAZE_ROOMS: usize = 20;
const MAZE_ROOMS: usize = 20;
const ROOM_NEIGHBOURS: usize = 3;
const ROOM_NEIGHBORS: usize = 3;


/// Number of bats.
const BATS: usize = 2;
const BATS: usize = 2;
/// Number of pits.
const PITS: usize = 2;
const PITS: usize = 2;
/// Initial number of arrows.
const ARROWS: usize = 5;
const ARROWS: usize = 5;


/// Fractional chance of waking the Wumpus on entry to its room.
const WAKE_WUMPUS_PROB: f32 = 0.75;
const WAKE_WUMPUS_PROB: f32 = 0.75;


type RoomNum = usize;
////////////
// PLAYER //
////////////


/// Description of the current player state.
#[derive(Debug)]
struct Player {
struct Player {
/// Player location.
room: usize,
room: RoomNum,
/// Remaining number of arrows.
arrows: usize,
arrows: usize,
}
}


impl Player {
impl Player {
/// Make a new player starting in the given room.
fn new(room: usize) -> Self {
fn new(room: RoomNum) -> Self {
Player {
Player {
arrows: ARROWS,
arrows: ARROWS,
Line 77: Line 92:
}
}


/// Dangerous things that can be in a room.
//////////
#[derive(PartialEq)]
// ROOM //
//////////

#[derive(Debug, PartialEq)]
enum Danger {
enum Danger {
Wumpus,
Wumpus,
Line 88: Line 100:
}
}


/// Room description.
#[derive(Default, Debug)]
#[derive(Default)]
struct Room {
struct Room {
id: usize,
id: RoomNum,
/// The indices of neighboring rooms.
neighbours: [Cell<Option<usize>>; ROOM_NEIGHBOURS],
neighbours: [Option<RoomNum>; ROOM_NEIGHBORS],
/// Possible danger in the room.
dangers: Vec<Danger>,
dangers: Vec<Danger>,
}
}


impl Room {
impl Room {
fn new(id: usize) -> Self {
fn new(id: RoomNum) -> Self {
let default_room = Room::default();
let default_room = Room::default();


Room {
Room { id, ..default_room }
id,
..default_room
}
}

fn missing_neighbours(&self) -> usize {
self.neighbours.iter().filter(|n| n.get().is_none()).count()
}
}


fn neighbour_ids(&self) -> Vec<usize> {
fn neighbour_ids(&self) -> Vec<RoomNum> {
self.neighbours.iter()
self.neighbours.iter().cloned().filter_map(|n| n).collect()
.filter(|n| n.get().is_some())
.map(|n| n.get().unwrap())
.collect()
}
}
}
}


//////////
/// The Maze,
// MAZE //
//////////

#[derive(Debug)]
struct Maze {
struct Maze {
/// Room list.
rooms: Vec<Room>,
rooms: Vec<Room>,
rng: Rc<RefCell<ThreadRng>>,
}
}


impl Maze {
impl Maze {
// List of adjacencies used to wire up the dodecahedron.
// Builds a vector of interconnected rooms by looping each room and finding three other rooms
// https://stackoverflow.com/a/44096541/364875
// to link to it
const ADJS: [[usize; 3]; 20] = [
fn new(rng: Rc<RefCell<ThreadRng>>) -> Self {
let rooms: Vec<Room> = (0..MAZE_ROOMS)
[1, 4, 7],
[0, 2, 9],
.map(|idx| Room::new(idx as usize))
[1, 3, 11],
[2, 4, 13],
[0, 3, 5],
[4, 6, 14],
[5, 7, 16],
[0, 6, 8],
[7, 9, 17],
[1, 8, 10],
[9, 11, 18],
[2, 10, 12],
[11, 13, 19],
[3, 12, 14],
[5, 13, 15],
[14, 16, 19],
[6, 15, 17],
[8, 16, 18],
[10, 17, 19],
[12, 15, 18],
];

// Builds a vector of rooms comprising a dodecahedron.
fn new() -> Self {
let mut rooms: Vec<Room> = (0..MAZE_ROOMS)
.map(|idx| Room::new(idx as RoomNum))
.collect();
.collect();


for (i, room) in rooms.iter_mut().enumerate() {
// for each room, see how many missing neighbours there are and them
for room in rooms.iter() {
for (j, nb) in room.neighbours.iter_mut().enumerate() {
for idx in 0..room.neighbours.len() {
*nb = Some(Maze::ADJS[i][j]);
if room.neighbours[idx].get().is_none() {
// a suitable neighbour room is a room that is not the current one, not yet
// linked to the current one and that still has available neighbours
let neighbour = rooms.iter()
.find(|n| true &&
n.id != room.id
&& n.missing_neighbours() != 0
&& !room.neighbour_ids().contains(&n.id)
)
.expect("Cannot find a suitable neighbour");

room.neighbours[idx].set(Some(neighbour.id));

// link back the current room to the neighbour so that Room #0 has a link to
// Room #1 and Room #1 has a link to Room #0
for i in 0..ROOM_NEIGHBOURS {
if neighbour.neighbours[i].get().is_none() {
neighbour.neighbours[i].set(Some(room.id));
break;
}
}
}
}
}
}
}


let mut maze = Maze {
let mut maze = Maze { rooms };
rooms,
rng,
};


// place the wumpus, pits and bats in empty rooms
// place the wumpus, pits and bats in empty rooms
Line 185: Line 185:
}
}


/// Return a randomly-selected empty room.
fn rnd_empty_room(&mut self) -> usize {
fn rnd_empty_room(&mut self) -> RoomNum {
let empty_rooms: Vec<_> = self.rooms.iter()
.filter(|n| n.dangers.is_empty())
let empty_rooms: Vec<_> = self.rooms.iter().filter(|n| n.dangers.is_empty()).collect();
.collect();


empty_rooms
empty_rooms.choose(&mut rand::thread_rng()).unwrap().id
.choose(RefCell::borrow_mut(&self.rng).deref_mut())
.unwrap()
.id
}
}


/// Retrun the id of a random empty neighbour if any
fn rnd_empty_neighbour(&mut self, room: usize) -> Option<usize> {
fn rnd_empty_neighbour(&mut self, room: RoomNum) -> Option<RoomNum> {
let neighbour_ids = self.rooms[room].neighbour_ids();
let neighbour_ids = self.rooms[room].neighbour_ids();


let empty_neighbours: Vec<_> = neighbour_ids.iter()
let empty_neighbours: Vec<_> = neighbour_ids
.iter()
.filter(|&n| self.rooms[*n].dangers.is_empty())
.filter(|&n| self.rooms[*n].dangers.is_empty())
.collect();
.collect();
Line 207: Line 205:
}
}


let empty_neighbour = empty_neighbours
let empty_neighbour = empty_neighbours.choose(&mut rand::thread_rng()).unwrap();
.choose(RefCell::borrow_mut(&self.rng).deref_mut())
.unwrap();


Some(**empty_neighbour)
Some(**empty_neighbour)
}
}


fn describe_room(&self, room: usize) -> String {
/// Current room description string.
fn describe_room(&self, room: RoomNum) -> String {
let mut description = format!("You are in room #{}", room);
let mut description = format!("You are in room #{}", room);


Line 227: Line 224:
}
}


description.push_str(&format!("\nExits go to: {}",
description.push_str(&format!(
"\nExits go to: {}",
self.rooms[room].neighbours
.iter()
self.rooms[room]
.map(|n| n.get().unwrap().to_string())
.neighbours
.collect::<Vec<String>>()
.iter()
.join(", ")));
.map(|n| n.unwrap().to_string())
.collect::<Vec<String>>()
.join(", ")
));


description
description
}
}


/// Adjacent room contains a non-wumpus danger.
fn is_danger_nearby(&self, room: usize, danger: Danger) -> bool {
fn is_danger_nearby(&self, room: RoomNum, danger: Danger) -> bool {
self.rooms[room].neighbours.iter().find(|n| {
self.rooms[n.get().unwrap()]
self.rooms[room]
.dangers.contains(&danger)
.neighbours
}).is_some()
.iter()
.any(|n| self.rooms[n.unwrap()].dangers.contains(&danger))
}
}


/// Index of neighboring room given by user `destination`, else an error message.
fn parse_room(&self, destination: &str, current_room: usize) -> Result<usize, ()> {
fn parse_room(&self, destination: &str, current_room: RoomNum) -> Result<RoomNum, ()> {
let destination: Result<usize, _> = destination.parse();
let destination: Result<RoomNum, _> = destination.parse();


// check that the given destination is both a number an the number of a linked room
// check that the given destination is both a number an the number of a linked room
Line 258: Line 260:
}
}


/// Current game state.
///////////////
// MAIN LOOP //
///////////////

enum Status {
enum Status {
Normal,
Normal,
Line 270: Line 269:


fn main() {
fn main() {
let rng = Rc::new(RefCell::new(rand::thread_rng()));
let mut maze = Maze::new();
let mut maze = Maze::new(rng.clone());
let mut player = Player::new(maze.rnd_empty_room());
let mut player = Player::new(maze.rnd_empty_room());
let mut status = Status::Normal;
let mut status = Status::Normal;
Line 291: Line 289:
loop {
loop {
let mut input = String::new();
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Cannot read from 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();


match status {
match status {
Status::Quitting => {
Status::Quitting => match input {
match input {
"y" => {
"y" => {
println!("Goodbye, braveheart!");
println!("Goodbye, braveheart!");
exit(0);
exit(0);
}
"n" => {
println!("Good. the Wumpus is looking for you!");
status = Status::Normal;
}
_ => println!("That doesn't make any sense")
}
}
}
"n" => {
println!("Good. the Wumpus is looking for you!");
status = Status::Normal;
}
_ => println!("That doesn't make any sense"),
},
Status::Moving => {
Status::Moving => {
if let Ok(room) = maze.parse_room(input, player.room) {
if let Ok(room) = maze.parse_room(input, player.room) {
Line 326: Line 324:
describe(&maze, &player);
describe(&maze, &player);
} else {
} else {
println!("There are no tunnels from here to that room. Where do you wanto do go?");
println!(
"There are no tunnels from here to that room. Where do you wanto do go?"
);
}
}
}
}
Line 336: Line 336:
} else {
} else {
// 75% chances of waking up the wumpus that would go into another room
// 75% chances of waking up the wumpus that would go into another room
if RefCell::borrow_mut(rng.deref()).gen::<f32>() < WAKE_WUMPUS_PROB {
if rand::random::<f32>() < WAKE_WUMPUS_PROB {
let wumpus_room = maze.rooms.iter()
let wumpus_room = maze
.rooms
.iter()
.find(|r| r.dangers.contains(&Danger::Wumpus))
.find(|r| r.dangers.contains(&Danger::Wumpus))
.unwrap()
.unwrap()
Line 348: Line 350:
}
}


maze.rooms[wumpus_room].dangers.retain(|d| d != &Danger::Wumpus);
maze.rooms[wumpus_room]
.dangers
.retain(|d| d != &Danger::Wumpus);
maze.rooms[new_wumpus_room].dangers.push(Danger::Wumpus);
maze.rooms[new_wumpus_room].dangers.push(Danger::Wumpus);
println!("You heard a rumbling in a nearby cavern.");
println!("You heard a rumbling in a nearby cavern.");
Line 355: Line 359:


player.arrows -= 1;
player.arrows -= 1;
if player.arrows == 0 {
if player.arrows == 0 {
println!("You ran out of arrows.\nGAME OVER");
println!("You ran out of arrows.\nGAME OVER");
exit(1);
exit(1);
Line 363: Line 367:
}
}
} else {
} else {
println!("There are no tunnels from here to that room. Where do you wanto do shoot?");
println!(
"There are no tunnels from here to that room. Where do you wanto do shoot?"
);
}
}
}
}
_ => {
_ => match input {
match input {
"h" => println!("{}", HELP),
"h" => println!("{}", HELP),
"q" => {
"q" => {
println!("Are you so easily scared? [y/n]");
println!("Are you so easily scared? [y/n]");
status = Status::Quitting;
status = Status::Quitting;
}
"m" => {
println!("Where?");
status = Status::Moving;
}
"s" => {
println!("Where?");
status = Status::Shooting;
}
_ => println!("That doesn't make any sense")
}
}
}
"m" => {
println!("Where?");
status = Status::Moving;
}
"s" => {
println!("Where?");
status = Status::Shooting;
}
_ => println!("That doesn't make any sense"),
},
}
}


Line 389: Line 393:
}
}
}
}

</lang>
#[test]
fn test_maze_connected() {
use std::collections::HashSet;
let maze = Maze::new();
let n = maze.rooms.len();

fn exists_path(i: RoomNum, j: RoomNum, vis: &mut HashSet<RoomNum>, maze: &Maze) -> bool {
if i == j {
return true;
}
vis.insert(i);
maze.rooms[i].neighbours.iter().any(|neighbour| {
// Check that all rooms have three neighbors.
let k = neighbour.unwrap();
!vis.contains(&k) && exists_path(k, j, vis, maze)
})
}
for i in 0..n {
for j in 0..n {
assert!(exists_path(i, j, &mut HashSet::new(), &maze));
}
}
}</syntaxhighlight>

Latest revision as of 22:32, 30 August 2022

Hunt The Wumpus/Rust is part of Hunt_The_Wumpus. You may find other members of Hunt_The_Wumpus at Category:Hunt_The_Wumpus.

Rust version of Hunt The Wumpus .

Code

//! *Hunt the Wumpus* reimplementation in Rust.

use rand;
use rand::prelude::SliceRandom;
use std::io;
use std::io::Write;
use std::process::exit;

/// Help message.
const HELP: &str = "\
Welcome to \"Hunt the Wumpus\"

The wumpus lives in a cave of 20 rooms. Each room has 3
tunnels to other rooms. (The tunnels form a dodecahedron:
http://en.wikipedia.org/wiki/dodecahedron)

Hazards:

 Bottomless pits: Two rooms have bottomless pits in them. If you go
   there, you fall into the pit (& lose)!

 Super bats: Two other rooms have super bats. If you go
   there, a bat grabs you and takes you to some other room
   at random (which may be troublesome).

Wumpus:

   The wumpus is not bothered by hazards. (He has sucker
   feet and is too big for a bat to lift.)  Usually he is
   asleep. Two things wake him up: your shooting an arrow,
   or your entering his room.  If the wumpus wakes, he moves
   one room or stays still.  After that, if he is where you
   are, he eats you up and you lose!

You:

   Each turn you may move or shoot a crooked arrow.

   Moving: You can move one room (through one tunnel).

   Arrows: You have 5 arrows. You lose when you run out.
      You can only shoot to nearby rooms. If the arrow hits
      the wumpus, you win.

Warnings:

   When you are one room away from a wumpus or hazard, the
   computer says:

   Wumpus:  \"You smell something terrible nearby.\"
   Bat:  \"You hear a rustling.\"
   Pit:  \"You feel a cold wind blowing from a nearby cavern.\"
";

/// The maze is an dodecahedron.
const MAZE_ROOMS: usize = 20;
const ROOM_NEIGHBORS: usize = 3;

/// Number of bats.
const BATS: usize = 2;
/// Number of pits.
const PITS: usize = 2;
/// Initial number of arrows.
const ARROWS: usize = 5;

/// Fractional chance of waking the Wumpus on entry to its room.
const WAKE_WUMPUS_PROB: f32 = 0.75;

type RoomNum = usize;

/// Description of the current player state.
struct Player {
    /// Player location.
    room: RoomNum,
    /// Remaining number of arrows.
    arrows: usize,
}

impl Player {
    /// Make a new player starting in the given room.
    fn new(room: RoomNum) -> Self {
        Player {
            arrows: ARROWS,
            room,
        }
    }
}

/// Dangerous things that can be in a room.
#[derive(PartialEq)]
enum Danger {
    Wumpus,
    Bat,
    Pit,
}

/// Room description.
#[derive(Default)]
struct Room {
    id: RoomNum,
    /// The indices of neighboring rooms.
    neighbours: [Option<RoomNum>; ROOM_NEIGHBORS],
    /// Possible danger in the room.
    dangers: Vec<Danger>,
}

impl Room {
    fn new(id: RoomNum) -> Self {
        let default_room = Room::default();

        Room { id, ..default_room }
    }

    fn neighbour_ids(&self) -> Vec<RoomNum> {
        self.neighbours.iter().cloned().filter_map(|n| n).collect()
    }
}

/// The Maze,
struct Maze {
    /// Room list.
    rooms: Vec<Room>,
}

impl Maze {
    // List of adjacencies used to wire up the dodecahedron.
    // https://stackoverflow.com/a/44096541/364875
    const ADJS: [[usize; 3]; 20] = [
        [1, 4, 7],
        [0, 2, 9],
        [1, 3, 11],
        [2, 4, 13],
        [0, 3, 5],
        [4, 6, 14],
        [5, 7, 16],
        [0, 6, 8],
        [7, 9, 17],
        [1, 8, 10],
        [9, 11, 18],
        [2, 10, 12],
        [11, 13, 19],
        [3, 12, 14],
        [5, 13, 15],
        [14, 16, 19],
        [6, 15, 17],
        [8, 16, 18],
        [10, 17, 19],
        [12, 15, 18],
    ];

    // Builds a vector of rooms comprising a dodecahedron.
    fn new() -> Self {
        let mut rooms: Vec<Room> = (0..MAZE_ROOMS)
            .map(|idx| Room::new(idx as RoomNum))
            .collect();

        for (i, room) in rooms.iter_mut().enumerate() {
            for (j, nb) in room.neighbours.iter_mut().enumerate() {
                *nb = Some(Maze::ADJS[i][j]);
            }
        }

        let mut maze = Maze { rooms };

        // place the wumpus, pits and bats in empty rooms
        let empty_room = maze.rnd_empty_room();
        maze.rooms[empty_room].dangers.push(Danger::Wumpus);

        for _ in 0..PITS {
            let empty_room = maze.rnd_empty_room();
            maze.rooms[empty_room].dangers.push(Danger::Pit);
        }

        for _ in 0..BATS {
            let empty_room = maze.rnd_empty_room();
            maze.rooms[empty_room].dangers.push(Danger::Bat);
        }

        maze
    }

    /// Return a randomly-selected empty room.
    fn rnd_empty_room(&mut self) -> RoomNum {
        let empty_rooms: Vec<_> = self.rooms.iter().filter(|n| n.dangers.is_empty()).collect();

        empty_rooms.choose(&mut rand::thread_rng()).unwrap().id
    }

    /// Retrun the id of a random empty neighbour if any
    fn rnd_empty_neighbour(&mut self, room: RoomNum) -> Option<RoomNum> {
        let neighbour_ids = self.rooms[room].neighbour_ids();

        let empty_neighbours: Vec<_> = neighbour_ids
            .iter()
            .filter(|&n| self.rooms[*n].dangers.is_empty())
            .collect();

        if empty_neighbours.is_empty() {
            return None;
        }

        let empty_neighbour = empty_neighbours.choose(&mut rand::thread_rng()).unwrap();

        Some(**empty_neighbour)
    }

    /// Current room description string.
    fn describe_room(&self, room: RoomNum) -> String {
        let mut description = format!("You are in room #{}", room);

        if self.is_danger_nearby(room, Danger::Pit) {
            description.push_str("\nYou feel a cold wind blowing from a nearby cavern.");
        }
        if self.is_danger_nearby(room, Danger::Bat) {
            description.push_str("\nYou hear a rustling.");
        }
        if self.is_danger_nearby(room, Danger::Wumpus) {
            description.push_str("\nYou smell something terrible nearby.");
        }

        description.push_str(&format!(
            "\nExits go to: {}",
            self.rooms[room]
                .neighbours
                .iter()
                .map(|n| n.unwrap().to_string())
                .collect::<Vec<String>>()
                .join(", ")
        ));

        description
    }

    /// Adjacent room contains a non-wumpus danger.
    fn is_danger_nearby(&self, room: RoomNum, danger: Danger) -> bool {
        self.rooms[room]
            .neighbours
            .iter()
            .any(|n| self.rooms[n.unwrap()].dangers.contains(&danger))
    }

    /// Index of neighboring room given by user `destination`, else an error message.
    fn parse_room(&self, destination: &str, current_room: RoomNum) -> Result<RoomNum, ()> {
        let destination: Result<RoomNum, _> = destination.parse();

        // check that the given destination is both a number an the number of a linked room
        if let Ok(room) = destination {
            if self.rooms[current_room].neighbour_ids().contains(&room) {
                return Ok(room);
            }
        }

        Err(())
    }
}

/// Current game state.
enum Status {
    Normal,
    Quitting,
    Moving,
    Shooting,
}

fn main() {
    let mut maze = Maze::new();
    let mut player = Player::new(maze.rnd_empty_room());
    let mut status = Status::Normal;

    let describe = |maze: &Maze, player: &Player| {
        println!("{}", maze.describe_room(player.room));
        println!("What do you want to do? (m)ove or (s)hoot?");
    };

    let prompt = || {
        print!("> ");
        io::stdout().flush().expect("Error flushing");
    };

    describe(&maze, &player);
    prompt();

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

        match status {
            Status::Quitting => match input {
                "y" => {
                    println!("Goodbye, braveheart!");
                    exit(0);
                }
                "n" => {
                    println!("Good. the Wumpus is looking for you!");
                    status = Status::Normal;
                }
                _ => println!("That doesn't make any sense"),
            },
            Status::Moving => {
                if let Ok(room) = maze.parse_room(input, player.room) {
                    if maze.rooms[room].dangers.contains(&Danger::Wumpus) {
                        println!("The wumpus ate you up!\nGAME OVER");
                        exit(0);
                    } else if maze.rooms[room].dangers.contains(&Danger::Pit) {
                        println!("You fall into a bottomless pit!\nGAME OVER");
                        exit(0);
                    } else if maze.rooms[room].dangers.contains(&Danger::Bat) {
                        println!("The bats whisk you away!");
                        player.room = maze.rnd_empty_room();
                    } else {
                        player.room = room;
                    }

                    status = Status::Normal;
                    describe(&maze, &player);
                } else {
                    println!(
                        "There are no tunnels from here to that room. Where do you wanto do go?"
                    );
                }
            }
            Status::Shooting => {
                if let Ok(room) = maze.parse_room(input, player.room) {
                    if maze.rooms[room].dangers.contains(&Danger::Wumpus) {
                        println!("YOU KILLED THE WUMPUS! GOOD JOB, BUDDY!!!");
                        exit(0);
                    } else {
                        // 75% chances of waking up the wumpus that would go into another room
                        if rand::random::<f32>() < WAKE_WUMPUS_PROB {
                            let wumpus_room = maze
                                .rooms
                                .iter()
                                .find(|r| r.dangers.contains(&Danger::Wumpus))
                                .unwrap()
                                .id;

                            if let Some(new_wumpus_room) = maze.rnd_empty_neighbour(wumpus_room) {
                                if new_wumpus_room == player.room {
                                    println!("You woke up the wumpus and he ate you!\nGAME OVER");
                                    exit(1);
                                }

                                maze.rooms[wumpus_room]
                                    .dangers
                                    .retain(|d| d != &Danger::Wumpus);
                                maze.rooms[new_wumpus_room].dangers.push(Danger::Wumpus);
                                println!("You heard a rumbling in a nearby cavern.");
                            }
                        }

                        player.arrows -= 1;
                        if player.arrows == 0 {
                            println!("You ran out of arrows.\nGAME OVER");
                            exit(1);
                        }

                        status = Status::Normal;
                    }
                } else {
                    println!(
                        "There are no tunnels from here to that room. Where do you wanto do shoot?"
                    );
                }
            }
            _ => match input {
                "h" => println!("{}", HELP),
                "q" => {
                    println!("Are you so easily scared? [y/n]");
                    status = Status::Quitting;
                }
                "m" => {
                    println!("Where?");
                    status = Status::Moving;
                }
                "s" => {
                    println!("Where?");
                    status = Status::Shooting;
                }
                _ => println!("That doesn't make any sense"),
            },
        }

        prompt();
    }
}

#[test]
fn test_maze_connected() {
    use std::collections::HashSet;
    let maze = Maze::new();
    let n = maze.rooms.len();

    fn exists_path(i: RoomNum, j: RoomNum, vis: &mut HashSet<RoomNum>, maze: &Maze) -> bool {
        if i == j {
            return true;
        }
        vis.insert(i);
        maze.rooms[i].neighbours.iter().any(|neighbour| {
            // Check that all rooms have three neighbors.
            let k = neighbour.unwrap();
            !vis.contains(&k) && exists_path(k, j, vis, maze)
        })
    }
    for i in 0..n {
        for j in 0..n {
            assert!(exists_path(i, j, &mut HashSet::new(), &maze));
        }
    }
}