Compare commits

...

7 commits

Author SHA1 Message Date
2960b6dfc4 feat: Implemented file hierarchy initialization
- Bump version to 0.5.1
- Added Config::save method
- cli: Added init command
2025-12-05 17:16:40 +03:00
f88e010b4f feat: Added interactions with accounts/quests/map
- Bump version to 0.5.0
- Added Quest::complete_for_account public function
- Added Map::unlock_room_for_account public function
- cli: Added "account unlock" command
- cli: "account complete" now uses Quest::complete_for_account
- cli: refactored logging
2025-12-05 15:38:14 +03:00
790fa88fe3 refactor(cli)!: Moved CLI stuff to crate::cli
- Bump version to 0.4.0
- Added Config::try_load
- Added Config.verbose field
- Made Config.path public
- Added -q/--quiet flag to CLI

BREAKING CHANGE: Moved CLI-related objects to squad-quest-cli::cli
2025-12-04 17:37:01 +03:00
47f55105dd refactor!: Moved CLI to inner package
- Increased version to 0.3.0
- Repository now is a Cargo workspace
- Added squad-quest-cli package
- Removed CLI-specific dependencies from main crate
- Removed bin target from main package
2025-12-04 13:56:53 +03:00
5d6aa0422d feat: Added CLI rooms (dis-)connect functionality 2025-12-04 12:54:22 +03:00
b9f75e426c feat: Added Map
- Implemented Map
- Partially implemented CLI interaction with map
- Added load_map test
2025-12-03 17:01:40 +03:00
dc94f2060c feat: Added interaction with accounts in CLI
- Account creation
- Account deletion
- Account balance management
- Account quest completion
- Added account CRUD test in tests/io.rs
2025-12-02 16:12:42 +03:00
21 changed files with 1244 additions and 368 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target /target
/cli/target

12
Cargo.lock generated
View file

@ -332,12 +332,20 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "squad-quest" name = "squad-quest"
version = "0.2.0" version = "0.5.0"
dependencies = [
"serde",
"toml",
]
[[package]]
name = "squad-quest-cli"
version = "0.5.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
"clap_derive",
"serde", "serde",
"squad-quest",
"toml", "toml",
] ]

View file

@ -1,11 +1,19 @@
[workspace]
members = ["cli"]
[workspace.package]
version = "0.5.1"
edition = "2024"
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
license = "MIT"
[package] [package]
name = "squad-quest" name = "squad-quest"
version = "0.2.0" edition.workspace = true
edition = "2024" version.workspace = true
repository.workspace = true
license.workspace = true
[dependencies] [dependencies]
chrono = "0.4.42"
clap = { version = "4.5.53", features = ["derive"] }
clap_derive = "4.5.49"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
toml = "0.9.8" toml = "0.9.8"

View file

@ -1,10 +1,4 @@
# Default config quests_path = "quests"
accounts_path = "accounts"
# Path to quests folder relative to config map = "map.toml"
quests_path = "./quests" verbose = true
# Path to accounts folder relative to config
accounts_path = "./accounts"
# Path to map .toml file relative to config
map = "./map.toml"

13
cli/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "squad-quest-cli"
version.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
[dependencies]
chrono = "0.4.42"
clap = { version = "4.5.53", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
squad-quest = { version = "0.5.0", path = ".." }
toml = "0.9.8"

66
cli/src/cli/account.rs Normal file
View file

@ -0,0 +1,66 @@
use clap::{Args,Subcommand,ValueEnum};
#[derive(Subcommand)]
pub enum AccountCommands {
/// List accounts
List,
/// Create empty account
Create(AccountCreateArgs),
/// Update balance value
Balance(AccountBalanceArgs),
/// Approve account answer for quest
Complete(AccountCompleteArgs),
/// Delete account
Delete(AccountDeleteArgs),
/// Unlock room for account if it has enough balance
Unlock(AccountUnlockArgs),
}
#[derive(Args)]
pub struct AccountCreateArgs {
/// Account will be created with this id
pub id: String,
}
#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum AccountBalanceActions {
Set,
Add,
Remove,
}
#[derive(Args)]
pub struct AccountBalanceArgs {
/// Account id
pub id: String,
/// What to do with the balance
#[arg(value_enum)]
pub action: AccountBalanceActions,
/// Amount of doing
pub value: u32,
/// If action is remove, set balance to 0 if the result is negative instead of returning error
#[arg(short,long)]
pub negative_ok: bool,
}
#[derive(Args)]
pub struct AccountCompleteArgs {
/// Id of the account
pub account: String,
/// Id of the quest
pub quest: u16,
}
#[derive(Args)]
pub struct AccountDeleteArgs {
/// Id of the account to delete
pub id: String,
}
#[derive(Args)]
pub struct AccountUnlockArgs {
/// Id of the account
pub account: String,
/// Id of the room to unlock
pub room: u16,
}

57
cli/src/cli/map.rs Normal file
View file

@ -0,0 +1,57 @@
use clap::{Args,Subcommand};
#[derive(Subcommand)]
pub 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)]
pub struct MapAddArgs {
/// Name of the room
pub name: String,
/// Price of the room
pub value: u32,
/// Optional description for the room
#[arg(long,short)]
pub description: Option<String>,
}
#[derive(Args)]
pub struct MapConnectArgs {
/// First room ID
pub first: u16,
/// Second room ID
pub second: u16,
}
#[derive(Args)]
pub struct MapDeleteArgs {
/// ID of the room to delete
pub id: u16,
}
#[derive(Args)]
pub struct MapUpdateArgs {
/// ID of the room to update
pub id: u16,
/// Room name
#[arg(short,long)]
pub name: Option<String>,
/// Room description
#[arg(short,long)]
pub description: Option<String>,
/// Room price
#[arg(short,long)]
pub value: Option<u32>,
}

