diff --git a/demo/shiritori.js b/demo/shiritori.js index f43f5bf..f1ee92c 100644 --- a/demo/shiritori.js +++ b/demo/shiritori.js @@ -4,21 +4,6 @@ const input = document.querySelector("#shiritori-input"); const players = document.querySelector("#shiritori-players"); let id = null; -function displayWord(_word, end, delay) { - let { word, reading } = _word; - p = document.createElement("p"); - p.innerHTML = reading || word === reading ? (word === "" ? reading : `${word}(${reading})`) : word; - if (delay != 0) { - p.style.animationDelay = `${delay}s`; - } - if (end) { - div.append(p); - } else { - div.prepend(p); - } - div.scrollTop = div.offsetHeight; -} - ws.onmessage = e => { const { event, data } = JSON.parse(e.data); switch (event) { @@ -27,17 +12,14 @@ ws.onmessage = e => { break; case "word": let waiting = data.author === id; - displayWord(data.word, true, 0); - input.placeholder = waiting ? "Waiting for other players..." : `${data.next_mora}…`; + p = document.createElement("p"); + p.innerHTML = data.reading || data.word === data.reading ? (data.word === "" ? data.reading : `${data.word}(${data.reading})`) : data.word; + div.appendChild(p); + div.scrollTop = div.offsetHeight; + input.placeholder = waiting ? "Waiting for other players..." : `${data.next_char}…`; input.disabled = waiting; if (!waiting) input.focus(); break; - case "history": - console.log(data); - for (let i = 0; i < data.words.length; i++) { - displayWord(data.words[i], false, 0.1 * i); - } - break; case "playerCount": let otherPlayers = data.players - 1; players.innerHTML = `${otherPlayers === 0 ? "No" : otherPlayers} other player${otherPlayers === 1 ? "" : "s"} online.`; diff --git a/demo/style.css b/demo/style.css index 5b1a5e7..3f1984b 100644 --- a/demo/style.css +++ b/demo/style.css @@ -12,28 +12,11 @@ div#content { } div#shiritori p { margin: 0; - animation-duration: 1s; - animation-name: slidein; - animation-fill-mode: forwards; - opacity: 0; -} -@keyframes slidein { - from { - transform: translateX(-32px); - opacity: 0; - } - - to { - transform: none; - opacity: 1; - } } div#shiritori { padding: 2px; - height: 24em; - font-size: 0.875em; + height: 8em; overflow-y: scroll; - text-align: center; } div#shiritori, input#shiritori-input { padding: 0.25em; diff --git a/src/database.rs b/src/database.rs index 19a3256..59563be 100644 --- a/src/database.rs +++ b/src/database.rs @@ -6,7 +6,6 @@ use rusqlite::{Connection, Result, params}; pub struct Database { conn: Connection, - pub last_word_id: i64, } #[derive(From, Debug)] @@ -29,26 +28,17 @@ impl Database { conn.execute( "CREATE TABLE IF NOT EXISTS word ( id INTEGER PRIMARY KEY, - word TEXT, - reading TEXT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + word TEXT, reading TEXT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, )", params![], )?; - let last_word_id = match conn - .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 - }; - Ok(Self { conn, last_word_id }) + Ok(Self { conn }) } pub fn load_words_before(&self, before_id: i64) -> Result> { self.conn - .prepare("SELECT id, word, reading, timestamp FROM word WHERE id < ? ORDER BY id DESC LIMIT 10")? + .prepare("SELECT id, word, reading, timestamp FROM word WHERE id < ? DESC LIMIT 10")? .query_map(params![before_id], |row| { Ok(Word { id: row.get(0)?, @@ -59,16 +49,4 @@ impl Database { })? .collect::>>() } - - 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, - ], - )?; - self.last_word_id += 1; - Ok(()) - } } diff --git a/src/main.rs b/src/main.rs index 4fea4ce..b51f3f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ pub use word::*; 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 mut server = Server::new(PORT) + .unwrap_or_else(|_| panic!("Failed to start server at port {PORT}")); server.run(); } diff --git a/src/server.rs b/src/server.rs index c8ba785..f7efebb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,13 +1,9 @@ -use crate::DatabaseCreationError; -use crate::lookup; -use crate::Word; -use crate::Database; +use crate::dictionary::lookup; use simple_websockets::{Event, Responder, Message, EventHub}; use std::collections::HashMap; use serde::Serialize; use wana_kana::{ConvertJapanese, IsJapaneseStr}; -use derive_more::From; #[derive(Serialize)] struct MessageResponse { @@ -29,11 +25,9 @@ enum MessageResponseData { }, Word { author: u64, - word: Word, - next_mora: String, - }, - History { - words: Vec, + word: String, + reading: Option, + next_char: char, }, PlayerCount { players: u64, @@ -48,7 +42,6 @@ impl MessageResponseData { String::from(match self { Self::Greeting { .. } => "greeting", Self::Word { .. } => "word", - Self::History { .. } => "history", Self::PlayerCount { .. } => "playerCount", Self::Error { .. } => "error", }) @@ -68,89 +61,29 @@ impl MessageResponseData { pub struct Server { event_hub: EventHub, - database: Database, clients: HashMap, - next_mora: Option, + next_char: Option, + last_response: Option, last_client_id: Option, } -#[derive(From, Debug)] -pub enum ServerCreationError { - SimpleWebsocketsError(simple_websockets::Error), - DatabaseCreationError(DatabaseCreationError), -} - -#[derive(From, Debug)] -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(), - } -} - -// 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(port: u16, testing: bool) -> Result { + pub fn new(port: u16) -> Result { Ok(Server { event_hub: simple_websockets::launch(port)?, - database: Database::new(testing)?, clients: HashMap::new(), - next_mora: None, + next_char: None, + last_response: None, last_client_id: None, }) } pub fn run(&mut self) { loop { - match match self.event_hub.poll_event() { + match self.event_hub.poll_event() { 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(()) => {}, - Err(error) => println!("{:?}", error), } } } @@ -162,31 +95,27 @@ impl Server { } } - fn handle_connection(&mut self, client_id: u64, responder: Responder) -> Result<(), ServerError> { + fn handle_connection(&mut self, client_id: u64, responder: Responder) { println!("A client connected with id #{}", client_id); - responder.send(MessageResponseData::Greeting { - id: client_id - }.into_message()); - responder.send(MessageResponseData::History { - words: self.database.load_words_before(self.database.last_word_id + 1)? - }.into_message()); + responder.send(MessageResponseData::Greeting { id: client_id }.into_message()); + if let Some(ref last_response) = self.last_response { + responder.send(last_response.to_message()); + } self.clients.insert(client_id, responder); self.broadcast_player_count(); - Ok(()) } - fn handle_disconnection(&mut self, client_id: u64) -> Result<(), ServerError> { + fn handle_disconnection(&mut self, client_id: u64) { println!("Client #{} disconnected.", client_id); self.clients.remove(&client_id); self.broadcast_player_count(); - Ok(()) } - fn handle_message(&mut self, client_id: u64, message: Message) -> Result<(), ServerError> { + fn handle_message(&mut self, client_id: u64, message: Message) { // Ignore binary messages let message = match message { Message::Text(message) => message, - Message::Binary(_) => return Ok(()), + Message::Binary(_) => return, }; // Debug @@ -199,23 +128,31 @@ impl Server { } else { match lookup(&message) { Some(entry) => { - println!("{}, {:?}", get_starting_mora(&entry.reading), self.next_mora); 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)?; + } else if self.next_char.is_none() || entry.reading.chars().next().unwrap().to_string().to_hiragana().chars().next().unwrap() == self.next_char.unwrap() { + self.next_char = { + // If final character is lengthener or not kana + // Use semifinal + let mut final_chars = entry.reading.chars().rev(); + let final_char = final_chars.next().unwrap(); + Some(if final_char == 'ー' || !final_char.to_string().is_kana() { + final_chars.next().unwrap() + } else { + final_char + }.to_string().to_hiragana().chars().next().unwrap()) + }; MessageResponseData::Word { author: client_id, - word, - next_mora: self.next_mora.as_deref().unwrap().to_owned(), + word: entry.kanji.to_owned(), + reading: Some(entry.reading.to_owned()), + next_char: self.next_char.unwrap(), } } else { MessageResponseData::Error { - message: String::from("Wrong starting mora!"), + message: String::from("Wrong starting kana!"), } } } @@ -235,9 +172,10 @@ impl Server { for (_client, responder) in self.clients.iter() { responder.send(response.to_message()); } + self.last_response = Some(response); self.last_client_id = Some(client_id); }, - }; - Ok(()) + } + } } diff --git a/src/word.rs b/src/word.rs index cfa1701..08bc818 100644 --- a/src/word.rs +++ b/src/word.rs @@ -1,26 +1,11 @@ use chrono::{DateTime, Utc}; use serde::Serialize; -use jisho::Entry; -use std::convert::From; #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct Word { - #[serde(skip_serializing)] - pub id: Option, + pub id: i64, pub word: String, pub reading: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub timestamp: Option>, -} - -impl From for Word { - fn from(entry: Entry) -> Self { - Self { - id: None, - word: entry.kanji, - reading: entry.reading, - timestamp: None, - } - } + pub timestamp: DateTime, }