423 lines
10 KiB
Rust
423 lines
10 KiB
Rust
/// As a dare to myself, I won't use any error-handling crates.
|
|
|
|
#[derive (Debug)]
|
|
enum GameError {
|
|
IoError,
|
|
}
|
|
|
|
impl From <std::io::Error> for GameError {
|
|
fn from (_: std::io::Error) -> Self {
|
|
Self::IoError
|
|
}
|
|
}
|
|
|
|
type Result <T> = std::result::Result <T, GameError>;
|
|
|
|
/// Prints a string, naively wrapping at 80-character boundaries
|
|
/// This means a period might be wrapped separate from the sentence it ends
|
|
/// , and frankly I think that's part of the charm.
|
|
|
|
fn print (mut s: &str) {
|
|
const COLS: usize = 80;
|
|
|
|
while s.len () > COLS {
|
|
println! ("{}", &s [0..80]);
|
|
s = &s [80..];
|
|
}
|
|
|
|
println! ("{}", s);
|
|
}
|
|
|
|
fn read_input () -> Result <String> {
|
|
{
|
|
use std::io::Write;
|
|
let mut stdout = std::io::stdout ();
|
|
stdout.write_all (b"> ")?;
|
|
stdout.flush ()?;
|
|
}
|
|
|
|
let mut buffer = String::new ();
|
|
std::io::stdin ().read_line (&mut buffer)?;
|
|
|
|
// I don't know why I need the type annotation here, but I do.
|
|
let x: &[_] = &['\r', '\n'];
|
|
let buffer = buffer.trim_end_matches (x).to_string ();
|
|
|
|
Ok (buffer)
|
|
}
|
|
|
|
fn print_help () -> Response {
|
|
Response::PrintMany (vec! [
|
|
"All commands are ASCII and case-insensitive.",
|
|
"Commands should start with a verb like LOOK.",
|
|
"e.g. `look table`",
|
|
"Single-word verbs are better, e.g. prefer `hint` over `give me a hint`",
|
|
"When in doubt, try generic verbs like `look` or `use` over specific verbs like `actuate`, `type`, or `consolidate`.",
|
|
])
|
|
}
|
|
|
|
fn line_response <S: Into <String>> (line: S) -> Response {
|
|
Response::Print (line.into ())
|
|
}
|
|
|
|
fn undetected_item () -> Response {
|
|
line_response ("That ITEM does not exist in this ROOM, or you have not noticed it.")
|
|
}
|
|
|
|
fn just <T> (t: T) -> Vec <T> {
|
|
vec! [t]
|
|
}
|
|
|
|
#[derive (Debug, PartialEq)]
|
|
enum PlayerAction {
|
|
Quit,
|
|
Help,
|
|
Hint,
|
|
Nonsense,
|
|
Wait,
|
|
Look (ItemName),
|
|
LookAround,
|
|
Use (ItemName),
|
|
}
|
|
|
|
#[derive (Clone, Copy, Debug, PartialEq)]
|
|
enum ItemName {
|
|
Nonsense,
|
|
|
|
Door,
|
|
EmergencyExit,
|
|
Keypad,
|
|
Note,
|
|
Table,
|
|
}
|
|
|
|
fn _item_name_display (x: ItemName) -> &'static str {
|
|
match x {
|
|
ItemName::Nonsense => "NONSENSE",
|
|
|
|
ItemName::Door => "DOOR",
|
|
ItemName::EmergencyExit => "EMERGENCY EXIT",
|
|
ItemName::Keypad => "KEYPAD",
|
|
ItemName::Note => "NOTE",
|
|
ItemName::Table => "TABLE",
|
|
}
|
|
}
|
|
|
|
fn parse_input (s: &str) -> PlayerAction {
|
|
let s = s.to_lowercase ();
|
|
|
|
if s == "quit" || s == "quit game" {
|
|
return PlayerAction::Quit;
|
|
}
|
|
|
|
if s == "look" || s == "look around" {
|
|
return PlayerAction::LookAround;
|
|
}
|
|
|
|
if let Some (rest) = s.strip_prefix ("look at the ") {
|
|
return PlayerAction::Look (parse_item_name (rest));
|
|
}
|
|
|
|
if let Some (rest) = s.strip_prefix ("look at ") {
|
|
return PlayerAction::Look (parse_item_name (rest));
|
|
}
|
|
|
|
if let Some (rest) = s.strip_prefix ("look ") {
|
|
return PlayerAction::Look (parse_item_name (rest));
|
|
}
|
|
|
|
if let Some (rest) = s.strip_prefix ("examine ") {
|
|
return PlayerAction::Look (parse_item_name (rest));
|
|
}
|
|
|
|
if let Some (rest) = s.strip_prefix ("use the ") {
|
|
return PlayerAction::Use (parse_item_name (rest));
|
|
}
|
|
|
|
if let Some (rest) = s.strip_prefix ("use ") {
|
|
return PlayerAction::Use (parse_item_name (rest));
|
|
}
|
|
|
|
if s == "do nothing" {
|
|
return PlayerAction::Wait;
|
|
}
|
|
if s == "wait" {
|
|
return PlayerAction::Wait;
|
|
}
|
|
|
|
if s == "help" {
|
|
return PlayerAction::Help;
|
|
}
|
|
if s == "hint" {
|
|
return PlayerAction::Hint;
|
|
}
|
|
|
|
PlayerAction::Nonsense
|
|
}
|
|
|
|
fn parse_item_name (s: &str) -> ItemName {
|
|
if s == "door" {
|
|
return ItemName::Door;
|
|
}
|
|
|
|
if s == "emergency exit" {
|
|
return ItemName::EmergencyExit;
|
|
}
|
|
|
|
if s == "keypad" {
|
|
return ItemName::Keypad;
|
|
}
|
|
|
|
if s == "note" {
|
|
return ItemName::Note;
|
|
}
|
|
|
|
if s == "table" {
|
|
return ItemName::Table;
|
|
}
|
|
|
|
ItemName::Nonsense
|
|
}
|
|
|
|
#[derive (Clone, Default)]
|
|
struct StateRoom1 {
|
|
detected_keypad: bool,
|
|
detected_note: bool,
|
|
}
|
|
|
|
#[derive (PartialEq)]
|
|
enum Response {
|
|
Print (String),
|
|
PrintMany (Vec <&'static str>),
|
|
Sleep (u32),
|
|
Quit,
|
|
JokeEnding,
|
|
}
|
|
|
|
#[derive (Clone, Default)]
|
|
struct State {
|
|
room_1: StateRoom1,
|
|
}
|
|
|
|
impl State {
|
|
fn room_1 (&mut self, input: &str) -> Vec <Response> {
|
|
use Response::*;
|
|
|
|
let action = parse_input (input);
|
|
|
|
match action {
|
|
PlayerAction::Quit => {
|
|
vec! [
|
|
line_response ("Bye."),
|
|
Quit,
|
|
]
|
|
}
|
|
PlayerAction::Help => {
|
|
just (print_help ())
|
|
},
|
|
PlayerAction::Hint => {
|
|
just (line_response ("Hint for this room: Try using the `help` command."))
|
|
},
|
|
PlayerAction::Nonsense => {
|
|
vec! [
|
|
line_response ("I couldn't understand that. Try `help` or `hint`."),
|
|
line_response ("`hint` may contain spoilers. `help` will not."),
|
|
]
|
|
},
|
|
PlayerAction::Wait => {
|
|
just (line_response ("You wait around a bit. You can hear humming from the electrical lights, and the distant rumble of the building's HVAC system. The room smells faintly of fresh paint. Nothing has changed."))
|
|
},
|
|
PlayerAction::LookAround => {
|
|
let mut output = vec! [
|
|
line_response ("You are in a small room. In one corner is a TABLE. Obvious exits are a DOOR, and an EMERGENCY EXIT."),
|
|
];
|
|
|
|
if self.room_1.detected_note {
|
|
output.push (line_response ("You have noticed a NOTE on the TABLE."));
|
|
}
|
|
if self.room_1.detected_keypad {
|
|
output.push (line_response ("You have noticed the DOOR is locked by an electronic KEYPAD."));
|
|
}
|
|
|
|
output
|
|
}
|
|
PlayerAction::Look (item_name) => {
|
|
match item_name {
|
|
ItemName::Door => {
|
|
self.room_1.detected_keypad = true;
|
|
just (line_response ("You examine the DOOR. It is firmly locked, and you don't have any lock-picking tools. On the DOOR is an electronic KEYPAD."))
|
|
},
|
|
ItemName::EmergencyExit => {
|
|
just (line_response ("The EMERGENCY EXIT reads, \"Emergency exit. Push bar to open. Alarm will sound. Door will unlock in 10 seconds.\". The EMERGENCY EXIT is period-accurate for an American Wal-Mart c. 2020 C.E."))
|
|
},
|
|
ItemName::Keypad => {
|
|
if ! self.room_1.detected_keypad {
|
|
return just (undetected_item ());
|
|
}
|
|
|
|
just (line_response ("The DOOR is locked by an electronic KEYPAD. A soft amber power light indicates that the KEYPAD is likely functional. The KEYPAD buttons are the digits 0-9, Enter, and Clear. Experience tells you that the key code is likely 4 or 5 digits long."))
|
|
},
|
|
ItemName::Note => {
|
|
if ! self.room_1.detected_note {
|
|
return vec! [
|
|
undetected_item (),
|
|
];
|
|
}
|
|
|
|
just (Response::PrintMany (vec! [
|
|
"You read the NOTE.",
|
|
"",
|
|
"Welcome to SEROTONIN DEPOSITORY.",
|
|
"As you play, keep in mind:",
|
|
"- LOOKing at ITEMS is not always safe",
|
|
"- TAKEing an item may be bad long-term",
|
|
"- WAITing counts as an action",
|
|
"- LOOKing AROUND is always safe",
|
|
"- Other NOTEs may contain non-truths",
|
|
"The code for this first KEYPAD is 1234.",
|
|
"",
|
|
" -- Phayle Sayf",
|
|
"",
|
|
"You notice that the NOTE is _not_ period-accurate.",
|
|
]))
|
|
},
|
|
ItemName::Table => {
|
|
self.room_1.detected_note = true;
|
|
vec! [
|
|
Response::Print ("You look at the TABLE. Your instincts tell you that it is period-accurate. Upon the TABLE sits a NOTE.".into ()),
|
|
]
|
|
},
|
|
_ => {
|
|
vec! [
|
|
undetected_item (),
|
|
]
|
|
},
|
|
}
|
|
},
|
|
PlayerAction::Use (item_name) => {
|
|
match item_name {
|
|
ItemName::Door => {
|
|
let mut output = vec! [
|
|
line_response ("You can't USE the DOOR, it is locked."),
|
|
];
|
|
|
|
if ! self.room_1.detected_keypad {
|
|
self.room_1.detected_keypad = true;
|
|
output.push (Response::Print ("You notice an electronic KEYPAD on the DOOR.".into ()));
|
|
}
|
|
|
|
output
|
|
},
|
|
ItemName::EmergencyExit => {
|
|
vec! [
|
|
Response::Print ("You push on the emergency exit. An alarm starts sounding. Your ADVENTURE GAME ENTHUSIAST friend is going to be very mad at you.".into ()),
|
|
Response::Sleep (5000),
|
|
Response::Print ("The alarm is still sounding. You are getting embarrassed, but you have committed to this path of action.".into ()),
|
|
Response::Sleep (5000),
|
|
Response::Print ("The emergency exit unlocks, and you walk out of the game. Bye.".into ()),
|
|
Response::JokeEnding,
|
|
Response::Quit,
|
|
]
|
|
},
|
|
_ => {
|
|
vec! [
|
|
undetected_item (),
|
|
]
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn game () -> Result <()> {
|
|
for line in [
|
|
"Welcome to SEROTONIN DEPOSITORY, the only adventure game ever made.",
|
|
"",
|
|
"You have been consensually kidnapped by a diabolical ADVENTURE GAME ENTHUSIAST and encouraged to solve PUZZLES for their sick PLEASURE. The only winning move is to solve all the PUZZLES.",
|
|
"",
|
|
"Press ENTER if you dare to begin.",
|
|
] {
|
|
print (line);
|
|
}
|
|
|
|
let input = read_input ()?;
|
|
|
|
if ! input.is_empty () {
|
|
print ("That was more than just ENTER but OKAY, overachiever.");
|
|
}
|
|
|
|
print ("");
|
|
|
|
let mut state = State::default ();
|
|
|
|
print ("You are in a small room. In one corner is a TABLE.");
|
|
|
|
'main_loop: loop {
|
|
let input = read_input ()?;
|
|
let responses = state.room_1 (&input);
|
|
|
|
for response in responses.into_iter () {
|
|
match response {
|
|
Response::Print (line) => print (&line),
|
|
Response::PrintMany (lines) => {
|
|
for line in lines {
|
|
print (line);
|
|
}
|
|
},
|
|
Response::Sleep (x) => std::thread::sleep (std::time::Duration::from_millis (x.into ())),
|
|
Response::Quit => break 'main_loop,
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok (())
|
|
}
|
|
|
|
fn main () -> Result <()> {
|
|
game ()?;
|
|
|
|
Ok (())
|
|
}
|
|
|
|
#[cfg (test)]
|
|
mod test {
|
|
#[test]
|
|
fn parse_input () {
|
|
use super::{
|
|
ItemName,
|
|
PlayerAction::*,
|
|
};
|
|
|
|
for (input, expected) in [
|
|
("", Nonsense),
|
|
("look at the table", Look (ItemName::Table)),
|
|
("look at table", Look (ItemName::Table)),
|
|
("look table", Look (ItemName::Table)),
|
|
("LOOK TABLE", Look (ItemName::Table)),
|
|
("wait", Wait),
|
|
("help", Help),
|
|
("hint", Hint),
|
|
].into_iter () {
|
|
let actual = super::parse_input (input);
|
|
assert_eq! (actual, expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn joke_ending () {
|
|
use super::{
|
|
Response,
|
|
State,
|
|
};
|
|
|
|
let mut state = State::default ();
|
|
|
|
let responses = state.room_1 ("use emergency exit");
|
|
|
|
assert! (responses.contains (&Response::Quit));
|
|
assert! (responses.contains (&Response::JokeEnding));
|
|
}
|
|
}
|