43
cli/src/cli/mod.rs Normal file
View file

@ -0,0 +1,43 @@
use std::path::PathBuf;
use clap::{Args,Parser,Subcommand};
pub mod account;
pub mod map;
pub mod quest;
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
/// Path to config
#[arg(short, long)]
pub config: PathBuf,
/// Object to make operation on
#[command(subcommand)]
pub command: Objects,
/// Suppress most output
#[arg(short, long)]
pub quiet: bool,
}
#[derive(Subcommand)]
pub enum Objects {
/// Initialize new SquadQuest in current working directory
Init(InitArgs),
/// Operations on the quests
#[command(subcommand)]
Quest(quest::QuestCommands),
/// Operations on the accounts
#[command(subcommand)]
Account(account::AccountCommands),
/// Operations on the map rooms
#[command(subcommand)]
Map(map::MapCommands),
}
#[derive(Args)]
pub struct InitArgs {
#[arg(long,short)]
pub path: Option<PathBuf>,
}

131
cli/src/cli/quest.rs Normal file
View file

@ -0,0 +1,131 @@
use squad_quest::quest::QuestDifficulty as LibQuestDifficulty;
use toml::value::Date;
use serde::Deserialize;
use clap::{Args,Subcommand,ValueEnum};
#[derive(Deserialize)]
struct DateWrapper {
date: Date,
}
fn parse_date(arg: &str) -> Result<Date,toml::de::Error> {
let toml_str = format!("date = {arg}");
let wrapper: DateWrapper = toml::from_str(&toml_str)?;
Ok(wrapper.date)
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum QuestDifficulty {
/// Easy quest
Easy,
/// Normal quest
Normal,
/// Hard quest
Hard,
/// Special case of hard quests.
Secret,
}
impl From<QuestDifficulty> for LibQuestDifficulty {
fn from(value: QuestDifficulty) -> Self {
match value {
QuestDifficulty::Easy => LibQuestDifficulty::Easy,
QuestDifficulty::Normal => LibQuestDifficulty::Normal,
QuestDifficulty::Hard => LibQuestDifficulty::Hard,
QuestDifficulty::Secret => LibQuestDifficulty::Secret,
}
}
}
#[derive(Subcommand)]
pub enum QuestCommands {
/// List available quests
List(QuestListArgs),
/// Create new quest and automatically assign it id
Create(QuestCreateArgs),
/// Update existing quest
Update(QuestUpdateArgs),
/// Delete quest
Delete(QuestDeleteArgs),
/// Make certain quests public
Daily,
/// Publish quest with specified id
Publish(QuestPublishArgs),
}
#[derive(Args)]
pub struct QuestListArgs {
/// Only list id and name of the quest
#[arg(short, long)]
pub short: bool,
}
#[derive(Args)]
pub struct QuestCreateArgs {
/// Difficulty of the quest
#[arg(value_enum)]
pub difficulty: QuestDifficulty,
/// Reward for the quest
pub reward: u32,
/// Name of the quest
pub name: String,
/// Visible description of the quest
pub description: String,
/// Answer for the quest for admins
pub answer: String,
/// Create quest and make it public immediately
#[arg(short,long)]
pub public: bool,
/// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(short,long,value_parser = parse_date)]
pub available: Option<Date>,
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(short,long,value_parser = parse_date)]
pub deadline: Option<Date>,
}
#[derive(Args)]
pub struct QuestUpdateArgs {
/// Id of the quest to update
pub id: u16,
/// Difficulty of the quest
#[arg(value_enum,long)]
pub difficulty: Option<QuestDifficulty>,
/// Reward for the quest
#[arg(long)]
pub reward: Option<u32>,
/// Name of the quest
#[arg(long)]
pub name: Option<String>,
/// Visible description of the quest
#[arg(long)]
pub description: Option<String>,
/// Answer for the quest for admins
#[arg(long)]
pub answer: Option<String>,
/// Create quest and make it public immediately
#[arg(long)]
pub public: Option<bool>,
/// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(long,value_parser = parse_date)]
pub available: Option<Date>,
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(long,value_parser = parse_date)]
pub deadline: Option<Date>,
}
#[derive(Args)]
pub struct QuestDeleteArgs {
/// Id of the quest to delete
pub id: u16,
}
#[derive(Args)]
pub struct QuestPublishArgs {
/// Id of the quest to publish
pub id: u16,
/// Make it non-public instead
#[arg(long,short)]
pub reverse: bool,
}

1
cli/src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod cli;

450
cli/src/main.rs Normal file
View file

