diff --git a/Cargo.lock b/Cargo.lock index e572bb9..682f802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -59,12 +74,41 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cc" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "compact_str" version = "0.8.0" @@ -79,6 +123,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossterm" version = "0.28.1" @@ -149,11 +199,16 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" name = "el_diabolo" version = "0.2.3" dependencies = [ + "chrono", "crossterm", + "homedir", "macros", "petgraph", "rand", "ratatui", + "serde", + "serde_json", + "tempfile", "whoami", ] @@ -173,6 +228,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -218,6 +279,41 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "homedir" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" +dependencies = [ + "cfg-if", + "nix", + "widestring", + "windows", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -321,6 +417,12 @@ dependencies = [ "syn", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "mio" version = "1.0.2" @@ -335,10 +437,31 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.18.0" +name = "nix" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "parking_lot" @@ -474,9 +597,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.4.1", "errno", @@ -503,6 +626,44 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -584,6 +745,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -712,6 +886,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -734,6 +914,68 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index e057f89..24490f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,11 @@ rand = "0.8.5" petgraph = "0.6.5" whoami = "1.5.2" macros = { path = "./macros" } +serde = {version="1.0.210", features = ["derive"]} +serde_json = "1.0.128" +homedir = "0.3.4" +tempfile = "3.14.0" +chrono = "0.4.38" [package.metadata.deb] maintainer = "Joachim Lusiardi " diff --git a/src/highscore.rs b/src/highscore.rs new file mode 100644 index 0000000..c95cec0 --- /dev/null +++ b/src/highscore.rs @@ -0,0 +1,92 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::{fs::File, io::Write, path::Path}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct HighscoreEntry { + pub player_name: String, + pub experience: usize, + pub gold: usize, + pub timestamp: i64, +} + +impl HighscoreEntry { + pub fn new(player_name: String, experience: usize, gold: usize) -> Self { + HighscoreEntry { + player_name, + experience, + gold, + timestamp: Utc::now().timestamp(), + } + } + pub fn get_date(&self) -> String { + let dt = DateTime::from_timestamp(self.timestamp, 0).unwrap(); + format!("{}", dt.format("%Y-%m-%d")) + } +} + +pub type HighscoreList = Vec; + +pub trait Sortable { + fn insert_and_load>(path: P, score: HighscoreEntry) -> Self; +} + +impl Sortable for HighscoreList { + fn insert_and_load>(path: P, score: HighscoreEntry) -> Self { + let file = File::open(&path); + + let mut result: HighscoreList = match file { + Ok(file) => { + if let Ok(result) = serde_json::from_reader(&file) { + result + } else { + HighscoreList::new() + } + } + Err(_) => HighscoreList::new(), + }; + result.push(score); + result.sort_by_key(|k| k.experience); + result.reverse(); + let mut file = File::create(path).unwrap(); + file.write_all(serde_json::to_string(&result).unwrap().as_bytes()) + .unwrap(); + file.sync_all().unwrap(); + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use tempfile::NamedTempFile; + + #[test] + fn test_highscore_list() { + let file = NamedTempFile::new().unwrap(); + let tmpfile = file.path(); + + let list = HighscoreList::insert_and_load( + Path::new(tmpfile), + HighscoreEntry::new("noob".to_string(), 23, 0), + ); + assert_eq!(list.len(), 1); + assert_eq!(list.first().unwrap().experience, 23); + + let list = HighscoreList::insert_and_load( + Path::new(tmpfile), + HighscoreEntry::new("noob".to_string(), 42, 1), + ); + assert_eq!(list.len(), 2); + assert_eq!(list.first().unwrap().experience, 42); + + let list = HighscoreList::insert_and_load( + Path::new(tmpfile), + HighscoreEntry::new("noob".to_string(), 41, 2), + ); + assert_eq!(list.len(), 3); + assert_eq!(list.first().unwrap().experience, 42); + assert_eq!(list[1].experience, 41); + } +} diff --git a/src/main.rs b/src/main.rs index dfc3468..c78d738 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,10 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; +use highscore::HighscoreEntry; +use highscore::HighscoreList; +use highscore::Sortable; +use homedir::my_home; use ratatui::prelude::*; use ratatui::widgets::{Block, BorderType, Borders, Wrap}; use ratatui::{ @@ -29,6 +33,7 @@ use crate::position::HasPosition; mod artifacts; mod constants; mod game; +mod highscore; mod level; mod level_generator; mod level_ladder; @@ -207,19 +212,43 @@ fn main() -> Result<()> { } ticks += 1; } + handle_game_end_screen(start_time, game, terminal); + stdout().execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + Ok(()) +} + +fn handle_game_end_screen( + start_time: Instant, + mut game: Game, + mut terminal: Terminal>, +) { let playtime = start_time.elapsed(); + let statsfile = my_home() + .unwrap() + .unwrap() + .as_path() + .join(".eldiablo_highscore.json"); + let highscores = HighscoreList::insert_and_load( + statsfile, + HighscoreEntry::new( + game.get_player().get_name(), + game.get_player().get_experience(), + game.get_player().get_gold(), + ), + ); loop { let _ = terminal.draw(|frame| { let mut area = frame.area(); let w = area.width / 2; let h = area.height / 2; - area.x += w - 20; + area.x += w - 30; area.y += h - 10; - area.width = 40; - area.height = 20; + area.width = 60; + area.height = 10; let block = Block::default() .title_top(Line::from(" Game ended ").centered()) - .title_bottom(Line::from("Press `q` to quit!")) + // .title_bottom(Line::from("Press `q` to quit!")) .borders(Borders::ALL) .border_style(Style::default().fg(Color::White)) .border_type(BorderType::Rounded) @@ -245,16 +274,33 @@ fn main() -> Result<()> { text += format!("\nYou played {} seconds.", playtime.as_secs()).as_str(); let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); frame.render_widget(paragraph, area); + let block = Block::default() + .title_top(Line::from(" Highscore ").centered()) + .title_bottom(Line::from("Press `q` to quit!")) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::White)) + .border_type(BorderType::Rounded) + .style(Style::default().bg(Color::Black)); + let mut text = "".to_string(); + for e in &highscores { + text += &format!( + "{}: {} XP / {} Gold on {}\n", + e.player_name, + e.experience, + e.gold, + e.get_date() + ); + } + let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); + area.y += 10; + frame.render_widget(paragraph, area); }); - if event::poll(std::time::Duration::from_millis(16))? { - if let event::Event::Key(key) = event::read()? { + if event::poll(std::time::Duration::from_millis(16)).unwrap() { + if let event::Event::Key(key) = event::read().unwrap() { if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { break; } } } } - stdout().execute(LeaveAlternateScreen)?; - disable_raw_mode()?; - Ok(()) } diff --git a/src/player.rs b/src/player.rs index cbe0a51..146f327 100644 --- a/src/player.rs +++ b/src/player.rs @@ -99,7 +99,7 @@ impl Player { pub fn defense(&self) -> usize { self.defense } - + pub fn add_to_inventory(&mut self, potion: &Potion) -> bool { if self.inventory.len() < self.inventory_slots { self.inventory.push(*potion);