diff --git a/app/controllers/Main.scala b/app/controllers/Main.scala index db702e8c60c1d..bfdff3890b9bd 100644 --- a/app/controllers/Main.scala +++ b/app/controllers/Main.scala @@ -144,5 +144,7 @@ final class Main( env.memo.picfitApi.bodyImage .upload(rel, image) .map(url => JsonOk(Json.obj("imageUrl" -> url))) + .recover: + case e: Exception => JsonBadRequest(jsonError(e.getMessage)) case None => JsonBadRequest(jsonError("Image content only")) } diff --git a/bin/mongodb/puzzle-add-theme.js b/bin/mongodb/puzzle-add-theme.js new file mode 100644 index 0000000000000..4d87241fcb3c3 --- /dev/null +++ b/bin/mongodb/puzzle-add-theme.js @@ -0,0 +1,11 @@ +puzzleDb = connect(`mongodb://localhost:27317/puzzler`); +ids = ''.split(' '); +ids.forEach(id => { + puzzleDb.puzzle2_round.updateOne( + { _id: 'lichess:' + id }, + { + $push: { t: '+vukovicMate' }, + }, + ); +}); +puzzleDb.puzzle2_puzzle.updateMany({ _id: { $in: ids } }, { $set: { dirty: true } }); diff --git a/modules/core/src/main/game/Game.scala b/modules/core/src/main/game/Game.scala index 7dcb833233177..345722c7b2c6d 100644 --- a/modules/core/src/main/game/Game.scala +++ b/modules/core/src/main/game/Game.scala @@ -258,8 +258,7 @@ case class Game( yield w -> b def averageUsersRating: Option[IntRating] = players.flatMap(_.rating) match - // case a :: b :: Nil => Some((a + b).map(_ / 2)) - case a :: b :: Nil => Some((a + b)) + case a :: b :: Nil => Some((a + b).map(_ / 2)) case a :: Nil => Some((a + IntRating(1500)).map(_ / 2)) case _ => None diff --git a/modules/coreI18n/src/main/key.scala b/modules/coreI18n/src/main/key.scala index 54709bfa51958..8606a864ab997 100644 --- a/modules/coreI18n/src/main/key.scala +++ b/modules/coreI18n/src/main/key.scala @@ -1278,6 +1278,8 @@ object I18nKey: val `intermezzoDescription`: I18nKey = "puzzleTheme:intermezzoDescription" val `killBoxMate`: I18nKey = "puzzleTheme:killBoxMate" val `killBoxMateDescription`: I18nKey = "puzzleTheme:killBoxMateDescription" + val `vukovicMate`: I18nKey = "puzzleTheme:vukovicMate" + val `vukovicMateDescription`: I18nKey = "puzzleTheme:vukovicMateDescription" val `knightEndgame`: I18nKey = "puzzleTheme:knightEndgame" val `knightEndgameDescription`: I18nKey = "puzzleTheme:knightEndgameDescription" val `long`: I18nKey = "puzzleTheme:long" diff --git a/modules/puzzle/src/main/PuzzleTheme.scala b/modules/puzzle/src/main/PuzzleTheme.scala index 8dacb99dc2d7f..c5582232b24c5 100644 --- a/modules/puzzle/src/main/PuzzleTheme.scala +++ b/modules/puzzle/src/main/PuzzleTheme.scala @@ -50,6 +50,7 @@ object PuzzleTheme: val intermezzo = PuzzleTheme(i.intermezzo, i.intermezzoDescription) val kingsideAttack = PuzzleTheme(i.kingsideAttack, i.kingsideAttackDescription) val killBoxMate = PuzzleTheme(i.killBoxMate, i.killBoxMateDescription) + val vukovicMate = PuzzleTheme(i.vukovicMate, i.vukovicMateDescription) val knightEndgame = PuzzleTheme(i.knightEndgame, i.knightEndgameDescription) val long = PuzzleTheme(i.long, i.longDescription) val master = PuzzleTheme(i.master, i.masterDescription) @@ -141,6 +142,7 @@ object PuzzleTheme: dovetailMate, hookMate, killBoxMate, + vukovicMate, smotheredMate ), I18nKey.puzzle.specialMoves -> List( diff --git a/modules/relay/src/main/RelayFetch.scala b/modules/relay/src/main/RelayFetch.scala index a2782b901773b..c3b85eb992f42 100644 --- a/modules/relay/src/main/RelayFetch.scala +++ b/modules/relay/src/main/RelayFetch.scala @@ -166,11 +166,10 @@ final private class RelayFetch( ) private def reportBroadcastFailure(r: RelayRound.WithTour): Unit = - if r.round.sync.log.alwaysFails then + if r.round.sync.log.alwaysFails && r.tour.official && r.round.shouldHaveStarted then r.round.sync.log.events.lastOption .filterNot(_.isTimeout) .flatMap(_.error) - .ifTrue(r.tour.official && r.round.shouldHaveStarted) .filterNot(_.contains("Cannot parse move")) .filterNot(_.contains("Cannot parse pgn")) .filterNot(_.contains("Found an empty PGN")) diff --git a/public/images/puzzle-themes/vukovicMate.svg b/public/images/puzzle-themes/vukovicMate.svg new file mode 100644 index 0000000000000..e18eabaabdbb4 --- /dev/null +++ b/public/images/puzzle-themes/vukovicMate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/translation/source/puzzleTheme.xml b/translation/source/puzzleTheme.xml index dfaa121997ba2..5ad8952a72744 100644 --- a/translation/source/puzzleTheme.xml +++ b/translation/source/puzzleTheme.xml @@ -59,6 +59,8 @@ Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as "Zwischenzug" or "In between". Kill box mate A rook is next to the enemy king and supported by a queen that also blocks the king's escape squares. The rook and the queen catch the enemy king in a 3 by 3 "kill box". + Vukovic mate + A rook and knight team up to mate the king. The rook delivers mate while supported by a third piece, and the knight is used to block the king's escape squares. Knight endgame An endgame with only knights and pawns. Long puzzle diff --git a/ui/analyse/css/study/_player.scss b/ui/analyse/css/study/_player.scss index b475b4041fc7a..a8ea747c218df 100644 --- a/ui/analyse/css/study/_player.scss +++ b/ui/analyse/css/study/_player.scss @@ -63,13 +63,13 @@ $player-height: 1.6rem; .analyse__clock { @extend %roboto, %flex-center-nowrap; - - height: 100%; - font-size: 1.2em; @include padding-direction(0, 0.8em, 0, 0.6em); + height: 100%; border-radius: 0 4px 0 0; box-shadow: none; + font-size: 1.2em; + font-weight: normal; } &-bot .analyse__clock { diff --git a/ui/analyse/src/explorer/explorerConfig.ts b/ui/analyse/src/explorer/explorerConfig.ts index 72935df2c22e3..f78cae3ae25b9 100644 --- a/ui/analyse/src/explorer/explorerConfig.ts +++ b/ui/analyse/src/explorer/explorerConfig.ts @@ -1,7 +1,7 @@ import { h, VNode } from 'snabbdom'; import { myUserId, type Prop, prop } from 'common'; import * as licon from 'common/licon'; -import { snabDialog } from 'common/dialog'; +import { type Dialog, snabDialog } from 'common/dialog'; import { bind, dataIcon, iconTag, onInsert } from 'common/snabbdom'; import { storedProp, storedJsonProp, type StoredProp, storedStringProp } from 'common/storage'; import { ExplorerDb, ExplorerSpeed, ExplorerMode } from './interfaces'; @@ -100,7 +100,6 @@ export class ExplorerConfigCtrl { } this.data.db('player'); this.data.playerName.value(name); - this.data.playerName.open(false); }; removePlayer = (name?: string) => { @@ -313,9 +312,10 @@ const monthSection = (ctrl: ExplorerConfigCtrl) => ]); const playerModal = (ctrl: ExplorerConfigCtrl) => { + let dlg: Dialog; const onSelect = (name: string | undefined) => { ctrl.selectPlayer(name); - ctrl.root.redraw(); + dlg.close(); }; const nameToOptionalColor = (name: string | undefined) => { if (!name) { @@ -333,6 +333,7 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => { ctrl.data.playerName.open(false); ctrl.root.redraw(); }, + onInsert: dialog => (dlg = dialog).show(), modal: true, vnodes: [ h('h2', 'Personal opening explorer'), diff --git a/ui/common/src/dialog.ts b/ui/common/src/dialog.ts index c82e6dede364e..b7c2cb454db29 100644 --- a/ui/common/src/dialog.ts +++ b/ui/common/src/dialog.ts @@ -245,7 +245,7 @@ class DialogWrapper implements Dialog { const justThen = Date.now(); const cancelOnInterval = (e: PointerEvent) => { - if (Date.now() - justThen < 200) return; + if (Date.now() - justThen < 200 || !dialog.isConnected) return; const r = dialog.getBoundingClientRect(); if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) this.close('cancel');