@ -0,0 +1,450 @@
use std::{fs::DirBuilder, path::{Path, PathBuf}};
use clap::Parser;
use squad_quest_cli::cli::{Cli,Objects,account::*,map::*,quest::*};
use squad_quest::{SquadObject, account::Account, config::Config, error::Error, map::{Map, Room}, quest::Quest};
use toml::value::Date;
use chrono::{Datelike, NaiveDate, Utc};
fn print_quest_short(quest: &Quest) {
println!("Quest #{}: {}", quest.id, quest.name);
}
fn print_quest_long(quest: &Quest) {
print_quest_short(quest);
println!("Difficulty: {:?}", quest.difficulty);
println!("Description:\n{}", quest.description);
println!("Answer:\n{}", quest.answer);
}
fn do_and_log(result: Result<(),Error>, log: bool, ok_text: String) {
match result {
Ok(_) if log => println!("{ok_text}"),
Err(error) if log => eprintln!("Error: {error}"),
_ => {},
}
}
fn load_config_silent(quiet: bool, path: PathBuf) -> Config {
match quiet {
false => Config::load(path.clone()),
true => {
match Config::try_load(path.clone()) {
Ok(mut config) => {
config.verbose = false;
config
},
Err(_) => {
let path = path.clone().parent().unwrap_or(&Path::new(".")).to_owned();
Config {
verbose: false,
path,
..Default::default()
}
}
}
},
}
}
fn main() {
let cli = Cli::parse();
let config = load_config_silent(cli.quiet, cli.config.clone());
let map_save = |map: Map, map_path: PathBuf| { map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) };
match &cli.command {
Objects::Init(args) => {
let path = match args.path.clone() {
Some(path) => path,
None => PathBuf::new(),
};
match DirBuilder::new().recursive(true).create(path.clone()) {
Ok(_) if !cli.quiet => println!("Created directory {:?}", path),
Err(error) => {
if !cli.quiet { eprintln!("Error: {error}"); }
return;
},
_ => {},
}
let config = Config {
path: path.clone(),
..Default::default()
};
do_and_log(config.save(path.clone()), !cli.quiet, format!("Created file {:?}/config.toml", path));
let mut config_path = path.clone();
config_path.push("config.toml");
let mut config = load_config_silent(true, config_path);
config.verbose = Config::default().verbose;
let map = Map::default();
let map_path = config.full_map_path();
do_and_log(map_save(map, map_path.clone()), !cli.quiet, format!("Created file {:?}/map.toml", map_path));
let quests_path = config.full_quests_path();
let accounts_path = config.full_accounts_path();
for path in [quests_path, accounts_path] {
match DirBuilder::new().recursive(true).create(path.clone()) {
Ok(_) if !cli.quiet => println!("Created directory {:?}", path),
Err(error) => {
if !cli.quiet { eprintln!("Error: {error}"); }
return;
},
_ => {},
}
}
},
Objects::Quest(commands) => {
let mut quests = config.load_quests();
let mut path = config.full_quests_path();
match commands {
QuestCommands::List(args) => {
for quest in quests {
if args.short {
print_quest_short(&quest);
} else {
print_quest_long(&quest);
}
}
},
QuestCommands::Create(args) => {
quests.sort_by(|a,b| a.id.cmp(&b.id));
let next_id = match quests.last() {
Some(quest) => quest.id + 1u16,
None => 0u16
};
let mut check_path = path.clone();
check_path.push(format!("{next_id}.toml"));
match std::fs::exists(&check_path) {
Ok(exists) => {
if exists {
if !cli.quiet { eprintln!("Error: {:?} is not empty.", path); }
return;
}
},
Err(error) => {
if !cli.quiet {
eprintln!("Error: {error}");
}
return;
}
}
let quest = Quest {
id: next_id,
difficulty: args.difficulty.into(),
reward: args.reward,
name: args.name.clone(),
description: args.description.clone(),
answer: args.answer.clone(),
public: args.public,
available_on: args.available.clone(),
deadline: args.deadline.clone()
};
do_and_log(quest.save(path), !cli.quiet, format!("Created quest #{}.", quest.id));
},
QuestCommands::Update(args) => {
let Some(quest) = quests.iter().find(|q| q.id == args.id) else {
if !cli.quiet { eprintln!("Error: Quest #{} not found.", args.id); }
return;
};
let quest = Quest {
id: args.id,
difficulty: match args.difficulty {
Some(diff) => diff.into(),
None => quest.difficulty
},
reward: args.reward.unwrap_or(quest.reward),
name: args.name.clone().unwrap_or(quest.name.clone()),
description: args.description.clone().unwrap_or(quest.description.clone()),
answer: args.answer.clone().unwrap_or(quest.answer.clone()),
public: args.public.unwrap_or(quest.public),
available_on: args.available.clone().or(quest.available_on.clone()),
deadline: args.deadline.clone().or(quest.deadline.clone())
};
do_and_log(quest.save(path), !cli.quiet, format!("Updated quest #{}.", quest.id));
},
QuestCommands::Delete(args) => {
path.push(format!("{}.toml", args.id));
match Quest::delete(path) {
Ok(_) => {
if !cli.quiet { println!("Deleted quest #{}.", args.id); }
let mut accounts = config.load_accounts();
let accounts_path = config.full_accounts_path();
for account in accounts.iter_mut() {
if let Some(index) = account.quests_completed.iter().position(|qid| *qid == args.id) {
account.quests_completed.remove(index);
do_and_log(account.save(accounts_path.clone()), !cli.quiet, format!("Removed quest #{} from account \"{}\" completed quests", args.id, account.id));
}
}
},
Err(error) if !cli.quiet => {
eprintln!("Error: {error}");
},
_ => {},
}
},
QuestCommands::Daily => {
let today: NaiveDate = Utc::now().date_naive();
let toml_today = Date {
year: today.year() as u16,
month: today.month() as u8,
day: today.day() as u8
};
for quest in quests.iter_mut().filter(|q| !q.public && q.available_on.is_some_and(|date| date.le(&toml_today))) {
quest.public = true;
do_and_log(quest.save(path.clone()), !cli.quiet, format!("Published quest #{}.", quest.id));
}
},
QuestCommands::Publish(args) => {
let quest = quests.iter_mut().find(|q| q.id == args.id);
match quest {
Some(quest) => {
let not_str = if args.reverse {" not "} else {" "};
if quest.public != args.reverse {
if !cli.quiet { eprintln!("Error: quest #{} is already{}public.", quest.id, not_str); }
return;
}
quest.public = !args.reverse;
do_and_log(quest.save(path), !cli.quiet, format!("Published quest #{}.", quest.id));
},
None if !cli.quiet => eprintln!("Error: quest #{} not found.", args.id),
_ => {},
}
},
}
},
Objects::Account(commands) => {
let mut accounts = config.load_accounts();
let mut path = config.full_accounts_path();
match commands {
AccountCommands::List => {
for account in accounts {
println!("\"{}\": Balance {}", account.id, account.balance);
}
},
AccountCommands::Create(args) => {
let account = Account {
id: args.id.clone(),
..Default::default()
};
if let Some(_) = accounts.iter().find(|a| a.id == account.id) {
if !cli.quiet { eprintln!("Error: account \"{}\" exists.", account.id); }
return;
}
do_and_log(account.save(path), !cli.quiet, format!("Created account \"{}\".", account.id));
},
AccountCommands::Balance(args) => {
let Some(account) = accounts.iter_mut().find(|a| a.id == args.id) else {
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.id); }
return;
};
match args.action {
AccountBalanceActions::Set => {
account.balance = args.value;
},
AccountBalanceActions::Add => {
account.balance += args.value;
},
AccountBalanceActions::Remove => {
if args.value > account.balance {
if args.negative_ok {
account.balance = 0u32;
} else {
if !cli.quiet { eprintln!("Error: account \"{}\" balance is less than {}.", account.id, args.value); }
return;
}
} else {
account.balance -= args.value;
}
}
}
do_and_log(account.save(path), !cli.quiet, format!("Updated balance of account \"{}\".", account.id));
},
AccountCommands::Complete(args) => {
let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else {
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account); }
return;
};
let quests = config.load_quests();
let quest = match quests.iter().find(|q| q.id == args.quest) {
Some(quest) => quest,
None => {
if !cli.quiet { eprintln!("Error: quest #{} not found.", args.quest); }
return;
},
};
match quest.complete_for_account(account) {
Err(error) if !cli.quiet => println!("Error: {error}"),
Ok(_) => do_and_log(account.save(path), !cli.quiet, format!("Completed quest #{} on account \"{}\".", args.quest, account.id)),
_ => {},
}
},
AccountCommands::Delete(args) => {
path.push(format!("{}.toml", args.id));
do_and_log(Account::delete(path), !cli.quiet, format!("Deleted account \"{}\".", args.id))
},
AccountCommands::Unlock(args) => {
let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else {
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account) };
return;
};
let map = match Map::load(config.full_map_path()) {
Ok(map) => map,
Err(error) => {
if !cli.quiet { eprintln!("Error: {error}"); }
return;
}
};
if let Err(error) = map.unlock_room_for_account(args.room, account) {
eprintln!("Error: {error}");
return;
}
do_and_log(account.save(path), !cli.quiet, format!("Unlocked room #{} for account \"{}\"", args.room, args.account));
},
}
},
Objects::Map(commands) => {
let map_path = config.full_map_path();
let mut map = match Map::load(map_path.clone()) {
Ok(map) => map,
Err(error) => {
if !cli.quiet { eprintln!("Error: {error}"); }
return;
}
};
match commands {
MapCommands::List => {
for room in map.room {
println!("Room #{}: {}; Connections: {:?}", room.id, room.name, room.children);
}
},
MapCommands::Add(args) => {
map.room.sort_by(|a,b| a.id.cmp(&b.id));
let last_id = match map.room.last() {
Some(r) => r.id + 1u16,
None => 0u16
};
let room = Room {
id: last_id,
name: args.name.clone(),
value: args.value,
description: args.description.clone(),
..Default::default()
};
let r_id = room.id;
map.room.push(room);
do_and_log(map_save(map, map_path), !cli.quiet, format!("Created room #{r_id}."))
},
MapCommands::Delete(args) => {
let Some(room) = map.room.iter().find(|r| r.id == args.id) else {
if !cli.quiet { eprintln!("Error: room #{} not found.", args.id); }
return;
};
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, map_path) {
Ok(_) => {
if !cli.quiet { println!("Deleted room #{r_id}."); }
let mut accounts = config.load_accounts();
let accounts_path = config.full_accounts_path();
for account in accounts.iter_mut() {
if let Some(index) = account.rooms_unlocked.iter().position(|rid| *rid == r_id) {
account.rooms_unlocked.remove(index);
do_and_log(account.save(accounts_path.clone()), !cli.quiet, format!("Removed room #{r_id} from account \"{}\" unlocked rooms.", account.id));
}
}
},
Err(error) if !cli.quiet => {
eprintln!("Error: {error}");
},
_ => {},
}
},
MapCommands::Update(args) => {
let Some(room) = map.room.iter_mut().find(|r| r.id == args.id) else {
if !cli.quiet { eprintln!("Error: room #{} not found", args.id); }
return;
};
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;
}
do_and_log(map_save(map, map_path), !cli.quiet, format!("Updated room #{}.", args.id))
},
MapCommands::Connect(args) | MapCommands::Disconnect(args) => {
let connect = match commands {
MapCommands::Connect(_) => true,
_ => false,
};
// We iterate twice to make references first->second and second->first
for (first, second) in [(args.first, args.second),(args.second, args.first)] {
let Some(room) = map.room.iter_mut().find(|r| r.id == first) else {
if !cli.quiet { eprintln!("Error: room #{} not found.", first); }
return;
};
match room.children.iter().position(|id| *id == second) {
Some(_) if connect && !cli.quiet => println!("Room #{} already has reference to #{}.", first, second),
None if connect => room.children.push(second),
Some(id) if !connect => {room.children.remove(id as usize);},
None if !connect && !cli.quiet => println!("Room #{} has no reference to #{}.", first, second),
_ => {},
}
}
let connected = if connect { "Connected" } else { "Disconnected" };
do_and_log(map_save(map, map_path), !cli.quiet, format!("{connected} rooms #{} <-> #{}.", args.first, args.second));
},
}
}
}
}

View file

@ -1,303 +0,0 @@
use std::path::PathBuf;
use clap::{Parser,Subcommand,Args,ValueEnum};
use serde::Deserialize;
use squad_quest::{SquadObject, config::Config, quest::{Quest,QuestDifficulty as LibQuestDifficulty}};
use toml::value::Date;
use chrono::{Datelike, NaiveDate, Utc};
#[derive(Deserialize)]
struct DateWrapper {
date: Date
}
fn parse_date(arg: &str) -> Result<Date,toml::de::Error> {
let toml_str = format!("date = {arg}");
let wrapper: DateWrapper = toml::from_str(&toml_str)?;
Ok(wrapper.date)
}
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
/// Path to config
#[arg(short, long)]
config: PathBuf,
/// Object to make operation on
#[command(subcommand)]
command: Objects,
}
#[derive(Subcommand)]
enum Objects {
/// Operations on the quests
#[command(subcommand)]
Quest(QuestCommands)
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum QuestDifficulty {
/// Easy quest
Easy,
/// Normal quest
Normal,
/// Hard quest
Hard,
/// Special case of hard quests.
Secret
}
impl From<QuestDifficulty> for LibQuestDifficulty {
fn from(value: QuestDifficulty) -> Self {
match value {
QuestDifficulty::Easy => LibQuestDifficulty::Easy,
QuestDifficulty::Normal => LibQuestDifficulty::Normal,
QuestDifficulty::Hard => LibQuestDifficulty::Hard,
QuestDifficulty::Secret => LibQuestDifficulty::Secret,
}
}
}
#[derive(Subcommand)]
enum QuestCommands {
/// List available quests
List(QuestListArgs),
/// Create new quest and automatically assign it id
Create(QuestCreateArgs),
/// Update existing quest
Update(QuestUpdateArgs),
/// Delete quest
Delete(QuestDeleteArgs),
/// Make certain quests public
Daily,
/// Publish quest with specified id
Publish(QuestPublishArgs),
}
#[derive(Args)]
struct QuestListArgs {
/// Only list id and name of the quest
#[arg(short, long)]
short: bool
}
#[derive(Args)]
struct QuestCreateArgs { /// Difficulty of the quest #[arg(value_enum)]
difficulty: QuestDifficulty,
/// Reward for the quest
reward: u32,
/// Name of the quest
name: String,
/// Visible description of the quest
description: String,
/// Answer for the quest for admins
answer: String,
/// Create quest and make it public immediately
#[arg(short,long)]
public: bool,
/// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(short,long,value_parser = parse_date)]
available: Option<Date>,
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(short,long,value_parser = parse_date)]
deadline: Option<Date>,
}
#[derive(Args)]
struct QuestUpdateArgs {
/// Id of the quest to update
id: u16,
/// Difficulty of the quest
#[arg(value_enum,long)]
difficulty: Option<QuestDifficulty>,
/// Reward for the quest
#[arg(long)]
reward: Option<u32>,
/// Name of the quest
#[arg(long)]
name: Option<String>,
/// Visible description of the quest
#[arg(long)]
description: Option<String>,
/// Answer for the quest for admins
#[arg(long)]
answer: Option<String>,
/// Create quest and make it public immediately
#[arg(long)]
public: Option<bool>,
/// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(long,value_parser = parse_date)]
available: Option<Date>,
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
#[arg(long,value_parser = parse_date)]
deadline: Option<Date>,
}
#[derive(Args)]
struct QuestDeleteArgs {
/// Id of the quest to delete
id: u16
}
#[derive(Args)]
struct QuestPublishArgs {
/// Id of the quest to publish
id: u16,
/// Make it non-public instead
#[arg(long,short)]
reverse: bool,
}
fn print_quest_short(quest: &Quest) {
println!("Quest #{}: {}", quest.id, quest.name);
}
fn print_quest_long(quest: &Quest) {
print_quest_short(quest);
println!("Difficulty: {:?}", quest.difficulty);
println!("Description:\n{}", quest.description);
println!("Answer:\n{}", quest.answer);
}
fn main() {
let cli = Cli::parse();
let config = Config::load(cli.config.clone());
match &cli.command {
Objects::Quest(commands) => {
match commands {
QuestCommands::List(args) => {
let quests = config.load_quests();
for quest in quests {
if args.short {
print_quest_short(&quest);
} else {
print_quest_long(&quest);
}
}
},
QuestCommands::Create(args) => {
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
};
let path = config.full_quests_path();
let mut quest_path = path.clone();
quest_path.push(format!("{next_id}.toml"));
match std::fs::exists(&quest_path) {
Ok(exists) => {
if exists {
panic!("Error: {:?} is not empty.", quest_path);
}
},
Err(error) => {
panic!("Error while retrieving {:?}: {}.", quest_path, error);
}
}
let quest = Quest {
id: next_id,
difficulty: args.difficulty.into(),
reward: args.reward,
name: args.name.clone(),
description: args.description.clone(),
answer: args.answer.clone(),
public: args.public,
available_on: args.available.clone(),
deadline: args.deadline.clone()
};
if let Err(error) = quest.save(path) {
eprintln!("Error while saving quest: {error}.");
} else {
println!("Successfully saved quest #{}.", quest.id);
}
},
QuestCommands::Update(args) => {
let quests = config.load_quests();
let Some(quest) = quests.iter().find(|q| q.id == args.id) else {
panic!("Error: Quest #{} not found.", args.id);
};
let quest = Quest {
id: args.id,
difficulty: match args.difficulty {
Some(diff) => diff.into(),
None => quest.difficulty
},
reward: args.reward.unwrap_or(quest.reward),
name: args.name.clone().unwrap_or(quest.name.clone()),
description: args.description.clone().unwrap_or(quest.description.clone()),
answer: args.answer.clone().unwrap_or(quest.answer.clone()),
public: args.public.unwrap_or(quest.public),
available_on: args.available.clone().or(quest.available_on.clone()),
deadline: args.deadline.clone().or(quest.deadline.clone())
};
let path = config.full_quests_path();
match quest.save(path) {
Ok(_) => println!("Updated quest #{}", quest.id),
Err(error) => eprintln!("Error while updating quest: {error}")
}
},
QuestCommands::Delete(args) => {
let mut path = config.full_quests_path();
path.push(format!("{}.toml", args.id));
match Quest::delete(path) {
Ok(_) => println!("Successfully deleted quest #{}", args.id),
Err(error) => eprintln!("Error deleting quest #{}: {}", args.id, error),
}
},
QuestCommands::Daily => {
let mut quests = config.load_quests();
let today: NaiveDate = Utc::now().date_naive();
let toml_today = Date {
year: today.year() as u16,
month: today.month() as u8,
day: today.day() as u8
};
let path = config.full_quests_path();
for quest in quests.iter_mut().filter(|q| !q.public && q.available_on.is_some_and(|date| date.le(&toml_today))) {
println!("Quest #{} will be published.", quest.id);
quest.public = true;
if let Err(error) = quest.save(path.clone()) {
eprintln!("Error while saving quest: {error}.");
}
}
},
QuestCommands::Publish(args) => {
let mut quests = config.load_quests();
let quest = quests.iter_mut().find(|q| q.id == args.id);
let path = config.full_quests_path();
match quest {
Some(quest) => {
let not_str = if args.reverse {" not "} else {" "};
if quest.public != args.reverse {
println!("Quest #{} is already{}public", quest.id, not_str);
return;
}
quest.public = !args.reverse;
if let Err(error) = quest.save(path) {
eprintln!("Error while saving quest: {error}.");
};
},
None => {
eprintln!("Error: couldn't find quest with id {}.", args.id);
}
}
}
}
}
}
}

