From da653bf1ab52ff8374844c85c4ae2e9d35cc2b8f Mon Sep 17 00:00:00 2001 From: Geoffrey Allott Date: Sat, 13 Mar 2021 20:17:48 +0000 Subject: [PATCH] new main menu and game list --- Cargo.lock | 75 +++++++++++++++ Cargo.toml | 1 + site/img/holdem.svg | 8 ++ site/img/whist.svg | 8 ++ site/modules/gamelist.js | 196 +++++++++++++++++++++++++++++++++------ site/modules/mainmenu.js | 35 +++++++ site/modules/socket.js | 5 +- site/modules/words.js | 65 +++++++++++++ site/style.css | 1 + site/style/game-list.css | 111 +++++++++++++--------- site/style/mainmenu.css | 33 +++++++ site/style/poker.css | 1 + src/api.rs | 2 +- src/client.rs | 4 +- src/filter.rs | 92 ++++++++++++++++++ src/main.rs | 1 + 16 files changed, 562 insertions(+), 76 deletions(-) create mode 100644 site/img/holdem.svg create mode 100644 site/img/whist.svg create mode 100644 site/modules/mainmenu.js create mode 100644 site/modules/words.js create mode 100644 site/style/mainmenu.css create mode 100644 src/filter.rs diff --git a/Cargo.lock b/Cargo.lock index 64a8404..50c7c4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,12 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "async-attributes" version = "1.1.2" @@ -356,6 +362,18 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -698,6 +716,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futures" version = "0.3.13" @@ -1030,6 +1054,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.88" @@ -1092,6 +1129,19 @@ dependencies = [ "socket2", ] +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -1215,6 +1265,7 @@ dependencies = [ "hex", "itertools", "log", + "nom", "pin-project", "rand 0.8.3", "rand_chacha 0.3.0", @@ -1290,6 +1341,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.7.3" @@ -1702,6 +1759,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.4.20" @@ -1780,6 +1843,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.2.0" @@ -2258,3 +2327,9 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/Cargo.toml b/Cargo.toml index 37eda43..d84cea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ futures = "0.3" getrandom = "0.2" hex = { version = "0.4", features = ["serde"] } itertools = "0.10" +nom = "6" pin-project = "1.0" rand = "0.8" rand_chacha = "0.3" diff --git a/site/img/holdem.svg b/site/img/holdem.svg new file mode 100644 index 0000000..e5ab9af --- /dev/null +++ b/site/img/holdem.svg @@ -0,0 +1,8 @@ + + + + A + ♠ + + + diff --git a/site/img/whist.svg b/site/img/whist.svg new file mode 100644 index 0000000..8f8178d --- /dev/null +++ b/site/img/whist.svg @@ -0,0 +1,8 @@ + + + + A + ♣ + + + diff --git a/site/modules/gamelist.js b/site/modules/gamelist.js index 5207190..193e43c 100644 --- a/site/modules/gamelist.js +++ b/site/modules/gamelist.js @@ -1,30 +1,30 @@ +import { random_title } from "./words.js"; + export class GameList { - constructor(container, games, send) { + constructor(container, filter, games, send) { this.container = container; this.games = games; this.send = send; + this.container.innerText = ""; + const game_list_outside = document.createElement("div"); game_list_outside.classList.add("game-list-outside"); this.game_list_container = document.createElement("div"); this.game_list_container.classList.add("game-list-container"); - this.game_list = document.createElement("div"); + this.game_list = document.createElement("table"); this.game_list.classList.add("game-list"); this.game_list_container.append(this.game_list); game_list_outside.append(this.game_list_container); + const game_list_new = document.createElement("button"); + game_list_new.classList.add("game-list-new"); + game_list_new.innerText = "New"; + game_list_new.onclick = () => this.show_new_game_popup(); + game_list_outside.append(game_list_new); this.game_list_filter_input = document.createElement("input"); + this.game_list_filter_input.value = filter; game_list_outside.append(this.game_list_filter_input); - const game_list_options = document.createElement("button"); - const game_list_option_menu = document.createElement("div"); - game_list_option_menu.classList.add("game-list-option-menu"); - game_list_option_menu.classList.add("hidden"); - const game_list_option_logout = document.createElement("div"); - game_list_option_logout.innerText = "Logout"; - game_list_option_logout.onclick = () => this.send({type: "Logout"}); - game_list_option_menu.append(game_list_option_logout); - game_list_options.append(game_list_option_menu); - game_list_options.onclick = () => game_list_option_menu.classList.toggle("hidden"); - game_list_outside.append(game_list_options); + this.container.append(game_list_outside); for (const game of this.games) { @@ -53,37 +53,175 @@ export class GameList { game_element(game) { console.log("getting game element for ", game); - const game_element = document.createElement("div"); + const game_element = document.createElement("tr"); game_element.classList.add("game-summary"); - const id = document.createElement("div"); + const id = document.createElement("td"); id.innerText = "#" + game.id; id.classList.add("game-id"); game_element.append(id); - const title = document.createElement("div"); + const format = document.createElement("td"); + format.innerText = game.settings.format; + format.classList.add("game-format"); + game_element.append(format); + + const title = document.createElement("td"); title.innerText = game.settings.title; title.classList.add("game-title"); game_element.append(title); - const format = document.createElement("div"); - format.innerText = game.settings.format; - format.classList.add("game-format"); - game_element.append(format); + game_element.onclick = () => this.show_game_popup(game); + + return game_element; + } + + clear_game_popup(game) { + this.container.querySelectorAll(".game-popup-container").forEach(element => element.remove()); + } + + show_game_popup(game) { + this.clear_game_popup(); + + const game_popup_container = document.createElement("div"); + game_popup_container.classList.add("game-popup-container"); + game_popup_container.onclick = () => this.clear_game_popup(); + const game_popup = document.createElement("div"); + game_popup.classList.add("game-popup"); + game_popup.onclick = event => event.stopPropagation(); + + const table = document.createElement("table"); + + const title_row = document.createElement("tr"); + const title_value = document.createElement("th"); + title_value.setAttribute("colspan", 2); + title_value.innerText = game.settings.title; + title_value.classList.add("game-title"); + title_row.append(title_value); + table.append(title_row); + + const id_row = document.createElement("tr"); + const id_header = document.createElement("th"); + id_header.innerText = "id"; + const id_value = document.createElement("td"); + id_value.innerText = game.id; + id_row.append(id_header); + id_row.append(id_value); + table.append(id_row); - const settings = document.createElement("ul"); - settings.classList.add("game-settings"); for (const setting of Object.keys(game.settings)) { - if (setting !== "id" && setting !== "title" && setting !== "format") { - const li = document.createElement("li"); - li.innerText = setting + ": " + game.settings[setting]; - settings.append(li); + if (setting === "title") continue; + const row = document.createElement("tr"); + const header = document.createElement("th"); + header.innerText = setting; + const value = document.createElement("td"); + value.innerText = game.settings[setting]; + row.append(header); + row.append(value); + table.append(row); + } + + game_popup.append(table); + + const button = document.createElement("button"); + button.innerText = "Join"; + button.onclick = () => this.send({type: "JoinGame", id: game.id}); + game_popup.append(button); + + game_popup_container.append(game_popup); + + this.container.append(game_popup_container); + } + + show_new_game_popup() { + this.clear_game_popup(); + + const game_popup_container = document.createElement("div"); + game_popup_container.classList.add("game-popup-container"); + game_popup_container.onclick = () => this.clear_game_popup(); + const game_popup = document.createElement("div"); + game_popup.classList.add("game-popup"); + game_popup.onclick = event => event.stopPropagation(); + + const settings = {}; + + const table = document.createElement("table"); + + const title_row = document.createElement("tr"); + const title_value = document.createElement("th"); + title_value.setAttribute("colspan", 2); + const title_input = document.createElement("input"); + title_input.classList.add("game-title"); + title_input.value = random_title(); + title_input.onchange = () => settings["title"] = title_input.value; + title_input.onchange(); + title_value.append(title_input); + title_row.append(title_value); + table.append(title_row); + + const format_row = document.createElement("tr"); + const format_title = document.createElement("th"); + format_title.innerText = "Format"; + format_row.append(format_title); + const format_value = document.createElement("td"); + const format_input = document.createElement("select"); + for (const format of ["KnockOutWhist", "TexasHoldEm"]) { + const game_format = document.createElement("option"); + game_format.setAttribute("value", format); + game_format.innerText = format; + format_input.append(game_format); + if (this.game_list_filter_input.value.includes(format)) { + format_input.value = format; } } - game_element.append(settings); + format_input.onchange = () => { + settings["format"] = format_input.value; + table.className = format_input.value; + }; + format_input.onchange(); + format_value.append(format_input); + format_row.append(format_value); + table.append(format_row); - game_element.onclick = () => this.send({type: "JoinGame", id: game.id}); + const fields = [ + { name: "max_players", display: "Max Players", value: 2, formats: ["TexasHoldEm", "KnockOutWhist"] }, + { name: "small_blind", display: "Small Blind", value: 25, formats: ["TexasHoldEm"] }, + { name: "starting_stack", display: "Starting Stack", value: 1000, formats: ["TexasHoldEm"] }, + { name: "action_timeout", display: "Action Timeout", value: 30000, formats: ["TexasHoldEm"] }, + ]; - return game_element; + for (const {name, display, value, formats} of fields) { + const row = document.createElement("tr"); + row.classList.add("game-field"); + for (const format of formats) { + row.classList.add(format); + } + const title = document.createElement("th"); + title.innerText = display; + row.append(title); + const cell = document.createElement("td"); + const input = document.createElement("input"); + input.setAttribute("type", "number"); + input.value = value; + input.onchange = () => settings[name] = Number(input.value); + input.onchange(); + cell.append(input); + row.append(cell); + table.append(row); + } + + game_popup.append(table); + + const button = document.createElement("button"); + button.innerText = "Create"; + button.onclick = () => { + this.send({type: "CreateGame", settings}); + this.clear_game_popup(); + }; + game_popup.append(button); + + game_popup_container.append(game_popup); + + this.container.append(game_popup_container); } } diff --git a/site/modules/mainmenu.js b/site/modules/mainmenu.js new file mode 100644 index 0000000..b561872 --- /dev/null +++ b/site/modules/mainmenu.js @@ -0,0 +1,35 @@ +export class MainMenu { + constructor(container, send) { + this.container = container; + this.send = send; + + this.container.innerText = ""; + + const menu_container = document.createElement("div"); + menu_container.classList.add("main-menu"); + + const texas_hold_em = document.createElement("div"); + texas_hold_em.classList.add("game-format-icon"); + const texas_hold_em_img = document.createElement("img"); + texas_hold_em_img.setAttribute("src", "img/holdem.svg"); + const texas_hold_em_text = document.createElement("p"); + texas_hold_em_text.innerText = "Texas Hold 'em"; + texas_hold_em.append(texas_hold_em_img); + texas_hold_em.append(texas_hold_em_text); + texas_hold_em.onclick = () => this.send({type: "JoinLobby", filter: "format: TexasHoldEm"}); + menu_container.append(texas_hold_em); + + const knock_out_whist = document.createElement("div"); + knock_out_whist.classList.add("game-format-icon"); + const knock_out_whist_img = document.createElement("img"); + knock_out_whist_img.setAttribute("src", "img/whist.svg"); + const knock_out_whist_text = document.createElement("p"); + knock_out_whist_text.innerText = "Knock-Out Whist"; + knock_out_whist.append(knock_out_whist_img); + knock_out_whist.append(knock_out_whist_text); + knock_out_whist.onclick = () => this.send({type: "JoinLobby", filter: "format: KnockOutWhist"}); + menu_container.append(knock_out_whist); + + this.container.append(menu_container); + } +} diff --git a/site/modules/socket.js b/site/modules/socket.js index f4edb7d..987bee8 100644 --- a/site/modules/socket.js +++ b/site/modules/socket.js @@ -1,5 +1,6 @@ const svgns = "http://www.w3.org/2000/svg"; +import { MainMenu } from "./mainmenu.js"; import { GameList } from "./gamelist.js"; import { Chatroom } from "./chatroom.js"; import { KnockOutWhist } from "./whist.js"; @@ -53,7 +54,7 @@ export class Socket { break; case "LoginSuccess": this.hide_login(); - this.send({type: "JoinLobby", filter: ""}); + this.game = new MainMenu(this.container, message => this.send(message)); this.state = "LoggedIn"; break; case "JoinLobbyFailure": @@ -61,7 +62,7 @@ export class Socket { this.state = "LoggedIn"; break; case "JoinLobbySuccess": - this.game = new GameList(this.container, message.games, message => this.send(message)) + this.game = new GameList(this.container, message.filter, message.games, message => this.send(message)) this.state = "InLobby"; break; case "NewGame": diff --git a/site/modules/words.js b/site/modules/words.js new file mode 100644 index 0000000..080e01b --- /dev/null +++ b/site/modules/words.js @@ -0,0 +1,65 @@ +const adjectives = [ + "Amazing", + "Based", + "Crazy", + "Devilish", + "Exciting", + "Fabulous", + "Great", + "Historic", + "Incredible", + "Jacked", + "Kingly", + "Luxurious", + "Mega", + "Nice", + "Optimal", + "Peerless", + "Quality", + "Renowned", + "Superb", + "Terrific", + "Ultimate", + "Vivacious", + "Wholesome", + "Xenial", + "Youthful", + "Zesty", +]; + +const nouns = [ + "Adventure", + "Business", + "Contest", + "Diversion", + "Event", + "Festivity", + "Game", + "Huddle", + "Incident", + "Juncture", + "Kingdom", + "Lark", + "Merriment", + "Nucleus", + "Occupation", + "Pastime", + "Quorum", + "Recreation", + "Story", + "Tournament", + "Undertaking", + "Venture", + "Wonder", + "Xenolith", + "Yarn", + "Zeitgeist", +]; + +function random_int(max) { + return Math.floor(Math.random() * max); +} + +export function random_title() { + return adjectives[random_int(26)] + " " + nouns[random_int(26)]; +} diff --git a/site/style.css b/site/style.css index b255927..e3f71c0 100644 --- a/site/style.css +++ b/site/style.css @@ -1,4 +1,5 @@ @import url("style/login.css"); +@import url("style/mainmenu.css"); @import url("style/game-list.css"); @import url("style/chatroom.css"); @import url("style/poker.css"); diff --git a/site/style/game-list.css b/site/style/game-list.css index d2506f5..03d76b8 100644 --- a/site/style/game-list.css +++ b/site/style/game-list.css @@ -3,7 +3,7 @@ height: 100%; display: grid; overflow: hidden; - font-size: 4vw; + font-size: 3vw; grid: auto-flow / 1fr 8vw; } @@ -23,25 +23,6 @@ font-size: inherit; } -.game-list-option-menu { - display: block; - position: absolute; - bottom: 6vw; - right: 0; -} - -.game-list-option-menu > div { - padding-left: 2vw; - padding-right: 2vw; - border: 1px solid grey; - font-size: 8vw; - background-color: white; -} - -.game-list-option-menu > div:hover { - background-color: #dfefff; -} - .game-list-container { grid-column: 1 / span 2; grid-row: 1; @@ -53,6 +34,74 @@ .game-list { width: 100%; + border-collapse: collapse; +} + +.game-popup-container { + background-color: rgba(0, 0, 100, 0.5); + width: 100%; + height: 100%; + position: fixed; + display: grid; + grid: 1fr 2fr 1fr / 1fr 2fr 1fr; +} + +.game-popup { + grid-column: 2 / 3; + grid-row: 2 / 3; + background-color: skyblue; + border-radius: 2vw; + padding: 4vw; + font-size: 3vw; +} + +.game-popup > table { + border-collapse: collapse; + width: 100%; + height: calc(100% - 4vw); +} + +.game-popup > table.TexasHoldEm tr.game-field { + display: none; +} + +.game-popup > table.TexasHoldEm tr.game-field.TexasHoldEm { + display: table-row; +} + +.game-popup > table.KnockOutWhist tr.game-field { + display: none; +} + +.game-popup > table.KnockOutWhist tr.game-field.KnockOutWhist { + display: table-row; +} + +.game-popup input, .game-popup select { + width: 100%; + height: 100%; + font-size: inherit; + text-align: right; +} + +.game-popup .game-title { + text-align: center; +} + +.game-popup th { + background-color: darkgrey; + border: 1px solid black; +} + +.game-popup td { + background-color: lightgrey; + border: 1px solid black; + text-align: right; +} + +.game-popup > button { + width: 100%; + font-size: 4vw; } @keyframes new-game { @@ -73,13 +122,7 @@ .game-summary { border: 1px solid grey; border-top: none; - height: 30vw; font-family: sans; - display: grid; - grid: 'id title format' - 'settings settings settings' - 'settings settings settings'; - /*animation: new-game 2s; TODO */ cursor: pointer; } @@ -88,26 +131,10 @@ } .game-id { - width: 25vw; font-style: italic; color: grey; - grid-area: id; } .game-title { - text-align: center; font-weight: bold; - grid-area: title; - justify-self: center; -} - -.game-format { - width: 25vw; - grid-area: format; - justify-self: end; - text-align: right; -} - -.game-settings { - grid-area: settings; } diff --git a/site/style/mainmenu.css b/site/style/mainmenu.css new file mode 100644 index 0000000..0965336 --- /dev/null +++ b/site/style/mainmenu.css @@ -0,0 +1,33 @@ +.main-menu { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} + +.game-format-icon { + width: 200px; + height: 200px; + margin: 20px; + padding: 10px; + background: #8080ff; + border: 2px solid black; + border-radius: 20px; + cursor: pointer; +} + +.game-format-icon:hover { + background: #a0a0ff; +} + +.game-format-icon > img { + width: 200px; + height: 160px; + cursor-events: none; +} + +.game-format-icon > p { + text-align: center; + font-family: sans; + font-size: 20px; + cursor-events: none; +} diff --git a/site/style/poker.css b/site/style/poker.css index ff6cb75..00ce239 100644 --- a/site/style/poker.css +++ b/site/style/poker.css @@ -10,6 +10,7 @@ stroke-width: 1px; transition: fill 0.5s; pointer-events: none; + cursor: pointer; } .fold-control.active, .call-control.active, .bet-control.active { diff --git a/src/api.rs b/src/api.rs index 1f9e581..2817214 100644 --- a/src/api.rs +++ b/src/api.rs @@ -44,7 +44,7 @@ pub enum ServerMessage { LogoutSuccess, CreateGameSuccess { id: i64 }, CreateGameFailure { reason: String }, - JoinLobbySuccess { games: Vec }, + JoinLobbySuccess { filter: String, games: Vec }, JoinLobbyFailure { reason: String }, NewGame { game: GameSummary }, JoinGameSuccess { summary: GameSummary, actions: Vec }, diff --git a/src/client.rs b/src/client.rs index bbe6cdf..0169278 100644 --- a/src/client.rs +++ b/src/client.rs @@ -132,14 +132,14 @@ impl ConnectionState { } } (&mut ClientState::LoggedIn { username, .. }, ClientMessage::JoinLobby { filter }) => { - let mut game_list = GameList::new(filter); + let mut game_list = GameList::new(filter.clone()); match self.server.game_list(0).await { Ok(games) => { for game in games.clone() { game_list.push(game); } self.client = ClientState::LoggedIn { username, state: LoggedInState::InLobby { game_list } }; - ServerMessage::JoinLobbySuccess { games } + ServerMessage::JoinLobbySuccess { filter, games } } Err(err) => ServerMessage::JoinLobbyFailure { reason: err.to_string() }, } diff --git a/src/filter.rs b/src/filter.rs new file mode 100644 index 0000000..ba6c303 --- /dev/null +++ b/src/filter.rs @@ -0,0 +1,92 @@ +use nom::{error::ParseError, IResult, sequence::{delimited, preceded, terminated, tuple}, multi::{many0_count, many1, separated_list1}, bytes::complete::{escaped_transform, tag, take_while1, take_until}, character::{is_alphabetic, complete::{alpha1, multispace0, none_of}}, combinator::{map, value}, branch::alt}; + +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>), +} + +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>); + +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>); + +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); + } +} diff --git a/src/main.rs b/src/main.rs index c4f4e96..c921544 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ mod card; mod client; mod config; mod dealer; +mod filter; mod game; mod pubsub; mod rng; -- 2.34.1