diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 0138ebb..0000000 --- a/src/client.rs +++ /dev/null @@ -1,117 +0,0 @@ -// --------------- -// Module overview -// --------------- -// -// DiscordInfo: describes a Discord user's information -// -// ClientInfo: describes a client's identifying information, -// both for websocket and Discord connections. -// Webscoket connections have the option to store -// DiscordInfo as well for verified connections. -// -// Client: describes a client, holding ClientInfo and Rc - -use crate::room::Room; - -use simple_websockets::{Message, Responder}; -use std::cell::RefCell; -use std::cmp::{Eq, PartialEq}; -use std::hash::{Hash, Hasher}; -use std::rc::Rc; - -pub struct DiscordInfo { - pub username: String, - pub discriminator: u16, - pub id: u64, -} - -impl PartialEq for DiscordInfo { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for DiscordInfo {} - -pub enum ClientInfo { - Ws { - id: u64, - responder: Responder, - discord_info: Option, - }, - Discord { - discord_info: DiscordInfo, - }, -} - -impl ClientInfo { - fn id(&self) -> u64 { - match self { - Self::Ws { - id, discord_info, .. - } => match discord_info { - // Discord-verified websocket connection - Some(discord_info) => discord_info.id, - // Anonymous websocket connection - None => *id, - }, - // Discord connection - Self::Discord { discord_info } => discord_info.id, - } - } -} - -impl PartialEq for ClientInfo { - fn eq(&self, other: &Self) -> bool { - self.id() == other.id() - } -} - -impl Eq for ClientInfo {} - -impl Hash for ClientInfo { - fn hash(&self, state: &mut H) { - self.id().hash(state) - } -} - -pub struct Client { - info: ClientInfo, - pub room: Rc>, -} - -impl Client { - pub fn new(info: ClientInfo, room: Rc>) -> Self { - Self { info, room } - } - - pub fn id(&self) -> u64 { - match self.info { - ClientInfo::Ws { id, .. } => id, - ClientInfo::Discord { .. } => unimplemented!("no id for Discord connections"), - } - } - - pub fn send(&self, message: Message) -> bool { - match &self.info { - ClientInfo::Ws { responder, .. } => responder.send(message), - ClientInfo::Discord { .. } => { - unimplemented!("no networking implementation for Discord connections") - } - } - } -} - -impl PartialEq for Client { - fn eq(&self, other: &Self) -> bool { - self.info.eq(&other.info) - } -} - -impl Eq for Client {} - -impl Hash for Client { - fn hash(&self, state: &mut H) { - self.info.hash(state) - } -} diff --git a/src/database.rs b/src/database.rs index e4c217c..19a3256 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,8 +1,8 @@ -use crate::word::Word; +use crate::Word; use derive_more::From; -use rusqlite::{params, Connection, Result}; use std::path::PathBuf; +use rusqlite::{Connection, Result, params}; pub struct Database { conn: Connection, @@ -15,23 +15,16 @@ pub enum DatabaseCreationError { IoError(std::io::Error), } -#[derive(Default)] -pub enum DatabaseType { - #[default] - InMemory, - OnDisk(String), -} - -#[derive(Default)] -pub struct DatabaseSettings { - pub db_type: DatabaseType, -} - impl Database { - pub fn new(settings: DatabaseSettings) -> Result { - let conn = match settings.db_type { - DatabaseType::InMemory => Connection::open_in_memory(), - DatabaseType::OnDisk(path) => Connection::open(PathBuf::from(path)), + pub fn new( + testing: bool, + ) -> Result { + let conn = if testing { + Connection::open_in_memory() + } else { + let path = PathBuf::from("shiritori.db"); + //fs::create_dir_all(path.parent().unwrap())?; + Connection::open(path) }?; conn.execute( "CREATE TABLE IF NOT EXISTS word ( @@ -46,11 +39,10 @@ impl Database { .prepare("SELECT id FROM word ORDER BY id DESC LIMIT 1")? .query_map(params![], |row| row.get(0))? .collect::>>()? - .first() - { - Some(id) => *id, - None => 0, // first database entry is id 1 - }; + .first() { + Some(id) => *id, + None => 0, // first database entry is id 1 + }; Ok(Self { conn, last_word_id }) } @@ -71,7 +63,10 @@ impl Database { pub fn add_word(&mut self, word: &Word) -> Result<()> { self.conn.execute( "INSERT INTO word (word, reading) VALUES (?1, ?2)", - params![word.word, word.reading,], + params![ + word.word, + word.reading, + ], )?; self.last_word_id += 1; Ok(()) diff --git a/src/dictionary.rs b/src/dictionary.rs index 4d50ee8..6399e6c 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -1,9 +1,8 @@ use wana_kana::{ConvertJapanese, IsJapaneseStr}; -fn raw_lookup(input: &str) -> Option<&jisho::Entry> { +pub fn lookup(input: &str) -> Option<&jisho::Entry> { let input = input.trim(); - jisho::lookup(input).into_iter().find(|&word| { - ( + jisho::lookup(input).into_iter().find(|&word| ( // If input has no kanji, // we can just compare the input to the reading verbatim // ensuring both are hiragana @@ -12,32 +11,5 @@ fn raw_lookup(input: &str) -> Option<&jisho::Entry> { // is verbosely the same. // However, this will cause problems for some words. // For example, 照り焼き will be accepted but 照焼 won't. - (input == word.kanji) - ) - }) -} - -pub fn lookup(query: &str) -> Option { - match raw_lookup(&query) { - Some(result) => Some(result.clone()), - None => { - if query.is_hiragana() { - // looking up ごりら doesn't return ゴリラ - // jisho::lookup for some reason refers to input string, - // so cloning is required - match raw_lookup(&query.to_katakana()) { - Some(entry) => Some(entry.clone()), - None => None, - } - } else if query.is_katakana() { - // looking up シリトリ doesn't return 尻取り - match raw_lookup(&query.to_hiragana()) { - Some(entry) => Some(entry.clone()), - None => None, - } - } else { - None - } - } - } + (input == word.kanji))) } diff --git a/src/main.rs b/src/main.rs index ddc4eb2..4fea4ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,18 @@ -mod client; mod database; -mod dictionary; -mod response; -mod room; -mod server; -mod utils; -mod word; +pub use database::*; -use crate::server::{Server, ServerSettings}; +mod dictionary; +pub use dictionary::*; + +mod server; +pub use server::*; + +mod word; +pub use word::*; fn main() { - let settings = ServerSettings::default(); - let mut server = Server::new(&settings).unwrap_or_else(|error| { - panic!( - "Failed to start server at port {}: {:?}", - settings.port, error - ) - }); + const PORT: u16 = 8080; + let mut server = Server::new(PORT, true) + .unwrap_or_else(|error| panic!("Failed to start server at port {PORT}: {:?}", error)); server.run(); } diff --git a/src/response.rs b/src/response.rs deleted file mode 100644 index 6df3c5c..0000000 --- a/src/response.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::word::Word; - -use serde::Serialize; -use simple_websockets::Message; - -#[derive(Serialize)] -pub struct MessageResponse { - event: String, - data: MessageResponseData, -} - -impl MessageResponse { - pub fn to_message(&self) -> Message { - Message::Text(serde_json::to_string(&self).unwrap()) - } -} - -#[derive(Serialize)] -#[serde(untagged)] -pub enum MessageResponseData { - Greeting { - id: u64, - next_mora: Option, - }, - Word { - author: u64, - word: Word, - next_mora: String, - }, - History { - words: Vec, - }, - PlayerCount { - players: u64, - }, - Error { - message: String, - }, -} - -impl MessageResponseData { - pub fn get_name(&self) -> String { - String::from(match self { - Self::Greeting { .. } => "greeting", - Self::Word { .. } => "word", - Self::History { .. } => "history", - Self::PlayerCount { .. } => "playerCount", - Self::Error { .. } => "error", - }) - } - - pub fn into_response(self) -> MessageResponse { - MessageResponse { - event: self.get_name(), - data: self, - } - } - - pub fn into_message(self) -> Message { - self.into_response().to_message() - } -} diff --git a/src/room.rs b/src/room.rs deleted file mode 100644 index 224611d..0000000 --- a/src/room.rs +++ /dev/null @@ -1,107 +0,0 @@ -// --------------- -// Module overview -// --------------- -// -// RoomSettings: Stores database settings for room. -// In future, could store passphrase, etc. -// -// Room: Game state abstracted away from any networking. -// Any events that aren't game-related are handled by Server. - -use crate::database::{Database, DatabaseCreationError, DatabaseSettings}; -use crate::dictionary::lookup; -use crate::response::MessageResponseData; -use crate::utils::{get_final_mora, get_starting_mora}; -use crate::word::Word; - -use wana_kana::{ConvertJapanese, IsJapaneseStr}; - -#[derive(Default)] -pub struct RoomSettings { - pub database_settings: DatabaseSettings, -} - -pub struct Room { - database: Database, - next_mora: Option, - last_client_id: Option, -} - -impl Room { - pub fn new(settings: RoomSettings) -> Result { - Ok(Self { - database: Database::new(settings.database_settings)?, - next_mora: None, - last_client_id: None, - }) - } - - pub fn next_mora(&self) -> &Option { - &self.next_mora - } - - pub fn get_history(&self) -> rusqlite::Result> { - self.database - .load_words_before(self.database.last_word_id + 1) - } - - // Err(&str) will be converted to MessageResponseData::Error - pub fn handle_query( - &mut self, - query: &str, - client_id: u64, - ) -> Result { - // Ensure query isn't from last player - if Some(client_id) == self.last_client_id { - return Err("It's not your turn!"); - }; - - // Ensure query is in Japanese - let query = query.to_string(); - let query = if query.is_japanese() { - query - } else { - let kana = query.to_kana(); - if kana.is_japanese() { - kana - } else { - return Err("Not Japanese!"); - } - }; - - // Look up word in dictionary - let dictionary_result = lookup(&query); - - // Send result - match dictionary_result { - Some(entry) => { - if entry - .reading - .chars() - .last() - .unwrap() - .to_string() - .to_hiragana() - == "ん" - { - Err("Can't end with ん!") - } else if self.next_mora.is_none() - || get_starting_mora(&entry.reading).eq(self.next_mora.as_deref().unwrap()) - { - let word: Word = entry.clone().into(); - self.next_mora = Some(get_final_mora(&word.reading)); - self.database.add_word(&word).unwrap(); // TODO: replace .unwrap() with ? - self.last_client_id = Some(client_id); - Ok(MessageResponseData::Word { - author: client_id, - word, - next_mora: self.next_mora.as_deref().unwrap().to_owned(), - }) - } else { - Err("Wrong starting mora!") - } - } - None => Err("Not in dictionary!"), - } - } -} diff --git a/src/server.rs b/src/server.rs index 597c102..c5d4f1f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,13 +1,79 @@ -use crate::client::{Client, ClientInfo}; -use crate::database::{DatabaseCreationError, DatabaseSettings, DatabaseType}; -use crate::response::MessageResponseData; -use crate::room::{Room, RoomSettings}; +use crate::DatabaseCreationError; +use crate::lookup; +use crate::Word; +use crate::Database; -use derive_more::From; -use simple_websockets::{Event, EventHub, Message, Responder}; -use std::cell::RefCell; +use simple_websockets::{Event, Responder, Message, EventHub}; use std::collections::HashMap; -use std::rc::Rc; +use serde::Serialize; +use wana_kana::{ConvertJapanese, IsJapaneseStr}; +use derive_more::From; + +#[derive(Serialize)] +struct MessageResponse { + event: String, + data: MessageResponseData, +} + +impl MessageResponse { + fn to_message(&self) -> Message { + Message::Text(serde_json::to_string(&self).unwrap()) + } +} + +#[derive(Serialize)] +#[serde(untagged)] +enum MessageResponseData { + Greeting { + id: u64, + next_mora: Option, + }, + Word { + author: u64, + word: Word, + next_mora: String, + }, + History { + words: Vec, + }, + PlayerCount { + players: u64, + }, + Error { + message: String, + }, +} + +impl MessageResponseData { + fn get_name(&self) -> String { + String::from(match self { + Self::Greeting { .. } => "greeting", + Self::Word { .. } => "word", + Self::History { .. } => "history", + Self::PlayerCount { .. } => "playerCount", + Self::Error { .. } => "error", + }) + } + + fn into_response(self) -> MessageResponse { + MessageResponse { + event: self.get_name(), + data: self, + } + } + + fn into_message(self) -> Message { + self.into_response().to_message() + } +} + +pub struct Server { + event_hub: EventHub, + database: Database, + clients: HashMap, + next_mora: Option, + last_client_id: Option, +} #[derive(From, Debug)] pub enum ServerCreationError { @@ -20,203 +86,196 @@ pub enum ServerError { DatabaseError(rusqlite::Error), } -pub struct ServerSettings { - pub port: u16, - pub testing: bool, -} - -impl Default for ServerSettings { - fn default() -> Self { - Self { - port: 8080, - testing: true, - } +fn get_starting_mora(word: &str) -> String { + if word.is_empty() { + return String::from(""); + } + let word = word.to_hiragana(); + let mut iter = word.chars(); + let starting_char = iter.next().unwrap(); + let second_char = iter.next().unwrap(); + match second_char { + 'ゃ' | 'ゅ' | 'ょ' => format!("{starting_char}{second_char}"), + _ => starting_char.to_string(), } } -pub struct Server { - event_hub: EventHub, - lobby: Rc>, - rooms: HashMap, - clients: HashMap, +// Trim off lengtheners and non-kana that are irrelevant to shiritori +// TODO: Use slices +fn trim_irrelevant_chars(word: &str) -> String { + let mut iter = word.chars().rev().peekable(); + while let Some(c) = iter.peek() { + if *c == 'ー' || !c.to_string().is_kana() { + iter.next(); + } else { + break; + } + } + iter.rev().collect() +} + +// Get final mora, which could be multiple chars e.g. しゃ +// TODO: Use slices +fn get_final_mora(word: &str) -> String { + let word = trim_irrelevant_chars(word).to_hiragana(); + if word.is_empty() { + return String::from(""); + } + let mut iter = word.chars().rev(); + let final_char = iter.next().unwrap(); + match final_char { + 'ゃ' | 'ゅ' | 'ょ' => format!("{}{final_char}", match iter.next() { + Some(c) => c.to_string(), + None => String::from(""), + }), + _ => final_char.to_string(), + } } impl Server { - pub fn new(settings: &ServerSettings) -> Result { - Ok(Self { - event_hub: simple_websockets::launch(settings.port)?, - lobby: Rc::new(RefCell::new(Room::new(RoomSettings { - database_settings: DatabaseSettings { - db_type: match settings.testing { - true => DatabaseType::InMemory, - false => DatabaseType::OnDisk("shiritori.sb".to_string()), - }, - }, - ..Default::default() - })?)), - rooms: HashMap::new(), + pub fn new(port: u16, testing: bool) -> Result { + Ok(Server { + event_hub: simple_websockets::launch(port)?, + database: Database::new(testing)?, clients: HashMap::new(), + next_mora: None, + last_client_id: None, }) } - pub fn get_client(&self, id: u64) -> Option<&Client> { - self.clients.get(&id) - } - pub fn run(&mut self) { loop { match match self.event_hub.poll_event() { - Event::Connect(client_id, responder) => { - self.handle_connection(client_id, responder) - } + Event::Connect(client_id, responder) => self.handle_connection(client_id, responder), Event::Disconnect(client_id) => self.handle_disconnection(client_id), Event::Message(client_id, message) => self.handle_message(client_id, message), } { - Ok(()) => {} + Ok(()) => {}, Err(error) => println!("{:?}", error), } } } - fn new_client(&mut self, client_id: u64, responder: Responder) -> &Client { - let client = Client::new( - ClientInfo::Ws { - id: client_id, - responder, - discord_info: None, - }, - self.lobby.clone(), - ); - self.clients.insert(client_id, client); - self.clients.get(&client_id).unwrap() + fn broadcast_player_count(&self) { + let response = MessageResponseData::PlayerCount { players: self.clients.len() as u64 }.into_response(); + for (_client, responder) in self.clients.iter() { + responder.send(response.to_message()); + } } - fn handle_connection( - &mut self, - client_id: u64, - responder: Responder, - ) -> Result<(), ServerError> { - // Debug + fn handle_connection(&mut self, client_id: u64, responder: Responder) -> Result<(), ServerError> { println!("A client connected with id #{}", client_id); - - { - // Initialize client - let client = self.new_client(client_id, responder); - - // Get immutable access to room - let room = client.room.borrow(); - - // Send client greeting - client.send( - MessageResponseData::Greeting { - id: client_id, - next_mora: room.next_mora().clone(), - } - .into_message(), - ); - - // Sent recent message history - client.send( - MessageResponseData::History { - words: room.get_history()?, - } - .into_message(), - ); - } - - // Number of clients on Rc> reference counter will be one more - self.broadcast_player_count(&self.lobby); - + responder.send(MessageResponseData::Greeting { + id: client_id, + next_mora: self.next_mora.clone(), + }.into_message()); + responder.send(MessageResponseData::History { + words: self.database.load_words_before(self.database.last_word_id + 1)? + }.into_message()); + self.clients.insert(client_id, responder); + self.broadcast_player_count(); Ok(()) } fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> { - // Debug println!("Client #{} disconnected.", client_id); - - // Remove client - // At this point, client should be dropped - let client = self.clients.remove(&client_id).unwrap(); - - // Get room - let room = client.room; - - // Number of clients on Rc> reference counter will be one less - self.broadcast_player_count(&room); - + self.clients.remove(&client_id); + self.broadcast_player_count(); Ok(()) } - fn for_client_in_room(&self, room: &Rc>, mut closure: impl FnMut(&Client) -> ()) { - for (_id, client) in self - .clients - .iter() - .filter(|(_id, client)| Rc::>::ptr_eq(room, &client.room)) - { - closure(client); - } - } - - fn client_count_in_room(&self, room: &Rc>) -> usize { - let mut count: usize = 0; - self.for_client_in_room(room, |_| count += 1); - count - } - - fn broadcast_player_count(&self, room: &Rc>) { - let response = MessageResponseData::PlayerCount { - players: self.client_count_in_room(room) as u64, - } - .into_response(); - for (_id, client) in self.clients.iter() { - client.send(response.to_message()); - } - } - - fn announce_to_room(&self, room: &Rc>, response_data: MessageResponseData) { - let response = response_data.into_response(); - self.for_client_in_room(room, |client| { - client.send(response.to_message()); - }); - } - fn handle_message(&mut self, client_id: u64, message: Message) -> Result<(), ServerError> { // Ignore binary messages let message = match message { - Message::Text(message) => { - // Debug - println!( - "Received a message from client #{}: {:?}", - client_id, message - ); - - message - } - Message::Binary(message) => { - // Debug - println!( - "Received a binary message from client #{}: {:?}", - client_id, message - ); - - return Ok(()); - } + Message::Text(message) => message, + Message::Binary(_) => return Ok(()), }; - let client = self.get_client(client_id).unwrap(); + // Debug + println!("Received a message from client #{}: {:?}", client_id, message); - match client.room.borrow_mut().handle_query(&message, client_id) { - // Broadcast new words to all clients in same room - Ok(response) => self.announce_to_room(&client.room, response), - // Send errors to only this client - Err(message) => { - client.send( - MessageResponseData::Error { - message: message.to_string(), - } - .into_message(), - ); + let response = if Some(client_id) == self.last_client_id { + MessageResponseData::Error { + message: String::from("It's not your turn!"), } + } else { + let query = if message.is_japanese() { + Some(message) + } else { + let kana = message.to_kana(); + if kana.is_japanese() { + Some(kana) + } else { + None + } + }; + // For some reason result will keep a reference to + // katakana_query and hiragana_query, meaning that + // these need to be kept around as a variable + // to prevent them from being dropped. + let (katakana_query, hiragana_query) = match &query { + Some(query) => ( + Some(query.to_katakana()), + Some(query.to_hiragana()) + ), + None => (None, None), + }; + let result = match &query { + Some(query) => match lookup(query) { + Some(result) => Some(result), + None => { + if query.is_hiragana() { + // looking up ごりら doesn't return ゴリラ + lookup(katakana_query.as_ref().unwrap()) + } else if query.is_katakana() { + // looking up シリトリ doesn't return 尻取り + lookup(hiragana_query.as_ref().unwrap()) + } else { + None + } + } + }, + None => None, + }; + match result { + Some(entry) => { + if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "ん" { + MessageResponseData::Error { + message: String::from("Can't end with ん!"), + } + } else if self.next_mora.is_none() || get_starting_mora(&entry.reading).eq(self.next_mora.as_deref().unwrap()) { + let word: Word = entry.clone().into(); + self.next_mora = Some(get_final_mora(&word.reading)); + self.database.add_word(&word)?; + MessageResponseData::Word { + author: client_id, + word, + next_mora: self.next_mora.as_deref().unwrap().to_owned(), + } + } else { + MessageResponseData::Error { + message: String::from("Wrong starting mora!"), + } + } + } + None => MessageResponseData::Error { + message: String::from("Not in dictionary!"), + }, + } + }.into_response(); + + match response.data { + // Send errors to only this client + MessageResponseData::Error { .. } => { + self.clients.get(&client_id).unwrap().send(response.to_message()); + }, + // Broadcast everything else to all clients + _ => { + for (_client, responder) in self.clients.iter() { + responder.send(response.to_message()); + } + self.last_client_id = Some(client_id); + }, }; Ok(()) } diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 0684417..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,50 +0,0 @@ -use wana_kana::{ConvertJapanese, IsJapaneseStr}; - -pub fn get_starting_mora(word: &str) -> String { - if word.is_empty() { - return String::from(""); - } - let word = word.to_hiragana(); - let mut iter = word.chars(); - let starting_char = iter.next().unwrap(); - let second_char = iter.next().unwrap(); - match second_char { - 'ゃ' | 'ゅ' | 'ょ' => format!("{starting_char}{second_char}"), - _ => starting_char.to_string(), - } -} - -// Trim off lengtheners and non-kana that are irrelevant to shiritori -// TODO: Use slices -fn trim_irrelevant_chars(word: &str) -> String { - let mut iter = word.chars().rev().peekable(); - while let Some(c) = iter.peek() { - if *c == 'ー' || !c.to_string().is_kana() { - iter.next(); - } else { - break; - } - } - iter.rev().collect() -} - -// Get final mora, which could be multiple chars e.g. しゃ -// TODO: Use slices -pub fn get_final_mora(word: &str) -> String { - let word = trim_irrelevant_chars(word).to_hiragana(); - if word.is_empty() { - return String::from(""); - } - let mut iter = word.chars().rev(); - let final_char = iter.next().unwrap(); - match final_char { - 'ゃ' | 'ゅ' | 'ょ' => format!( - "{}{final_char}", - match iter.next() { - Some(c) => c.to_string(), - None => String::from(""), - } - ), - _ => final_char.to_string(), - } -} diff --git a/src/word.rs b/src/word.rs index 8414f87..cfa1701 100644 --- a/src/word.rs +++ b/src/word.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; -use jisho::Entry; use serde::Serialize; +use jisho::Entry; use std::convert::From; #[derive(Serialize)]