Restructuring, prepare for room, Discord support
This commit is contained in:
parent
1fbb0ede50
commit
ed11799bf5
8 changed files with 492 additions and 239 deletions
113
src/client.rs
Normal file
113
src/client.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
// ---------------
|
||||
// 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 std::cmp::{PartialEq, Eq};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use simple_websockets::{Responder, Message};
|
||||
|
||||
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,4 +1,4 @@
|
|||
use crate::Word;
|
||||
use crate::word::Word;
|
||||
|
||||
use derive_more::From;
|
||||
use std::path::PathBuf;
|
||||
|
@ -15,16 +15,23 @@ 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(
|
||||
testing: bool,
|
||||
) -> Result<Self, DatabaseCreationError> {
|
||||
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)
|
||||
pub fn new(settings: DatabaseSettings) -> Result<Self, DatabaseCreationError> {
|
||||
let conn = match settings.db_type {
|
||||
DatabaseType::InMemory => Connection::open_in_memory(),
|
||||
DatabaseType::OnDisk(path) => Connection::open(PathBuf::from(path)),
|
||||
}?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS word (
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use wana_kana::{ConvertJapanese, IsJapaneseStr};
|
||||
|
||||
pub fn lookup(input: &str) -> Option<&jisho::Entry> {
|
||||
fn raw_lookup(input: &str) -> Option<&jisho::Entry> {
|
||||
let input = input.trim();
|
||||
jisho::lookup(input).into_iter().find(|&word| (
|
||||
// If input has no kanji,
|
||||
|
@ -13,3 +13,28 @@ pub fn lookup(input: &str) -> Option<&jisho::Entry> {
|
|||
// For example, 照り焼き will be accepted but 照焼 won't.
|
||||
(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -1,18 +1,17 @@
|
|||
mod client;
|
||||
mod database;
|
||||
pub use database::*;
|
||||
|
||||
mod dictionary;
|
||||
pub use dictionary::*;
|
||||
|
||||
mod response;
|
||||
mod room;
|
||||
mod server;
|
||||
pub use server::*;
|
||||
|
||||
mod utils;
|
||||
mod word;
|
||||
pub use word::*;
|
||||
|
||||
use crate::server::{Server, ServerSettings};
|
||||
|
||||
fn main() {
|
||||
const PORT: u16 = 8080;
|
||||
let mut server = Server::new(PORT, true)
|
||||
.unwrap_or_else(|error| panic!("Failed to start server at port {PORT}: {:?}", error));
|
||||
let settings = ServerSettings::default();
|
||||
let mut server = Server::new(&settings)
|
||||
.unwrap_or_else(|error| panic!("Failed to start server at port {}: {:?}", settings.port, error));
|
||||
server.run();
|
||||
}
|
||||
|
|
62
src/response.rs
Normal file
62
src/response.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
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()
|
||||
}
|
||||
}
|
92
src/room.rs
Normal file
92
src/room.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
// ---------------
|
||||
// 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_starting_mora, get_final_mora};
|
||||
use crate::word::Word;
|
||||
|
||||
use wana_kana::{IsJapaneseStr, ConvertJapanese};
|
||||
|
||||
#[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!"),
|
||||
}
|
||||
}
|
||||
}
|
344
src/server.rs
344
src/server.rs
|
@ -1,80 +1,14 @@
|
|||
use crate::DatabaseCreationError;
|
||||
use crate::lookup;
|
||||
use crate::Word;
|
||||
use crate::Database;
|
||||
use crate::client::{Client, ClientInfo};
|
||||
use crate::database::{DatabaseSettings, DatabaseType, DatabaseCreationError};
|
||||
use crate::response::MessageResponseData;
|
||||
use crate::room::{Room, RoomSettings};
|
||||
|
||||
use simple_websockets::{Event, Responder, Message, EventHub};
|
||||
use std::collections::HashMap;
|
||||
use serde::Serialize;
|
||||
use wana_kana::{ConvertJapanese, IsJapaneseStr};
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use simple_websockets::{Event, Responder, Message, EventHub};
|
||||
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)]
|
||||
pub enum ServerCreationError {
|
||||
SimpleWebsocketsError(simple_websockets::Error),
|
||||
|
@ -86,63 +20,49 @@ pub enum ServerError {
|
|||
DatabaseError(rusqlite::Error),
|
||||
}
|
||||
|
||||
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 ServerSettings {
|
||||
pub port: u16,
|
||||
pub testing: bool,
|
||||
}
|
||||
|
||||
// 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;
|
||||
impl Default for ServerSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: 8080,
|
||||
testing: true,
|
||||
}
|
||||
}
|
||||
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(),
|
||||
}
|
||||
pub struct Server {
|
||||
event_hub: EventHub,
|
||||
lobby: Rc<RefCell<Room>>,
|
||||
rooms: HashMap<String, Room>,
|
||||
clients: HashMap<u64, Client>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(port: u16, testing: bool) -> Result<Self, ServerCreationError> {
|
||||
Ok(Server {
|
||||
event_hub: simple_websockets::launch(port)?,
|
||||
database: Database::new(testing)?,
|
||||
pub fn new(settings: &ServerSettings) -> Result<Self, ServerCreationError> {
|
||||
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(),
|
||||
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() {
|
||||
|
@ -156,126 +76,114 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
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 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 handle_connection(&mut self, client_id: u64, responder: Responder) -> Result<(), ServerError> {
|
||||
// Debug
|
||||
println!("A client connected with id #{}", client_id);
|
||||
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();
|
||||
|
||||
{
|
||||
// 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<RefCell<Room>> reference counter will be one more
|
||||
self.broadcast_player_count(&self.lobby);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> {
|
||||
// Debug
|
||||
println!("Client #{} disconnected.", client_id);
|
||||
self.clients.remove(&client_id);
|
||||
self.broadcast_player_count();
|
||||
|
||||
// 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<RefCell<Room>> reference counter will be one less
|
||||
self.broadcast_player_count(&room);
|
||||
|
||||
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> {
|
||||
// Ignore binary messages
|
||||
let message = match message {
|
||||
Message::Text(message) => message,
|
||||
Message::Binary(_) => return Ok(()),
|
||||
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(());
|
||||
}
|
||||
};
|
||||
|
||||
// Debug
|
||||
println!("Received a message from client #{}: {:?}", client_id, message);
|
||||
let client = self.get_client(client_id).unwrap();
|
||||
|
||||
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 {
|
||||
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
|
||||
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);
|
||||
},
|
||||
Err(message) => {
|
||||
client.send(MessageResponseData::Error { message: message.to_string() }.into_message());
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
|
47
src/utils.rs
Normal file
47
src/utils.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
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(),
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue