Compare commits

..

No commits in common. 'c4eece7ffd66725f890b29007b5cf369e2cc9c30' and '1fbb0ede508d9e4e4616d7e17a7f67d1c9c32bcb' have entirely different histories.

@ -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<Room>
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<DiscordInfo>,
},
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<H: Hasher>(&self, state: &mut H) {
self.id().hash(state)
}
}
pub struct Client {
info: ClientInfo,
pub room: Rc<RefCell<Room>>,
}
impl Client {
pub fn new(info: ClientInfo, room: Rc<RefCell<Room>>) -> 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<H: Hasher>(&self, state: &mut H) {
self.info.hash(state)
}
}

@ -1,8 +1,8 @@
use crate::word::Word; use crate::Word;
use derive_more::From; use derive_more::From;
use rusqlite::{params, Connection, Result};
use std::path::PathBuf; use std::path::PathBuf;
use rusqlite::{Connection, Result, params};
pub struct Database { pub struct Database {
conn: Connection, conn: Connection,
@ -15,23 +15,16 @@ pub enum DatabaseCreationError {
IoError(std::io::Error), IoError(std::io::Error),
} }
#[derive(Default)]
pub enum DatabaseType {
#[default]
InMemory,
OnDisk(String),
}
#[derive(Default)]
pub struct DatabaseSettings {
pub db_type: DatabaseType,
}
impl Database { impl Database {
pub fn new(settings: DatabaseSettings) -> Result<Self, DatabaseCreationError> { pub fn new(
let conn = match settings.db_type { testing: bool,
DatabaseType::InMemory => Connection::open_in_memory(), ) -> Result<Self, DatabaseCreationError> {
DatabaseType::OnDisk(path) => Connection::open(PathBuf::from(path)), 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( conn.execute(
"CREATE TABLE IF NOT EXISTS word ( "CREATE TABLE IF NOT EXISTS word (
@ -46,8 +39,7 @@ impl Database {
.prepare("SELECT id FROM word ORDER BY id DESC LIMIT 1")? .prepare("SELECT id FROM word ORDER BY id DESC LIMIT 1")?
.query_map(params![], |row| row.get(0))? .query_map(params![], |row| row.get(0))?
.collect::<Result<Vec<i64>>>()? .collect::<Result<Vec<i64>>>()?
.first() .first() {
{
Some(id) => *id, Some(id) => *id,
None => 0, // first database entry is id 1 None => 0, // first database entry is id 1
}; };
@ -71,7 +63,10 @@ impl Database {
pub fn add_word(&mut self, word: &Word) -> Result<()> { pub fn add_word(&mut self, word: &Word) -> Result<()> {
self.conn.execute( self.conn.execute(
"INSERT INTO word (word, reading) VALUES (?1, ?2)", "INSERT INTO word (word, reading) VALUES (?1, ?2)",
params![word.word, word.reading,], params![
word.word,
word.reading,
],
)?; )?;
self.last_word_id += 1; self.last_word_id += 1;
Ok(()) Ok(())

@ -1,9 +1,8 @@
use wana_kana::{ConvertJapanese, IsJapaneseStr}; 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(); let input = input.trim();
jisho::lookup(input).into_iter().find(|&word| { jisho::lookup(input).into_iter().find(|&word| (
(
// If input has no kanji, // If input has no kanji,
// we can just compare the input to the reading verbatim // we can just compare the input to the reading verbatim
// ensuring both are hiragana // ensuring both are hiragana
@ -12,32 +11,5 @@ fn raw_lookup(input: &str) -> Option<&jisho::Entry> {
// is verbosely the same. // is verbosely the same.
// However, this will cause problems for some words. // However, this will cause problems for some words.
// For example, 照り焼き will be accepted but 照焼 won't. // For example, 照り焼き will be accepted but 照焼 won't.
(input == word.kanji) (input == word.kanji)))
)
})
}
pub fn lookup(query: &str) -> Option<jisho::Entry> {
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
}
}
}
} }

@ -1,21 +1,18 @@
mod client;
mod database; mod database;
pub use database::*;
mod dictionary; mod dictionary;
mod response; pub use dictionary::*;
mod room;
mod server; mod server;
mod utils; pub use server::*;
mod word;
use crate::server::{Server, ServerSettings}; mod word;
pub use word::*;
fn main() { fn main() {
let settings = ServerSettings::default(); const PORT: u16 = 8080;
let mut server = Server::new(&settings).unwrap_or_else(|error| { let mut server = Server::new(PORT, true)
panic!( .unwrap_or_else(|error| panic!("Failed to start server at port {PORT}: {:?}", error));
"Failed to start server at port {}: {:?}",
settings.port, error
)
});
server.run(); server.run();
} }

@ -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<String>,
},
Word {
author: u64,
word: Word,
next_mora: String,
},
History {
words: Vec<Word>,
},
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()
}
}

@ -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<String>,
last_client_id: Option<u64>,
}
impl Room {
pub fn new(settings: RoomSettings) -> Result<Self, DatabaseCreationError> {
Ok(Self {
database: Database::new(settings.database_settings)?,
next_mora: None,
last_client_id: None,
})
}
pub fn next_mora(&self) -> &Option<String> {
&self.next_mora
}
pub fn get_history(&self) -> rusqlite::Result<Vec<Word>> {
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<MessageResponseData, &str> {
// 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!"),
}
}
}

@ -1,13 +1,79 @@
use crate::client::{Client, ClientInfo}; use crate::DatabaseCreationError;
use crate::database::{DatabaseCreationError, DatabaseSettings, DatabaseType}; use crate::lookup;
use crate::response::MessageResponseData; use crate::Word;
use crate::room::{Room, RoomSettings}; use crate::Database;
use derive_more::From; use simple_websockets::{Event, Responder, Message, EventHub};
use simple_websockets::{Event, EventHub, Message, Responder};
use std::cell::RefCell;
use std::collections::HashMap; 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<String>,
},
Word {
author: u64,
word: Word,
next_mora: String,
},
History {
words: Vec<Word>,
},
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<u64, Responder>,
next_mora: Option<String>,
last_client_id: Option<u64>,
}
#[derive(From, Debug)] #[derive(From, Debug)]
pub enum ServerCreationError { pub enum ServerCreationError {
@ -20,203 +86,196 @@ pub enum ServerError {
DatabaseError(rusqlite::Error), DatabaseError(rusqlite::Error),
} }
pub struct ServerSettings { fn get_starting_mora(word: &str) -> String {
pub port: u16, if word.is_empty() {
pub testing: bool, 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(),
}
} }
impl Default for ServerSettings { // Trim off lengtheners and non-kana that are irrelevant to shiritori
fn default() -> Self { // TODO: Use slices
Self { fn trim_irrelevant_chars(word: &str) -> String {
port: 8080, let mut iter = word.chars().rev().peekable();
testing: true, while let Some(c) = iter.peek() {
if *c == 'ー' || !c.to_string().is_kana() {
iter.next();
} else {
break;
} }
} }
iter.rev().collect()
} }
pub struct Server { // Get final mora, which could be multiple chars e.g. しゃ
event_hub: EventHub, // TODO: Use slices
lobby: Rc<RefCell<Room>>, fn get_final_mora(word: &str) -> String {
rooms: HashMap<String, Room>, let word = trim_irrelevant_chars(word).to_hiragana();
clients: HashMap<u64, Client>, 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 { impl Server {
pub fn new(settings: &ServerSettings) -> Result<Self, ServerCreationError> { pub fn new(port: u16, testing: bool) -> Result<Self, ServerCreationError> {
Ok(Self { Ok(Server {
event_hub: simple_websockets::launch(settings.port)?, event_hub: simple_websockets::launch(port)?,
lobby: Rc::new(RefCell::new(Room::new(RoomSettings { database: Database::new(testing)?,
database_settings: DatabaseSettings {
db_type: match settings.testing {
true => DatabaseType::InMemory,
false => DatabaseType::OnDisk("shiritori.sb".to_string()),
},
},
..Default::default()
})?)),
rooms: HashMap::new(),
clients: HashMap::new(), 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) { pub fn run(&mut self) {
loop { loop {
match match self.event_hub.poll_event() { match match self.event_hub.poll_event() {
Event::Connect(client_id, responder) => { Event::Connect(client_id, responder) => self.handle_connection(client_id, responder),
self.handle_connection(client_id, responder)
}
Event::Disconnect(client_id) => self.handle_disconnection(client_id), Event::Disconnect(client_id) => self.handle_disconnection(client_id),
Event::Message(client_id, message) => self.handle_message(client_id, message), Event::Message(client_id, message) => self.handle_message(client_id, message),
} { } {
Ok(()) => {} Ok(()) => {},
Err(error) => println!("{:?}", error), Err(error) => println!("{:?}", error),
} }
} }
} }
fn new_client(&mut self, client_id: u64, responder: Responder) -> &Client { fn broadcast_player_count(&self) {
let client = Client::new( let response = MessageResponseData::PlayerCount { players: self.clients.len() as u64 }.into_response();
ClientInfo::Ws { for (_client, responder) in self.clients.iter() {
id: client_id, responder.send(response.to_message());
responder, }
discord_info: None,
},
self.lobby.clone(),
);
self.clients.insert(client_id, client);
self.clients.get(&client_id).unwrap()
} }
fn handle_connection( fn handle_connection(&mut self, client_id: u64, responder: Responder) -> Result<(), ServerError> {
&mut self,
client_id: u64,
responder: Responder,
) -> Result<(), ServerError> {
// Debug
println!("A client connected with id #{}", client_id); println!("A client connected with id #{}", client_id);
responder.send(MessageResponseData::Greeting {
{
// 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, id: client_id,
next_mora: room.next_mora().clone(), next_mora: self.next_mora.clone(),
} }.into_message());
.into_message(), responder.send(MessageResponseData::History {
); words: self.database.load_words_before(self.database.last_word_id + 1)?
}.into_message());
// Sent recent message history self.clients.insert(client_id, responder);
client.send( self.broadcast_player_count();
MessageResponseData::History {
words: room.get_history()?,
}
.into_message(),
);
}
// Number of clients on Rc<RefCell<Room>> reference counter will be one more
self.broadcast_player_count(&self.lobby);
Ok(()) Ok(())
} }
fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> { fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> {
// Debug
println!("Client #{} disconnected.", client_id); println!("Client #{} disconnected.", client_id);
self.clients.remove(&client_id);
// Remove client self.broadcast_player_count();
// 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<RefCell<Room>> reference counter will be one less
self.broadcast_player_count(&room);
Ok(()) Ok(())
} }
fn for_client_in_room(&self, room: &Rc<RefCell<Room>>, mut closure: impl FnMut(&Client) -> ()) {
for (_id, client) in self
.clients
.iter()
.filter(|(_id, client)| Rc::<RefCell<Room>>::ptr_eq(room, &client.room))
{
closure(client);
}
}
fn client_count_in_room(&self, room: &Rc<RefCell<Room>>) -> usize {
let mut count: usize = 0;
self.for_client_in_room(room, |_| count += 1);
count
}
fn broadcast_player_count(&self, room: &Rc<RefCell<Room>>) {
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<RefCell<Room>>, 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> { fn handle_message(&mut self, client_id: u64, message: Message) -> Result<(), ServerError> {
// Ignore binary messages // Ignore binary messages
let message = match message { let message = match message {
Message::Text(message) => { Message::Text(message) => message,
// Debug Message::Binary(_) => return Ok(()),
println!( };
"Received a message from client #{}: {:?}",
client_id, message
);
message
}
Message::Binary(message) => {
// Debug // Debug
println!( println!("Received a message from client #{}: {:?}", client_id, message);
"Received a binary message from client #{}: {:?}",
client_id, message
);
return Ok(()); 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
let client = self.get_client(client_id).unwrap(); // katakana_query and hiragana_query, meaning that
// these need to be kept around as a variable
match client.room.borrow_mut().handle_query(&message, client_id) { // to prevent them from being dropped.
// Broadcast new words to all clients in same room let (katakana_query, hiragana_query) = match &query {
Ok(response) => self.announce_to_room(&client.room, response), Some(query) => (
// Send errors to only this client Some(query.to_katakana()),
Err(message) => { Some(query.to_hiragana())
client.send( ),
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 { MessageResponseData::Error {
message: message.to_string(), message: String::from("Wrong starting mora!"),
}
}
}
None => MessageResponseData::Error {
message: String::from("Not in dictionary!"),
},
} }
.into_message(), }.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(()) Ok(())
} }

@ -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(),
}
}

@ -1,6 +1,6 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use jisho::Entry;
use serde::Serialize; use serde::Serialize;
use jisho::Entry;
use std::convert::From; use std::convert::From;
#[derive(Serialize)] #[derive(Serialize)]

Loading…
Cancel
Save