diff --git a/package.json b/package.json index 132954387e..815d1b6dd5 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "express-http-proxy": "^2.0.0", "fork-ts-checker-webpack-plugin": "^9.0.0", "globals": "^15.8.0", - "goban": "=8.3.64", + "goban": "=8.3.68", "gulp": "^5.0.0", "gulp-clean-css": "^4.3.0", "gulp-eslint-new": "^2.2.0", diff --git a/src/views/Play/AvailableQuickMatches.tsx b/src/views/Play/AvailableQuickMatches.tsx new file mode 100644 index 0000000000..3fefe3e84a --- /dev/null +++ b/src/views/Play/AvailableQuickMatches.tsx @@ -0,0 +1,138 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import * as React from "react"; +import { socket } from "@/lib/sockets"; +import { useRefresh, useUser } from "@/lib/hooks"; +import { rankString } from "@/lib/rank_utils"; +import { _, llm_pgettext, pgettext } from "@/lib/translate"; +import { shortDurationString } from "goban"; +import { SPEED_OPTIONS } from "./SPEED_OPTIONS"; +/* +import * as data from "@/lib/data"; +import * as preferences from "@/lib/preferences"; +import moment from "moment"; +*/ + +export function AvailableQuickMatches(): JSX.Element { + const available = React.useRef<{ [uuid: string]: any }>({}); + const refresh = useRefresh(); + const user = useUser(); + + React.useEffect(() => { + socket.send("automatch/available/subscribe", undefined); + + function onAdd(entry: any) { + console.log("onAdd", entry); + available.current[entry.uuid] = entry; + refresh(); + } + + function onRemove(uuid: string) { + console.log("onRemove", uuid); + delete available.current[uuid]; + refresh(); + } + + socket.on("automatch/available/add", onAdd); + socket.on("automatch/available/remove", onRemove); + + return () => { + socket.send("automatch/available/unsubscribe", undefined); + socket.off("automatch/available/add", onAdd); + socket.off("automatch/available/remove", onRemove); + }; + }, []); + + const available_list = Object.values(available.current).filter( + (entry) => entry.player.id !== user.id, + ); + available_list.sort((a, b) => { + const a_speed = a.preferences.size_speed_options[0].speed; + const b_speed = b.preferences.size_speed_options[0].speed; + const a_speed_value = + a_speed === "blitz" ? 0 : a_speed === "rapid" ? 1 : a_speed === "live" ? 2 : 3; + const b_speed_value = + b_speed === "blitz" ? 0 : b_speed === "rapid" ? 1 : b_speed === "live" ? 2 : 3; + if (a_speed_value !== b_speed_value) { + return a_speed_value - b_speed_value; + } + return a.player.bounded_rank - b.player.bounded_rank; + }); + + return ( +
+

+ {llm_pgettext( + "Active automatch searches", + "Active quick match searches by other players", + )} +

+
+
+

{_("Size")}

+
+
+

{pgettext("Clock settings header for a new game", "Clock")}

+
+
+

{_("Handicap")}

+
+
+

{_("Rank")}

+
+ + {available_list.map((entry) => { + const prefs = entry.preferences; + const system = prefs.time_control.value.system; + let speed = prefs.size_speed_options[0].speed; + const size = prefs.size_speed_options[0].size; + const handicap = prefs.handicap.value; + + try { + const speed_options = (SPEED_OPTIONS as any)[size][speed][system]; + speed = + system === "fischer" + ? `${shortDurationString( + speed_options.initial_time, + )} + ${shortDurationString(speed_options.time_increment)}` + : `${shortDurationString(speed_options.main_time)} + ${ + speed_options.periods + }x${shortDurationString(speed_options.period_time)}`; + } catch (e) { + console.error(e); + } + + return ( + +
{size}
+
{speed}
+
+ {handicap === "enabled" ? ( + + ) : ( + + )} +
+
{rankString(entry.player.bounded_rank)}
+
+ ); + })} +
+
+ ); +} diff --git a/src/views/Play/AvailableQuickmatches.styl b/src/views/Play/AvailableQuickmatches.styl new file mode 100644 index 0000000000..7594d14734 --- /dev/null +++ b/src/views/Play/AvailableQuickmatches.styl @@ -0,0 +1,14 @@ +.AvailableQuickMatches-container { + padding-top: 2rem; + display: flex; + flex-direction: column; + align-items: center; +} + +.AvailableQuickMatches { + max-width: 40rem; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-gap: 0.5rem; + justify-items: center; +} diff --git a/src/views/Play/QuickMatch.styl b/src/views/Play/QuickMatch.styl index 6ba487e3a7..631e767a7e 100644 --- a/src/views/Play/QuickMatch.styl +++ b/src/views/Play/QuickMatch.styl @@ -44,7 +44,7 @@ } -#FindGame { +#QuickMatch { margin-top: 1rem; margin-right: 2rem; margin-left: 2rem; @@ -542,7 +542,7 @@ } @media (max-width: hamburger-cutoff) { - #FindGame { + #QuickMatch { display: flex; flex-direction: column; margin-right: 0rem; diff --git a/src/views/Play/QuickMatch.tsx b/src/views/Play/QuickMatch.tsx index b5f70fe196..aaa7ce02a2 100644 --- a/src/views/Play/QuickMatch.tsx +++ b/src/views/Play/QuickMatch.tsx @@ -49,6 +49,8 @@ import { socket } from "@/lib/sockets"; import { sfx } from "@/lib/sfx"; import { Link } from "react-router-dom"; import Select from "react-select"; +import { SPEED_OPTIONS } from "./SPEED_OPTIONS"; +import { AvailableQuickMatches } from "./AvailableQuickMatches"; moment.relativeTimeThreshold("m", 56); export interface SelectOption { @@ -135,194 +137,6 @@ const select_styles = { }), }; -interface GameSpeedOptions { - [size: string]: { - [speed: string]: { - time_estimate: string; - fischer: { - initial_time: number; - time_increment: number; - time_estimate: string; - }; - byoyomi?: { - main_time: number; - periods: number; - period_time: number; - time_estimate: string; - }; - }; - }; -} - -const SPEED_OPTIONS: GameSpeedOptions = { - "9x9": { - blitz: { - //time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(5, "minutes").humanize(), - fischer: { - initial_time: 30, - time_increment: 3, - //time_estimate: "~ 4-" + moment.duration(6, "minutes").humanize(), - time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(), - }, - byoyomi: { - main_time: 30, - periods: 5, - period_time: 10, - time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(), - }, - }, - rapid: { - //time_estimate: "\u223c 7\u2212" + moment.duration(14, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(10, "minutes").humanize(), - fischer: { - initial_time: 120, - time_increment: 5, - time_estimate: "\u223c 7\u2212" + moment.duration(9, "minutes").humanize(), - }, - byoyomi: { - main_time: 120, - periods: 5, - period_time: 30, - time_estimate: "\u223c 8\u2212" + moment.duration(14, "minutes").humanize(), - }, - }, - live: { - //time_estimate: "\u223c 9\u2212" + moment.duration(17, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(15, "minutes").humanize(), - fischer: { - initial_time: 180, - time_increment: 10, - time_estimate: "\u223c 9\u2212" + moment.duration(13, "minutes").humanize(), - }, - byoyomi: { - main_time: 300, - periods: 5, - period_time: 30, - time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(), - }, - }, - correspondence: { - time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"), - fischer: { - initial_time: 86400 * 3, - time_increment: 86400, - time_estimate: "", - }, - }, - }, - "13x13": { - blitz: { - //time_estimate: "\u223c 8\u2212" + moment.duration(10, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(10, "minutes").humanize(), - fischer: { - initial_time: 30, - time_increment: 3, - time_estimate: "\u223c 8\u2212" + moment.duration(15, "minutes").humanize(), - }, - byoyomi: { - main_time: 30, - periods: 5, - period_time: 10, - time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(), - }, - }, - rapid: { - //time_estimate: "\u223c 16\u2212" + moment.duration(25, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(20, "minutes").humanize(), - fischer: { - initial_time: 180, - time_increment: 5, - time_estimate: "\u223c 16\u2212" + moment.duration(20, "minutes").humanize(), - }, - byoyomi: { - main_time: 180, - periods: 5, - period_time: 30, - time_estimate: "\u223c 18\u2212" + moment.duration(25, "minutes").humanize(), - }, - }, - live: { - //time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(30, "minutes").humanize(), - fischer: { - initial_time: 300, - time_increment: 10, - time_estimate: "\u223c 20\u2212" + moment.duration(30, "minutes").humanize(), - }, - byoyomi: { - main_time: 600, - periods: 5, - period_time: 30, - time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(), - }, - }, - correspondence: { - time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"), - fischer: { - initial_time: 86400 * 3, - time_increment: 86400, - time_estimate: "", - }, - }, - }, - "19x19": { - blitz: { - //time_estimate: "\u223c 10\u2212" + moment.duration(15, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(15, "minutes").humanize(), - fischer: { - initial_time: 30, - time_increment: 3, - time_estimate: "\u223c 10\u2212" + moment.duration(15, "minutes").humanize(), - }, - byoyomi: { - main_time: 30, - periods: 5, - period_time: 10, - time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(), - }, - }, - rapid: { - //time_estimate: "\u223c 21\u2212" + moment.duration(31, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(25, "minutes").humanize(), - fischer: { - initial_time: 300, - time_increment: 5, - time_estimate: "\u223c 21\u2212" + moment.duration(31, "minutes").humanize(), - }, - byoyomi: { - main_time: 300, - periods: 5, - period_time: 30, - time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(), - }, - }, - live: { - //time_estimate: "\u223c 26\u2212" + moment.duration(52, "minutes").humanize(), - time_estimate: "\u223c " + moment.duration(40, "minutes").humanize(), - fischer: { - initial_time: 600, - time_increment: 10, - time_estimate: "\u223c 26\u2212" + moment.duration(52, "minutes").humanize(), - }, - byoyomi: { - main_time: 1200, - periods: 5, - period_time: 30, - time_estimate: "\u223c 28\u2212" + moment.duration(49, "minutes").humanize(), - }, - }, - correspondence: { - time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"), - fischer: { - initial_time: 86400 * 3, - time_increment: 86400, - time_estimate: "", - }, - }, - }, -}; - export function QuickMatch(): JSX.Element { const user = useUser(); const refresh = useRefresh(); @@ -594,62 +408,66 @@ export function QuickMatch(): JSX.Element { // Construction of the pane we need to show... return ( -
- {/* Board Size */} -
-
- {_("Board Size")} -
- -
- {(["9x9", "13x13", "19x19"] as Size[]).map((s) => ( - - ))} -
+ <> +
+ {/* Board Size */} +
+
+ {_("Board Size")} +
- -
+
+ {(["9x9", "13x13", "19x19"] as Size[]).map((s) => ( + + ))} +
- {/* Game Speed */} -
-
- {pgettext("Clock settings header for a new game", "Game Clock")} - o.value === game_clock)} + onChange={(opt) => { + if (opt) { + setGameClock(opt.value as "flexible" | "exact"); + } + }} + options={game_clock_options} + components={{ Option: RenderOptionWithDescription }} + /> +
+ +
+ {( + ["blitz", "rapid", "live", "correspondence"] as JGOFTimeControlSpeed[] + ).map((speed) => { const opt = SPEED_OPTIONS[board_size as any][speed]; return ( @@ -739,214 +557,218 @@ export function QuickMatch(): JSX.Element {
); - }, - )} + })} +
-
- {/* Opponent */} -
-
- {_("Opponent")} -
+ {/* Opponent */} +
+
+ {_("Opponent")} +
-
-
{ - if (search_active) { - return; +
+
-
- {pgettext("Play a human opponent", "Human")} -
-
- - {" - "} - + onClick={() => { + if (search_active) { + return; + } + setOpponent("human"); + }} + > +
+ {pgettext("Play a human opponent", "Human")} +
+
+ + {" - "} + +
-
-
{ - if (search_active) { - return; +
-
- {pgettext("Play a computer opponent", "Computer")} -
-
- -   - - - + onClick={() => { + if (search_active) { + return; + } + setOpponent("bot"); + }} + > +
+ {pgettext("Play a computer opponent", "Computer")} +
+
+ +   + + + +
-
- {/* Play Button */} -
-
- {_("Handicap")} - o.value === handicaps)} + isSearchable={false} + minMenuHeight={400} + maxMenuHeight={400} + menuPlacement="auto" + onChange={(opt) => { + if (opt) { + setHandicaps(opt.value as "enabled" | "standard" | "disabled"); + } + }} + options={[ + { + label: _( + "Handicaps balance games between players of different ranks by adjusting starting stones and komi points.", + ), + options: handicap_options, + }, + ]} + components={{ + Option: RenderOptionWithDescription, + }} + /> +
-
- {automatch_manager.active_live_automatcher && ( -
-
- - {pgettext("Cancel automatch", "Searching for game...")} - -
-
- )} - - {bot_spinner && ( -
-
- cancel_bot_game.current()} - > - {_("Cancel")} - +
+ {automatch_manager.active_live_automatcher && ( +
+
+ + {pgettext("Cancel automatch", "Searching for game...")} + +
-
- )} - - {correspondence_spinner && ( -
-
{_("Finding you a game...")}
-
- {_( - 'This can take several minutes. You will be notified when your match has been found. To view or cancel your automatch requests, please see the list below labeled "Your Automatch Requests".', - )} + )} + + {bot_spinner && ( +
+
+ cancel_bot_game.current()} + > + {_("Cancel")} + +
-
- +
+
+ +
-
- )} - {user.anonymous && ( -
- {_("Please sign in to play")} -
- {_("Register for Free")} - {" | "} - {_("Sign in")} + )} + {user.anonymous && ( +
+ {_("Please sign in to play")} +
+ {_("Register for Free")} + {" | "} + {_("Sign in")} +
-
- )} + )} - {!search_active && !user.anonymous && ( - - )} + {!search_active && !user.anonymous && ( + + )} +
-
+ + ); } diff --git a/src/views/Play/SPEED_OPTIONS.ts b/src/views/Play/SPEED_OPTIONS.ts new file mode 100644 index 0000000000..9b87b57239 --- /dev/null +++ b/src/views/Play/SPEED_OPTIONS.ts @@ -0,0 +1,206 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import moment from "moment"; +import { pgettext } from "@/lib/translate"; + +export interface GameSpeedOptions { + [size: string]: { + [speed: string]: { + time_estimate: string; + fischer: { + initial_time: number; + time_increment: number; + time_estimate: string; + }; + byoyomi?: { + main_time: number; + periods: number; + period_time: number; + time_estimate: string; + }; + }; + }; +} +export const SPEED_OPTIONS: GameSpeedOptions = { + "9x9": { + blitz: { + //time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(5, "minutes").humanize(), + fischer: { + initial_time: 30, + time_increment: 3, + //time_estimate: "~ 4-" + moment.duration(6, "minutes").humanize(), + time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(), + }, + byoyomi: { + main_time: 30, + periods: 5, + period_time: 10, + time_estimate: "\u223c 4\u2212" + moment.duration(6, "minutes").humanize(), + }, + }, + rapid: { + //time_estimate: "\u223c 7\u2212" + moment.duration(14, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(10, "minutes").humanize(), + fischer: { + initial_time: 120, + time_increment: 5, + time_estimate: "\u223c 7\u2212" + moment.duration(9, "minutes").humanize(), + }, + byoyomi: { + main_time: 120, + periods: 5, + period_time: 30, + time_estimate: "\u223c 8\u2212" + moment.duration(14, "minutes").humanize(), + }, + }, + live: { + //time_estimate: "\u223c 9\u2212" + moment.duration(17, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(15, "minutes").humanize(), + fischer: { + initial_time: 180, + time_increment: 10, + time_estimate: "\u223c 9\u2212" + moment.duration(13, "minutes").humanize(), + }, + byoyomi: { + main_time: 300, + periods: 5, + period_time: 30, + time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(), + }, + }, + correspondence: { + time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"), + fischer: { + initial_time: 86400 * 3, + time_increment: 86400, + time_estimate: "", + }, + }, + }, + "13x13": { + blitz: { + //time_estimate: "\u223c 8\u2212" + moment.duration(10, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(10, "minutes").humanize(), + fischer: { + initial_time: 30, + time_increment: 3, + time_estimate: "\u223c 8\u2212" + moment.duration(15, "minutes").humanize(), + }, + byoyomi: { + main_time: 30, + periods: 5, + period_time: 10, + time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(), + }, + }, + rapid: { + //time_estimate: "\u223c 16\u2212" + moment.duration(25, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(20, "minutes").humanize(), + fischer: { + initial_time: 180, + time_increment: 5, + time_estimate: "\u223c 16\u2212" + moment.duration(20, "minutes").humanize(), + }, + byoyomi: { + main_time: 180, + periods: 5, + period_time: 30, + time_estimate: "\u223c 18\u2212" + moment.duration(25, "minutes").humanize(), + }, + }, + live: { + //time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(30, "minutes").humanize(), + fischer: { + initial_time: 300, + time_increment: 10, + time_estimate: "\u223c 20\u2212" + moment.duration(30, "minutes").humanize(), + }, + byoyomi: { + main_time: 600, + periods: 5, + period_time: 30, + time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(), + }, + }, + correspondence: { + time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"), + fischer: { + initial_time: 86400 * 3, + time_increment: 86400, + time_estimate: "", + }, + }, + }, + "19x19": { + blitz: { + //time_estimate: "\u223c 10\u2212" + moment.duration(15, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(15, "minutes").humanize(), + fischer: { + initial_time: 30, + time_increment: 3, + time_estimate: "\u223c 10\u2212" + moment.duration(15, "minutes").humanize(), + }, + byoyomi: { + main_time: 30, + periods: 5, + period_time: 10, + time_estimate: "\u223c 11\u2212" + moment.duration(17, "minutes").humanize(), + }, + }, + rapid: { + //time_estimate: "\u223c 21\u2212" + moment.duration(31, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(25, "minutes").humanize(), + fischer: { + initial_time: 300, + time_increment: 5, + time_estimate: "\u223c 21\u2212" + moment.duration(31, "minutes").humanize(), + }, + byoyomi: { + main_time: 300, + periods: 5, + period_time: 30, + time_estimate: "\u223c 20\u2212" + moment.duration(35, "minutes").humanize(), + }, + }, + live: { + //time_estimate: "\u223c 26\u2212" + moment.duration(52, "minutes").humanize(), + time_estimate: "\u223c " + moment.duration(40, "minutes").humanize(), + fischer: { + initial_time: 600, + time_increment: 10, + time_estimate: "\u223c 26\u2212" + moment.duration(52, "minutes").humanize(), + }, + byoyomi: { + main_time: 1200, + periods: 5, + period_time: 30, + time_estimate: "\u223c 28\u2212" + moment.duration(49, "minutes").humanize(), + }, + }, + correspondence: { + time_estimate: pgettext("Game speed: multi-day games", "Daily Correspondence"), + fischer: { + initial_time: 86400 * 3, + time_increment: 86400, + time_estimate: "", + }, + }, + }, +}; diff --git a/src/views/Play/index.ts b/src/views/Play/index.ts index 80e5233ae2..571c05c284 100644 --- a/src/views/Play/index.ts +++ b/src/views/Play/index.ts @@ -17,3 +17,4 @@ export * from "./Play"; export * from "./ChallengeLists"; +export * from "./AvailableQuickMatches"; diff --git a/yarn.lock b/yarn.lock index fd274771f8..a9c613f773 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6006,10 +6006,10 @@ glogg@^2.2.0: dependencies: sparkles "^2.1.0" -goban@=8.3.64: - version "8.3.64" - resolved "https://registry.yarnpkg.com/goban/-/goban-8.3.64.tgz#e126e5b09501831dd6840beb84a5aafae915164c" - integrity sha512-iT2FGTpYOJ3Co8lzSoDl3Uz1fqnseJWg+22Hd3C61Guc3dPApo3I2INZFMpNaHG1KaExmf6i5WVKwpmIzcd9zQ== +goban@=8.3.68: + version "8.3.68" + resolved "https://registry.yarnpkg.com/goban/-/goban-8.3.68.tgz#f63a4285705d27f9ae7286b801b1522e93aa73c5" + integrity sha512-CSqe/wqPQNKEp1dBDeb+p1LrnmcVTwoCZ3/g7fJpIf0TjcW3K3ZxkDyacH1LY9eAVeJZUN42vrFw+tHw+KLOGg== dependencies: eventemitter3 "^5.0.0"