diff --git a/package.json b/package.json index fe1dc85436..238ff00a40 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "express": "^4.17.1", "express-http-proxy": "^1.6.0", "fork-ts-checker-webpack-plugin": "^8.0.0", - "goban": "=0.7.11", + "goban": "=0.7.13", "gulp": "^4.0.2", "gulp-clean-css": "^4.3.0", "gulp-eslint-new": "^1.7.1", diff --git a/src/lib/misc.ts b/src/lib/misc.ts index ca17ea2a82..726ad1f935 100644 --- a/src/lib/misc.ts +++ b/src/lib/misc.ts @@ -155,8 +155,9 @@ export function uuid(): string { }); } export function getOutcomeTranslation(outcome: string) { - /* Note: for the case statements, don't simply do `pgettext("Game outcome", outcome)`, - * the system to parse out strings to translate needs the text. */ + /* Note: Do not simply do `pgettext("Game outcome", outcome)` + * The translation system needs to read these strings to parse them out and + * prepare them for translating. */ switch (outcome) { case "resign": case "r": @@ -178,6 +179,10 @@ export function getOutcomeTranslation(outcome: string) { return pgettext("Game outcome", "Abandonment"); } + if (outcome.indexOf("Server Decision") === 0) { + return pgettext("Game outcome", "Server Decision") + " " + outcome.substring(16); + } + if (/[0-9.]+/.test(outcome)) { const num: number = +outcome.match(/([0-9.]+)/)[1]; const rounded_num = Math.round(num * 2) / 2; diff --git a/src/views/Game/AntiGrief.styl b/src/views/Game/AntiGrief.styl new file mode 100644 index 0000000000..6b83d818b3 --- /dev/null +++ b/src/views/Game/AntiGrief.styl @@ -0,0 +1,34 @@ +/* + * 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 . + */ + + +.AntiStalling, .AntiEscaping { + display: inline-flex; + align-items: center; + justify-content: space-around; + flex-direction: column; + margin: 0.5rem; + width: calc(100% - 2.2rem) !important; + min-height: 8rem; + text-align: center; + font-size: 1.1rem; + border-radius: 0.5rem; + + button { + white-space: nowrap !important; + } +} diff --git a/src/views/Game/AntiGrief.tsx b/src/views/Game/AntiGrief.tsx new file mode 100644 index 0000000000..5b96c62910 --- /dev/null +++ b/src/views/Game/AntiGrief.tsx @@ -0,0 +1,283 @@ +/* + * 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 { Card } from "material"; +import { pgettext, _ } from "translate"; +import { useGoban } from "./goban_context"; +import { useUser } from "hooks"; +import { JGOFClockWithTransmitting, JGOFTimeControl } from "goban"; +import { browserHistory } from "../../ogsHistory"; +import { toast } from "toast"; + +let on_game_page = false; +let live_game = false; +let live_game_id = 0; +let live_game_phase = null; +let last_toast = null; + +function checkForLeavingLiveGame(pathname: string) { + try { + const goban = window["global_goban"]; + const was_on_page = on_game_page; + const was_live_game = live_game; + + if (goban) { + const path = `/game/${goban.game_id}`; + if (pathname === path) { + on_game_page = true; + live_game = goban.engine.time_control.speed !== "correspondence"; + live_game_id = goban.game_id; + live_game_phase = goban.engine?.phase; + if (last_toast) { + last_toast.close(); + } + } else { + on_game_page = false; + } + } + + if (was_on_page && !on_game_page && was_live_game && live_game_phase === "play") { + const t = toast( +
+ {_( + "You have left a live game. If you do not return you will forfeit the match.", + )} +
, + ); + last_toast = t; + + const game_id = live_game_id; // capture the game id + t.on("close", () => { + last_toast = null; + browserHistory.push(`/game/${game_id}`); + }); + } + } catch (e) { + console.error(e); + } +} +browserHistory.listen((obj) => { + checkForLeavingLiveGame(obj?.location?.pathname); +}); + +export function AntiGrief(): JSX.Element { + checkForLeavingLiveGame(location?.pathname); + + return ( + <> + + + + ); +} +function AntiEscaping(): JSX.Element { + const user = useUser(); + const goban = useGoban(); + const [phase, setPhase] = React.useState(goban?.engine?.phase); + const [clock, setClock] = React.useState(goban?.last_clock as any); + const [expiration, setExpiration] = React.useState(null); + const [show, setShow] = React.useState(false); + + React.useEffect(() => { + setShow(false); + setClock(goban?.last_clock as any); + goban.on("clock", setClock); + + return () => { + goban.off("clock", setClock); + }; + }, [goban, setClock]); + + React.useEffect(() => { + setPhase(goban?.engine?.phase); + }, [goban?.engine?.phase]); + + React.useEffect(() => { + const handleAutoResign = (data?: { player_id: number; expiration: number }) => { + setShow(false); + setExpiration(data?.expiration); + }; + const handleClearAutoResign = () => { + setShow(false); + setExpiration(null); + }; + + goban.on("auto-resign", handleAutoResign); + goban.on("clear-auto-resign", handleClearAutoResign); + + return () => { + goban.off("auto-resign", handleAutoResign); + goban.off("clear-auto-resign", handleClearAutoResign); + }; + }, [goban]); + + React.useEffect(() => { + if (expiration) { + const timer = setTimeout(() => { + setExpiration(null); + setShow(true); + }, 30 * 1000); + return () => { + clearTimeout(timer); + }; + } + }, [expiration]); + + if (phase !== "play") { + return null; + } + + const time_control: JGOFTimeControl = goban?.engine?.time_control; + + if (!time_control) { + return null; + } + + if (time_control.speed === "correspondence") { + return null; + } + + if (!clock || clock.pause_state) { + return null; + } + + if ( + user.id !== goban?.engine?.config?.black_player_id && + user.id !== goban?.engine?.config?.white_player_id && + !user.is_moderator + ) { + return null; + } + + if (!show) { + return null; + } + + const my_color = user.id === goban?.engine?.config?.black_player_id ? "black" : "white"; + + return ( + +
+ {pgettext( + "This message is shown when one player has left the game", + "Your opponent is no longer connected. You can wait and see if they come back, or end the game.", + )} +
+
+ + + {/* + +
+ */} + +
+
+ ); +} + +function AntiStalling(): JSX.Element { + const user = useUser(); + const goban = useGoban(); + const [estimate, setEstimate] = React.useState(null); + const [phase, setPhase] = React.useState(goban?.engine?.phase); + + React.useEffect(() => { + const onScoreEstimate = (estimate) => { + setEstimate(estimate); + }; + + onScoreEstimate(goban.config?.stalling_score_estimate); + goban.on("stalling_score_estimate", onScoreEstimate); + + return () => { + goban.off("stalling_score_estimate", onScoreEstimate); + }; + }, [goban, goban.config?.stalling_score_estimate]); + + React.useEffect(() => { + setPhase(goban?.engine?.phase); + }, [goban?.engine?.phase]); + + if (!estimate) { + return null; + } + + if (phase !== "play" && phase !== "stone removal") { + return null; + } + + if ( + user.id !== goban?.engine?.config?.black_player_id && + user.id !== goban?.engine?.config?.white_player_id && + !user.is_moderator + ) { + return null; + } + + if (estimate.move_number + 1 < goban.engine?.cur_move?.move_number) { + // If we've placed a move since the estimate was made, we don't need to show it anymore + return null; + } + + // capitalize first letter + const predicted_winner = + estimate.predicted_winner.charAt(0).toUpperCase() + estimate.predicted_winner.slice(1); + const win_rate = ( + (estimate.win_rate > 0.5 ? estimate.win_rate : 1.0 - estimate.win_rate) * 100.0 + ).toFixed(1); + + return ( + +
+ {pgettext( + "This message is shown when the server thinks the game is over, but one player is stalling the game by continually playing useless moves", + "Predicted winner: ", + )}{" "} + {_(predicted_winner)} ({win_rate}%) +
+
+ +
+
+ ); +} diff --git a/src/views/Game/PlayControls.tsx b/src/views/Game/PlayControls.tsx index f110ae442b..2474b11abe 100644 --- a/src/views/Game/PlayControls.tsx +++ b/src/views/Game/PlayControls.tsx @@ -57,6 +57,7 @@ import { is_valid_url } from "url_validation"; import { enableTouchAction } from "./touch_actions"; import { ConditionalMoveTreeDisplay } from "./ConditionalMoveTreeDisplay"; import { useUser } from "hooks"; +import { AntiGrief } from "./AntiGrief"; import * as moment from "moment"; @@ -269,6 +270,7 @@ export function PlayControls({ )} )} + {((mode === "play" && phase === "stone removal") || null) && ( {_("Stone Removal Phase")} )} @@ -468,6 +470,7 @@ export function PlayControls({ )} + {(mode === "conditional" || null) && (
diff --git a/yarn.lock b/yarn.lock index 0475988826..43a11a3698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5352,10 +5352,10 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -goban@=0.7.11: - version "0.7.11" - resolved "https://registry.yarnpkg.com/goban/-/goban-0.7.11.tgz#e8fedbc1605b65119f76e45b1b59512fdb97407e" - integrity sha512-49viNOEoE6fQvRGKTiCRKmvFtLYu3AMq9ipK4/TjwP42U8gNzyaoo+vygGUFIK0t/Jd1R7QRm65OeSKuxKArwA== +goban@=0.7.13: + version "0.7.13" + resolved "https://registry.yarnpkg.com/goban/-/goban-0.7.13.tgz#a17ca1ab1931dc8b313c72ca894cdfbea463a8e3" + integrity sha512-sKoHlvBiexrjYSDXPvmIeW/ciGJkOkuKOt2E2dLwmTFqF9yfUTcDGFXHTATDDLEJt8XrgLXCa1vD00pJYEtFlA== dependencies: eventemitter3 "^5.0.0"