use std::io::{ Write, stdin, stdout, }; /// As a dare to myself, I won't use any error-handling crates. #[derive (Debug)] enum GameError { IoError, } impl From for GameError { fn from (_: std::io::Error) -> Self { Self::IoError } } type Result = std::result::Result ; /// 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 print_help () { print ("All commands are ASCII and case-insensitive."); print ("Commands should start with a verb like LOOK."); print ("e.g. `look table`"); print ("Single-word verbs are better, e.g. prefer `hint` over `give me a hint`"); print ("When in doubt, try generic verbs like `look` or `use` over specific verbs like `actuate`, `type`, or `consolidate`."); } fn print_undetected_item () { print ("That ITEM does not exist in this ROOM, or you have not detected it."); } fn read_input () -> Result { { let mut stdout = stdout (); stdout.write_all (b"> ")?; stdout.flush ()?; } let mut buffer = String::new (); 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) } #[derive (Debug, PartialEq)] enum PlayerAction { Quit, Help, Hint, Nonsense, Wait, Look (ItemName), LookAround, } #[derive (Clone, Copy, Debug, PartialEq)] enum ItemName { Nonsense, Door, EmergencyExit, Keypad, Note, Table, } fn item_name_display (x: ItemName) -> &'static str { match x { Nonsense => "NONSENSE", Door => "DOOR", EmergencyExit => "EMERGENCY EXIT", Keypad => "KEYPAD", Note => "NOTE", 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 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 (Default)] struct StateRoom1 { detected_keypad: bool, detected_note: bool, } #[derive (Default)] struct State { quitting: bool, room_1: StateRoom1, } fn room_1 (state: &mut State) -> Result <()> { let input = read_input ()?; let action = parse_input (&input); match action { PlayerAction::Quit => { print ("Bye."); state.quitting = true; } PlayerAction::Help => { print_help (); }, PlayerAction::Hint => { print ("Hint for this room: Try using the `help` command."); }, PlayerAction::Nonsense => { print ("I couldn't understand that. Try `help` or `hint`."); print ("`hint` may contain spoilers. `help` will not."); }, PlayerAction::Wait => { print ("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 => { print ("You are in a small room. In one corner is a TABLE. Obvious exits are a locked DOOR, and an EMERGENCY EXIT."); } PlayerAction::Look (item_name) => { match item_name { ItemName::Nonsense => { print_undetected_item (); }, ItemName::Door => { print ("You examine the DOOR. It is firmly locked, and you don't have any lock-picking tools. On the DOOR is an electronic KEYPAD."); state.room_1.detected_keypad = true; }, ItemName::EmergencyExit => { print ("The EMERGENCY EXIT reads, \"Emergency exit. Push bar to open. Alarm will sound. Door will unlock in 15 seconds.\". The EMERGENCY EXIT is period-accurate for an American Wal-Mart c. 2020 C.E."); }, ItemName::Keypad => { if state.room_1.detected_keypad { print ("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."); } else { print_undetected_item (); } }, ItemName::Note => { if state.room_1.detected_note { for x in [ "You pick up the NOTE and read it.", "", "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.", ].into_iter () { print (x); } } else { print_undetected_item (); } }, ItemName::Table => { print ("You look at the TABLE. Your instincts tell you that it is period-accurate. Upon the TABLE sits a NOTE."); state.room_1.detected_note = true; }, } }, } Ok (()) } fn main () -> Result <()> { print ("Welcome to SEROTONIN DEPOSITORY, the only adventure game ever made."); print (""); print ("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."); print (""); print ("Press ENTER if you dare to begin."); 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."); while ! state.quitting { room_1 (&mut state)?; } 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); } } }