parent
430702f1c5
commit
3b0e75b7a1
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>shiritori</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="https://tegakituesday.com/favicon.ico">
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<h1><ruby>字<rp>(</rp><rt>じ</rt><rp>)<rp></ruby>ちゃん<span style="font-size: 0.75em">の</span>しりとり</h1>
|
||||||
|
<div id="shiritori"></div>
|
||||||
|
<input id="shiritori-input" onfocusout="setTimeout(() => this.focus(), 100)" autofocus>
|
||||||
|
<p id="shiritori-players"></p>
|
||||||
|
</div>
|
||||||
|
<script src="shiritori.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,38 @@
|
|||||||
|
ws = new WebSocket("ws://localhost:8080");
|
||||||
|
const div = document.querySelector("#shiritori");
|
||||||
|
const input = document.querySelector("#shiritori-input");
|
||||||
|
const players = document.querySelector("#shiritori-players");
|
||||||
|
let id = null;
|
||||||
|
|
||||||
|
ws.onmessage = e => {
|
||||||
|
const { event, data } = JSON.parse(e.data);
|
||||||
|
switch (event) {
|
||||||
|
case "greeting":
|
||||||
|
id = data.id;
|
||||||
|
break;
|
||||||
|
case "word":
|
||||||
|
let waiting = data.author === id;
|
||||||
|
p = document.createElement("p");
|
||||||
|
p.innerHTML = data.reading || data.word === data.reading ? (data.word === "" ? data.reading : `<ruby>${data.word}<rp>(</rp><rt>${data.reading}<rt/><rp>)</rp></ruby>`) : 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 "playerCount":
|
||||||
|
let otherPlayers = data.players - 1;
|
||||||
|
players.innerHTML = `${otherPlayers === 0 ? "No" : otherPlayers} other player${otherPlayers === 1 ? "" : "s"} online.`;
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
alert(data.message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('keypress', e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
ws.send(input.value);
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,49 @@
|
|||||||
|
* {
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: ghostwhite;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: slategrey;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
div#content {
|
||||||
|
width: max-content;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
div#shiritori p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
div#shiritori {
|
||||||
|
padding: 2px;
|
||||||
|
height: 8em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
div#shiritori, input#shiritori-input {
|
||||||
|
padding: 0.25em;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
input#shiritori-input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
border-top: none;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
input#shiritori-input:focus {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(0, 0, 0, 0.125);
|
||||||
|
}
|
||||||
|
div#shiritori {
|
||||||
|
border-bottom: none;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
p#shiritori-players {
|
||||||
|
text-align: center;
|
||||||
|
}
|
@ -1,3 +1,158 @@
|
|||||||
|
use simple_websockets::{Event, Responder, Message};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use serde::Serialize;
|
||||||
|
use wana_kana::{ConvertJapanese, IsJapaneseStr};
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
author: u64,
|
||||||
|
word: String,
|
||||||
|
reading: Option<String>,
|
||||||
|
next_char: char,
|
||||||
|
},
|
||||||
|
PlayerCount {
|
||||||
|
players: u64,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageResponseData {
|
||||||
|
fn get_name(&self) -> String {
|
||||||
|
String::from(match self {
|
||||||
|
Self::Greeting { .. } => "greeting",
|
||||||
|
Self::Word { .. } => "word",
|
||||||
|
Self::PlayerCount { .. } => "playerCount",
|
||||||
|
Self::Error { .. } => "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_response(self) -> MessageResponse {
|
||||||
|
MessageResponse {
|
||||||
|
event: self.get_name(),
|
||||||
|
data: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_message(self) -> Message {
|
||||||
|
self.to_response().to_message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn broadcast_player_count(clients: &mut HashMap<u64, Responder>) {
|
||||||
|
let response = MessageResponseData::PlayerCount { players: clients.len() as u64 }.to_response();
|
||||||
|
for (_client, responder) in clients.iter() {
|
||||||
|
responder.send(response.to_message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
let event_hub = simple_websockets::launch(8080)
|
||||||
|
.expect("failed to listen on port 8080");
|
||||||
|
let mut clients: HashMap<u64, Responder> = HashMap::new();
|
||||||
|
let mut next_char: Option<char> = None;
|
||||||
|
let mut last_response: Option<MessageResponse> = None;
|
||||||
|
let mut last_client_id: Option<u64> = None;
|
||||||
|
loop {
|
||||||
|
match event_hub.poll_event() {
|
||||||
|
Event::Connect(client_id, responder) => {
|
||||||
|
println!("A client connected with id #{}", client_id);
|
||||||
|
responder.send(MessageResponseData::Greeting { id: client_id }.to_message());
|
||||||
|
if let Some(ref last_response) = last_response {
|
||||||
|
responder.send(last_response.to_message());
|
||||||
|
}
|
||||||
|
clients.insert(client_id, responder);
|
||||||
|
broadcast_player_count(&mut clients);
|
||||||
|
},
|
||||||
|
Event::Disconnect(client_id) => {
|
||||||
|
println!("Client #{} disconnected.", client_id);
|
||||||
|
clients.remove(&client_id);
|
||||||
|
broadcast_player_count(&mut clients);
|
||||||
|
},
|
||||||
|
Event::Message(client_id, message) => {
|
||||||
|
// Ignore binary messages
|
||||||
|
let message = match message {
|
||||||
|
Message::Text(message) => message,
|
||||||
|
Message::Binary(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
println!("Received a message from client #{}: {:?}", client_id, message);
|
||||||
|
|
||||||
|
let response = if Some(client_id) == last_client_id {
|
||||||
|
MessageResponseData::Error {
|
||||||
|
message: String::from("It's not your turn!"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match jisho::lookup(&message.trim()).iter().next() {
|
||||||
|
Some(entry) => {
|
||||||
|
if entry.reading.chars().last().unwrap().to_string().to_hiragana() == "ん" {
|
||||||
|
MessageResponseData::Error {
|
||||||
|
message: String::from("Can't end with ん!"),
|
||||||
|
}
|
||||||
|
} else if next_char.is_none() || entry.reading.chars().next().unwrap().to_string().to_hiragana().chars().next().unwrap() == next_char.unwrap() {
|
||||||
|
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: entry.kanji.to_owned(),
|
||||||
|
reading: Some(entry.reading.to_owned()),
|
||||||
|
next_char: next_char.unwrap(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MessageResponseData::Error {
|
||||||
|
message: String::from("Wrong starting kana!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => MessageResponseData::Error {
|
||||||
|
message: String::from("Not in dictionary!"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}.to_response();
|
||||||
|
|
||||||
|
match response.data {
|
||||||
|
// Send errors to only this client
|
||||||
|
MessageResponseData::Error { .. } => {
|
||||||
|
clients.get(&client_id).unwrap().send(response.to_message());
|
||||||
|
},
|
||||||
|
// Broadcast everything else to all clients
|
||||||
|
_ => {
|
||||||
|
for (_client, responder) in clients.iter() {
|
||||||
|
responder.send(response.to_message());
|
||||||
|
}
|
||||||
|
last_response = Some(response);
|
||||||
|
last_client_id = Some(client_id);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue