diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 42a9f85cf8..cac7699074 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -23,6 +23,7 @@ "Baduk", "badukpop", "benjito", + "bezier", "bitfield", "boardsize", "byoyomi", diff --git a/src/components/ChallengeModal/ChallengeModal.styl b/src/components/ChallengeModal/ChallengeModal.styl index 211f14f209..90a76100ee 100644 --- a/src/components/ChallengeModal/ChallengeModal.styl +++ b/src/components/ChallengeModal/ChallengeModal.styl @@ -131,4 +131,108 @@ display: inline-flex; align-items: middle; } + + .computer-opponents { + width: 100%; + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 60vh; + overflow: auto; + } + + .header.computer-settings-expanded { + .computer-opponents { + max-height: 20vh; + } + } + + .bot-categories { + display: flex; + flex-direction: column; + gap: 0.5rem; + + h1 { + font-size: 0.9rem; + text-transform: uppercase; + themed: color shade1; + font-weight: bold !important; + padding-bottom: 0.5rem; + } + + .bot-category { + margin-top: 1rem; + } + } + + .bot-options { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + width: 100%; + align-items: flex-start; + // justify-content: space-around; + } + + .bot-option { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + border-radius: 0.25rem; + padding-right: 0.5rem; + padding: 0.2rem; + gap: 1rem; + border: 2px solid transparent; + overflow: hidden; + flex-wrap: wrap; + width: 13rem; + + &:hover { + themed: background-color shade4; + } + + &.disabled { + opacity: 0.5; + background-color: transparent !important; + cursor: not-allowed; + } + + .disabled-reason { + flex-grow: 1; + font-size: 0.8rem; + themed: color shade1; + display: inline-block; + width: 12rem; + } + + .username-rank { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .username { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 6rem; + } + + .rank { + display: inline-block; + white-space: nowrap; + max-width: 2rem; + } + + .PlayerIcon { + border-radius: 0.3rem; + themed: background-color shade5; + } + + &.selected { + themed: border-color, primary; + } + } } diff --git a/src/components/ChallengeModal/ChallengeModal.tsx b/src/components/ChallengeModal/ChallengeModal.tsx index cfc1e08922..1ca360eab2 100644 --- a/src/components/ChallengeModal/ChallengeModal.tsx +++ b/src/components/ChallengeModal/ChallengeModal.tsx @@ -18,14 +18,15 @@ import * as React from "react"; import * as data from "@/lib/data"; import * as player_cache from "@/lib/player_cache"; +import * as preferences from "@/lib/preferences"; import { OgsResizeDetector } from "@/components/OgsResizeDetector"; import { browserHistory } from "@/lib/ogsHistory"; -import { _, pgettext, interpolate } from "@/lib/translate"; +import { _, pgettext, interpolate, llm_pgettext } from "@/lib/translate"; import { post, del } from "@/lib/requests"; import { Modal, openModal } from "@/components/Modal"; import { socket } from "@/lib/sockets"; -import { rankString, getUserRating, amateurRanks, allRanks } from "@/lib/rank_utils"; +import { rankString, amateurRanks, allRanks } from "@/lib/rank_utils"; import { CreatedChallengeInfo, RuleSet } from "@/lib/types"; import { errorLogger, errorAlerter, rulesText, dup } from "@/lib/misc"; import { PlayerIcon } from "@/components/PlayerIcon"; @@ -41,7 +42,7 @@ import { notification_manager, NotificationManagerEvents, } from "@/components/Notifications/NotificationManager"; -import { one_bot, bot_count, bots_list } from "@/lib/bots"; +import { one_bot, bot_count, bots_list, getAcceptableTimeSetting, Bot } from "@/lib/bots"; import { goban_view_mode } from "@/views/Game/util"; import { GobanRenderer, @@ -61,6 +62,7 @@ import { saveTimeControlSettings, updateSystem, } from "@/components/TimeControl/TimeControlUpdates"; +import { SPEED_OPTIONS } from "@/views/Play/SPEED_OPTIONS"; export type ChallengeDetails = rest_api.ChallengeDetails; @@ -985,6 +987,9 @@ export class ChallengeModalBody extends React.Component< // game name and privacy basicSettings = () => { const mode = this.props.mode; + const bots = bots_list(); + const selected_bot = bots.find((bot) => bot.id === this.state.conf.bot_id); + return (
{pgettext("Computer opponent", "AI Player")} -
-
- -   - - +
+ + {selected_bot ? selected_bot.username : ""} + + {selected_bot && ( + + -
+ )}
)} @@ -1740,6 +1743,176 @@ export class ChallengeModalBody extends React.Component< ); }; + renderComputerOpponents() { + interface Category { + sort_index: number; + label: string; + lower_bound: number; + upper_bound: number; + } + + const user = data.get("user"); + let available_bots: (Bot & { category?: Category })[] = bots_list().filter((b) => b.id > 0); + const board_size = `${this.state.challenge.game.width}x${this.state.challenge.game.height}`; + console.log(board_size, this.state.challenge.game.speed, this.state.time_control.system); + console.log(this.state.challenge.game.speed); + + const categories = [ + { + sort_index: 1, + label: pgettext("Bot strength category", "Beginner"), + lower_bound: -99, + upper_bound: 10, + }, + { + sort_index: 2, + label: pgettext("Bot strength category", "Intermediate"), + lower_bound: 11, + upper_bound: 25, + }, + { + sort_index: 2, + label: pgettext("Bot strength category", "Advanced"), + lower_bound: 26, + upper_bound: 99, + }, + ]; + available_bots = available_bots.filter((b) => { + const speed_settings = (SPEED_OPTIONS as any)?.[board_size]?.[ + this.state.time_control.speed + ]?.[this.state.time_control.system]; + if (!speed_settings) { + return false; + } + + const settings = { + rank: user.ranking, + width: this.state.challenge.game.width, + height: this.state.challenge.game.height, + ranked: true, + handicap: this.state.challenge.game.handicap, + system: this.state.time_control.system, + speed: this.state.time_control.speed, + [this.state.time_control.system]: speed_settings, + }; + const [options, message] = getAcceptableTimeSetting(b, settings); + if (!options) { + b.disabled = message || undefined; + } else if (options && options._config_version && options._config_version === 0) { + b.disabled = llm_pgettext( + "Bot is not configured correctly", + "Bot is not configured correctly", + ); + } else { + b.disabled = undefined; + } + + for (const category of categories) { + if ( + b.ranking && + b.ranking >= category.lower_bound && + b.ranking <= category.upper_bound + ) { + b.category = category; + break; + } + } + + return true; + }); + + // testing + //available_bots = [...available_bots, ...available_bots]; + //available_bots = [...available_bots, ...available_bots]; + //available_bots = [...available_bots, ...available_bots]; + + available_bots.sort((a, b) => { + if (a.category!.sort_index !== b.category!.sort_index) { + return a.category!.sort_index - b.category!.sort_index; + } + + if (a.disabled && !b.disabled) { + return 1; + } + if (b.disabled && !a.disabled) { + return -1; + } + + return (a.ranking || 0) - (b.ranking || 0); + }); + + const selected_bot_value = available_bots.find((b) => b.id === this.state.conf.bot_id); + if (selected_bot_value?.disabled) { + this.upstate("conf.bot_id", 0); + } + + return available_bots.length <= 0 ? ( +
+ {_("No bots available that can play with the selected settings")} +
+ ) : ( +
+ {categories.map((category) => { + return ( +
+

{category.label}

+ +
+ {available_bots + //.filter((bot) => !bot.disabled) + .filter( + (bot) => bot.ranking && bot.ranking >= category.lower_bound, + ) + .filter( + (bot) => bot.ranking && bot.ranking <= category.upper_bound, + ) + .map((bot) => { + return ( +
{ + if (!bot.disabled) { + this.upstate("conf.bot_id", bot.id); + } + }} + > + + + {bot.username} + {!preferences.get("hide-ranks") && ( + + ({rankString(bot.ranking || 0)}) + + )} + + + {bot.disabled && ( + + {bot.disabled} + + )} +
+ ); + })} +
+
+ ); + })} +
+ ); + } + render() { const user = data.get("user"); const mode = this.props.mode; @@ -1762,46 +1935,53 @@ export class ChallengeModalBody extends React.Component< return (
-
-

- {mode === "open" && {_("Custom Game")}} - {mode === "demo" && ( - - {this.props.game_record_mode - ? pgettext("Game record from real life game", "Game Record") - : _("Demo Board")} - ? - - )} - {mode === "player" && ( - - -   {player_username} - - )} - {mode === "computer" && {_("Computer")}} -

-
-
-
-
- {this.basicSettings()} - {!this.state.initial_state && this.additionalSettings()} +
+ {mode !== "computer" ? ( +

+ {mode === "open" && {_("Custom Game")}} + {mode === "demo" && ( + + {this.props.game_record_mode + ? pgettext("Game record from real life game", "Game Record") + : _("Demo Board")} + ? + + )} + {mode === "player" && ( + + +   {player_username} + + )} +

+ ) : ( +
+

{_("Pick your computer opponent")}:

+
{this.renderComputerOpponents()}
- -
- {mode !== "demo" && this.advancedSettings()} - {mode === "demo" && this.advancedDemoSettings()} -
+ )}
- {/* {speed_warning && ( -
- - - {speed_warning} - + {(mode !== "computer" || this.state.show_computer_settings) && ( +
+
+
+ {this.basicSettings()} + {!this.state.initial_state && this.additionalSettings()} +
+ +
+ {mode !== "demo" && this.advancedSettings()} + {mode === "demo" && this.advancedDemoSettings()} +
- )} */} + )}
{this.props.modal.close ? ( @@ -1827,8 +2007,21 @@ export class ChallengeModalBody extends React.Component< : _("Create Demo")} )} + + {mode === "computer" && ( + + )} + {!user?.anonymous && mode === "computer" && ( - )} @@ -1847,7 +2040,9 @@ export class ChallengeModalBody extends React.Component< )}
- {mode !== "demo" && this.preferredGameSettings()} + {(mode !== "computer" || this.state.show_computer_settings) && + mode !== "demo" && + this.preferredGameSettings()}
); } @@ -1906,6 +2101,12 @@ export class ChallengeModalBody extends React.Component< } return this.bulkUpstate([[key, event_or_value]]); } + + toggleComputerSettings = () => { + this.setState({ + show_computer_settings: !this.state.show_computer_settings, + }); + }; } export function challenge( @@ -1973,8 +2174,9 @@ export function createDemoBoard( />, ); } -export function challengeComputer() { - return challenge(undefined, null, true); + +export function challengeComputer(settings?: ChallengeModalConfig) { + return challenge(undefined, null, true, settings); } export function challengeRematch( goban: GobanRenderer, @@ -2096,7 +2298,7 @@ function isStandardBoardSize(board_size: string): boolean { return board_size in standard_board_sizes; } -interface ChallengeModalConfig { +export interface ChallengeModalConfig { challenge: { min_ranking?: number; max_ranking?: number; diff --git a/src/lib/bots.ts b/src/lib/bots.ts index c5813ecbfe..46ede9b257 100644 --- a/src/lib/bots.ts +++ b/src/lib/bots.ts @@ -33,6 +33,7 @@ interface Events { } export interface Bot extends User { config: BotConfig; + disabled?: string; // if not undefined, the string describes why } export const bot_event_emitter = new EventEmitter(); @@ -369,7 +370,7 @@ export function getAcceptableTimeSetting( null, llm_pgettext( "Unable to find a compatible game setting for bot", - "Unable to find a compatible settings", + "Bot cannot play at this speed", ), ]; } catch (e) { diff --git a/src/views/Play/CustomGames.styl b/src/views/Play/CustomGames.styl index 03947865cf..8e160a4e73 100644 --- a/src/views/Play/CustomGames.styl +++ b/src/views/Play/CustomGames.styl @@ -87,7 +87,7 @@ margin-top: 2rem; margin-bottom: 8rem; display: flex; - gap: 1rem; + gap: 2rem; justify-content: center; &.showing-custom-games { @@ -97,5 +97,6 @@ button { height: 3rem; font-size: 1.3rem; + width: 17rem; } } \ No newline at end of file diff --git a/src/views/Play/CustomGames.tsx b/src/views/Play/CustomGames.tsx index f8f4a3dbae..8abe461bfb 100644 --- a/src/views/Play/CustomGames.tsx +++ b/src/views/Play/CustomGames.tsx @@ -431,15 +431,6 @@ export function CustomGames(): JSX.Element { >
- -
diff --git a/src/views/Play/QuickMatch.styl b/src/views/Play/QuickMatch.styl index 4330fe5390..07fec2a394 100644 --- a/src/views/Play/QuickMatch.styl +++ b/src/views/Play/QuickMatch.styl @@ -80,35 +80,6 @@ margin-right: 0.5rem; } - .finding-game-container { - // flex-shrink: 0; - // flex-grow: 0; - // flex-basis: 2.5rem; - font-size: 1.3rem; - width: 100%; - height: 100%; - display: flex; - flex-direction: row; - justify-content: space-around; - align-items: center; - // align-content: center; - padding-bottom: 2.5rem; - padding-top: 1rem; - - .spinner { - margin: 0; - height: 20px; - width: 20px; - margin-left: 1rem; - margin-right: 1rem; - } - - button { - margin-left: 2rem; - font-size: 1.1rem; - } - } - .automatch-row { display: flex; justify-content: space-around; @@ -179,14 +150,6 @@ } } - .automatch-settings-corr { - flex: 0; - text-align: justify; - font-size: 1rem; - padding: 0.5rem; - padding-left: 3rem; - } - .custom-game-row { display: flex; justify-content: space-around; @@ -279,20 +242,6 @@ font-size: 0.9rem; } - .PlayButton-container { - width: 100%; - } - - .play-button { - // margin-top: 3rem; - width: 100%; - height: 6rem; - font-size: 1.5rem; - line-height: 1.5rem; - text-align: center; - margin-bottom: 0; - } - .game-speed-option-container { // margin-top: 1rem; margin-bottom: 2rem; @@ -401,18 +350,6 @@ padding-top: 0.5rem; } - .opponent-rank-range { - // display: flex; - text-align: center; - - // justify-content: space-around; - select { - text-align: center; - margin-left: 0.5rem; - margin-right: 0.5rem; - } - } - .opponent-rank-range-description { font-size: 0.9rem; themed: color shade1; @@ -421,25 +358,6 @@ padding-left: 0.5rem; margin-bottom: 0.5rem; } - - .computer-select { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - - select { - flex-grow: 1; - min-width: 10rem; - max-width: 10rem; - width: 10rem; - overflow: hidden; - } - - .fa { - padding-left: 1rem; - } - } } } @@ -524,33 +442,6 @@ margin-bottom: 0.5rem; } - .computer-select { - .disabled .option-label { - themed: color shade2; - } - - .option-label { - display: flex; - justify-content: space-between; - gap: 0.5rem; - - > span { - display: flex; - align-items: center; - gap: 0.5rem; - } - } - - > div { - width: 100%; - max-width: 12rem; - } - - &.error { - border: 1px solid red; - } - } - .size-button { margin-right: 0.2rem; margin-left: 0.2rem; @@ -577,6 +468,66 @@ .activity.popular:after { themed: color, shade1; } + + .opponent-rank-range { + display: flex; + justify-content: space-around; + align-items: center; + gap: 0.5rem; + } +} + +.PlayButton-container { + display: flex; + justify-content: space-between; + gap: 2rem; + + .play-button { + // margin-top: 3rem; + width: 100%; + height: 6rem; + font-size: 1.5rem; + line-height: 1.5rem; + text-align: center; + margin-bottom: 0; + } + + .finding-game-container { + // flex-shrink: 0; + // flex-grow: 0; + // flex-basis: 2.5rem; + font-size: 1.3rem; + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + // align-content: center; + padding-bottom: 2.5rem; + padding-top: 1rem; + + .spinner { + margin: 0; + height: 20px; + width: 20px; + margin-left: 1rem; + margin-right: 1rem; + } + + button { + margin-left: 2rem; + font-size: 1.1rem; + } + } + + .automatch-settings-corr { + flex: 0; + text-align: justify; + font-size: 1rem; + padding: 0.5rem; + padding-left: 3rem; + } } @media (max-width: hamburger-cutoff) { @@ -591,14 +542,6 @@ .speed-options { margin-bottom: 0; } - - .computer-select { - select { - min-width: calc(100vw - 6rem) !important; - max-width: calc(100vw - 6rem) !important; - width: calc(100vw - 6rem) !important; - } - } } .BoardSize-header { @@ -613,6 +556,7 @@ right: 0; z-index: z.dock; themed: background-color, bg; + gap: 1rem; .play-button { margin: 0; @@ -665,10 +609,4 @@ } } } - - .computer-select { - > div { - max-width: auto; - } - } } \ No newline at end of file diff --git a/src/views/Play/QuickMatch.tsx b/src/views/Play/QuickMatch.tsx index b0e16b4d42..a9f00cde64 100644 --- a/src/views/Play/QuickMatch.tsx +++ b/src/views/Play/QuickMatch.tsx @@ -22,6 +22,7 @@ import moment from "moment"; import { AutomatchPreferences, + JGOFTimeControl, JGOFTimeControlSpeed, shortDurationString, Size, @@ -34,23 +35,13 @@ import { alert } from "@/lib/swal_config"; import { useRefresh, useUser } from "@/lib/hooks"; //import { Toggle } from "@/components/Toggle"; import { MiniGoban } from "@/components/MiniGoban"; -import { rankString } from "@/lib/rank_utils"; -import { errorAlerter, uuid } from "@/lib/misc"; +import { uuid } from "@/lib/misc"; import { LoadingButton } from "@/components/LoadingButton"; -import { post } from "@/lib/requests"; -import { browserHistory } from "@/lib/ogsHistory"; -import { - ChallengeDetails, - RejectionDetails, - rejectionDetailsToMessage, -} from "@/components/ChallengeModal"; -import { notification_manager, NotificationManagerEvents } from "@/components/Notifications"; +import { challengeComputer, ChallengeModalConfig } from "@/components/ChallengeModal"; import { socket } from "@/lib/sockets"; -import { sfx } from "@/lib/sfx"; import { Link } from "react-router-dom"; -import Select, { components } from "react-select"; +import Select from "react-select"; import { SPEED_OPTIONS } from "./SPEED_OPTIONS"; -import { PlayerIcon } from "@/components/PlayerIcon"; import { useHaveActiveGameSearch } from "./hooks"; moment.relativeTimeThreshold("m", 56); @@ -139,57 +130,6 @@ const RenderOptionWithDescription = (props: { ); }; -const RenderBotOption = (props: { - data: Bot & { disabled?: string }; - innerProps: any; - innerRef: any; - isFocused: boolean; - isSelected: boolean; -}) => { - const opt = props.data; - //console.log(opt.username, props.isSelected); - return ( -
-
- - - {opt.username} ({rankString(opt.ranking || 0)}) - - - - - - -
-
- {props.data.disabled ? props.data.disabled : ""} -
-
- ); -}; - -const RenderBotValue = (props: any) => { - const opt = props.data; - return ( - - - {opt.username} ({rankString(opt.ranking || 0)}) - - ); -}; - const select_styles = { menu: ({ ...css }) => ({ ...css, @@ -214,7 +154,6 @@ export function QuickMatch(): JSX.Element { const [time_control_system, setTimeControlSystem] = preferences.usePreference("automatch.time-control"); const [opponent, setOpponent] = preferences.usePreference("automatch.opponent"); - const [selected_bot, setSelectedBot] = preferences.usePreference("automatch.bot"); const [lower_rank_diff, setLowerRankDiff] = preferences.usePreference( "automatch.lower-rank-diff", ); @@ -223,8 +162,6 @@ export function QuickMatch(): JSX.Element { ); const [correspondence_spinner, setCorrespondenceSpinner] = React.useState(false); - const [bot_spinner, setBotSpinner] = React.useState(false); - const cancel_bot_game = React.useRef<() => void>(() => {}); const [game_clock, setGameClock] = preferences.usePreference("automatch.game-clock"); const have_active_game_search = useHaveActiveGameSearch(); @@ -389,158 +326,64 @@ export function QuickMatch(): JSX.Element { multiple_speeds, ]); - const playComputer = React.useCallback(() => { - const settings = { - rank: user.ranking, - width: parseInt(board_size), - height: parseInt(board_size), - ranked: true, - handicap: handicaps === "disabled" ? false : true, - system: time_control_system, - speed: game_speed, - [time_control_system]: SPEED_OPTIONS[board_size][game_speed][time_control_system], - }; - const [options, message] = getAcceptableTimeSetting(selected_bot, settings); - if (!options) { - console.error("Failed to find acceptable time setting", message); - void alert.fire(_("Please select a bot")); - return; + const time_control: JGOFTimeControl = + time_control_system === "fischer" + ? { + system: "fischer", + speed: game_speed, + initial_time: SPEED_OPTIONS[board_size][game_speed].fischer.initial_time, + time_increment: SPEED_OPTIONS[board_size][game_speed].fischer.time_increment, + max_time: SPEED_OPTIONS[board_size][game_speed].fischer.initial_time * 10, + pause_on_weekends: false, + } + : { + system: "byoyomi", + speed: game_speed, + main_time: SPEED_OPTIONS[board_size][game_speed].byoyomi!.main_time, + period_time: SPEED_OPTIONS[board_size][game_speed].byoyomi!.period_time, + periods: SPEED_OPTIONS[board_size][game_speed].byoyomi!.periods, + pause_on_weekends: false, + }; + + const playComputerX = React.useCallback(() => { + // Try to guess whether the player wants a ranked game or not by looking at + // the past challenges. + let ranked = data.get(`challenge.challenge.${game_speed}`)?.game?.ranked; + if (ranked === undefined) { + ranked = + data.get(`challenge.challenge.blitz`)?.game?.ranked ?? + data.get(`challenge.challenge.rapid`)?.game?.ranked ?? + data.get(`challenge.challenge.live`)?.game?.ranked ?? + true; } - const challenge: ChallengeDetails = { - initialized: false, - min_ranking: -99, - max_ranking: 99, - challenger_color: "automatic", - rengo_auto_start: 0, - game: { - name: _("Quick Match"), - rules: "chinese", - ranked: true, - width: board_size === "9x9" ? 9 : board_size === "13x13" ? 13 : 19, - height: board_size === "9x9" ? 9 : board_size === "13x13" ? 13 : 19, - handicap: handicaps === "disabled" ? 0 : -1, - komi_auto: "automatic", - disable_analysis: false, - initial_state: null, - private: false, - rengo: false, - rengo_casual_mode: false, - pause_on_weekends: true, - time_control: time_control_system, - time_control_parameters: - time_control_system === "fischer" - ? { - system: "fischer", - speed: game_speed, - initial_time: - SPEED_OPTIONS[board_size as any][game_speed].fischer.initial_time, - time_increment: - SPEED_OPTIONS[board_size as any][game_speed].fischer - .time_increment, - max_time: - SPEED_OPTIONS[board_size as any][game_speed].fischer - .initial_time * 10, - pause_on_weekends: true, - } - : { - system: "byoyomi", - speed: game_speed, - main_time: - SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.main_time, - periods: - SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.periods, - period_time: - SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.period_time, - periods_min: - SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.periods, - periods_max: - SPEED_OPTIONS[board_size as any][game_speed].byoyomi!.periods, - pause_on_weekends: true, - }, + const settings: ChallengeModalConfig = { + challenge: { + challenger_color: "automatic", + invite_only: false, + game: { + width: parseInt(board_size), + height: parseInt(board_size), + ranked, + handicap: handicaps === "disabled" ? 0 : -1, + time_control, + rules: "japanese", + komi_auto: "automatic", + disable_analysis: false, + initial_state: null, + private: false, + }, + }, + conf: { + restrict_rank: false, }, + time_control, }; - const bot_id = selected_bot; - if (!bot_id) { - void alert.fire(_("Please select a bot")); - return; - } + console.log(settings); - setBotSpinner(true); - post(`players/${bot_id}/challenge`, challenge) - .then((res) => { - const challenge_id = res.challenge; - - const game_id = typeof res.game === "object" ? res.game.id : res.game; - let keepalive_interval: ReturnType | undefined; - - const checkForReject = ( - notification: NotificationManagerEvents["notification"], - ) => { - console.log("challenge rejection check notification:", notification); - if (notification.type === "gameOfferRejected") { - /* non checked delete to purge old notifications that - * could be around after browser refreshes, connection - * drops, etc. */ - notification_manager.deleteNotification(notification); - if (notification.game_id === game_id) { - onRejected(notification.message, notification.rejection_details); - } - } - }; - - const active_check = () => { - keepalive_interval = setInterval(() => { - socket.send("challenge/keepalive", { - challenge_id: challenge_id, - game_id: game_id, - }); - }, 1000); - socket.send("game/connect", { game_id: game_id }); - socket.on(`game/${game_id}/gamedata`, onGamedata); - }; - - const onGamedata = () => { - off(); - alert.close(); - //sfx.play("game_accepted"); - sfx.play("game_started", 3000); - //sfx.play("setup-bowl"); - browserHistory.push(`/game/${game_id}`); - }; - - const onRejected = (message?: string, details?: RejectionDetails) => { - off(); - alert.close(); - void alert.fire({ - text: - (details && rejectionDetailsToMessage(details)) || - message || - _("Game offer was rejected"), - }); - }; - - const off = () => { - clearTimeout(keepalive_interval); - socket.send("game/disconnect", { game_id: game_id }); - socket.off(`game/${game_id}/gamedata`, onGamedata); - //socket.off(`game/${game_id}/rejected`, onRejected); - notification_manager.event_emitter.off("notification", checkForReject); - cancel_bot_game.current = () => {}; - setBotSpinner(false); - }; - - cancel_bot_game.current = off; - - notification_manager.event_emitter.on("notification", checkForReject); - active_check(); - }) - .catch((err) => { - setBotSpinner(false); - errorAlerter(err); - }); - }, [selected_bot, board_size, handicaps, game_speed, time_control_system, refresh]); + challengeComputer(settings); + }, [board_size, handicaps, time_control_system, game_speed]); const play = React.useCallback(() => { if (data.get("user").anonymous) { @@ -548,29 +391,15 @@ export function QuickMatch(): JSX.Element { return; } - if (opponent === "bot") { - playComputer(); - } else { - doAutomatch(); - } - }, [doAutomatch, playComputer]); + doAutomatch(); + }, [doAutomatch]); const dismissCorrespondenceSpinner = React.useCallback(() => { setCorrespondenceSpinner(false); }, []); - /* - const newComputerGame = React.useCallback(() => { - if (bot_count() === 0) { - void alert.fire(_("Sorry, all bots seem to be offline, please try again later.")); - return; - } - challengeComputer(); - }, []); - */ - const automatch_search_active = - !!automatch_manager.active_live_automatcher || correspondence_spinner || bot_spinner; + !!automatch_manager.active_live_automatcher || correspondence_spinner; function isSizeActive(size: Size) { if (game_clock === "multiple") { @@ -690,8 +519,6 @@ export function QuickMatch(): JSX.Element { return (a.ranking || 0) - (b.ranking || 0); }); - const selected_bot_value = available_bots.find((b) => b.id === selected_bot) || undefined; - /* Filter available quick matches to the applicable ones for button highlighting */ const available_human_match_count_by_size: { [size: string]: number } = { "9x9": 0, @@ -815,6 +642,23 @@ export function QuickMatch(): JSX.Element { return " activity "; } + const lower_rank_diff_options = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0].map((v) => ({ + value: v.toString(), + label: v === 0 ? "0" : `- ${v}`, + description: v === 0 ? llm_pgettext("Player is the same rank as you", "Your rank") : "", + })); + + const upper_rank_diff_options = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((v) => ({ + value: v.toString(), + label: v === 0 ? "0" : `+ ${v}`, + description: v === 0 ? llm_pgettext("Player is the same rank as you", "Your rank") : "", + })); + + // ensure a valid handicap value is selected + if (!handicap_options.find((o) => o.value === handicaps)) { + setHandicaps("standard"); + } + return ( <>
@@ -1050,126 +894,6 @@ export function QuickMatch(): JSX.Element {
{/* Opponent */} -
-
- {_("Opponent")} -
- -
-
{ - if (automatch_search_active) { - return; - } - setOpponent("human"); - }} - > -
- {pgettext("Play a human opponent", "Human")} -
-
- - {" - "} - -
-
{_("Rank range")}
-
-
{ - if (automatch_search_active || game_clock === "multiple") { - return; - } - setOpponent("bot"); - }} - > -
- {pgettext("Play a computer opponent", "Computer")} -
-
0 && - opponent === "bot" && - (!selected_bot || - !selected_bot_value || - selected_bot_value.disabled) - ? "error" - : "") - } - > - o.value === lower_rank_diff.toString(), + )} + isSearchable={false} + isDisabled={automatch_search_active} + onChange={(opt) => { + if (opt) { + setLowerRankDiff(parseInt(opt.value)); + } + }} + options={[ + { + options: lower_rank_diff_options, + }, + ]} + components={{ + Option: RenderOptionWithDescription, + }} + /> - {bot_spinner && ( -
-
- cancel_bot_game.current()} - > - {_("Cancel")} - -
-
- )} + {/* + + */} - {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".', - )} -
-
- -
-
- )} - {user.anonymous && ( -
- {_("Please sign in to play")} -
- {_("Register for Free")} - {" | "} - {_("Sign in")} -
-
- )} + {" - "} - {!automatch_search_active && !user.anonymous && ( -
+ +
+ {/* Bot */} + {user.anonymous && ( +
+ {_("Please sign in to play")} +
+ {_("Register for Free")} + {" | "} + {_("Sign in")} +
+
+ )} + + {!automatch_search_active && !user.anonymous && ( + + )} + + {/* Human */} + {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".', + )} +
+
+ +
+
+ )} + {user.anonymous && ( +
+ {_("Please sign in to play")} +
+ {_("Register for Free")} + {" | "} + {_("Sign in")} +
+
+ )} + + {!automatch_search_active && !user.anonymous && ( + + )} +
); }