♻️ refactor: all the game code is in its own crate now.

This is so that I can make a telnet version without introducing networking /
async dependencies into the CLI version
main
_ 2021-10-25 00:02:29 +00:00
parent 7dd6a872e4
commit 1c5519c761
3 changed files with 48 additions and 446 deletions

3
Cargo.lock generated
View File

@ -5,6 +5,9 @@ version = 3
[[package]] [[package]]
name = "kajam_10" name = "kajam_10"
version = "0.1.0" version = "0.1.0"
dependencies = [
"kajam_10_game",
]
[[package]] [[package]]
name = "kajam_10_game" name = "kajam_10_game"

View File

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
kajam_10_game = { path = "game" }
[workspace] [workspace]
members = [ members = [

View File

@ -1,3 +1,8 @@
use kajam_10_game::{
Response,
State,
};
/// As a dare to myself, I won't use any error-handling crates. /// As a dare to myself, I won't use any error-handling crates.
#[derive (Debug)] #[derive (Debug)]
@ -13,380 +18,6 @@ impl From <std::io::Error> for GameError {
type Result <T> = std::result::Result <T, GameError>; type Result <T> = std::result::Result <T, GameError>;
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,
Nonsense,
RoomSpecific (PlayerActionRoomSpecific),
}
#[derive (Debug, PartialEq)]
enum PlayerActionRoomSpecific {
Hint,
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 {
use PlayerAction::*;
use PlayerActionRoomSpecific::*;
let s = s.to_lowercase ();
let look = |rest| RoomSpecific (Look (parse_item_name (rest)));
let activate = |rest| RoomSpecific (Use (parse_item_name (rest)));
if s == "quit" || s == "quit game" {
Quit
}
else if s == "help" || s == "help me" {
Help
}
else if s == "look" || s == "look around" {
RoomSpecific (LookAround)
}
else if let Some (rest) = s.strip_prefix ("look at the ") {
look (rest)
}
else if let Some (rest) = s.strip_prefix ("look at ") {
look (rest)
}
else if let Some (rest) = s.strip_prefix ("look ") {
look (rest)
}
else if let Some (rest) = s.strip_prefix ("examine ") {
look (rest)
}
else if let Some (rest) = s.strip_prefix ("use the ") {
activate (rest)
}
else if let Some (rest) = s.strip_prefix ("use ") {
activate (rest)
}
else if
s == "do nothing" ||
s == "wait"
{
RoomSpecific (Wait)
}
else if s == "hint" {
RoomSpecific (Hint)
}
else {
Nonsense
}
}
fn parse_item_name (s: &str) -> ItemName {
let s = s.trim ();
if s == "door" {
ItemName::Door
}
else if s == "emergency exit" {
ItemName::EmergencyExit
}
else if s == "keypad" {
ItemName::Keypad
}
else if s == "note" {
ItemName::Note
}
else if s == "table" {
ItemName::Table
}
else {
ItemName::Nonsense
}
}
macro_rules! require_detection {
($condition:expr $(,)?) => {
if ! $condition {
return vec! [
Response::FailedDetectionCheck,
undetected_item (),
];
}
};
}
#[derive (Clone)]
enum RoomName {
/// Starting room with the dead-simple note and keypad puzzle.
Room1,
/// Duplicate of starting room so I can change things around a little.
Room2,
}
impl Default for RoomName {
fn default () -> Self {
Self::Room1
}
}
#[derive (Clone, Default)]
struct StateRoom1 {
detected_keypad: bool,
detected_note: bool,
read_note: bool,
}
#[derive (PartialEq)]
enum Response {
// These are handled by the runtime
Print (String),
PrintMany (Vec <&'static str>),
Sleep (u32),
Quit,
// These are just useful markers for the automated tests
JokeEnding,
FailedDetectionCheck,
PlayerVictory,
}
#[derive (Clone, Default)]
struct State {
current_room: RoomName,
room_1: StateRoom1,
}
impl State {
fn step (&mut self, input: &str) -> Vec <Response> {
let action = parse_input (input);
match action {
PlayerAction::Quit => vec! [
line_response ("Bye."),
Response::Quit,
],
PlayerAction::Help => just (print_help ()),
PlayerAction::Nonsense => vec! [
line_response ("I couldn't understand that. Try `help` or `hint`."),
line_response ("`hint` may contain spoilers. `help` will not."),
],
PlayerAction::RoomSpecific (x) => self.room_1 (x),
}
}
fn room_1 (&mut self, action: PlayerActionRoomSpecific) -> Vec <Response> {
use PlayerActionRoomSpecific::*;
match action {
Hint => {
just (line_response ("Hint for this room: Try using the `help` command."))
},
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."))
},
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
}
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 => {
require_detection! (self.room_1.detected_keypad);
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 => {
require_detection! (self.room_1.detected_note);
self.room_1.read_note = true;
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;
just (line_response ("You look at the TABLE. Your instincts tell you that it is period-accurate. Upon the TABLE sits a NOTE."))
},
_ => {
just (undetected_item ())
},
}
},
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 (line_response ("You notice an electronic KEYPAD on the DOOR."));
}
output
},
ItemName::EmergencyExit => {
vec! [
line_response ("You push on the emergency exit. An alarm starts sounding. Your ADVENTURE GAME ENTHUSIAST friend is going to be very mad at you."),
Response::Sleep (5000),
line_response ("The alarm is still sounding. You are getting embarrassed, but you have committed to this path of action."),
Response::Sleep (5000),
line_response ("The emergency exit unlocks, and you walk out of the game. Bye."),
Response::JokeEnding,
Response::Quit,
]
},
ItemName::Keypad => {
require_detection! (self.room_1.detected_keypad);
if ! self.room_1.read_note {
return just (line_response ("You can't USE the KEYPAD, you don't know the code for it. You would normally try guessing, but it would take the programmer all day to implement that level of interaction."));
}
vec! [
line_response ("You USE the code on the KEYPAD. The door opens, and the game immediately crashes."),
Response::PlayerVictory,
Response::Quit,
]
},
ItemName::Note => {
require_detection! (self.room_1.detected_note);
just (line_response ("You can't think of any way to USE the NOTE that would be better than LOOKing at it to read it."))
},
ItemName::Table => {
just (line_response ("You can't think of any way to USE the TABLE that would be better than LOOKing at it."))
},
_ => {
just (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.step (&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 (())
}
/// Prints a string, naively wrapping at 80-character boundaries /// Prints a string, naively wrapping at 80-character boundaries
/// This means a period might be wrapped separate from the sentence it ends /// This means a period might be wrapped separate from the sentence it ends
/// , and frankly I think that's part of the charm. /// , and frankly I think that's part of the charm.
@ -420,80 +51,47 @@ fn read_input () -> Result <String> {
Ok (buffer) Ok (buffer)
} }
fn game () -> Result <()> {
let mut state = State::default ();
let responses = state.step ("");
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 ())),
_ => (),
}
}
'main_loop: loop {
let input = read_input ()?;
let responses = state.step (&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 <()> { fn main () -> Result <()> {
game ()?; game ()?;
Ok (()) Ok (())
} }
#[cfg (test)]
mod test {
use super::{
Response,
State,
};
#[test]
fn parse_input () {
use super::{
ItemName,
PlayerAction::*,
PlayerActionRoomSpecific::*,
};
for (input, expected) in [
("", Nonsense),
("help", Help),
("look at the table", RoomSpecific (Look (ItemName::Table))),
("look at table", RoomSpecific (Look (ItemName::Table))),
("look table", RoomSpecific (Look (ItemName::Table))),
("look note ", RoomSpecific (Look (ItemName::Note))),
("LOOK TABLE", RoomSpecific (Look (ItemName::Table))),
("wait", RoomSpecific (Wait)),
("hint", RoomSpecific (Hint)),
].into_iter () {
let actual = super::parse_input (input);
assert_eq! (actual, expected);
}
}
#[test]
fn joke_ending () {
let mut state = State::default ();
let responses = state.step ("use emergency exit");
assert! (responses.contains (&Response::Quit));
assert! (responses.contains (&Response::JokeEnding));
}
#[test]
fn detection_check () {
let mut state = State::default ();
let responses = state.step ("look keypad");
assert! (responses.contains (&Response::FailedDetectionCheck));
state.step ("look door");
let responses = state.step ("look keypad");
assert! (! responses.contains (&Response::FailedDetectionCheck));
}
#[test]
fn happy_path () {
let mut state = State::default ();
for line in [
"look table",
"look note",
"look door",
] {
let responses = state.step (line);
assert! (! responses.contains (&Response::PlayerVictory));
}
let responses = state.step ("use keypad");
assert! (responses.contains (&Response::PlayerVictory));
}
}