use crate::story::loader::Story;
use crate::engine::{room::Room, parser::{parse_command, Command}};
use crossterm::{
execute,
terminal::{Clear, ClearType},
style::{Print, SetForegroundColor, ResetColor, Color},
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
#[derive(Debug, Serialize, Deserialize)]
struct SavedGame {
current_room: String,
inventory: HashSet<String>,
}
pub struct Game {
story: Story,
current_room: String,
inventory: HashSet<String>,
}
#[derive(Debug)]
pub enum GameError {
InvalidRoom,
ItemNotFound,
RoomNotFound,
}
impl std::fmt::Display for GameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GameError::InvalidRoom => write!(f, "Invalid room"),
GameError::ItemNotFound => write!(f, "Item not found"),
GameError::RoomNotFound => write!(f, "Room not found"),
}
}
}
impl std::error::Error for GameError {}
impl Game {
pub fn new(story: Story) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
current_room: story.story.start_room.clone(),
story,
inventory: HashSet::new(),
})
}
pub async fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
self.clear_screen()?;
println!("=========================================");
println!(" Welcome to: {}", self.story.story.title);
println!("=========================================");
// Show initial room
self.show_room()?;
loop {
print!("> ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let command = input.trim().to_lowercase();
if command == "quit" || command == "exit" || command == "q" {
println!("Thanks for playing! :D");
break;
}
match self.process_command(&command).await {
Ok(output) => {
if !output.is_empty() {
println!("{}", output);
}
}
Err(e) => println!("Error: {}", e),
}
// Show room description again after certain commands
if matches!(parse_command(&command), Command::Go(_) | Command::North | Command::South | Command::East | Command::West) {
self.show_room()?;
}
}
Ok(())
}
async fn process_command(&mut self, input: &str) -> Result<String, GameError> {
match parse_command(&command) {
Command::Look => self.look(),
Command::Go(direction) => self.go(&direction),
Command::North => self.go("north"),
Command::South => self.go("south"),
Command::East => self.go("east"),
Command::West => self.go("west"),
Command::Take(item) => self.take(&item),
Command::Drop(item) => self.drop(&item),
Command::Inventory => self.show_inventory(),
Command::Save(filename) => self.save(&filename),
Command::Load(filename) => self.load(&filename),
Command::ListSaves => Ok(self.list_saves()),
Command::Help => Ok(self.help()),
Command::Unknown => Ok("I don't understand that command. Type 'help' for available commands. :0".to_string()),
}
}
fn look(&self) -> Result<String, GameError> {
let room = self.get_current_room()?;
let mut output = String::new();
output.push_str(&format!("\n{}", room.title));
output.push_str(&format!("\n{}", room.description));
if !room.items.is_empty() {
output.push_str("\n\nYou see: ");
for (i, item_id) in room.items.iter().enumerate() {
if let Some(item) = self.story.items.get(item_id) {
if i > 0 { output.push_str(", "); }
output.push_str(&item.name);
}
}
}
if !room.exits.is_empty() {
output.push_str("\n\nExits: ");
let exit_names: Vec<String> = room.exits.keys().cloned().collect();
output.push_str(&exit_names.join(", "));
}
Ok(output)
}
fn go(&mut self, direction: &str) -> Result<String, GameError> {
let current_room = self.get_current_room()?;
if let Some(next_room_id) = current_room.exits.get(direction) {
if self.story.rooms.contains_key(next_room_id) {
self.current_room = next_room_id.clone();
Ok(format!("You go {}.", direction))
} else {
Err(GameError::RoomNotFound)
}
} else {
Ok(format!("You can't go {} from here.", direction))
}
}
fn take(&mut self, item_name: &str) -> Result<String, GameError> {
// Find the item by partial name match
let mut found_item_id = None;
{
let current_room = self.get_current_room()?;
for item_id in ¤t_room.items {
if let Some(item) = self.story.items.get(item_id) {
if item.name.to_lowercase().contains(&item_name.to_lowercase()) {
found_item_id = Some(item_id.clone());
break;
}
}
}
}
if let Some(item_id) = found_item_id {
// We need to remove the item from the room and add to inventory
if let Some(room) = self.story.rooms.get_mut(&self.current_room) {
room.items.retain(|id| id != &item_id);
}
self.inventory.insert(item_id.clone());
if let Some(item) = self.story.items.get(&item_id) {
Ok(format!("You take the {}.", item.name))
} else {
Ok("You take the item.".to_string())
}
} else {
Err(GameError::ItemNotFound)
}
}
fn drop(&mut self, item_name: &str) -> Result<String, GameError> {
// Find the item in inventory by partial name match
let mut found_item_id = None;
for item_id in &self.inventory {
if let Some(item) = self.story.items.get(item_id) {
if item.name.to_lowercase().contains(&item_name.to_lowercase()) {
found_item_id = Some(item_id.clone());
break;
}
}
}
if let Some(item_id) = found_item_id {
self.inventory.remove(&item_id);
if let Some(room) = self.story.rooms.get_mut(&self.current_room) {
room.items.push(item_id.clone());
}
if let Some(item) = self.story.items.get(&item_id) {
Ok(format!("You drop the {}.", item.name))
} else {
Ok("You drop the item.".to_string())
}
} else {
Err(GameError::ItemNotFound)
}
}
fn show_inventory(&self) -> Result<String, GameError> {
if self.inventory.is_empty() {
Ok("Your inventory is empty. :0".to_string())
} else {
let mut output = "You are carrying:\n".to_string();
for item_id in &self.inventory {
if let Some(item) = self.story.items.get(item_id) {
output.push_str(&format!("- {}\n", item.name));
}
}
// Remove the trailing newline
if output.ends_with('\n') {
output.pop();
}
Ok(output)
}
}
fn help(&self) -> String {
"Available commands:
- look: Look around the current room
- go [direction]: Move in a direction (north, south, east, west)
- n/s/e/w: Short forms for directions
- take [item]: Pick up an item
- drop [item]: Drop an item
- inventory: Check your inventory
- save [filename]: Save the current game
- load [filename]: Load a saved game
- help: Show this help
- quit: Exit the game
Example: 'go north' or 'take key' :D".to_string()
}
fn save(&self, filename: &str) -> Result<String, GameError> {
let save_data = SavedGame {
current_room: self.current_room.clone(),
inventory: self.inventory.clone(),
};
let json = serde_json::to_string(&save_data).map_err(|_| GameError::InvalidRoom)?;
fs::write(filename, json).map_err(|_| GameError::InvalidRoom)?;
Ok(format!("Game saved to {}", filename))
}
fn load(&mut self, filename: &str) -> Result<String, GameError> {
let json = fs::read_to_string(filename).map_err(|_| GameError::InvalidRoom)?;
let save_data: SavedGame = serde_json::from_str(&json).map_err(|_| GameError::InvalidRoom)?;
self.current_room = save_data.current_room;
self.inventory = save_data.inventory;
Ok(format!("Game loaded from {}", filename))
}
fn get_save_files(&self) -> Vec<String> {\n let mut saves = Vec::new();\n if let Ok(entries) = fs::read_dir(\".\") {\n for entry in entries {\n if let Ok(entry) = entry {\n let path = entry.path();\n if path.extension().map_or(false, |ext| ext == \"save\") {\n if let Some(name) = path.file_name().and_then(|n| n.to_str()) {\n saves.push(name.to_string());\n }\n }\n }\n }\n }\n saves\n }\n \n fn list_saves(&self) -> String {\n let saves = self.get_save_files();\n if saves.is_empty() {\n \"No save files found. :0\".to_string()\n } else {\n let mut output = \"Available saves:\\n\".to_string();\n for save in saves {\n output.push_str(&format!(\"- {}\\n\", save));\n }\n // Remove the trailing newline\n if output.ends_with('\\n') {\n output.pop();\n }\n output\n }\n }
fn show_room(&self) -> Result<(), Box<dyn std::error::Error>> {
let room = self.get_current_room()?;
execute!(
io::stdout(),
SetForegroundColor(Color::Cyan),
Print(format!("{}\n", room.title)),
ResetColor
)?;
execute!(
io::stdout(),
Print(format!("{}\n", room.description)),
)?;
Ok(())
}
fn get_current_room(&self) -> Result<&Room, GameError> {
self.story.rooms.get(&self.current_room).ok_or(GameError::InvalidRoom)
}
fn clear_screen(&self) -> Result<(), Box<dyn std::error::Error>> {
execute!(io::stdout(), Clear(ClearType::All))?;
Ok(())
}
}// Basic game loop implemented
// Inventory system - take/drop items :D
// Save system with json files
// Better error messages
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_game_creation() {
// Create a minimal story for testing
let mut rooms = HashMap::new();
rooms.insert(
"test_room".to_string(),
Room {
title: "Test Room".to_string(),
description: "A test room".to_string(),
exits: HashMap::new(),
items: vec!["test_item".to_string()],
}
);
let mut items = HashMap::new();
items.insert(
"test_item".to_string(),
Item {
name: "Test Item".to_string(),
description: "A test item".to_string(),
}
);
let story = Story {
story: StoryInfo {
title: "Test Story".to_string(),
start_room: "test_room".to_string(),
},
rooms,
items,
};
let game = Game::new(story);
assert!(game.is_ok());
}
#[test]
fn test_take_item() {
// Create a story with an item in a room
let mut rooms = HashMap::new();
let mut exits = HashMap::new();
exits.insert("north".to_string(), "other_room".to_string());
rooms.insert(
"start".to_string(),
Room {
title: "Start Room".to_string(),
description: "A starting room".to_string(),
exits,
items: vec!["key".to_string()],
}
);
rooms.insert(
"other_room".to_string(),
Room {
title: "Other Room".to_string(),
description: "Another room".to_string(),
exits: HashMap::new(),
items: vec![],
}
);
let mut items = HashMap::new();
items.insert(
"key".to_string(),
Item {
name: "Brass Key".to_string(),
description: "An old brass key".to_string(),
}
);
let story = Story {
story: StoryInfo {
title: "Test Story".to_string(),
start_room: "start".to_string(),
},
rooms,
items,
};
let mut game = Game::new(story).unwrap();
// Test taking an item that exists
let result = game.take("key");
assert!(result.is_ok());
assert!(result.unwrap().contains("take the"));
// Verify the item is now in inventory
assert!(game.inventory.contains("key"));
// Verify the item is no longer in the room
let room = game.story.rooms.get("start").unwrap();
assert!(!room.items.contains(&"key".to_string()));
}
#[test]
fn test_drop_item() {
// Create a story for testing
let mut rooms = HashMap::new();
rooms.insert(
"start".to_string(),
Room {
title: "Start Room".to_string(),
description: "A starting room".to_string(),
exits: HashMap::new(),
items: vec![],
}
);
let mut items = HashMap::new();
items.insert(
"key".to_string(),
Item {
name: "Brass Key".to_string(),
description: "An old brass key".to_string(),
}
);
let story = Story {
story: StoryInfo {
title: "Test Story".to_string(),
start_room: "start".to_string(),
},
rooms,
items,
};
let mut game = Game::new(story).unwrap();
// Add an item directly to inventory for testing drop
game.inventory.insert("key".to_string());
// Test dropping an item that exists in inventory
let result = game.drop("key");
assert!(result.is_ok());
assert!(result.unwrap().contains("drop the"));
// Verify the item is no longer in inventory
assert!(!game.inventory.contains("key"));
// Verify the item is now in the room
let room = game.story.rooms.get("start").unwrap();
assert!(room.items.contains(&"key".to_string()));
}
#[test]
fn test_go_command() {
// Create a story with connected rooms
let mut rooms = HashMap::new();
let mut exits = HashMap::new();
exits.insert("north".to_string(), "north_room".to_string());
rooms.insert(
"start".to_string(),
Room {
title: "Start Room".to_string(),
description: "A starting room".to_string(),
exits,
items: vec![],
}
);
rooms.insert(
"north_room".to_string(),
Room {
title: "North Room".to_string(),
description: "A room to the north".to_string(),
exits: HashMap::new(),
items: vec![],
}
);
let items = HashMap::new();
let story = Story {
story: StoryInfo {
title: "Test Story".to_string(),
start_room: "start".to_string(),
},
rooms,
items,
};
let mut game = Game::new(story).unwrap();
// Test moving to a connected room
let result = game.go("north");
assert!(result.is_ok());
assert!(result.unwrap().contains("go north"));
// Verify the current room has changed
assert_eq!(game.current_room, "north_room");
}
#[test]
fn test_look_command() {
let mut rooms = HashMap::new();
let mut exits = HashMap::new();
exits.insert("north".to_string(), "other_room".to_string());
rooms.insert(
"start".to_string(),
Room {
title = "Start Room".to_string(),
description = "A starting room with exits north".to_string(),
exits,
items: vec!["key".to_string()],
}
);
let mut items = HashMap::new();
items.insert(
"key".to_string(),
Item {
name: "Brass Key".to_string(),
description: "An old brass key".to_string(),
}
);
let story = Story {
story: StoryInfo {
title: "Test Story".to_string(),
start_room: "start".to_string(),
},
rooms,
items,
};
let game = Game::new(story).unwrap();
// Test looking at the current room
let result = game.look();
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Start Room"));
assert!(output.contains("A starting room with exits north"));
assert!(output.contains("key")); // Should show the item in the room
assert!(output.contains("north")); // Should show the exit
}
#[test]
fn test_show_inventory() {
let mut rooms = HashMap::new();
rooms.insert(
"start".to_string(),
Room {
title: "Start Room".to_string(),
description: "A starting room".to_string(),
exits: HashMap::new(),
items: vec![],
}
);
let mut items = HashMap::new();
items.insert(
"key".to_string(),
Item {
name: "Brass Key".to_string(),
description: "An old brass key".to_string(),
}
);
let story = Story {
story: StoryInfo {
title: "Test Story".to_string(),
start_room: "start".to_string(),
},
rooms,
items,
};
let mut game = Game::new(story).unwrap();
// Test empty inventory
let result = game.show_inventory();
assert!(result.is_ok());
assert!(result.unwrap().contains("empty"));
// Add an item to inventory
game.inventory.insert("key".to_string());
// Test non-empty inventory
let result = game.show_inventory();
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.contains("Brass Key"));
}
}