feat: Added Map
- Implemented Map - Partially implemented CLI interaction with map - Added load_map test
This commit is contained in:
parent
dc94f2060c
commit
b9f75e426c
6 changed files with 371 additions and 25 deletions
185
src/bin/cli.rs
185
src/bin/cli.rs
|
|
@ -1,8 +1,8 @@
|
|||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::{Parser,Subcommand,Args,ValueEnum};
|
||||
use serde::Deserialize;
|
||||
use squad_quest::{SquadObject, account::Account, config::Config, quest::{Quest,QuestDifficulty as LibQuestDifficulty}};
|
||||
use squad_quest::{SquadObject, account::Account, config::Config, error::Error, map::{Map, Room}, quest::{Quest,QuestDifficulty as LibQuestDifficulty}};
|
||||
use toml::value::Date;
|
||||
use chrono::{Datelike, NaiveDate, Utc};
|
||||
|
||||
|
|
@ -37,6 +37,9 @@ enum Objects {
|
|||
/// Operations on the accounts
|
||||
#[command(subcommand)]
|
||||
Account(AccountCommands),
|
||||
/// Operations on the map rooms
|
||||
#[command(subcommand)]
|
||||
Map(MapCommands),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
|
|
@ -208,6 +211,62 @@ struct AccountDeleteArgs {
|
|||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum MapCommands {
|
||||
/// List all rooms with connections
|
||||
List,
|
||||
/// Add new room to map
|
||||
Add(MapAddArgs),
|
||||
/// Connect two rooms
|
||||
Connect(MapConnectArgs),
|
||||
/// Disconnect two rooms if they're connected
|
||||
Disconnect(MapConnectArgs),
|
||||
/// Remove all connections with the room
|
||||
Delete(MapDeleteArgs),
|
||||
/// Update room data
|
||||
Update(MapUpdateArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct MapAddArgs {
|
||||
/// Name of the room
|
||||
name: String,
|
||||
/// Price of the room
|
||||
value: u32,
|
||||
/// Optional description for the room
|
||||
#[arg(long,short)]
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct MapConnectArgs {
|
||||
/// First room ID
|
||||
first: u16,
|
||||
/// Second room ID
|
||||
second: u16,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct MapDeleteArgs {
|
||||
/// ID of the room to delete
|
||||
id: u16,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct MapUpdateArgs {
|
||||
/// ID of the room to update
|
||||
id: u16,
|
||||
/// Room name
|
||||
#[arg(short,long)]
|
||||
name: Option<String>,
|
||||
/// Room description
|
||||
#[arg(short,long)]
|
||||
description: Option<String>,
|
||||
/// Room price
|
||||
#[arg(short,long)]
|
||||
value: Option<u32>,
|
||||
}
|
||||
|
||||
fn print_quest_short(quest: &Quest) {
|
||||
println!("Quest #{}: {}", quest.id, quest.name);
|
||||
}
|
||||
|
|
@ -219,7 +278,7 @@ fn print_quest_long(quest: &Quest) {
|
|||
println!("Answer:\n{}", quest.answer);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), Error> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let config = Config::load(cli.config.clone());
|
||||
|
|
@ -241,9 +300,6 @@ fn main() {
|
|||
let mut quests = config.load_quests();
|
||||
quests.sort_by(|a,b| a.id.cmp(&b.id));
|
||||
let next_id = match quests.last() {
|
||||
Some(quest) if quest.id == u16::MAX => {
|
||||
panic!("Error: quest list contains quest with u16::MAX id.");
|
||||
},
|
||||
Some(quest) => quest.id + 1u16,
|
||||
None => 0u16
|
||||
};
|
||||
|
|
@ -342,8 +398,7 @@ fn main() {
|
|||
let not_str = if args.reverse {" not "} else {" "};
|
||||
|
||||
if quest.public != args.reverse {
|
||||
println!("Quest #{} is already{}public", quest.id, not_str);
|
||||
return;
|
||||
panic!("Quest #{} is already{}public", quest.id, not_str);
|
||||
}
|
||||
|
||||
quest.public = !args.reverse;
|
||||
|
|
@ -358,8 +413,8 @@ fn main() {
|
|||
},
|
||||
}
|
||||
},
|
||||
Objects::Account(args) => {
|
||||
match args {
|
||||
Objects::Account(commands) => {
|
||||
match commands {
|
||||
AccountCommands::List => {
|
||||
let accounts = config.load_accounts();
|
||||
|
||||
|
|
@ -376,8 +431,7 @@ fn main() {
|
|||
let accounts = config.load_accounts();
|
||||
|
||||
if let Some(_) = accounts.iter().find(|a| a.id == account.id) {
|
||||
eprintln!("Error: account {} exists.", account.id);
|
||||
return;
|
||||
panic!("Error: account {} exists.", account.id);
|
||||
}
|
||||
|
||||
let accounts_path = config.full_accounts_path();
|
||||
|
|
@ -397,8 +451,7 @@ fn main() {
|
|||
let account = match accounts.iter_mut().find(|a| a.id == args.id) {
|
||||
Some(acc) => acc,
|
||||
None => {
|
||||
eprintln!("Could not find account \"{}\"", args.id);
|
||||
return;
|
||||
panic!("Could not find account \"{}\"", args.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -414,8 +467,7 @@ fn main() {
|
|||
if args.negative_ok {
|
||||
account.balance = 0u32;
|
||||
} else {
|
||||
eprintln!("Error: balance ({}) is less than {}.", account.balance, args.value);
|
||||
return;
|
||||
panic!("Error: balance ({}) is less than {}.", account.balance, args.value);
|
||||
}
|
||||
} else {
|
||||
account.balance -= args.value;
|
||||
|
|
@ -440,16 +492,14 @@ fn main() {
|
|||
let account = match accounts.iter_mut().find(|a| a.id == args.account) {
|
||||
Some(acc) => acc,
|
||||
None => {
|
||||
eprintln!("Could not find account \"{}\"", args.account);
|
||||
return;
|
||||
panic!("Could not find account \"{}\"", args.account);
|
||||
}
|
||||
};
|
||||
|
||||
let quests = config.load_quests();
|
||||
|
||||
if let None = quests.iter().find(|q| q.id == args.quest) {
|
||||
eprintln!("Could not find quest #{}", args.quest);
|
||||
return;
|
||||
panic!("Could not find quest #{}", args.quest);
|
||||
}
|
||||
|
||||
match account.quests_completed.iter().find(|qid| **qid == args.quest) {
|
||||
|
|
@ -483,6 +533,101 @@ fn main() {
|
|||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
Objects::Map(commands) => {
|
||||
let map_path = config.full_map_path();
|
||||
let mut map = Map::load(map_path.clone())?;
|
||||
map.room.sort_by(|a,b| a.id.cmp(&b.id));
|
||||
match commands {
|
||||
MapCommands::List => {
|
||||
for room in map.room {
|
||||
println!("Room #{}: {}; Connections: {:?}", room.id, room.name, room.children);
|
||||
}
|
||||
},
|
||||
MapCommands::Add(args) => {
|
||||
let last_id = match map.room.last() {
|
||||
Some(r) => r.id + 1u16,
|
||||
None => 0u16
|
||||
};
|
||||
let room = Room {
|
||||
id: last_id,
|
||||
name: args.name.clone(),
|
||||
description: args.description.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let r_id = room.id;
|
||||
map.room.push(room);
|
||||
match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) {
|
||||
Ok(_) => {
|
||||
println!("Created room #{}.", r_id);
|
||||
println!("Successfully saved map.");
|
||||
},
|
||||
Err(error) => {
|
||||
eprintln!("Error while saving map: {error}");
|
||||
}
|
||||
}
|
||||
},
|
||||
MapCommands::Delete(args) => {
|
||||
let Some(room) = map.room.iter().find(|r| r.id == args.id) else {
|
||||
panic!("Error: Room #{} not found", args.id);
|
||||
};
|
||||
|
||||
let r_id = room.id;
|
||||
let index = map.room.iter().position(|r| r.eq(room)).unwrap();
|
||||
map.room.remove(index);
|
||||
|
||||
for room in map.room.iter_mut().filter(|r| r.children.contains(&r_id)) {
|
||||
let idx = room.children.iter()
|
||||
.position(|id| *id == r_id)
|
||||
.unwrap();
|
||||
room.children.remove(idx);
|
||||
}
|
||||
|
||||
match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) {
|
||||
Ok(_) => {
|
||||
println!("Removed room #{}.", r_id);
|
||||
println!("Successfully saved map.");
|
||||
},
|
||||
Err(error) => {
|
||||
eprintln!("Error while saving map: {error}");
|
||||
}
|
||||
}
|
||||
},
|
||||
MapCommands::Update(args) => {
|
||||
let Some(room) = map.room.iter_mut().find(|r| r.id == args.id) else {
|
||||
panic!("Error: Room #{} not found", args.id);
|
||||
};
|
||||
|
||||
if let Some(name) = &args.name {
|
||||
room.name = name.to_string();
|
||||
}
|
||||
|
||||
if args.description.is_some() {
|
||||
room.description = args.description.clone();
|
||||
}
|
||||
|
||||
if let Some(value) = args.value {
|
||||
room.value = value;
|
||||
}
|
||||
|
||||
match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) {
|
||||
Ok(_) => {
|
||||
println!("Updated room #{}.", args.id);
|
||||
println!("Successfully saved map.");
|
||||
},
|
||||
Err(error) => {
|
||||
eprintln!("Error while saving map: {error}");
|
||||
}
|
||||
}
|
||||
},
|
||||
MapCommands::Connect(_) => {
|
||||
todo!();
|
||||
},
|
||||
MapCommands::Disconnect(_) => {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ impl Config {
|
|||
out_vec
|
||||
}
|
||||
|
||||
/// Returns full path to quests folder
|
||||
/// Returns full path to accounts folder
|
||||
/// This path will be relative to $PWD, not to config.
|
||||
///
|
||||
/// # Examples
|
||||
|
|
@ -237,4 +237,29 @@ impl Config {
|
|||
|
||||
out_vec
|
||||
}
|
||||
|
||||
/// Returns full path to map.toml
|
||||
/// This path will be relative to $PWD, not to config.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use squad_quest::{config::Config,error::Error,map::Map,SquadObject};
|
||||
/// # fn main() {
|
||||
/// # let _ = wrapper();
|
||||
/// # }
|
||||
/// # fn wrapper() -> Result<(),Error> {
|
||||
///
|
||||
/// let path = "cfg/config.toml".into();
|
||||
/// let config = Config::load(path);
|
||||
///
|
||||
/// let map_path = config.full_map_path();
|
||||
/// let map = Map::load(map_path)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn full_map_path(&self) -> PathBuf {
|
||||
let mut path = self.path.clone();
|
||||
path.push(self.map.clone());
|
||||
path
|
||||
}
|
||||
}
|
||||
|
|
|
|||
108
src/map/mod.rs
108
src/map/mod.rs
|
|
@ -1,5 +1,107 @@
|
|||
//! Map, a.k.a. a graph of rooms
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// Graph for room nodes
|
||||
pub struct Map;
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{SquadObject, error::Error};
|
||||
|
||||
/// THE Graph. Actually, this is a Vec.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Map {
|
||||
/// Rooms go here
|
||||
pub room: Vec<Room>
|
||||
}
|
||||
|
||||
impl Default for Map {
|
||||
fn default() -> Self {
|
||||
Map { room: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl SquadObject for Map {
|
||||
fn load(path: PathBuf) -> Result<Self, Error> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(string) => {
|
||||
match toml::from_str::<Self>(&string) {
|
||||
Ok(object) => Ok(object),
|
||||
Err(error) => Err(Error::TomlDeserializeError(error))
|
||||
}
|
||||
},
|
||||
Err(error) => Err(Error::IoError(error))
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(path: PathBuf) -> Result<(), Error> {
|
||||
match Self::load(path.clone()) {
|
||||
Ok(_) => {
|
||||
if let Err(error) = fs::remove_file(path) {
|
||||
return Err(Error::IoError(error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self, path: PathBuf) -> Result<(), Error> {
|
||||
let filename = "map.toml".to_string();
|
||||
let mut full_path = path;
|
||||
full_path.push(filename);
|
||||
|
||||
let str = match toml::to_string_pretty(&self) {
|
||||
Ok(string) => string,
|
||||
Err(error) => {
|
||||
return Err(Error::TomlSerializeError(error));
|
||||
}
|
||||
};
|
||||
|
||||
let mut file = match fs::File::create(full_path) {
|
||||
Ok(f) => f,
|
||||
Err(error) => {
|
||||
return Err(Error::IoError(error));
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = file.write_all(str.as_bytes()) {
|
||||
return Err(Error::IoError(error));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Component of the map
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Room {
|
||||
/// Room id
|
||||
pub id: u16,
|
||||
/// Rooms that are connected with this
|
||||
pub children: Vec<u16>,
|
||||
/// Price of the room
|
||||
pub value: u32,
|
||||
/// Room name
|
||||
pub name: String,
|
||||
/// Room description
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
fn default_name() -> String {
|
||||
"Hall".to_string()
|
||||
}
|
||||
|
||||
impl Default for Room {
|
||||
fn default() -> Self {
|
||||
Room {
|
||||
id: u16::default(),
|
||||
children: Vec::new(),
|
||||
value: u32::default(),
|
||||
name: default_name(),
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
tests/io/map.toml
Normal file
1
tests/io/map.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use squad_quest::{account::Account, config::Config, quest::Quest};
|
||||
use squad_quest::{SquadObject, account::Account, config::Config, map::{Map, Room}, quest::Quest};
|
||||
|
||||
static CONFIG_PATH: &str = "./tests/main/config.toml";
|
||||
|
||||
|
|
@ -81,3 +81,50 @@ fn account_test() {
|
|||
|
||||
assert_eq!(*account, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_map() {
|
||||
let config = Config::load(CONFIG_PATH.into());
|
||||
|
||||
let room0 = Room {
|
||||
id: 0,
|
||||
children: vec![1, 2],
|
||||
value: 0,
|
||||
name: "Entrance".to_string(),
|
||||
description: Some("Enter the dungeon".to_string()),
|
||||
};
|
||||
|
||||
let room1 = Room {
|
||||
id: 1,
|
||||
children: vec![0, 3],
|
||||
value: 100,
|
||||
name: "Kitchen hall".to_string(),
|
||||
description: None,
|
||||
};
|
||||
|
||||
let room2 = Room {
|
||||
id: 2,
|
||||
children: vec![0],
|
||||
value: 250,
|
||||
name: "Room".to_string(),
|
||||
description: Some("Simple room with no furniture".to_string()),
|
||||
};
|
||||
|
||||
let room3 = Room {
|
||||
id: 3,
|
||||
children: vec![1],
|
||||
value: 175,
|
||||
name: "Kitchen".to_string(),
|
||||
description: Some("Knives are stored here".to_string()),
|
||||
};
|
||||
|
||||
let expected = Map {
|
||||
room: vec![room0, room1, room2, room3],
|
||||
};
|
||||
|
||||
let map_path = config.full_map_path();
|
||||
|
||||
let map = Map::load(map_path).unwrap();
|
||||
|
||||
assert_eq!(map.room, expected.room);
|
||||
}
|
||||
|
|
|
|||
26
tests/main/map.toml
Normal file
26
tests/main/map.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[[room]]
|
||||
id = 0
|
||||
children = [ 1, 2 ]
|
||||
value = 0
|
||||
name = "Entrance"
|
||||
description = "Enter the dungeon"
|
||||
|
||||
[[room]]
|
||||
id = 1
|
||||
children = [ 0, 3 ]
|
||||
value = 100
|
||||
name = "Kitchen hall"
|
||||
|
||||
[[room]]
|
||||
id = 2
|
||||
children = [ 0 ]
|
||||
value = 250
|
||||
name = "Room"
|
||||
description = "Simple room with no furniture"
|
||||
|
||||
[[room]]
|
||||
id = 3
|
||||
children = [ 1 ]
|
||||
value = 175
|
||||
name = "Kitchen"
|
||||
description = "Knives are stored here"
|
||||
Loading…
Add table
Add a link
Reference in a new issue