View file

@ -1,17 +1,17 @@
//! Configuration file that handles (de-)serializing other components //! Configuration file that handles (de-)serializing other components
use std::{fs::{self, DirEntry},path::{Path, PathBuf}}; use std::{fs::{self, DirEntry}, io::Write, path::{Path, PathBuf}};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::{SquadObject, account::Account, error::Error, quest::Quest}; use crate::{SquadObject, account::Account, error::Error, quest::Quest};
/// Struct for containing paths to other (de-)serializable things /// Struct for containing paths to other (de-)serializable things
#[derive(Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Config { pub struct Config {
/// Path to config directory /// Path to config directory
#[serde(skip)] #[serde(skip)]
path: PathBuf, pub path: PathBuf,
/// Path to serialized [quests][`crate::quest::Quest`] folder /// Path to serialized [quests][`crate::quest::Quest`] folder
pub quests_path: PathBuf, pub quests_path: PathBuf,
@ -20,7 +20,10 @@ pub struct Config {
pub accounts_path: PathBuf, pub accounts_path: PathBuf,
/// Path to serialized [map][`crate::map::Map`] file /// Path to serialized [map][`crate::map::Map`] file
pub map: PathBuf pub map: PathBuf,
/// If true, print to std{out/err}
pub verbose: bool,
} }
impl Default for Config { impl Default for Config {
@ -29,7 +32,8 @@ impl Default for Config {
path: ".".into(), path: ".".into(),
quests_path: "quests".into(), quests_path: "quests".into(),
accounts_path: "accounts".into(), accounts_path: "accounts".into(),
map: "map.toml".into() map: "map.toml".into(),
verbose: true,
} }
} }
} }
@ -67,8 +71,12 @@ fn handle_account_entry(account_entry: DirEntry) -> Result<Account, Error>{
} }
impl Config { impl Config {
/// Deserialize config from TOML /// Deserialize config from TOML.
/// Logs all errors and returns default config if that happens ///
/// This function wraps [try_load][Config::try_load].
///
/// Logs all errors if `config.verbose == true`.
/// Returns default config on error.
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
@ -81,25 +89,98 @@ impl Config {
let dir = path.parent() let dir = path.parent()
.unwrap_or(Path::new(".")) .unwrap_or(Path::new("."))
.to_owned(); .to_owned();
match Self::try_load(path) {
Ok(conf) => {
if conf.verbose {
println!("Successfully loaded config");
}
conf
},
Err(error) => {
let conf = Config {
path: dir,
..Default::default()
};
if conf.verbose {
println!("Error while loading config: {error}");
}
conf
}
}
}
/// Serialize config into TOML.
/// Config will be saved as `path/config.toml`
///
/// # Examples
/// ```rust
/// use squad_quest::config::Config;
///
/// let path = "cfg".into();
///
/// let config = Config::default();
///
/// if let Err(error) = config.save(path) {
/// // handle error
/// }
/// ```
pub fn save(&self, path: PathBuf) -> Result<(), Error> {
let mut path = path;
path.push("config.toml");
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(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(())
}
/// Deserialize config from TOML
///
/// # Examples
/// ```rust
/// use squad_quest::{config::Config,error::Error};
/// # fn main() {
/// # let _ = wrapper();
/// # }
/// # fn wrapper() -> Result<(), Error> {
/// let path = "cfg/config.toml".into();
/// let config = Config::try_load(path)?;
/// # Ok(())
/// # }
/// ```
pub fn try_load(path: PathBuf) -> Result<Self, Error> {
let dir = path.parent()
.unwrap_or(Path::new("."))
.to_owned();
match fs::read_to_string(path) { match fs::read_to_string(path) {
Ok(string) => { Ok(string) => {
match toml::from_str::<Config>(&string) { match toml::from_str::<Config>(&string) {
Ok(mut conf) => { Ok(mut conf) => {
println!("Successfully loaded config");
conf.path = dir; conf.path = dir;
conf Ok(conf)
}, },
Err(error) => { Err(error) => {
eprintln!("Error on parsing config: {error}"); Err(Error::TomlDeserializeError(error))
let mut cfg = Config::default();
cfg.path = dir;
cfg
} }
} }
}, },
Err(error) => { Err(error) => {
eprintln!("Error on reading config path: {error}"); Err(Error::IoError(error))
Config::default()
} }
} }
} }
@ -150,28 +231,33 @@ impl Config {
Ok(quest_entry) => { Ok(quest_entry) => {
match handle_quest_entry(quest_entry) { match handle_quest_entry(quest_entry) {
Ok(quest) => out_vec.push(quest), Ok(quest) => out_vec.push(quest),
Err(error) => { Err(error) if self.verbose => {
eprintln!("Error on loading single quest: {error}"); eprintln!("Error on loading single quest: {error}");
} },
_ => {},
} }
}, },
Err(error) => { Err(error) if self.verbose => {
eprintln!("Error on loading single quest: {error}"); eprintln!("Error on loading single quest: {error}");
} },
_ => {},
} }
} }
}, },
Err(error) => { Err(error) if self.verbose => {
eprintln!("Error on loading quests: {error}"); eprintln!("Error on loading quests: {error}");
} },
_ => {},
}
if self.verbose {
println!("Loaded {} quests successfully", out_vec.len());
} }
println!("Loaded {} quests successfully", out_vec.len());
out_vec out_vec
} }
/// Returns full path to quests folder /// Returns full path to accounts folder
/// This path will be relative to $PWD, not to config. /// This path will be relative to $PWD, not to config.
/// ///
/// # Examples /// # Examples
@ -217,24 +303,54 @@ impl Config {
Ok(acc_entry) => { Ok(acc_entry) => {
match handle_account_entry(acc_entry) { match handle_account_entry(acc_entry) {
Ok(quest) => out_vec.push(quest), Ok(quest) => out_vec.push(quest),
Err(error) => { Err(error) if self.verbose => {
eprintln!("Error on loading single account: {error}"); eprintln!("Error on loading single account: {error}");
} },
_ => {},
} }
}, },
Err(error) => { Err(error) if self.verbose => {
eprintln!("Error on loading single account: {error}"); eprintln!("Error on loading single account: {error}");
} },
_ => {},
} }
} }
}, },
Err(error) => { Err(error) if self.verbose => {
eprintln!("Error on loading accounts: {error}"); eprintln!("Error on loading accounts: {error}");
} },
_ => {},
} }
println!("Loaded {} accounts successfully", out_vec.len()); if self.verbose {
println!("Loaded {} accounts successfully", out_vec.len());
}
out_vec 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
}
} }

