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');