summaryrefslogtreecommitdiffstats
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/database.rs118
-rw-r--r--src/client/games/manager.rs131
-rw-r--r--src/client/games/mod.rs3
-rw-r--r--src/client/games/slots.rs61
-rw-r--r--src/client/games/traits.rs32
-rw-r--r--src/client/mod.rs2
6 files changed, 347 insertions, 0 deletions
diff --git a/src/client/database.rs b/src/client/database.rs
new file mode 100644
index 0000000..e1fa42d
--- /dev/null
+++ b/src/client/database.rs
@@ -0,0 +1,118 @@
1use rusqlite::Connection;
2
3pub fn init_db(conn: &Connection) {
4 conn.execute(
5 "CREATE TABLE IF NOT EXISTS hosts (
6 host TEXT PRIMARY KEY,
7 last_used DATETIME DEFAULT CURRENT_TIMESTAMP,
8 fingerprint TEXT
9 )",
10 [],
11 )
12 .unwrap();
13 // Migration for databases created before TOFU trust.
14 let _ = conn.execute("ALTER TABLE hosts ADD COLUMN fingerprint TEXT", []);
15 conn.execute(
16 "CREATE TABLE IF NOT EXISTS credentials (
17 host TEXT PRIMARY KEY,
18 username TEXT NOT NULL,
19 password TEXT NOT NULL
20 )",
21 [],
22 )
23 .unwrap();
24}
25
26pub fn get_last_host(conn: &Connection) -> Option<String> {
27 let mut stmt = conn
28 .prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1")
29 .unwrap();
30 let mut rows = stmt.query([]).unwrap();
31 if let Ok(Some(row)) = rows.next() {
32 row.get(0).ok()
33 } else {
34 None
35 }
36}
37
38pub fn save_host(conn: &Connection, host: &str) {
39 conn.execute(
40 "INSERT INTO hosts (host) VALUES (?1)
41 ON CONFLICT(host) DO UPDATE SET last_used = CURRENT_TIMESTAMP",
42 [host],
43 )
44 .unwrap();
45}
46
47pub fn save_credentials(
48 conn: &Connection,
49 host: &str,
50 username: &str,
51 password: &str,
52) -> Result<(), Box<dyn std::error::Error>> {
53 conn.execute(
54 "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)",
55 (host, username, password),
56 )?;
57 Ok(())
58}
59
60pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> {
61 let mut stmt = conn
62 .prepare("SELECT username, password FROM credentials WHERE host = ?1")
63 .unwrap();
64 let mut rows = stmt.query([host]).unwrap();
65 if let Ok(Some(row)) = rows.next() {
66 let username: String = row.get(0).unwrap();
67 let password: String = row.get(1).unwrap();
68 Some((username, password))
69 } else {
70 None
71 }
72}
73
74/// SHA-256 SPKI fingerprint of a host's certificate, if trusted.
75pub fn get_fingerprint(conn: &Connection, host: &str) -> Option<String> {
76 let mut stmt = conn
77 .prepare("SELECT fingerprint FROM hosts WHERE host = ?1")
78 .ok()?;
79 let mut rows = stmt.query([host]).ok()?;
80 match rows.next() {
81 Ok(Some(row)) => row.get(0).ok(),
82 _ => None,
83 }
84}
85
86pub fn save_fingerprint(
87 conn: &Connection,
88 host: &str,
89 fingerprint: &str,
90) -> Result<(), rusqlite::Error> {
91 // Hosts may not exist yet (fingerprint can be saved before first
92 // successful connect completes save_host).
93 conn.execute("INSERT OR IGNORE INTO hosts (host) VALUES (?1)", [host])?;
94 conn.execute(
95 "UPDATE hosts SET fingerprint = ?2 WHERE host = ?1",
96 (host, fingerprint),
97 )?;
98 Ok(())
99}
100
101/// Remove all trust and credentials for a host ("forget this host").
102pub fn forget_host(conn: &Connection, host: &str) -> Result<(), rusqlite::Error> {
103 conn.execute("DELETE FROM hosts WHERE host = ?1", [host])?;
104 conn.execute("DELETE FROM credentials WHERE host = ?1", [host])?;
105 Ok(())
106}
107
108/// List all remembered hosts with their fingerprints.
109pub fn get_all_hosts(conn: &Connection) -> Vec<(String, Option<String>)> {
110 let mut stmt = conn
111 .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC")
112 .unwrap();
113 let rows = stmt.query_map([], |row| {
114 Ok((row.get(0)?, row.get(1)?))
115 })
116 .unwrap();
117 rows.filter_map(|r| r.ok()).collect()
118}
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs
new file mode 100644
index 0000000..be96b1a
--- /dev/null
+++ b/src/client/games/manager.rs
@@ -0,0 +1,131 @@
1use super::slots::SlotsGame;
2use super::traits::{Game, GameError, GamePhase, GameState};
3use std::collections::HashMap;
4use std::io::Write;
5
6pub struct GameManager {
7 current_session: Option<GameState>,
8 pending_game_type: Option<String>,
9 games: HashMap<String, Box<dyn Game>>,
10}
11
12impl GameManager {
13 pub fn new() -> Self {
14 let mut games: HashMap<String, Box<dyn Game>> = HashMap::new();
15 games.insert("slots".to_string(), Box::new(SlotsGame::new()));
16
17 Self {
18 current_session: None,
19 pending_game_type: None,
20 games,
21 }
22 }
23
24 pub fn get_available_games(&self) -> Vec<(&str, &str)> {
25 self.games
26 .iter()
27 .map(|(k, v)| (k.as_str(), v.get_name()))
28 .collect()
29 }
30
31 pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> {
32 if self.current_session.is_some() {
33 return Err(GameError::InvalidAction);
34 }
35
36 if !self.games.contains_key(game_type) {
37 return Err(GameError::InvalidAction);
38 }
39
40 self.pending_game_type = Some(game_type.to_string());
41
42 self.current_session = Some(GameState {
43 game_type: game_type.to_string(),
44 bet_amount: 10,
45 phase: GamePhase::WaitingToStart,
46 data: HashMap::new(),
47 });
48
49 Ok(())
50 }
51
52 pub fn start_game<W: Write>(&mut self, stream: &mut W) -> Result<(), GameError> {
53 let game_type = self
54 .pending_game_type
55 .as_ref()
56 .ok_or(GameError::InvalidAction)?;
57
58 let state = self
59 .current_session
60 .as_mut()
61 .ok_or(GameError::InvalidAction)?;
62
63 if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Finished {
64 return Err(GameError::InvalidAction);
65 }
66
67 let bet_amount = state.bet_amount;
68
69 let writer = stream;
70 writeln!(writer, "PLAY_START {} {}", game_type, bet_amount)
71 .map_err(|_| GameError::NetworkError)?;
72
73 Ok(())
74 }
75
76 pub fn get_current_game(&self) -> Option<&dyn Game> {
77 self.current_session
78 .as_ref()
79 .and_then(|session| self.games.get(&session.game_type).map(|game| game.as_ref()))
80 }
81
82 pub fn get_current_state(&self) -> Option<&GameState> {
83 self.current_session.as_ref()
84 }
85
86 pub fn get_current_render_lines(&self) -> Option<usize> {
87 let session = self.current_session.as_ref()?;
88 let game = self.games.get(&session.game_type)?;
89 Some(game.render_lines(session))
90 }
91
92 pub fn is_in_game(&self) -> bool {
93 self.current_session.is_some()
94 }
95
96 pub fn is_game_started(&self) -> bool {
97 self.current_session
98 .as_ref()
99 .map(|s| s.phase != GamePhase::WaitingToStart)
100 .unwrap_or(false)
101 }
102
103 pub fn reset_phase_to_waiting(&mut self) {
104 if let Some(ref mut session) = self.current_session {
105 session.phase = GamePhase::WaitingToStart;
106 session.data.clear();
107 }
108 }
109
110 pub fn end_game(&mut self) {
111 self.current_session = None;
112 self.pending_game_type = None;
113 }
114
115 pub fn adjust_bet(&mut self, delta: i64) -> Result<u64, GameError> {
116 if let Some(ref session) = self.current_session {
117 if session.phase != GamePhase::WaitingToStart {
118 return Err(GameError::InvalidAction);
119 }
120 }
121
122 if let Some(ref mut session) = self.current_session {
123 let current_bet = session.bet_amount as i64;
124 let new_bet = (current_bet + delta).max(10) as u64;
125 session.bet_amount = new_bet;
126 Ok(new_bet)
127 } else {
128 Err(GameError::InvalidAction)
129 }
130 }
131}
diff --git a/src/client/games/mod.rs b/src/client/games/mod.rs
new file mode 100644
index 0000000..aa74574
--- /dev/null
+++ b/src/client/games/mod.rs
@@ -0,0 +1,3 @@
1pub mod manager;
2pub mod slots;
3pub mod traits;
diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs
new file mode 100644
index 0000000..6e2ebbe
--- /dev/null
+++ b/src/client/games/slots.rs
@@ -0,0 +1,61 @@
1use super::traits::{Game, GameState};
2
3pub struct SlotsGame;
4
5impl SlotsGame {
6 pub fn new() -> Self {
7 Self
8 }
9
10 fn render_reels(&self, reels: &str) -> String {
11 if reels.is_empty() {
12 "[ ? ] [ ? ] [ ? ]".to_string()
13 } else {
14 let chars: Vec<char> = reels.chars().collect();
15 format!(
16 "[ {} ] [ {} ] [ {} ]\r",
17 chars.get(0).unwrap_or(&'?'),
18 chars.get(1).unwrap_or(&'?'),
19 chars.get(2).unwrap_or(&'?')
20 )
21 }
22 }
23}
24
25impl Game for SlotsGame {
26 fn get_name(&self) -> &'static str {
27 "Slots"
28 }
29
30 fn render(&self, state: &GameState) -> String {
31 let reels = state.data.get("reels").cloned().unwrap_or_default();
32
33 format!(
34 "=== SLOTS ===\r\nBet: {} PLN\r\n{}\r\n\r\nPress [SPACE] to spin!\r\n\r\nControls: [+/-] Adjust Bet [Q] Quit",
35 state.bet_amount,
36 self.render_reels(&reels)
37 )
38 }
39
40 fn render_lines(&self, _state: &GameState) -> usize {
41 6
42 }
43
44 fn get_help(&self) -> String {
45 "SLOTS GAME\n\
46 =========\n\
47 Match symbols to win!\n\
48 \n\
49 Payouts:\n\
50 Three of a kind: 2x-28x bet\n\
51 Two of a kind: 1x-3x bet\n\
52 \n\
53 Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\
54 \n\
55 Controls:\n\
56 SPACE - Spin the reels\n\
57 +/- - Adjust bet amount\n\
58 Q - Exit the game"
59 .to_string()
60 }
61}
diff --git a/src/client/games/traits.rs b/src/client/games/traits.rs
new file mode 100644
index 0000000..aed0c24
--- /dev/null
+++ b/src/client/games/traits.rs
@@ -0,0 +1,32 @@
1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
4pub enum GameError {
5 InvalidAction,
6 NetworkError,
7}
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum GamePhase {
11 WaitingToStart,
12 Playing,
13 Finished,
14}
15
16#[derive(Debug, Clone)]
17pub struct GameState {
18 pub game_type: String,
19 pub bet_amount: u64,
20 pub phase: GamePhase,
21 pub data: HashMap<String, String>,
22}
23
24pub trait Game: Send + Sync {
25 fn get_name(&self) -> &'static str;
26
27 fn render(&self, state: &GameState) -> String;
28
29 fn render_lines(&self, state: &GameState) -> usize;
30
31 fn get_help(&self) -> String;
32}
diff --git a/src/client/mod.rs b/src/client/mod.rs
new file mode 100644
index 0000000..5b38158
--- /dev/null
+++ b/src/client/mod.rs
@@ -0,0 +1,2 @@
1pub mod database;
2pub mod games;