View file

@ -26,3 +26,41 @@ impl fmt::Display for Error {
} }
} }
} }
/// Error related to quest logic
#[derive(Debug)]
#[non_exhaustive]
pub enum QuestError {
/// Quest (self.0) is already completed for given account (self.1)
AlreadyCompleted(u16, String),
}
impl fmt::Display for QuestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyCompleted(quest_id, account_id) => write!(f, "quest #{quest_id} is already completed for account \"{account_id}\""),
}
}
}
/// Error related to map logic
#[derive(Debug)]
#[non_exhaustive]
pub enum MapError {
/// Room not found in map file
RoomNotFound(u16),
/// Room (self.0) is already unlocked on account (self.1)
RoomAlreadyUnlocked(u16, String),
/// Account (self.1) does not have much money (self.0)
InsufficientFunds(u16, String),
}
impl fmt::Display for MapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RoomNotFound(id) => write!(f, "could not find room #{id}"),
Self::RoomAlreadyUnlocked(room_id, account_id) => write!(f, "room #{room_id} is already unlocked on account \"{account_id}\""),
Self::InsufficientFunds(room_id, account_id) => write!(f, "account \"{account_id}\" does not have enough money to unlock room #{room_id}"),
}
}
}

View file

@ -1,5 +1,143 @@
//! Map, a.k.a. a graph of rooms //! Map, a.k.a. a graph of rooms
#![allow(dead_code)]
/// Graph for room nodes use std::{fs, io::Write, path::PathBuf};
pub struct Map;
use serde::{Deserialize, Serialize};
use crate::{SquadObject, account::Account, error::{Error, MapError}};
/// 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(())
}
}
impl Map {
/// Try to unlock room for account, or return [MapError]
///
/// # Examples
/// ```rust
/// use squad_quest::{account::Account,map::{Map,Room},error::MapError};
///
/// let map = Map {
/// room: vec![Room { id: 0, value: 100, ..Default::default() }],
/// };
///
/// let mut account = Account { balance: 100, ..Default::default() };
///
/// if let Err(error) = map.unlock_room_for_account(0, &mut account) {
/// // handle error
/// }
/// ```
pub fn unlock_room_for_account(&self, room_id: u16, account: &mut Account) -> Result<(), MapError> {
let Some(room) = self.room.iter().find(|r| r.id == room_id) else {
return Err(MapError::RoomNotFound(room_id));
};
if let Some(_) = account.rooms_unlocked.iter().find(|rid| **rid == room_id) {
return Err(MapError::RoomAlreadyUnlocked(room_id, account.id.clone()));
}
if account.balance < room.value {
return Err(MapError::InsufficientFunds(room_id, account.id.clone()));
}
account.balance -= room.value;
account.rooms_unlocked.push(room_id);
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,
}
}
}

