kajam_10/src/main.rs

311 lines
7.2 KiB
Rust
Raw Normal View History

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 <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 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 <String> {
{
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);
}
}
}