"serde",
"serde_derive",
"serde_json",
+ "sha2",
"signal-hook",
"signal-hook-async-std",
"tide",
serde = "1"
serde_derive = "1"
serde_json = "1"
+sha2 = "0.9"
signal-hook = "0.3"
signal-hook-async-std = "0.2"
tide = { version = "0.16.0", default-features = false, features = ["h1-server"] }
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+ <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+ <g id="text">
+ <text x="165" y="60" style="font: bold 50px serif;" fill="red">A</text>
+ <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+ </g>
+ <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
game_list_outside.append(game_list_new);
this.game_list_filter_input = document.createElement("input");
this.game_list_filter_input.value = filter;
+ this.game_list_filter_input.onchange = () => this.send({type: "JoinLobby", filter: this.game_list_filter_input.value});
game_list_outside.append(this.game_list_filter_input);
this.container.append(game_list_outside);
format_row.append(format_title);
const format_value = document.createElement("td");
const format_input = document.createElement("select");
- for (const format of ["KnockOutWhist", "TexasHoldEm"]) {
+ for (const format of ["KnockOutWhist", "TexasHoldEm", "Chatroom"]) {
const game_format = document.createElement("option");
game_format.setAttribute("value", format);
game_format.innerText = format;
knock_out_whist.onclick = () => this.send({type: "JoinLobby", filter: "format: KnockOutWhist"});
menu_container.append(knock_out_whist);
+ const chatroom = document.createElement("div");
+ chatroom.classList.add("game-format-icon");
+ const chatroom_img = document.createElement("img");
+ chatroom_img.setAttribute("src", "img/chatroom.svg");
+ const chatroom_text = document.createElement("p");
+ chatroom_text.innerText = "Chatroom";
+ chatroom.append(chatroom_img);
+ chatroom.append(chatroom_text);
+ chatroom.onclick = () => this.send({type: "JoinLobby", filter: "format: Chatroom"});
+ menu_container.append(chatroom);
+
this.container.append(menu_container);
}
}
display: table-row;
}
+.game-popup > table.Chatroom tr.game-field {
+ display: none;
+}
+
+.game-popup > table.Chatroom tr.game-field.Chatroom {
+ display: table-row;
+}
+
.game-popup input, .game-popup select {
width: 100%;
height: 100%;
+use sha2::{Digest, Sha256};
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method")]
pub enum Auth {
NoLogin,
Plain { password: String },
+ Sha256 {
+ salt: String,
+ #[serde(with = "hex")]
+ hash: [u8; 32],
+ },
}
impl Auth {
match self {
Auth::NoLogin => false,
Auth::Plain { password } => signature == password,
+ Auth::Sha256 { salt, hash } => {
+ let mut hasher = Sha256::new();
+ hasher.update(salt.as_bytes());
+ hasher.update(signature.as_bytes());
+ hasher.finalize()[..] == hash[..]
+ }
}
}
}
let from = game_list.games_len();
match self.server.game_list(from).await {
Ok(games) => {
- for game in games.clone() {
- game_list.push(game);
- }
+ game_list.update(games.len());
iter(games).map(|game| ServerMessage::NewGame { game }).boxed()
}
Err(err) => once(async move { ServerMessage::ProtocolError { reason: err.to_string() } }).boxed(),
}
}
(&mut ClientState::LoggedIn { username, .. }, ClientMessage::JoinLobby { filter }) => {
- let mut game_list = GameList::new(filter.clone());
+ let mut game_list = match GameList::new(filter.clone()) {
+ Ok(game_list) => game_list,
+ Err(reason) => return ServerMessage::JoinLobbyFailure { reason },
+ };
match self.server.game_list(0).await {
Ok(games) => {
- for game in games.clone() {
- game_list.push(game);
- }
+ game_list.update(games.len());
+ let games = games.into_iter().filter(|game| game_list.matches(game)).collect();
self.client = ClientState::LoggedIn { username, state: LoggedInState::InLobby { game_list } };
ServerMessage::JoinLobbySuccess { filter, games }
}
let mut interests = HashSet::new();
interests.insert(ClientInterest::GameList);
server_state.register_interests(interests).await;
- let mut game_list = GameList::new("".to_string());
+ let mut game_list = GameList::match_all();
loop {
let games_len = game_list.games_len();
match server_state.game_list(games_len).await {
Ok(games) => {
+ game_list.update(games.len());
for game in games {
info!("Starting new game {:?}", game);
let id = game.id();
- game_list.push(game);
if partition.start_dealer_for(id) {
let (server_state, update_stream) = server.new_state().await;
if let Ok(dealer) = Dealer::new(server_state, id).await {
+++ /dev/null
-use nom::{
- branch::alt,
- bytes::complete::{tag, take_until},
- character::complete::{alpha1, multispace0},
- combinator::map,
- error::ParseError,
- multi::{many0_count, separated_list1},
- sequence::{delimited, preceded, tuple},
- IResult,
-};
-
-fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
-where
- F: FnMut(&'a str) -> IResult<&'a str, O, E>,
-{
- delimited(multispace0, inner, multispace0)
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Field<'a> {
- Format(&'a str),
- Title(&'a str),
- Parens(Box<Filter<'a>>),
-}
-
-fn parse_field(input: &str) -> IResult<&str, Field> {
- let string = || alt((alpha1, delimited(tag("\""), take_until("\""), tag("\""))));
- let format = map(preceded(tuple((tag("format"), ws(tag(":")))), string()), Field::Format);
- let title = map(preceded(tuple((tag("title"), ws(tag(":")))), string()), Field::Title);
- let parens = delimited(tag("("), map(map(ws(parse_filter), Box::new), Field::Parens), tag(")"));
- alt((format, title, parens))(input)
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Not<'a> {
- negate: bool,
- field: Field<'a>,
-}
-
-fn parse_not(input: &str) -> IResult<&str, Not> {
- map(tuple((many0_count(ws(tag("not"))), parse_field)), |(count, field)| Not { negate: count % 2 == 1, field })(input)
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct And<'a>(Vec<Not<'a>>);
-
-fn parse_and(input: &str) -> IResult<&str, And> {
- map(separated_list1(ws(tag("and")), parse_not), And)(input)
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Or<'a>(Vec<And<'a>>);
-
-fn parse_or(input: &str) -> IResult<&str, Or> {
- map(separated_list1(ws(tag("or")), parse_and), Or)(input)
-}
-
-type Filter<'a> = Or<'a>;
-
-fn parse_filter(input: &str) -> IResult<&str, Filter> {
- parse_or(input)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn field_parse() {
- assert_eq!(Field::Format("KnockOutWhist"), parse_field("format: KnockOutWhist").unwrap().1);
- assert_eq!(Field::Title("Superb Game"), parse_field("title: \"Superb Game\"").unwrap().1);
- }
-
- #[test]
- fn filter_parse() {
- let expected = Or(vec![
- And(vec![Not {
- negate: false,
- field: Field::Parens(Box::new(Or(vec![And(vec![
- Not { negate: false, field: Field::Format("KnockOutWhist") },
- Not { negate: true, field: Field::Title("Superb Game") },
- ])]))),
- }]),
- And(vec![Not { negate: false, field: Field::Format("TexasHoldEm") }]),
- ]);
- let actual = parse_filter("(format: KnockOutWhist and not title: \"Superb Game\") or format: TexasHoldEm").unwrap().1;
- assert_eq!(expected, actual);
- }
-}
title: String,
}
+impl ChatroomSettings {
+ pub fn title(&self) -> &str {
+ &self.title
+ }
+}
+
#[derive(Debug, Clone)]
pub struct Chatroom {
id: i64,
--- /dev/null
+use nom::{
+ branch::alt,
+ bytes::complete::{tag, take_until},
+ character::complete::{alpha1, multispace0},
+ combinator::map,
+ error::ParseError,
+ multi::{many0_count, separated_list0, separated_list1},
+ sequence::{delimited, preceded, tuple},
+ IResult,
+};
+
+use super::{GameSettings, GameSummary};
+
+pub trait Filter {
+ fn matches(&self, summary: &GameSummary) -> bool;
+}
+
+fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
+where
+ F: FnMut(&'a str) -> IResult<&'a str, O, E>,
+{
+ delimited(multispace0, inner, multispace0)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum Field {
+ Format(String),
+ Title(String),
+ Parens(Box<Or>),
+}
+
+impl Filter for Field {
+ fn matches(&self, summary: &GameSummary) -> bool {
+ match self {
+ Field::Format(format) => match summary.settings {
+ GameSettings::Chatroom(_) => format.eq_ignore_ascii_case("Chatroom"),
+ GameSettings::KnockOutWhist(_) => format.eq_ignore_ascii_case("KnockOutWhist"),
+ GameSettings::TexasHoldEm(_) => format.eq_ignore_ascii_case("TexasHoldEm"),
+ },
+ Field::Title(title) => summary.settings.title().to_lowercase().contains(&title.to_lowercase()),
+ Field::Parens(filter) => filter.matches(summary),
+ }
+ }
+}
+
+fn parse_field(input: &str) -> IResult<&str, Field> {
+ let string = || map(alt((alpha1, delimited(tag("\""), take_until("\""), tag("\"")))), String::from);
+ let format = map(preceded(tuple((tag("format"), ws(tag(":")))), string()), Field::Format);
+ let title = map(preceded(tuple((tag("title"), ws(tag(":")))), string()), Field::Title);
+ let parens = delimited(tag("("), map(map(ws(parse_or), Box::new), Field::Parens), tag(")"));
+ alt((format, title, parens))(input)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct Not {
+ negate: bool,
+ field: Field,
+}
+
+impl Filter for Not {
+ fn matches(&self, summary: &GameSummary) -> bool {
+ self.negate ^ self.field.matches(summary)
+ }
+}
+
+fn parse_not(input: &str) -> IResult<&str, Not> {
+ map(tuple((many0_count(ws(tag("not"))), parse_field)), |(count, field)| Not { negate: count % 2 == 1, field })(input)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct And(Vec<Not>);
+
+impl Filter for And {
+ fn matches(&self, summary: &GameSummary) -> bool {
+ self.0.iter().all(|filter| filter.matches(summary))
+ }
+}
+
+fn parse_and(input: &str) -> IResult<&str, And> {
+ map(separated_list0(ws(tag("and")), parse_not), And)(input)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct Or(Vec<And>);
+
+impl Filter for Or {
+ fn matches(&self, summary: &GameSummary) -> bool {
+ self.0.iter().any(|filter| filter.matches(summary))
+ }
+}
+
+fn parse_or(input: &str) -> IResult<&str, Or> {
+ map(ws(separated_list1(ws(tag("or")), parse_and)), Or)(input)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct ParsedFilter(Or);
+
+impl ParsedFilter {
+ pub fn match_all() -> Self {
+ ParsedFilter(Or(vec![And(vec![])]))
+ }
+}
+
+impl Filter for ParsedFilter {
+ fn matches(&self, summary: &GameSummary) -> bool {
+ self.0.matches(summary)
+ }
+}
+
+pub fn parse_filter(input: &str) -> Result<ParsedFilter, String> {
+ parse_or(input)
+ .map(|(_, filter)| ParsedFilter(filter))
+ .map_err(|err| err.to_string())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn field_parse() {
+ assert_eq!(Field::Format(String::from("KnockOutWhist")), parse_field("format: KnockOutWhist").unwrap().1);
+ assert_eq!(Field::Title(String::from("Superb Game")), parse_field("title: \"Superb Game\"").unwrap().1);
+ }
+
+ #[test]
+ fn filter_parse() {
+ let expected = Or(vec![
+ And(vec![Not {
+ negate: false,
+ field: Field::Parens(Box::new(Or(vec![And(vec![
+ Not { negate: false, field: Field::Format(String::from("KnockOutWhist")) },
+ Not { negate: true, field: Field::Title(String::from("Superb Game")) },
+ ])]))),
+ }]),
+ And(vec![Not { negate: false, field: Field::Format(String::from("TexasHoldEm")) }]),
+ ]);
+ let actual = parse_or("(format: KnockOutWhist and not title: \"Superb Game\") or format: TexasHoldEm").unwrap().1;
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn filter_matches() {
+ let filter = parse_filter("format: KnockOutWhist and (title:Game or title:Play)").unwrap();
+ let games: Vec<GameSummary> = serde_json::from_str(
+ r#"[
+ {"id": 0, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}},
+ {"id": 1, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}},
+ {"id": 2, "settings": {"format":"Chatroom","title":"Game 2"}},
+ {"id": 3, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}},
+ {"id": 4, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}}
+ ]"#,
+ )
+ .unwrap();
+ assert!(filter.matches(&games[0]));
+ assert!(filter.matches(&games[1]));
+ assert!(!filter.matches(&games[2]));
+ assert!(!filter.matches(&games[3]));
+ assert!(filter.matches(&games[4]));
+ }
+
+ #[test]
+ fn empty_filter_matches() {
+ let filter = parse_filter(" ").unwrap();
+ let games: Vec<GameSummary> = serde_json::from_str(
+ r#"[
+ {"id": 0, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}},
+ {"id": 1, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}},
+ {"id": 2, "settings": {"format":"Chatroom","title":"Game 2"}},
+ {"id": 3, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}},
+ {"id": 4, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}}
+ ]"#,
+ )
+ .unwrap();
+ assert!(filter.matches(&games[0]));
+ assert!(filter.matches(&games[1]));
+ assert!(filter.matches(&games[2]));
+ assert!(filter.matches(&games[3]));
+ assert!(filter.matches(&games[4]));
+ }
+}
mod action;
mod chatroom;
+mod filter;
mod poker;
mod whist;
use crate::util::timestamp::Timestamp;
use self::chatroom::{Chatroom, ChatroomSettings};
+use self::filter::{parse_filter, Filter, ParsedFilter};
use self::poker::{TexasHoldEm, TexasHoldEmSettings};
use self::whist::{KnockOutWhist, KnockOutWhistSettings};
TexasHoldEm(TexasHoldEmSettings),
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "format")]
+impl GameSettings {
+ pub fn title(&self) -> &str {
+ match self {
+ GameSettings::Chatroom(settings) => settings.title(),
+ GameSettings::KnockOutWhist(settings) => settings.title(),
+ GameSettings::TexasHoldEm(settings) => settings.title(),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
pub struct GameList {
- filter: String,
- games: Vec<GameSummary>,
+ filter: ParsedFilter,
+ games_len: usize,
}
impl GameList {
- pub fn new(filter: String) -> Self {
- Self { filter, games: Vec::new() }
+ pub fn new(filter: String) -> Result<Self, String> {
+ let filter = parse_filter(&filter)?;
+ Ok(Self { filter, games_len: 0 })
+ }
+
+ pub fn match_all() -> Self {
+ Self { filter: ParsedFilter::match_all(), games_len: 0 }
}
pub fn games_len(&self) -> usize {
- self.games.len()
+ self.games_len
+ }
+
+ pub fn update(&mut self, new_games: usize) {
+ self.games_len += new_games;
}
- pub fn push(&mut self, game: GameSummary) {
- self.games.push(game);
+ pub fn matches(&self, game: &GameSummary) -> bool {
+ self.filter.matches(&game)
}
}
action_timeout: Option<i64>,
}
+impl TexasHoldEmSettings {
+ pub fn title(&self) -> &str {
+ &self.title
+ }
+}
+
#[derive(Clone, Debug)]
pub struct TexasHoldEm {
id: i64,
max_players: u32,
}
+impl KnockOutWhistSettings {
+ pub fn title(&self) -> &str {
+ &self.title
+ }
+}
+
#[derive(Clone, Debug)]
pub struct KnockOutWhist {
id: i64,
mod client;
mod config;
mod dealer;
-mod filter;
mod game;
mod pubsub;
mod rng;