View file

@ -3,7 +3,7 @@
use std::{fs, io::Write, path::PathBuf}; use std::{fs, io::Write, path::PathBuf};
use serde::{ Serialize, Deserialize }; use serde::{ Serialize, Deserialize };
use crate::{SquadObject, error::Error}; use crate::{SquadObject, account::Account, error::{Error, QuestError}};
use toml::value::Date; use toml::value::Date;
/// Difficulty of the quest /// Difficulty of the quest
@ -137,3 +137,32 @@ impl SquadObject for Quest {
Ok(()) Ok(())
} }
} }
impl Quest {
/// Complete quest for account and add reward to it's balance.
/// Does nothing and returns [QuestError::AlreadyCompleted]
/// if it is already completed.
///
/// # Examples
///
/// ```rust
/// use squad_quest::{account::Account,quest::Quest};
///
/// let quest = Quest::default();
/// let mut account = Account::default();
///
/// if let Err(error) = quest.complete_for_account(&mut account) {
/// // handle error
/// }
/// ```
pub fn complete_for_account(&self, account: &mut Account) -> Result<(),QuestError> {
match account.quests_completed.iter().find(|qid| **qid == self.id) {
Some(_) => Err(QuestError::AlreadyCompleted(self.id, account.id.clone())),
None => {
account.quests_completed.push(self.id);
account.balance += self.reward;
Ok(())
},
}
}
}

View file

@ -1,5 +1,4 @@
use squad_quest::{SquadObject, config::Config, error::Error, quest::Quest}; use squad_quest::{SquadObject, account::Account, config::Config, quest::Quest};
use std::path::PathBuf;
const CONFIG_PATH: &str = "tests/io/config.toml"; const CONFIG_PATH: &str = "tests/io/config.toml";
@ -7,23 +6,35 @@ const CONFIG_PATH: &str = "tests/io/config.toml";
// and Quest::save can override files, // and Quest::save can override files,
// so this test covers full quest CRUD // so this test covers full quest CRUD
#[test] #[test]
fn quest_crud() -> Result<(), Error> { fn quest_crud() {
let config = Config::load(CONFIG_PATH.into()); let config = Config::load(CONFIG_PATH.into());
let mut quests_path = PathBuf::from(CONFIG_PATH).parent().unwrap().to_owned(); let mut quests_path = config.full_quests_path();
quests_path.push(config.quests_path);
let quest = Quest::default(); let quest = Quest::default();
println!("{:?}", quests_path.clone()); quest.save(quests_path.clone()).unwrap();
quest.save(quests_path.clone())?;
let filename = format!("{}.toml", quest.id); let filename = format!("{}.toml", quest.id);
quests_path.push(filename); quests_path.push(filename);
Quest::delete(quests_path)?; Quest::delete(quests_path).unwrap();
}
Ok(())
#[test]
fn account_crud() {
let config = Config::load(CONFIG_PATH.into());
let mut accounts_path = config.full_accounts_path();
let account = Account::default();
account.save(accounts_path.clone()).unwrap();
let filename = format!("{}.toml", account.id);
accounts_path.push(filename);
Account::delete(accounts_path).unwrap();
} }

View file

@ -0,0 +1 @@
Placeholder file for git

1
tests/io/map.toml Normal file
View file

@ -0,0 +1 @@
room = []

View file

@ -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"; static CONFIG_PATH: &str = "./tests/main/config.toml";
@ -81,3 +81,50 @@ fn account_test() {
assert_eq!(*account, expected); 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
View 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"