Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class ListBotGamesResponse(
val status: GameEventType,
val outcome: Outcome?,
val index: Int,
val isPreAnalyzed: Boolean,
val created: Long,
val lastUpdated: Long,
val variant: Variant?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ data class ListGamesResponse(val entries: List<Entry>) {
val timeControlIncrement: Int?,
val status: GameEventType,
val index: Int,
val isPreAnalyzed: Boolean,
val winnerUserId: String?,
val created: Long,
val lastUpdated: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ data class ListUserBotGamesDto(val entries: List<Entry>) {
val status: GameEventType,
val outcome: Outcome?,
val moveIndex: Int,
val isPreAnalyzed: Boolean,
val created: Long,
val lastUpdated: Long,
val variant: Variant,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ data class ListUserGamesResponse(val entries: List<Entry>) {
val outcome: UserOutcome?,
val ratingFrom: Int?,
val ratingTo: Int?,
val isPreAnalyzed: Boolean,
val created: Long,
val lastUpdated: Long,
val numberOfMessages: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.elephantchess.db.utils.nextMove
import io.elephantchess.db.utils.plusSeconds
import io.elephantchess.engines.EnginePool
import io.elephantchess.engines.protocol.model.InfoLinesResult
import io.elephantchess.model.AnalysisStatus
import io.elephantchess.model.BotGameMoveType
import io.elephantchess.model.Engine
import io.elephantchess.model.GameEventType.*
Expand Down Expand Up @@ -231,6 +232,7 @@ class PlayerVsBotGameService(
status = record.gameStatus,
outcome = record.outcome,
moveIndex = record.currentHalfMoveIndex,
isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED,
created = record.created.toEpochMilliseconds(),
lastUpdated = record.lastUpdated.toEpochMilliseconds(),
variant = record.variant ?: Variant.XIANGQI,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ class PlayerVsPlayerGameService(
outcome = gameRecord.userOutcome(userId),
ratingFrom = ratingFrom,
ratingTo = ratingTo,
isPreAnalyzed = gameRecord.analysisStatus == AnalysisStatus.COMPLETED,
created = gameRecord.created.toEpochMilliseconds(),
lastUpdated = gameRecord.lastUpdated.toEpochMilliseconds(),
numberOfMessages = numberOfMessages,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.elephantchess.db.services.PlayerVsBotGameDaoService
import io.elephantchess.db.services.PlayerVsPlayerGameDaoService
import io.elephantchess.db.services.PuzzleResultDaoService
import io.elephantchess.db.utils.winnerUserId
import io.elephantchess.model.AnalysisStatus
import io.elephantchess.model.GameEventType.AUTO_CANCELED
import io.elephantchess.model.UserType
import io.elephantchess.servicelayer.dto.admin.ListBotGamesResponse
Expand Down Expand Up @@ -91,6 +92,7 @@ class AdminFeedService(
timeControlIncrement = record.timeControlIncrement,
status = record.gameStatus,
index = record.currentHalfMoveIndex,
isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED,
winnerUserId = record.winnerUserId(),
created = record.created.toEpochMilliseconds(),
lastUpdated = record.lastUpdated.toEpochMilliseconds(),
Expand Down Expand Up @@ -125,6 +127,7 @@ class AdminFeedService(
status = record.gameStatus,
outcome = record.outcome,
index = record.currentHalfMoveIndex,
isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED,
created = record.created.toEpochMilliseconds(),
lastUpdated = record.lastUpdated.toEpochMilliseconds(),
variant = record.variant,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.elephantchess.servicelayer.services
import io.elephantchess.db.dao.codegen.Tables.*
import io.elephantchess.db.utils.awaitExecute
import io.elephantchess.db.utils.awaitSingleValue
import io.elephantchess.model.AnalysisStatus.COMPLETED
import io.elephantchess.model.AnalysisStatus.PARTIALLY_COMPLETED
import io.elephantchess.model.GameEventType
import io.elephantchess.model.GameEventType.*
import io.elephantchess.model.Outcome
Expand Down Expand Up @@ -659,6 +661,50 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() {
assertEquals(PERPETUAL_CHECKING, response.gameEventType)
}

@Test
fun listUserGamesPreAnalyzedStatusTest() = runTest {
val gameId = createAndJoinGame(userId1, userId2, inviterColor = RED)

val beforeUpdateEntry =
pvpGameService
.listUserGames(userId1.id, beforeTs = null)
.entries
.find { it.gameId == gameId }
?: throw AssertionError("Expected game $gameId in list")

assertFalse(beforeUpdateEntry.isPreAnalyzed)

dslContext
.update(GAME)
.set(GAME.ANALYSIS_STATUS, PARTIALLY_COMPLETED)
.where(GAME.ID.eq(gameId))
.awaitExecute()

val afterPartialUpdateEntry =
pvpGameService
.listUserGames(userId1.id, beforeTs = null)
.entries
.find { it.gameId == gameId }
?: throw AssertionError("Expected game $gameId in list")

assertFalse(afterPartialUpdateEntry.isPreAnalyzed)

dslContext
.update(GAME)
.set(GAME.ANALYSIS_STATUS, COMPLETED)
.where(GAME.ID.eq(gameId))
.awaitExecute()

val afterUpdateEntry =
pvpGameService
.listUserGames(userId1.id, beforeTs = null)
.entries
.find { it.gameId == gameId }
?: throw AssertionError("Expected game $gameId in list")

assertTrue(afterUpdateEntry.isPreAnalyzed)
}

/**
* User should not be able to create more than 3 CREATED PvP games with the same settings.
*/
Expand Down Expand Up @@ -716,7 +762,6 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() {

// different time category (BLITZ vs RAPID): should succeed
pvpGameService.createGame(userId1, request.copy(timeControlBase = 3.minutes.inWholeSeconds.toInt()))

assertEquals(4, countGameByStatus(CREATED))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ abstract class ServiceTest : PostgresTest(), KoinComponent {
guestUserId: String? = null,
): Pair<SignUpRequest, String> {
val password = insecure().nextAlphanumeric(10)
val testUserIdentifier = "$i-${insecure().nextAlphanumeric(6)}"
val request = SignUpRequest(
username = "test$i",
email = "test$i@gmail.com",
username = "test$testUserIdentifier",
email = "test$testUserIdentifier@gmail.com",
password = password,
transferGuestData = transferGuestData,
)
Expand Down
18 changes: 18 additions & 0 deletions webapp/src/main/resources/public/js/admin/admin-feeds-dto.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class GameAnalyticsDto {
#timeControl;
#status;
#index;
#isPreAnalyzed;
#winnerUserId;
#created;
#lastUpdated;
Expand All @@ -50,6 +51,7 @@ class GameAnalyticsDto {
this.#timeControl = TimeControl.fromJson(json);
this.#status = json.status;
this.#index = json.index;
this.#isPreAnalyzed = json.isPreAnalyzed;
this.#winnerUserId = json.winnerUserId;
this.#created = json.created;
this.#lastUpdated = json.lastUpdated;
Expand Down Expand Up @@ -153,6 +155,13 @@ class GameAnalyticsDto {
return Number(this.#index);
}

/**
* @return {boolean}
*/
get isPreAnalyzed() {
return this.#isPreAnalyzed;
}

/**
* @return {string}
*/
Expand Down Expand Up @@ -246,6 +255,7 @@ class BotGameAnalyticsDto {
#status;
#outcome;
#index;
#isPreAnalyzed;
#created;
#lastUpdated;
#variant;
Expand All @@ -262,6 +272,7 @@ class BotGameAnalyticsDto {
this.#status = json.status;
this.#outcome = json.outcome;
this.#index = Number(json.index);
this.#isPreAnalyzed = json.isPreAnalyzed;
this.#created = Number(json.created);
this.#lastUpdated = Number(json.lastUpdated);
this.#variant = json.variant ?? Variant.XIANGQI;
Expand Down Expand Up @@ -351,6 +362,13 @@ class BotGameAnalyticsDto {
return this.#index;
}

/**
* @return {boolean}
*/
get isPreAnalyzed() {
return this.#isPreAnalyzed;
}

/**
* @return {number}
*/
Expand Down
5 changes: 5 additions & 0 deletions webapp/src/main/resources/public/js/admin/admin-feeds.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class AdminFeedsPage extends BasePage {

row.insertCell().innerText = entry.formattedStatus;
row.insertCell().innerText = entry.index.toString();
row.insertCell().innerText = entry.isPreAnalyzed ? '[v]' : '[ ]';
row.insertCell().innerText = entry.formattedCreated;
row.insertCell().innerText = entry.formattedLastUpdated;
row.insertCell().innerText = entry.formattedSourceType;
Expand Down Expand Up @@ -182,6 +183,10 @@ class AdminFeedsPage extends BasePage {
indexCell.className = 'value-cell';
indexCell.innerText = entry.index.toString();

const preAnalyzedCell = row.insertCell();
preAnalyzedCell.className = 'value-cell';
preAnalyzedCell.innerText = entry.isPreAnalyzed ? '[v]' : '[ ]';

const createdCell = row.insertCell();
createdCell.className = 'label-cell';
createdCell.innerText = entry.formattedCreated;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class BotGameEntryDto {
#status;
#outcome;
#moveIndex;
#isPreAnalyzed;
#variant;

/**
Expand All @@ -50,6 +51,7 @@ class BotGameEntryDto {
this.#status = json.status;
this.#outcome = json.outcome;
this.#moveIndex = json.moveIndex;
this.#isPreAnalyzed = json.isPreAnalyzed;
this.#created = json.created;
this.#lastUpdated = json.lastUpdated;
this.#variant = json.variant ?? Variant.XIANGQI;
Expand Down Expand Up @@ -158,6 +160,13 @@ class BotGameEntryDto {
return currentMoveIndexToFullMove(this.#moveIndex);
}

/**
* @return {boolean}
*/
get isPreAnalyzed() {
return this.#isPreAnalyzed;
}

get created() {
return this.#created;
}
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/main/resources/public/js/userdata/my-bot-games.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class MyBotGamesPage extends InfiniteScrollPage {
const customFenIndicatorPane = document.createElement('div');
customFenIndicatorPane.className = 'indicator-pane';

const preAnalysisIndicatorPane = document.createElement('div');
preAnalysisIndicatorPane.className = 'indicator-pane';

const outcomeIndicatorPane = document.createElement('div');
outcomeIndicatorPane.className = 'indicator-pane';

Expand All @@ -105,6 +108,7 @@ class MyBotGamesPage extends InfiniteScrollPage {
leftPane,
middlePane,
customFenIndicatorPane,
preAnalysisIndicatorPane,
outcomeIndicatorPane,
rightPane
);
Expand All @@ -131,6 +135,9 @@ class MyBotGamesPage extends InfiniteScrollPage {
if (entry.hasCustomStartFen) {
customFenIndicatorPane.append(buildCustomFenIcon(entry));
}
if (entry.isPreAnalyzed) {
preAnalysisIndicatorPane.append(buildPreAnalyzedIcon(entry.gameId));
}

// outcome indicator pane
const outcomeDiv = buildUserOutcomeDiv(entry);
Expand Down
33 changes: 33 additions & 0 deletions webapp/src/main/resources/public/js/userdata/my-game-item-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2026 Encelade SRL
* Copyright (C) 2026 elephantchess.io
* Copyright (C) 2026 Benoît Vleminckx (benckx)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
* @param gameId {string}
* @returns {HTMLDivElement}
*/
function buildPreAnalyzedIcon(gameId) {
const iconImg = buildImg('/images/icons/stats.png', 'icon');
iconImg.alt = 'This game is pre-analyzed';
iconImg.style.opacity = '75%';

const div = wrapInDiv(iconImg);
div.id = `pre-analyzed-${gameId}`;
addToolTip(div, 'This game is pre-analyzed');
return div;
}
9 changes: 9 additions & 0 deletions webapp/src/main/resources/public/js/userdata/my-games-dto.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class GameEntryDto {
#ratingFrom;
#ratingTo;
#numberOfMessages;
#isPreAnalyzed;
#variant;

/**
Expand Down Expand Up @@ -70,6 +71,7 @@ class GameEntryDto {
this.#created = json.created;
this.#lastUpdated = json.lastUpdated;
this.#numberOfMessages = json.numberOfMessages;
this.#isPreAnalyzed = json.isPreAnalyzed;
this.#variant = json.variant ?? Variant.XIANGQI;
}

Expand Down Expand Up @@ -251,6 +253,13 @@ class GameEntryDto {
return Number(this.#numberOfMessages);
}

/**
* @return {boolean}
*/
get isPreAnalyzed() {
return this.#isPreAnalyzed;
}

/**
* @return {string}
*/
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/main/resources/public/js/userdata/my-games.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,15 @@ class MyGamesPage extends InfiniteScrollPage {
const chatIndicatorPane = buildDivWithClass('indicator-pane');
const outcomeIndicatorPane = buildDivWithClass('indicator-pane');
const ratingDeltaIndicatorPane = buildDivWithClass('indicator-pane');
const preAnalyzedIndicatorPane = buildDivWithClass('indicator-pane');
const rightPane = buildDivWithClass('right-pane');
const item = buildAnchorWithClass(entry.gameUrl,null, 'my-game-item');

item.append(
variantPane,
timeControlPane,
middlePane,
preAnalyzedIndicatorPane,
chatIndicatorPane,
ratingDeltaIndicatorPane,
outcomeIndicatorPane,
Expand Down Expand Up @@ -330,6 +332,11 @@ class MyGamesPage extends InfiniteScrollPage {
}
ratingDeltaIndicatorPane.append(buildRatingModeDiv(entry));

// pre-analysis indicator pane
if (entry.isPreAnalyzed) {
preAnalyzedIndicatorPane.append(buildPreAnalyzedIcon(entry.gameId));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I would put this pane more on the left side of things, after the middle pane but before the chat pane

}

// outcome indicator pane
const outcomeDiv = buildUserOutcomeDiv(entry);
if (outcomeDiv != null) {
Expand Down
Loading