From 2d372e8eb67661e169338179ff84c1869910602b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 07:27:28 +0000 Subject: [PATCH 01/12] Initial plan From b031a9a5f6e06d52690baced66bf5125944f2bcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 07:35:27 +0000 Subject: [PATCH 02/12] Indicate pre-analyzed games in user and admin listings Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/012b00f7-d989-41be-ac8d-871cdca9747b Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../dto/admin/ListBotGamesResponse.kt | 1 + .../dto/admin/ListGamesResponse.kt | 1 + .../dto/botgame/ListUserBotGamesDto.kt | 1 + .../dto/game/ListUserGamesResponse.kt | 1 + .../services/PlayerVsBotGameService.kt | 6 ++++ .../services/PlayerVsPlayerGameService.kt | 5 ++++ .../services/admin/AdminFeedService.kt | 7 +++++ .../services/PlayerVsPlayerGameServiceTest.kt | 30 +++++++++++++++++++ .../public/js/admin/admin-feeds-dto.js | 12 ++++++++ .../resources/public/js/admin/admin-feeds.js | 5 ++++ .../public/js/userdata/my-bot-games-dto.js | 9 ++++++ .../public/js/userdata/my-bot-games.js | 24 +++++++++++++++ .../public/js/userdata/my-games-dto.js | 9 ++++++ .../resources/public/js/userdata/my-games.js | 27 +++++++++++++++++ .../templates/admin/admin_feeds.html | 2 ++ 15 files changed, 140 insertions(+) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt index 219ab65b8..8d607982f 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt @@ -22,6 +22,7 @@ data class ListBotGamesResponse( val status: GameEventType, val outcome: Outcome?, val index: Int, + val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt index 798cd5219..78137f107 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt @@ -19,6 +19,7 @@ data class ListGamesResponse(val entries: List) { val timeControlIncrement: Int?, val status: GameEventType, val index: Int, + val isPreAnalyzed: Boolean, val winnerUserId: String?, val created: Long, val lastUpdated: Long, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt index 7ffecc767..7045503c6 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt @@ -17,6 +17,7 @@ data class ListUserBotGamesDto(val entries: List) { val status: GameEventType, val outcome: Outcome?, val moveIndex: Int, + val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt index 6c00b24d5..fee050f0f 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt @@ -23,6 +23,7 @@ data class ListUserGamesResponse(val entries: List) { val outcome: UserOutcome?, val ratingFrom: Int?, val ratingTo: Int?, + val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, val numberOfMessages: Int diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt index 379b5ea7d..1b3fe56e7 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt @@ -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.* @@ -216,6 +217,7 @@ class PlayerVsBotGameService( status = record.gameStatus, outcome = record.outcome, moveIndex = record.currentHalfMoveIndex, + isPreAnalyzed = record.analysisStatus.isPreAnalyzed(), created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), ) @@ -225,6 +227,10 @@ class PlayerVsBotGameService( } } + private fun AnalysisStatus.isPreAnalyzed(): Boolean { + return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED + } + suspend fun fetchGameData(gameId: String): GetBotGameDataResponse { val game = pvbGameDaoService.fetchById(gameId) if (game == null) { diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt index af3bb2a30..a091d04a0 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt @@ -377,6 +377,7 @@ class PlayerVsPlayerGameService( outcome = gameRecord.userOutcome(userId), ratingFrom = ratingFrom, ratingTo = ratingTo, + isPreAnalyzed = gameRecord.analysisStatus.isPreAnalyzed(), created = gameRecord.created.toEpochMilliseconds(), lastUpdated = gameRecord.lastUpdated.toEpochMilliseconds(), numberOfMessages = numberOfMessages @@ -387,6 +388,10 @@ class PlayerVsPlayerGameService( } } + private fun AnalysisStatus.isPreAnalyzed(): Boolean { + return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED + } + suspend fun startGamesToPlaySession( userId: UserId, sendCb: (GamesToPlayUpdate) -> ChannelResult, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt index c29043874..714b1de8a 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt @@ -7,6 +7,7 @@ import io.elephantchess.db.services.AnalysisDaoService import io.elephantchess.db.services.PlayerVsBotGameDaoService import io.elephantchess.db.services.PlayerVsPlayerGameDaoService import io.elephantchess.db.services.PuzzleResultDaoService +import io.elephantchess.model.AnalysisStatus import io.elephantchess.db.utils.winnerUserId import io.elephantchess.model.GameEventType.AUTO_CANCELED import io.elephantchess.servicelayer.dto.admin.ListBotGamesResponse @@ -87,6 +88,7 @@ class AdminFeedService( timeControlIncrement = record.timeControlIncrement, status = record.gameStatus, index = record.currentHalfMoveIndex, + isPreAnalyzed = record.analysisStatus.isPreAnalyzed(), winnerUserId = record.winnerUserId(), created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), @@ -120,9 +122,14 @@ class AdminFeedService( status = record.gameStatus, outcome = record.outcome, index = record.currentHalfMoveIndex, + isPreAnalyzed = record.analysisStatus.isPreAnalyzed(), created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), ) } + private fun AnalysisStatus.isPreAnalyzed(): Boolean { + return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED + } + } diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index 45c6683e5..fc43627f5 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -3,6 +3,7 @@ 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.GameEventType import io.elephantchess.model.GameEventType.* import io.elephantchess.model.Outcome @@ -496,6 +497,35 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertEquals(PERPETUAL_CHECKING, response.gameEventType) } + @Test + fun listUserGamesPreAnalyzedStatusTest() = runTest { + val gameId = createAndJoinGame(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, 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) + } + private suspend fun createGame(inviterColor: Color): String { val request = CreateGameRequest( inviterColor = inviterColor, diff --git a/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js b/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js index bbdd30879..60192cfda 100644 --- a/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js +++ b/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js @@ -31,6 +31,7 @@ class GameAnalyticsDto { #timeControl; #status; #index; + #isPreAnalyzed; #winnerUserId; #created; #lastUpdated; @@ -49,6 +50,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; @@ -151,6 +153,10 @@ class GameAnalyticsDto { return Number(this.#index); } + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + /** * @return {string} */ @@ -235,6 +241,7 @@ class BotGameAnalyticsDto { #status; #outcome; #index; + #isPreAnalyzed; #created; #lastUpdated; @@ -250,6 +257,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); } @@ -338,6 +346,10 @@ class BotGameAnalyticsDto { return this.#index; } + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + /** * @return {number} */ diff --git a/webapp/src/main/resources/public/js/admin/admin-feeds.js b/webapp/src/main/resources/public/js/admin/admin-feeds.js index 43fb9ae3c..5dae1cf3d 100644 --- a/webapp/src/main/resources/public/js/admin/admin-feeds.js +++ b/webapp/src/main/resources/public/js/admin/admin-feeds.js @@ -103,6 +103,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; @@ -175,6 +176,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; diff --git a/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js b/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js index 4bcb778cf..c28e8620e 100644 --- a/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js +++ b/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js @@ -28,6 +28,7 @@ class BotGameEntryDto { #status; #outcome; #moveIndex; + #isPreAnalyzed; /** * @type {number} @@ -49,6 +50,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; } @@ -149,6 +151,13 @@ class BotGameEntryDto { return currentMoveIndexToFullMove(this.#moveIndex); } + /** + * @return {boolean} + */ + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + get created() { return this.#created; } diff --git a/webapp/src/main/resources/public/js/userdata/my-bot-games.js b/webapp/src/main/resources/public/js/userdata/my-bot-games.js index 99549ff7e..3c03619ac 100644 --- a/webapp/src/main/resources/public/js/userdata/my-bot-games.js +++ b/webapp/src/main/resources/public/js/userdata/my-bot-games.js @@ -76,6 +76,23 @@ class MyBotGamesPage extends InfiniteScrollPage { return div; } + /** + * @param entry {BotGameEntryDto} + * @returns {HTMLDivElement} + */ + function buildPreAnalyzedIcon(entry) { + const iconImg = document.createElement('img'); + iconImg.className = 'icon'; + iconImg.src = '/images/icons/database.png'; + iconImg.alt = 'This game is pre-analyzed'; + iconImg.style.opacity = '75%'; + + const div = wrapInDiv(iconImg); + div.id = `pre-analyzed-${entry.gameId}`; + addToolTip(div, 'This game is pre-analyzed'); + return div; + } + entries.forEach(entry => { // structure const leftPane = document.createElement('div'); @@ -87,6 +104,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'; @@ -101,6 +121,7 @@ class MyBotGamesPage extends InfiniteScrollPage { leftPane, middlePane, customFenIndicatorPane, + preAnalysisIndicatorPane, outcomeIndicatorPane, rightPane ); @@ -123,6 +144,9 @@ class MyBotGamesPage extends InfiniteScrollPage { if (entry.hasCustomStartFen) { customFenIndicatorPane.append(buildCustomFenIcon(entry)); } + if (entry.isPreAnalyzed) { + preAnalysisIndicatorPane.append(buildPreAnalyzedIcon(entry)); + } // outcome indicator pane const outcomeDiv = buildUserOutcomeDiv(entry); diff --git a/webapp/src/main/resources/public/js/userdata/my-games-dto.js b/webapp/src/main/resources/public/js/userdata/my-games-dto.js index b21c879df..a779331c1 100644 --- a/webapp/src/main/resources/public/js/userdata/my-games-dto.js +++ b/webapp/src/main/resources/public/js/userdata/my-games-dto.js @@ -34,6 +34,7 @@ class GameEntryDto { #ratingFrom; #ratingTo; #numberOfMessages; + #isPreAnalyzed; /** * @type {number} @@ -63,6 +64,7 @@ class GameEntryDto { this.#created = json.created; this.#lastUpdated = json.lastUpdated; this.#numberOfMessages = json.numberOfMessages; + this.#isPreAnalyzed = json.isPreAnalyzed; } /** @@ -236,4 +238,11 @@ class GameEntryDto { return Number(this.#numberOfMessages); } + /** + * @return {boolean} + */ + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + } diff --git a/webapp/src/main/resources/public/js/userdata/my-games.js b/webapp/src/main/resources/public/js/userdata/my-games.js index 417c69fca..7a377a5e2 100644 --- a/webapp/src/main/resources/public/js/userdata/my-games.js +++ b/webapp/src/main/resources/public/js/userdata/my-games.js @@ -239,6 +239,25 @@ class MyGamesPage extends InfiniteScrollPage { } } + /** + * @param entry {GameEntryDto} + * @returns {HTMLDivElement|null} + */ + function buildPreAnalyzedIcon(entry) { + if (entry.isPreAnalyzed) { + const iconImg = buildImg('/images/icons/database.png', 'icon'); + iconImg.alt = 'This game is pre-analyzed'; + iconImg.style.opacity = '75%'; + + const div = wrapInDiv(iconImg); + div.id = `pre-analyzed-${entry.gameId}`; + addToolTip(div, 'This game is pre-analyzed'); + return div; + } else { + return null; + } + } + /** * @param entry {GameEntryDto} * @returns {HTMLDivElement|null} @@ -264,6 +283,7 @@ 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'); @@ -272,6 +292,7 @@ class MyGamesPage extends InfiniteScrollPage { middlePane, chatIndicatorPane, ratingDeltaIndicatorPane, + preAnalyzedIndicatorPane, outcomeIndicatorPane, rightPane ); @@ -309,6 +330,12 @@ class MyGamesPage extends InfiniteScrollPage { ratingDeltaIndicatorPane.append(ratingDiv); } + // pre-analysis indicator pane + const preAnalyzedDiv = buildPreAnalyzedIcon(entry); + if (preAnalyzedDiv != null) { + preAnalyzedIndicatorPane.append(preAnalyzedDiv); + } + // outcome indicator pane const outcomeDiv = buildUserOutcomeDiv(entry); if (outcomeDiv != null) { diff --git a/webapp/src/main/resources/templates/admin/admin_feeds.html b/webapp/src/main/resources/templates/admin/admin_feeds.html index d2a19cc0f..d7b12248a 100644 --- a/webapp/src/main/resources/templates/admin/admin_feeds.html +++ b/webapp/src/main/resources/templates/admin/admin_feeds.html @@ -26,6 +26,7 @@

Last Game Updates

time status index + pre-analyzed created updated source @@ -49,6 +50,7 @@

Last Bot Game Updates

status outcome index + pre-analyzed created updated From c0435de54036ce1b0850dc5e03e40e7cbb6ceab4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 07:37:39 +0000 Subject: [PATCH 03/12] Refactor shared pre-analyzed status helper Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/012b00f7-d989-41be-ac8d-871cdca9747b Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../servicelayer/services/PlayerVsBotGameService.kt | 6 +----- .../servicelayer/services/PlayerVsPlayerGameService.kt | 5 +---- .../servicelayer/services/admin/AdminFeedService.kt | 6 +----- .../kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt | 5 +++++ 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt index 1b3fe56e7..b3c596aea 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt @@ -15,7 +15,6 @@ 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.* @@ -29,6 +28,7 @@ import io.elephantchess.servicelayer.exceptions.NotFoundException import io.elephantchess.servicelayer.exceptions.RequestTimeoutException import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.services.ws.PvbWebSocketSession +import io.elephantchess.servicelayer.utils.ops.isPreAnalyzed import io.elephantchess.servicelayer.utils.modelToProcess import io.elephantchess.servicelayer.utils.ops.isNonStandardFen import io.elephantchess.servicelayer.utils.ops.launchAtFixedRate @@ -227,10 +227,6 @@ class PlayerVsBotGameService( } } - private fun AnalysisStatus.isPreAnalyzed(): Boolean { - return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED - } - suspend fun fetchGameData(gameId: String): GetBotGameDataResponse { val game = pvbGameDaoService.fetchById(gameId) if (game == null) { diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt index a091d04a0..cb7f07f1b 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt @@ -27,6 +27,7 @@ import io.elephantchess.servicelayer.exceptions.NotFoundException import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.services.ws.GamesToPlayWebSocketSession import io.elephantchess.servicelayer.services.ws.PlayerVsPlayerWebSocketSession +import io.elephantchess.servicelayer.utils.ops.isPreAnalyzed import io.elephantchess.servicelayer.utils.ops.launchAtFixedRate import io.elephantchess.servicelayer.utils.ops.ratingUpdate import io.elephantchess.utils.EloCalculator.calculateElo @@ -388,10 +389,6 @@ class PlayerVsPlayerGameService( } } - private fun AnalysisStatus.isPreAnalyzed(): Boolean { - return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED - } - suspend fun startGamesToPlaySession( userId: UserId, sendCb: (GamesToPlayUpdate) -> ChannelResult, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt index 714b1de8a..edb6789d4 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt @@ -7,7 +7,6 @@ import io.elephantchess.db.services.AnalysisDaoService import io.elephantchess.db.services.PlayerVsBotGameDaoService import io.elephantchess.db.services.PlayerVsPlayerGameDaoService import io.elephantchess.db.services.PuzzleResultDaoService -import io.elephantchess.model.AnalysisStatus import io.elephantchess.db.utils.winnerUserId import io.elephantchess.model.GameEventType.AUTO_CANCELED import io.elephantchess.servicelayer.dto.admin.ListBotGamesResponse @@ -15,6 +14,7 @@ import io.elephantchess.servicelayer.dto.admin.ListGamesResponse import io.elephantchess.servicelayer.dto.admin.ListLastPuzzleByLoggedInUsersResponse import io.elephantchess.servicelayer.dto.admin.ListLastUserAnalysisResponse import io.elephantchess.servicelayer.services.UserCache +import io.elephantchess.servicelayer.utils.ops.isPreAnalyzed class AdminFeedService( private val analysisDaoService: AnalysisDaoService, @@ -128,8 +128,4 @@ class AdminFeedService( ) } - private fun AnalysisStatus.isPreAnalyzed(): Boolean { - return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED - } - } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt index eaf27f9eb..1b3d7a47d 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt @@ -7,6 +7,7 @@ import io.elephantchess.engines.process.EngineId import io.elephantchess.engines.process.FairyStockfishEngineId import io.elephantchess.engines.process.PikafishEngineId import io.elephantchess.engines.protocol.model.InfoLinesResult +import io.elephantchess.model.AnalysisStatus import io.elephantchess.servicelayer.dto.game.RatingUpdate import io.elephantchess.xiangqi.AbstractPieceType import io.elephantchess.xiangqi.Board @@ -15,6 +16,10 @@ import io.github.oshai.kotlinlogging.KotlinLogging private val logger = KotlinLogging.logger("Ops") +fun AnalysisStatus.isPreAnalyzed(): Boolean { + return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED +} + fun Game.ratingUpdate(): RatingUpdate? { return ratingUpdateRecord()?.let { record -> return RatingUpdate( From fb1ee197d7031b11f34e58a4a6b450f47e0b11e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 07:40:42 +0000 Subject: [PATCH 04/12] Extract shared pre-analyzed icon helper for user game pages Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/012b00f7-d989-41be-ac8d-871cdca9747b Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../public/js/admin/admin-feeds-dto.js | 6 ++++ .../public/js/userdata/my-bot-games.js | 19 +---------- .../public/js/userdata/my-game-item-utils.js | 33 +++++++++++++++++++ .../resources/public/js/userdata/my-games.js | 24 ++------------ .../templates/userdata/my_bot_games.html | 1 + .../templates/userdata/my_games.html | 1 + 6 files changed, 44 insertions(+), 40 deletions(-) create mode 100644 webapp/src/main/resources/public/js/userdata/my-game-item-utils.js diff --git a/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js b/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js index 60192cfda..eb8aecb78 100644 --- a/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js +++ b/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js @@ -153,6 +153,9 @@ class GameAnalyticsDto { return Number(this.#index); } + /** + * @return {boolean} + */ get isPreAnalyzed() { return this.#isPreAnalyzed; } @@ -346,6 +349,9 @@ class BotGameAnalyticsDto { return this.#index; } + /** + * @return {boolean} + */ get isPreAnalyzed() { return this.#isPreAnalyzed; } diff --git a/webapp/src/main/resources/public/js/userdata/my-bot-games.js b/webapp/src/main/resources/public/js/userdata/my-bot-games.js index 3c03619ac..ba99b81c7 100644 --- a/webapp/src/main/resources/public/js/userdata/my-bot-games.js +++ b/webapp/src/main/resources/public/js/userdata/my-bot-games.js @@ -76,23 +76,6 @@ class MyBotGamesPage extends InfiniteScrollPage { return div; } - /** - * @param entry {BotGameEntryDto} - * @returns {HTMLDivElement} - */ - function buildPreAnalyzedIcon(entry) { - const iconImg = document.createElement('img'); - iconImg.className = 'icon'; - iconImg.src = '/images/icons/database.png'; - iconImg.alt = 'This game is pre-analyzed'; - iconImg.style.opacity = '75%'; - - const div = wrapInDiv(iconImg); - div.id = `pre-analyzed-${entry.gameId}`; - addToolTip(div, 'This game is pre-analyzed'); - return div; - } - entries.forEach(entry => { // structure const leftPane = document.createElement('div'); @@ -145,7 +128,7 @@ class MyBotGamesPage extends InfiniteScrollPage { customFenIndicatorPane.append(buildCustomFenIcon(entry)); } if (entry.isPreAnalyzed) { - preAnalysisIndicatorPane.append(buildPreAnalyzedIcon(entry)); + preAnalysisIndicatorPane.append(buildPreAnalyzedIcon(entry.gameId)); } // outcome indicator pane diff --git a/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js b/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js new file mode 100644 index 000000000..21ddc2d8f --- /dev/null +++ b/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js @@ -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 . + */ + +/** + * @param gameId {string} + * @returns {HTMLDivElement} + */ +function buildPreAnalyzedIcon(gameId) { + const iconImg = buildImg('/images/icons/database.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; +} diff --git a/webapp/src/main/resources/public/js/userdata/my-games.js b/webapp/src/main/resources/public/js/userdata/my-games.js index 7a377a5e2..5aa5cf867 100644 --- a/webapp/src/main/resources/public/js/userdata/my-games.js +++ b/webapp/src/main/resources/public/js/userdata/my-games.js @@ -239,25 +239,6 @@ class MyGamesPage extends InfiniteScrollPage { } } - /** - * @param entry {GameEntryDto} - * @returns {HTMLDivElement|null} - */ - function buildPreAnalyzedIcon(entry) { - if (entry.isPreAnalyzed) { - const iconImg = buildImg('/images/icons/database.png', 'icon'); - iconImg.alt = 'This game is pre-analyzed'; - iconImg.style.opacity = '75%'; - - const div = wrapInDiv(iconImg); - div.id = `pre-analyzed-${entry.gameId}`; - addToolTip(div, 'This game is pre-analyzed'); - return div; - } else { - return null; - } - } - /** * @param entry {GameEntryDto} * @returns {HTMLDivElement|null} @@ -331,9 +312,8 @@ class MyGamesPage extends InfiniteScrollPage { } // pre-analysis indicator pane - const preAnalyzedDiv = buildPreAnalyzedIcon(entry); - if (preAnalyzedDiv != null) { - preAnalyzedIndicatorPane.append(preAnalyzedDiv); + if (entry.isPreAnalyzed) { + preAnalyzedIndicatorPane.append(buildPreAnalyzedIcon(entry.gameId)); } // outcome indicator pane diff --git a/webapp/src/main/resources/templates/userdata/my_bot_games.html b/webapp/src/main/resources/templates/userdata/my_bot_games.html index ceda42a6b..42cb5c148 100644 --- a/webapp/src/main/resources/templates/userdata/my_bot_games.html +++ b/webapp/src/main/resources/templates/userdata/my_bot_games.html @@ -8,6 +8,7 @@ + diff --git a/webapp/src/main/resources/templates/userdata/my_games.html b/webapp/src/main/resources/templates/userdata/my_games.html index aa37db850..86908d7bb 100644 --- a/webapp/src/main/resources/templates/userdata/my_games.html +++ b/webapp/src/main/resources/templates/userdata/my_games.html @@ -8,6 +8,7 @@ + From 7d5f32d19f7277b04e28d5e97071bc15e9f1ce56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 08:13:48 +0000 Subject: [PATCH 05/12] Apply review feedback on pre-analyzed semantics and icon Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/3b7f8d74-735f-47ef-ba65-87e07f32ccb7 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../services/PlayerVsBotGameService.kt | 4 ++-- .../services/PlayerVsPlayerGameService.kt | 3 +-- .../services/admin/AdminFeedService.kt | 6 +++--- .../elephantchess/servicelayer/utils/ops/Ops.kt | 5 ----- .../services/PlayerVsPlayerGameServiceTest.kt | 16 ++++++++++++++++ .../public/js/userdata/my-game-item-utils.js | 2 +- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt index b3c596aea..4da992fa9 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt @@ -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.* @@ -28,7 +29,6 @@ import io.elephantchess.servicelayer.exceptions.NotFoundException import io.elephantchess.servicelayer.exceptions.RequestTimeoutException import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.services.ws.PvbWebSocketSession -import io.elephantchess.servicelayer.utils.ops.isPreAnalyzed import io.elephantchess.servicelayer.utils.modelToProcess import io.elephantchess.servicelayer.utils.ops.isNonStandardFen import io.elephantchess.servicelayer.utils.ops.launchAtFixedRate @@ -217,7 +217,7 @@ class PlayerVsBotGameService( status = record.gameStatus, outcome = record.outcome, moveIndex = record.currentHalfMoveIndex, - isPreAnalyzed = record.analysisStatus.isPreAnalyzed(), + isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt index cb7f07f1b..82de39db1 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt @@ -27,7 +27,6 @@ import io.elephantchess.servicelayer.exceptions.NotFoundException import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.services.ws.GamesToPlayWebSocketSession import io.elephantchess.servicelayer.services.ws.PlayerVsPlayerWebSocketSession -import io.elephantchess.servicelayer.utils.ops.isPreAnalyzed import io.elephantchess.servicelayer.utils.ops.launchAtFixedRate import io.elephantchess.servicelayer.utils.ops.ratingUpdate import io.elephantchess.utils.EloCalculator.calculateElo @@ -378,7 +377,7 @@ class PlayerVsPlayerGameService( outcome = gameRecord.userOutcome(userId), ratingFrom = ratingFrom, ratingTo = ratingTo, - isPreAnalyzed = gameRecord.analysisStatus.isPreAnalyzed(), + isPreAnalyzed = gameRecord.analysisStatus == AnalysisStatus.COMPLETED, created = gameRecord.created.toEpochMilliseconds(), lastUpdated = gameRecord.lastUpdated.toEpochMilliseconds(), numberOfMessages = numberOfMessages diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt index edb6789d4..5d246a2cb 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt @@ -8,13 +8,13 @@ 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.servicelayer.dto.admin.ListBotGamesResponse import io.elephantchess.servicelayer.dto.admin.ListGamesResponse import io.elephantchess.servicelayer.dto.admin.ListLastPuzzleByLoggedInUsersResponse import io.elephantchess.servicelayer.dto.admin.ListLastUserAnalysisResponse import io.elephantchess.servicelayer.services.UserCache -import io.elephantchess.servicelayer.utils.ops.isPreAnalyzed class AdminFeedService( private val analysisDaoService: AnalysisDaoService, @@ -88,7 +88,7 @@ class AdminFeedService( timeControlIncrement = record.timeControlIncrement, status = record.gameStatus, index = record.currentHalfMoveIndex, - isPreAnalyzed = record.analysisStatus.isPreAnalyzed(), + isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, winnerUserId = record.winnerUserId(), created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), @@ -122,7 +122,7 @@ class AdminFeedService( status = record.gameStatus, outcome = record.outcome, index = record.currentHalfMoveIndex, - isPreAnalyzed = record.analysisStatus.isPreAnalyzed(), + isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt index 1b3d7a47d..eaf27f9eb 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt @@ -7,7 +7,6 @@ import io.elephantchess.engines.process.EngineId import io.elephantchess.engines.process.FairyStockfishEngineId import io.elephantchess.engines.process.PikafishEngineId import io.elephantchess.engines.protocol.model.InfoLinesResult -import io.elephantchess.model.AnalysisStatus import io.elephantchess.servicelayer.dto.game.RatingUpdate import io.elephantchess.xiangqi.AbstractPieceType import io.elephantchess.xiangqi.Board @@ -16,10 +15,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging private val logger = KotlinLogging.logger("Ops") -fun AnalysisStatus.isPreAnalyzed(): Boolean { - return this == AnalysisStatus.PARTIALLY_COMPLETED || this == AnalysisStatus.COMPLETED -} - fun Game.ratingUpdate(): RatingUpdate? { return ratingUpdateRecord()?.let { record -> return RatingUpdate( diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index fc43627f5..f3d99889e 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -4,6 +4,7 @@ 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 @@ -510,6 +511,21 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { 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) diff --git a/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js b/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js index 21ddc2d8f..f9614af64 100644 --- a/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js +++ b/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js @@ -22,7 +22,7 @@ * @returns {HTMLDivElement} */ function buildPreAnalyzedIcon(gameId) { - const iconImg = buildImg('/images/icons/database.png', 'icon'); + const iconImg = buildImg('/images/icons/stats.png', 'icon'); iconImg.alt = 'This game is pre-analyzed'; iconImg.style.opacity = '75%'; From 75cd745e99ca2000d7303965bb6aec7d23a0f1eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 08:45:36 +0000 Subject: [PATCH 06/12] Fix flaky test signup collisions in service tests Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/22a923c9-936d-4ed9-9ae8-6744819cfe48 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../io/elephantchess/servicelayer/services/ServiceTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt index 43c0132f0..8fab3214a 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt @@ -56,7 +56,8 @@ abstract class ServiceTest : PostgresTest(), KoinComponent { suspend fun signUpTestUser(i: Int = RandomUtils.nextInt(1_000, 1_000_000)): Pair { val password = randomAlphanumeric(10) - val request = SignUpRequest("test$i", "test$i@gmail.com", password) + val uniqueSuffix = "$i-${randomAlphanumeric(6)}" + val request = SignUpRequest("test$uniqueSuffix", "test$uniqueSuffix@gmail.com", password) val either = userService.signUp(request) return request to either.right().userId } From 840bc5d17b29c1eb506abe0d7ecf63d305d4f078 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 08:46:57 +0000 Subject: [PATCH 07/12] Rename test signup identifier variable for clarity Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/22a923c9-936d-4ed9-9ae8-6744819cfe48 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../io/elephantchess/servicelayer/services/ServiceTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt index 8fab3214a..320471fff 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt @@ -56,8 +56,8 @@ abstract class ServiceTest : PostgresTest(), KoinComponent { suspend fun signUpTestUser(i: Int = RandomUtils.nextInt(1_000, 1_000_000)): Pair { val password = randomAlphanumeric(10) - val uniqueSuffix = "$i-${randomAlphanumeric(6)}" - val request = SignUpRequest("test$uniqueSuffix", "test$uniqueSuffix@gmail.com", password) + val testUserIdentifier = "$i-${randomAlphanumeric(6)}" + val request = SignUpRequest("test$testUserIdentifier", "test$testUserIdentifier@gmail.com", password) val either = userService.signUp(request) return request to either.right().userId } From 35275654535eeb9a818d2c8ca1f773ca748bad30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 09:14:07 +0000 Subject: [PATCH 08/12] Merge master and resolve PR conflicts Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/a015f768-92d0-4614-90d3-70e1952af177 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .github/dependabot.yml | 2 +- CONTRIBUTING.md | 9 + README.md | 64 +- build.gradle | 388 ---- build.gradle.kts | 393 ++++ .../io/elephantchess/engines/EnginePool.kt | 15 +- .../engines/process/EngineProcess.kt | 9 + .../process/FairyStockfishEngineProcess.kt | 10 + .../engines/process/PikafishEngineProcess.kt | 7 + gradle.properties | 18 - gradle/libs.versions.toml | 90 + gradle/wrapper/gradle-wrapper.properties | 2 +- jooq-type-mappings.txt | 2 + .../scripts/CreateVietnameseTestUsers.kt | 60 + .../scripts/game/CreateListOfGamesToJoin.kt | 104 +- .../io/elephantchess/scripts/game/Utils.kt | 11 +- .../scripts/manchu/GenerateManchuGames.kt | 144 ++ settings.gradle | 16 - settings.gradle.kts | 23 + utils/src/main/resources/bad-words.txt | 2 + .../liquibase-changelog-generation.xml | 104 + .../main/resources/liquibase-changelog.xml | 124 ++ .../codegen/OffsetDateTimeInstantConverter.kt | 2 +- .../db/model/UserSessionRecord.kt | 1 + .../GameChatTypingStatusDaoService.kt | 50 + .../db/services/MoveAnalysisDaoService.kt | 9 +- .../db/services/PlayerVsBotGameDaoService.kt | 77 +- .../services/PlayerVsPlayerGameDaoService.kt | 107 +- .../db/services/PuzzleResultDaoService.kt | 10 +- .../db/services/ReferenceGameDaoService.kt | 132 +- .../db/services/ReferencePlayerDaoService.kt | 10 +- .../db/services/UserDaoService.kt | 78 +- .../db/services/UserSessionDaoService.kt | 40 + .../io/elephantchess/db/utils/DbUtils.kt | 4 +- .../io/elephantchess/db/utils/JooqOps.kt | 10 +- .../io/elephantchess/db/utils/RecordOps.kt | 31 +- .../KtorHtmlBuilderTagResolver.kt | 20 + .../KtorHtmlBuilderTagResolverTest.kt | 34 + .../elephantchess/servicelayer/KoinModule.kt | 1 + .../dto/admin/ListBotGamesResponse.kt | 2 + .../dto/admin/ListGamesResponse.kt | 4 +- .../dto/botgame/CreateBotGameRequest.kt | 2 + .../dto/botgame/GetBotGameDataResponse.kt | 2 + .../dto/botgame/ListUserBotGamesDto.kt | 2 + .../dto/database/DatabaseGameSummary.kt | 24 + .../dto/database/MyDbSearchesResponse.kt | 19 + .../dto/game/CreateGameRequest.kt | 2 + .../dto/game/GetGameDataResponse.kt | 2 + .../dto/game/ListUserGamesResponse.kt | 6 +- .../dto/gamedata/GameMetadataDto.kt | 2 + .../dto/global/GlobalGameStatsResponse.kt | 1 + .../dto/user/DeleteUserSessionsRequest.kt | 3 + .../dto/user/DeleteUserSessionsResponse.kt | 3 + .../dto/user/EmailAddressSettingsResponse.kt | 2 +- .../dto/user/EmailValidityStatus.kt | 43 + .../servicelayer/dto/user/SignUpRequest.kt | 3 +- .../dto/user/UserSessionsSettingsResponse.kt | 19 + .../servicelayer/dto/ws/GameToPlay.kt | 4 +- .../dto/ws/PlayerVsPlayerInput.kt | 3 +- .../dto/ws/PlayerVsPlayerUpdate.kt | 7 + .../servicelayer/services/DatabaseService.kt | 96 +- .../servicelayer/services/GameDataService.kt | 188 +- .../services/GlobalAnalyticsService.kt | 3 +- .../servicelayer/services/MailService.kt | 71 +- .../services/PlayerVsBotGameService.kt | 75 +- .../services/PlayerVsPlayerGameService.kt | 111 +- .../servicelayer/services/PuzzleService.kt | 2 + .../servicelayer/services/UserService.kt | 159 +- .../services/admin/AdminFeedService.kt | 6 +- .../EmailConfirmationLinkTagResolver.kt | 19 + .../services/resolvers/ResolverUtils.kt | 6 +- .../ws/PlayerVsPlayerWebSocketSession.kt | 8 +- .../servicelayer/utils/ops/Ops.kt | 9 +- .../mail_templates/email_confirmation.html | 11 + .../mail_templates/new_user_notification.html | 3 +- .../services/PlayerVsPlayerGameServiceTest.kt | 235 ++- .../servicelayer/services/PostgresTest.kt | 27 +- .../servicelayer/services/ServiceTest.kt | 27 +- .../servicelayer/services/ShardingTest.kt | 4 +- .../servicelayer/services/UserServiceTest.kt | 469 ++++- .../services/resolvers/ResolverUtilsTest.kt | 17 + .../elephantchess/webapp/WebAppKoinModule.kt | 3 + .../webapp/ops/ValidationRoutingOps.kt | 9 + .../rendering/BoardGuiExampleRenderer.kt | 7 +- .../webapp/rendering/ChangelogPageRenderer.kt | 58 + .../webapp/rendering/DatabasePageRenderer.kt | 178 +- .../webapp/rendering/DatabaseTagResolvers.kt | 8 +- .../webapp/rendering/FaqPageRenderer.kt | 55 + .../webapp/rendering/GamePageRenderer.kt | 104 + .../webapp/rendering/TagResolvers.kt | 40 +- .../rendering/UserProfilePageRenderer.kt | 48 +- ...icationRoutes.kt => EmailSettingRoutes.kt} | 29 +- .../webapp/routing/api/DatabaseRouting.kt | 6 + .../webapp/routing/api/GameDataRouting.kt | 21 +- .../webapp/routing/api/UserRouting.kt | 43 +- .../routing/html/HtmlDatabaseRouting.kt | 6 + .../webapp/routing/html/HtmlRouting.kt | 72 +- .../webapp/server/ApiServiceRoutingModule.kt | 1 - .../webapp/sitemap/SiteMapService.kt | 7 + webapp/src/main/resources/logback.xml | 1 + .../main/resources/modals/import-moves.html | 2 +- .../src/main/resources/modals/new-game.html | 22 + .../src/main/resources/modals/play-bot.html | 235 +-- webapp/src/main/resources/modals/signup.html | 6 + .../main/resources/public/css/about-faq.css | 62 + .../public/css/admin/admin-console.css | 2 +- .../src/main/resources/public/css/board.css | 4 + .../public/css/common/nice-table.css | 88 + .../css/database/database-game-viewer.css | 18 +- .../public/css/database/database.css | 51 +- .../public/css/how-to-play-xiangqi.css | 81 + .../src/main/resources/public/css/lobby.css | 306 ++- .../resources/public/css/my-db-searches.css | 55 + .../main/resources/public/css/my-games.css | 4 +- .../public/css/my-user-data-items.css | 66 +- .../resources/public/css/new-game-modal.css | 2 +- .../resources/public/css/play-bot-modal.css | 133 +- .../resources/public/css/player-vs-player.css | 48 +- .../resources/public/css/style-reactive.css | 107 +- .../src/main/resources/public/css/style.css | 269 ++- .../simple-board.js => css/test-board.css} | 13 +- .../resources/public/css/user-profile.css | 14 +- .../resources/public/css/user-settings.css | 65 + .../resources/public/dist/0.0.3/board-gui.js | 1735 +++++++++++++++ .../resources/public/dist/0.0.3/board.css | 414 ++++ .../resources/public/dist/0.0.3/xiangqi.js | 1405 +++++++++++++ .../resources/public/dist/0.0.4/board-gui.js | 1855 ++++++++++++++++ .../resources/public/dist/0.0.4/board.css | 418 ++++ .../resources/public/dist/0.0.4/xiangqi.js | 1405 +++++++++++++ .../resources/public/dist/0.1.0/board-gui.js | 1870 +++++++++++++++++ .../resources/public/dist/0.1.0/board.css | 418 ++++ .../resources/public/dist/0.1.0/xiangqi.js | 1470 +++++++++++++ .../public/images/icons/data-search.png | Bin 0 -> 22139 bytes .../resources/public/images/icons/dice.png | Bin 0 -> 16641 bytes .../resources/public/images/icons/link.png | Bin 0 -> 11789 bytes .../resources/public/images/icons/more.png | Bin 0 -> 4637 bytes .../public/images/icons/os/android.png | Bin 0 -> 12824 bytes .../public/images/icons/os/apple.png | Bin 0 -> 12650 bytes .../public/images/icons/os/linux.png | Bin 0 -> 39186 bytes .../public/images/icons/os/windows.png | Bin 0 -> 6027 bytes .../resources/public/images/icons/sound.png | Bin 0 -> 11475 bytes .../resources/public/images/icons/sunbed.png | Bin 0 -> 10501 bytes .../resources/public/images/icons/sunset.png | Bin 0 -> 27479 bytes .../public/images/icons/trophy-football.png | Bin 0 -> 16871 bytes .../resources/public/js/about/changelog.js | 44 + .../public/js/admin/admin-feeds-dto.js | 20 +- .../resources/public/js/admin/admin-feeds.js | 15 +- .../js/analysis-board/analysis-board.js | 36 +- .../js/analysis-board/analysis-cache.js | 1 + .../analysis-board/engine-analysis-widget.js | 61 + .../main/resources/public/js/browse-games.js | 34 +- .../resources/public/js/browse-pvp-games.js | 36 +- .../js/database/database-game-viewer.js | 177 +- .../public/js/database/database-search.js | 202 +- .../main/resources/public/js/global/global.js | 2 + .../public/js/how-to-play-xiangqi-page.js | 178 ++ .../public/js/infinite-scroll-page.js | 8 +- .../resources/public/js/lobby/lobby-client.js | 2 +- .../main/resources/public/js/lobby/lobby.js | 184 +- .../modal-handlers/new-game-modal-handler.js | 25 +- .../modal-handlers/play-bot-modal-handler.js | 210 +- .../js/modal-handlers/signup-modal-handler.js | 5 + .../main/resources/public/js/modules/enums.js | 5 + .../public/js/modules/generic-game-data.js | 11 +- .../public/js/modules/parser-common.js | 1 + .../public/js/modules/parser-iccs.js | 51 + .../js/modules/time-control-categories.js | 2 + .../main/resources/public/js/modules/ui.js | 41 +- .../main/resources/public/js/modules/user.js | 2 +- .../resources/public/js/modules/xiangqi.js | 67 +- .../player-vs-bot/player-vs-bot-controller.js | 9 +- .../js/player-vs-bot/player-vs-bot-dto.js | 16 + .../js/player-vs-bot/player-vs-bot-page.js | 34 +- .../player-vs-player-controller.js | 16 +- .../player-vs-player/player-vs-player-dto.js | 18 +- .../player-vs-player/player-vs-player-page.js | 91 +- .../main/resources/public/js/test-board.js | 52 + .../js/user-profile/user-profile-games.js | 98 + .../public/js/user-profile/user-profile.js | 8 + .../js/user-settings/user-sessions-page.js | 82 + .../js/user-settings/user-sessions-widget.js | 277 +++ .../public/js/user-settings/user-settings.js | 58 +- .../public/js/userdata/my-bot-games-dto.js | 9 + .../public/js/userdata/my-bot-games.js | 12 +- .../public/js/userdata/my-db-searches-dto.js | 108 + .../public/js/userdata/my-db-searches.js | 203 ++ .../public/js/userdata/my-games-dto.js | 22 + .../resources/public/js/userdata/my-games.js | 32 +- .../js/widgets/analysis-summary-report.js | 54 +- .../public/js/widgets/apex-chart-helper.js | 12 +- .../resources/public/js/widgets/board-gui.js | 292 ++- .../public/js/widgets/chat-box-widget.js | 103 + .../js/widgets/database-autocompletion.js | 7 + .../public/js/widgets/drop-down-menu.js | 96 +- .../public/js/widgets/eval-line-chart.js | 170 ++ .../resources/public/js/widgets/game-thumb.js | 21 +- .../public/js/widgets/move-tree-widget.js | 78 +- .../public/js/widgets/settings-manager.js | 471 ++++- .../js/widgets/user-turn-to-play-indicator.js | 13 +- .../resources/public/js/ws/games-to-play.js | 9 + .../main/resources/templates/about/about.html | 95 +- .../resources/templates/about/changelog.html | 426 +++- .../about/developers/board-gui-example.html | 23 +- .../main/resources/templates/about/faq.html | 41 +- .../templates/admin/admin_feeds.html | 2 + .../resources/templates/analysis_board.html | 2 + .../resources/templates/browse_pvb_games.html | 5 +- .../resources/templates/browse_pvp_games.html | 5 +- .../database/browse_db_event_games.html | 3 +- .../database/browse_db_player_games.html | 3 +- .../templates/database/database_event.html | 2 +- .../database/database_events_list.html | 1 + .../database/database_game_viewer.html | 14 +- .../templates/database/database_player.html | 1 - .../database/database_players_list.html | 1 + .../templates/database/database_search.html | 1 - .../email_confirmation_error.html | 20 + .../email_confirmation/email_confirmed.html | 15 + .../src/main/resources/templates/global.html | 4 + .../templates/how_to_play_xiangqi.html | 212 ++ .../src/main/resources/templates/lobby.html | 28 +- .../resources/templates/player_vs_bot.html | 7 +- .../templates/player_vs_player_game.html | 13 +- .../seven_kingdoms/seven_kingdoms_about.html | 2 +- .../seven_kingdoms/seven_kingdoms_game.html | 1 - .../seven_kingdoms_playground.html | 2 +- .../resources/templates/simple_board.html | 39 - .../main/resources/templates/test_board.html | 66 + .../unsubscribe/unsubscribed_from_all.html | 2 +- .../unsubscribed_from_newsletter.html | 2 +- .../templates/user_browse_pvp_games.html | 32 + .../resources/templates/user_profile.html | 19 +- .../resources/templates/user_sessions.html | 50 + .../resources/templates/user_settings.html | 62 +- .../templates/userdata/my_analysis.html | 1 - .../templates/userdata/my_bot_games.html | 1 - .../templates/userdata/my_db_searches.html | 32 + .../templates/userdata/my_games.html | 1 - .../templates/userdata/my_played_puzzles.html | 1 - .../analysis_summary_report.html | 1 + .../main/resources/web_fragments/base_js.html | 1 + .../resources/web_fragments/board_js.html | 2 +- .../resources/web_fragments/chat_box.html | 1 + .../faq_import_moves_formats_examples.html | 21 + .../main/resources/web_fragments/footer.html | 8 +- .../resources/web_fragments/game_thumb.html | 25 +- .../web_fragments/head_description.txt | 2 +- .../resources/web_fragments/head_keywords.txt | 6 + .../logged_as_guest_message.html | 11 +- .../web_fragments/settings_info_box.html | 207 +- .../main/resources/web_fragments/top_bar.html | 2 +- .../webapp/rendering/GamePageRendererTest.kt | 72 + .../testutils/ManchuGameMovesDtoCache.kt | 26 + .../src/main/resources/manchu.txt | 1000 +++++++++ .../xiangqi/AbstractPieceType.kt | 8 +- .../kotlin/io/elephantchess/xiangqi/Board.kt | 53 +- .../io/elephantchess/xiangqi/Variant.kt | 35 + 257 files changed, 21891 insertions(+), 1971 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml create mode 100644 scripts/src/main/kotlin/io/elephantchess/scripts/CreateVietnameseTestUsers.kt create mode 100644 scripts/src/main/kotlin/io/elephantchess/scripts/manchu/GenerateManchuGames.kt delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts create mode 100644 webapp-dao/src/main/kotlin/io/elephantchess/db/services/GameChatTypingStatusDaoService.kt create mode 100644 webapp-html-renderer/src/main/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolver.kt create mode 100644 webapp-html-renderer/src/test/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolverTest.kt create mode 100644 webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabaseGameSummary.kt create mode 100644 webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/MyDbSearchesResponse.kt create mode 100644 webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsRequest.kt create mode 100644 webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsResponse.kt create mode 100644 webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailValidityStatus.kt create mode 100644 webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/UserSessionsSettingsResponse.kt create mode 100644 webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/EmailConfirmationLinkTagResolver.kt create mode 100644 webapp-service-layer/src/main/resources/mail_templates/email_confirmation.html create mode 100644 webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtilsTest.kt create mode 100644 webapp/src/main/kotlin/io/elephantchess/webapp/rendering/ChangelogPageRenderer.kt create mode 100644 webapp/src/main/kotlin/io/elephantchess/webapp/rendering/FaqPageRenderer.kt create mode 100644 webapp/src/main/kotlin/io/elephantchess/webapp/rendering/GamePageRenderer.kt rename webapp/src/main/kotlin/io/elephantchess/webapp/routing/{EmailNotificationRoutes.kt => EmailSettingRoutes.kt} (69%) create mode 100644 webapp/src/main/resources/public/css/common/nice-table.css create mode 100644 webapp/src/main/resources/public/css/how-to-play-xiangqi.css create mode 100644 webapp/src/main/resources/public/css/my-db-searches.css rename webapp/src/main/resources/public/{js/simple-board.js => css/test-board.css} (74%) create mode 100644 webapp/src/main/resources/public/dist/0.0.3/board-gui.js create mode 100644 webapp/src/main/resources/public/dist/0.0.3/board.css create mode 100644 webapp/src/main/resources/public/dist/0.0.3/xiangqi.js create mode 100644 webapp/src/main/resources/public/dist/0.0.4/board-gui.js create mode 100644 webapp/src/main/resources/public/dist/0.0.4/board.css create mode 100644 webapp/src/main/resources/public/dist/0.0.4/xiangqi.js create mode 100644 webapp/src/main/resources/public/dist/0.1.0/board-gui.js create mode 100644 webapp/src/main/resources/public/dist/0.1.0/board.css create mode 100644 webapp/src/main/resources/public/dist/0.1.0/xiangqi.js create mode 100644 webapp/src/main/resources/public/images/icons/data-search.png create mode 100644 webapp/src/main/resources/public/images/icons/dice.png create mode 100644 webapp/src/main/resources/public/images/icons/link.png create mode 100644 webapp/src/main/resources/public/images/icons/more.png create mode 100644 webapp/src/main/resources/public/images/icons/os/android.png create mode 100644 webapp/src/main/resources/public/images/icons/os/apple.png create mode 100644 webapp/src/main/resources/public/images/icons/os/linux.png create mode 100644 webapp/src/main/resources/public/images/icons/os/windows.png create mode 100644 webapp/src/main/resources/public/images/icons/sound.png create mode 100644 webapp/src/main/resources/public/images/icons/sunbed.png create mode 100644 webapp/src/main/resources/public/images/icons/sunset.png create mode 100644 webapp/src/main/resources/public/images/icons/trophy-football.png create mode 100644 webapp/src/main/resources/public/js/about/changelog.js create mode 100644 webapp/src/main/resources/public/js/how-to-play-xiangqi-page.js create mode 100644 webapp/src/main/resources/public/js/modules/parser-iccs.js create mode 100644 webapp/src/main/resources/public/js/test-board.js create mode 100644 webapp/src/main/resources/public/js/user-profile/user-profile-games.js create mode 100644 webapp/src/main/resources/public/js/user-settings/user-sessions-page.js create mode 100644 webapp/src/main/resources/public/js/user-settings/user-sessions-widget.js create mode 100644 webapp/src/main/resources/public/js/userdata/my-db-searches-dto.js create mode 100644 webapp/src/main/resources/public/js/userdata/my-db-searches.js create mode 100644 webapp/src/main/resources/public/js/widgets/eval-line-chart.js create mode 100644 webapp/src/main/resources/templates/email_confirmation/email_confirmation_error.html create mode 100644 webapp/src/main/resources/templates/email_confirmation/email_confirmed.html create mode 100644 webapp/src/main/resources/templates/how_to_play_xiangqi.html delete mode 100644 webapp/src/main/resources/templates/simple_board.html create mode 100644 webapp/src/main/resources/templates/test_board.html create mode 100644 webapp/src/main/resources/templates/user_browse_pvp_games.html create mode 100644 webapp/src/main/resources/templates/user_sessions.html create mode 100644 webapp/src/main/resources/templates/userdata/my_db_searches.html create mode 100644 webapp/src/test/kotlin/io/elephantchess/webapp/rendering/GamePageRendererTest.kt create mode 100644 xiangqi-core-test-utils/src/main/kotlin/io/elephantchess/xiangqi/testutils/ManchuGameMovesDtoCache.kt create mode 100644 xiangqi-core-test-utils/src/main/resources/manchu.txt create mode 100644 xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Variant.kt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a86c77c96..dff8e7ce3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: labels: - "dependencies" ignore: - # Netty version is pinned to match io.ktor:ktor-server-netty in build.gradle. + # Netty version is pinned to match io.ktor:ktor-server-netty in build.gradle.kts. # Bump it manually when upgrading Ktor. - dependency-name: "io.netty:*" groups: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..4f91c0672 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +So far I'm the only one working on this project. It was just recently made public. So I'm not sure yet how the +contribution process might look like in the future. + +If you want to contribute, best is to first join our [Discord server](https://discord.gg/WEGDqnWXNg) to discuss your +ideas. + +The current [roadmap](https://github.com/users/benckx/projects/2/views/1) reflects the current priorities of the +project. Feel free to create issues to report bugs or request feature changes. Feel free to comment and give your +opinion the existing issues, or even to fork and open MRs. diff --git a/README.md b/README.md index 765185d6b..5babf70ab 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,14 @@ [elephantchess.io](https://elephantchess.io) is a web application to play and study [Chinese chess](https://en.wikipedia.org/wiki/Xiangqi) (or xiangqi 象棋). -Feel free to create issues to report bugs or request feature changes. The -current [roadmap](https://github.com/users/benckx/projects/2/views/1) on this repo reflects the current priorities of -the project. Feel free to comment and give your opinion the existing issues. - By default, the project is under GPL-3.0 license. Libraries (like the Kotlin xiangqi-core or the JavaScript [board-gui](https://elephantchess.io/about/developers/board-gui-example)) are under LGPL-3.0 license to allow for a more permissive use (i.e. to re-use the libraries in a commercial application). We have a little [Discord server](https://discord.gg/WEGDqnWXNg) for open discussion. +[![Build](https://github.com/benckx/elephantchess/actions/workflows/build.yml/badge.svg)](https://github.com/benckx/elephantchess/actions/workflows/build.yml) + ## Features The webapp offers the following features: @@ -25,6 +23,9 @@ The webapp offers the following features: try to specify when we talk about the "PostgreSQL database" or the "Database" as a feature. - Analysis board, where users can create complex analysis, with engine evaluation, multiple embedded variations, annotations (i.e. `??`, `?!` symbols) and comments. +- Manchu chess variant (also called Yitong): Black's pieces are set up and move the same as in xiangqi, but horses, + cannons, and one of the chariots are absent for Red. The remaining chariot has the combined powers of the chariot, + horse, and cannon. - By default, users are assigned a temporary guest user id. They can also sign up with an email and password to keep their data across devices and access all features. @@ -42,8 +43,8 @@ The webapp offers the following features: ## Glossary -- The engines are AI (or bots) used for the Play-against-bot (PvB) or Analysis Board features. In the chess communinity, - the term "engine" is prefered over AI or bot. +- The engines are AI (or bots) used for the Play-against-bot (PvB) or Analysis Board features. In the chess community, + the term "engine" is preferred over AI or bot. - In chess in general, the "columns" of the board are named "files" and the "rows" are named "ranks". - The Forsyth–Edwards Notation (or FEN) is a standard notation to represent chess positions as a single line of text. In Chinese chess, the starting position is encoded as @@ -60,17 +61,10 @@ The webapp offers the following features: from that position with a search depth of 10. - The UCI move notation is similar to the algebraic notation, except that the ranks are 0-based. Therefore, any move can be encoded as 4 characters. For example, move h3e3 is encoded in UCI as h2e2. -- The WXF notation is the traditional Chinese chess notation (e.g. `C2=5`), widely in use. It's not as convinient - technically because move can be ambiguous and the files are numbered from the right to the left relative to the player - and (so it's not the same numbers for the red and black players). Therefore it's not used in code. - -## Contribute - -So far I'm the only one working on this project. It was just recently made public. So I'm not sure yet how the -contribution process might look like. - -Best is to join our [Discord server](https://discord.gg/WEGDqnWXNg) to discuss first how you would like to contribute, -or to open or comment on the GitHub issues. +- The WXF notation is the traditional Chinese chess notation (e.g. `C2=5`), widely in use. It's not as convenient + technically because move can be ambiguous and the files are numbered from the right to the left relative to the + player (so it's not the same numbers for the red and black players). Therefore, it's not used in code, expect for + labeling moves in the GUI. # Run Locally @@ -255,22 +249,23 @@ Except for KTor, Koin, a Kubernetes client, Apache Commons, the project has few ## Gradle Modules -If you check the `settings.gradle` file, you will see that the project is made of several modules: +If you check the `settings.gradle.kts` file, you will see that the project is made of several modules: ``` -include('utils') -include('engine-api') -include('xiangqi-core') -include('xiangqi-core-test-utils') -include('seven-kingdoms-core') -include('seven-kingdoms-core-test-utils') -include('webapp-config') -include('webapp-dao') -include('webapp-dao-migration') -include('webapp-html-renderer') -include('webapp-model-common') -include('webapp-service-layer') -include('webapp') +include("utils") +include("engine-api") +include("xiangqi-core") +include("xiangqi-core-test-utils") +include("seven-kingdoms-core") +include("seven-kingdoms-core-test-utils") +include("webapp-config") +include("webapp-dao") +include("webapp-dao-migration") +include("webapp-html-renderer") +include("webapp-model-common") +include("webapp-service-layer") +include("webapp") +include("scripts") ``` ### utils @@ -516,7 +511,8 @@ best move: h2e2 ### xiangqi-core -Kotlin library providing a representation of a Chinese chess board. +Kotlin library providing a representation of a Chinese chess board. Supports both standard Xiangqi and the Manchu +chess (Yitong) variant. #### Example 1 @@ -588,7 +584,7 @@ development. [![](https://www.jitpack.io/v/benckx/elephantchess.svg)](https://www.jitpack.io/#benckx/elephantchess) -At the moment, you can use the libraries via JitPack. You only need to add the JitPack repository to your build.gradle +At the moment, you can use the libraries via JitPack. You only need to add the JitPack repository to your build file: ```Groovy @@ -692,7 +688,7 @@ Under `webapp/src/main/resources/public/js` there's usually a sub-folder for eac - etc. Complex app like PvP is usually organized with a **page** which updates the GUI, a **controller** which connects to -WebSockets and/or calls REST endpoints, and sometimes a DTO file +WebSockets and/or calls REST endpoints, sometimes a DTO file and/or a separate REST client file. ## JavaScript Libraries diff --git a/build.gradle b/build.gradle deleted file mode 100644 index f0492bc69..000000000 --- a/build.gradle +++ /dev/null @@ -1,388 +0,0 @@ -import liquibase.Contexts -import liquibase.Liquibase -import liquibase.database.core.H2Database -import liquibase.database.jvm.JdbcConnection -import liquibase.resource.DirectoryResourceAccessor -import org.h2.Driver -import org.jooq.codegen.GenerationTool -import org.jooq.meta.jaxb.* - -import java.sql.Connection -import java.sql.Statement - -buildscript { - repositories { - mavenCentral() - google() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath "org.jooq:jooq-codegen:$jooqVersion" - classpath "com.h2database:h2:$h2Version" - classpath "org.liquibase:liquibase-core:$liquibaseVersion" - } -} - -plugins { - id "com.github.ben-manes.versions" version "0.54.0" - id "org.jetbrains.kotlin.jvm" version "$kotlinVersion" - id "com.adarshr.test-logger" version "4.0.0" - id "org.jetbrains.kotlin.plugin.serialization" version "$kotlinVersion" -} - -allprojects { - apply plugin: 'kotlin' - apply plugin: 'com.adarshr.test-logger' - apply plugin: 'org.jetbrains.kotlin.plugin.serialization' - - kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of("21")) - } - } - - repositories { - google() - mavenCentral() - mavenLocal() - } - - dependencies { - implementation "io.github.oshai:kotlin-logging-jvm:8.0.02" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" - implementation "ch.qos.logback:logback-classic:$logbackVersion" - testImplementation "org.jetbrains.kotlin:kotlin-test" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion" - testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoVersion" - testImplementation "org.junit.jupiter:junit-jupiter-params:6.0.3" - } - - def nettyVersion = '4.2.12.Final' // aligned with io.ktor:ktor-server-netty - configurations.configureEach { - resolutionStrategy { - force "org.apache.commons:commons-lang3:$commonLang3Version" - force 'org.checkerframework:checker-qual:4.1.0' - force "io.netty:netty-buffer:$nettyVersion" - force "io.netty:netty-codec:$nettyVersion" - force "io.netty:netty-codec-base:$nettyVersion" - force "io.netty:netty-codec-compression:$nettyVersion" - force "io.netty:netty-codec-dns:$nettyVersion" - force "io.netty:netty-codec-http:$nettyVersion" - force "io.netty:netty-codec-http2:$nettyVersion" - force "io.netty:netty-codec-socks:$nettyVersion" - force "io.netty:netty-common:$nettyVersion" - force "io.netty:netty-handler:$nettyVersion" - force "io.netty:netty-handler-proxy:$nettyVersion" - force "io.netty:netty-resolver:$nettyVersion" - force "io.netty:netty-resolver-dns:$nettyVersion" - force "io.netty:netty-transport:$nettyVersion" - force "io.netty:netty-transport-classes-epoll:$nettyVersion" - force "io.netty:netty-transport-classes-kqueue:$nettyVersion" - force "io.netty:netty-transport-native-epoll:$nettyVersion" - force "io.netty:netty-transport-native-kqueue:$nettyVersion" - force "io.netty:netty-transport-native-unix-common:$nettyVersion" -// force "com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion" -// force "com.fasterxml.jackson.core:jackson-core:$jacksonVersion" - } - } - - test { - useJUnitPlatform() - minHeapSize = '1G' - maxHeapSize = '1G' - failOnNoDiscoveredTests = false - } -} - -tasks.register('dao-code-gen') { - doLast { - Connection conn = new Driver().connect("jdbc:h2:mem:test", null) - - Statement stmt = conn.createStatement() - stmt.execute("drop all OBJECTS") - stmt.execute("set schema PUBLIC") - stmt.close() - - def resourceAccessor = new DirectoryResourceAccessor(new File("${project.projectDir}/webapp-dao-migration/src/main/resources")) - def db = new H2Database() - db.setConnection(new JdbcConnection(conn)) - - def liquibase = new Liquibase("liquibase-changelog-generation.xml", resourceAccessor, db) - liquibase.update(new Contexts()) - conn.commit() - - def jdbc = new Jdbc() - .withDriver('org.h2.Driver') - .withUrl('jdbc:h2:mem:test') - - // mostly to use replace string field by enums - Map forcedTypeMap = [:] - new File('jooq-type-mappings.txt').eachLine { line -> - def parts = line.split(',') - def columnName = parts[0].trim() - def typeName = parts[1].trim() - forcedTypeMap.put(columnName, typeName) - } - - def forcedTypes = forcedTypeMap.collect { key, value -> - return new ForcedType() - .withIncludeExpression(key) - .withUserType(value) - .withEnumConverter(true) - } - - // Map every TIMESTAMP WITH TIME ZONE column to kotlin.time.Instant via a custom converter - // so generated POJOs/DAOs carry Instant fields instead of java.time.OffsetDateTime. - forcedTypes << new ForcedType() - .withIncludeTypes("(?i:TIMESTAMP\\s+WITH\\s+TIME\\s+ZONE|TIMESTAMP\\([0-9]+\\)\\s+WITH\\s+TIME\\s+ZONE|TIMESTAMPTZ)") - .withUserType("kotlin.time.Instant") - .withConverter("io.elephantchess.db.codegen.OffsetDateTimeInstantConverter") - - def database = new Database() - .withExcludes("DATABASECHANGELOG|DATABASECHANGELOGLOCK") - .withInputSchema("PUBLIC") - .withForcedTypes(forcedTypes) - - def generate = new Generate() - .withPojos(true) - .withDaos(true) // TODO: remove records - - def target = new Target() - .withPackageName('io.elephantchess.db.dao.codegen') - .withDirectory("${project.projectDir}/webapp-dao/build/jooq") - - def generator = new Generator() - .withDatabase(database) - .withGenerate(generate) - .withTarget(target) - - GenerationTool.generate(new Configuration().withJdbc(jdbc).withGenerator(generator)) - - conn.close() - } -} - -compileKotlin.dependsOn(tasks."dao-code-gen") - -// modules that are published as libraries (e.g. via JitPack) -def publishableModules = [ - 'engine-api', - 'xiangqi-core', - 'xiangqi-core-test-utils', - 'seven-kingdoms-core', - 'seven-kingdoms-core-test-utils', -] -configure(publishableModules.collect { project(":$it") }) { - apply plugin: 'maven-publish' - - group = 'io.elephantchess' - version = rootProject.hasProperty('publishVersion') ? rootProject.publishVersion : '1.0.0-SNAPSHOT' - - java { - withJavadocJar() - withSourcesJar() - } - - publishing { - publications { - maven(MavenPublication) { - from components.java - } - } - } -} - -project(":engine-api") { - dependencies { - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" - testImplementation project(":xiangqi-core") - } -} - -project(":xiangqi-core") { - dependencies { - testImplementation project(":xiangqi-core-test-utils") - } -} - -project(":xiangqi-core-test-utils") { - // no project dependencies; only relies on common deps from allprojects -} - -project(":seven-kingdoms-core") { - dependencies { - testImplementation project(":seven-kingdoms-core-test-utils") - } -} - -project(":seven-kingdoms-core-test-utils") { - dependencies { - implementation project(":seven-kingdoms-core") - api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" - api "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion" - } -} - -project(":webapp-dao-migration") { - dependencies { - implementation "org.liquibase:liquibase-core:$liquibaseVersion" - implementation "org.postgresql:postgresql:$postgresVersion" // JDBC driver - } -} - -project(":webapp-dao") { - sourceSets { - main { - java { - srcDirs "$buildDir/jooq" - } - } - } - - dependencies { - implementation project(":utils") - implementation project(":webapp-dao-migration") - implementation project(":seven-kingdoms-core") - implementation project(":webapp-config") - implementation project(":webapp-model-common") - implementation project(":xiangqi-core") - implementation project(":engine-api") - implementation "org.jooq:jooq:$jooqVersion" - implementation "org.jooq:jooq-kotlin:$jooqVersion" - implementation "org.jooq:jooq-kotlin-coroutines:$jooqVersion" - implementation "org.liquibase:liquibase-core:$liquibaseVersion" // needed to pass ResourceAccessor - implementation "org.postgresql:r2dbc-postgresql:1.1.1.RELEASE" - implementation "io.r2dbc:r2dbc-pool:1.0.2.RELEASE" - } -} - -project(":webapp-html-renderer") { - dependencies { - implementation project(":utils") - implementation "org.jsoup:jsoup:$jsoupVersion" - } -} - -project(":webapp-service-layer") { - dependencies { - api project(":utils") - api project(":webapp-config") - api project(":webapp-model-common") - api project(":webapp-html-renderer") - implementation project(":seven-kingdoms-core") - implementation project(":webapp-dao") - api "io.insert-koin:koin-core:$koinCoreVersion" - api project(":xiangqi-core") - api project(":engine-api") - api "org.apache.commons:commons-lang3:$commonLang3Version" - implementation "org.jsoup:jsoup:$jsoupVersion" - implementation "commons-validator:commons-validator:1.10.1" - implementation "com.auth0:java-jwt:4.5.2" - implementation "com.sun.mail:javax.mail:$javaxMailVersion" - implementation "io.github.reactivecircus.cache4k:cache4k:$cache4kVersion" - implementation "io.fabric8:kubernetes-client:7.6.1" - implementation "org.jooq:jooq:$jooqVersion" - implementation "io.ktor:ktor-client-core:$kTorVersion" - implementation "io.ktor:ktor-client-cio:$kTorVersion" - implementation "io.ktor:ktor-client-content-negotiation:$kTorVersion" - implementation "io.ktor:ktor-client-logging:$kTorVersion" - implementation "io.ktor:ktor-serialization-jackson:$kTorVersion" - implementation "io.ktor:ktor-serialization-kotlinx-json:$kTorVersion" - testImplementation project(":xiangqi-core-test-utils") - testImplementation project(":seven-kingdoms-core-test-utils") - testImplementation "org.apache.commons:commons-rng-simple:1.7" - testImplementation "org.apache.commons:commons-text:1.15.0" - testImplementation "com.h2database:h2:$h2Version" - testImplementation "org.liquibase:liquibase-core:$liquibaseVersion" - testImplementation "org.testcontainers:postgresql:$testContainerVersion" - } -} - -project(":webapp-config") { - dependencies { - implementation "commons-cli:commons-cli:1.11.0" - } -} - -project(":webapp") { - dependencies { - implementation project(":webapp-service-layer") - implementation "io.ktor:ktor-server-core:$kTorVersion" - implementation "io.ktor:ktor-server-netty:$kTorVersion" - implementation "io.ktor:ktor-server-status-pages:$kTorVersion" - implementation "io.ktor:ktor-server-default-headers:$kTorVersion" - implementation "io.ktor:ktor-server-content-negotiation:$kTorVersion" - implementation "io.ktor:ktor-server-caching-headers:$kTorVersion" - implementation "io.ktor:ktor-serialization-jackson:$kTorVersion" - implementation "io.ktor:ktor-serialization-kotlinx-json:$kTorVersion" - implementation "io.ktor:ktor-server-websockets:$kTorVersion" - implementation "io.ktor:ktor-server-default-headers:$kTorVersion" - implementation "io.ktor:ktor-server-compression:$kTorVersion" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" - implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion" - implementation "io.github.reactivecircus.cache4k:cache4k:$cache4kVersion" - } - - jar { - manifest { - attributes "Main-Class": "io.elephantchess.webapp.MainKt" - } - - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - - from { - configurations.runtimeClasspath.collect { - it.isDirectory() ? it : zipTree(it) - } - } - } -} - -project(":scripts") { - dependencies { - implementation project(":utils") - implementation project(":seven-kingdoms-core") - implementation project(":seven-kingdoms-core-test-utils") - implementation project(":webapp-model-common") - implementation project(":webapp-dao") - implementation project(":webapp-service-layer") - implementation project(":webapp-config") - implementation project(":webapp") - implementation "com.google.guava:guava:$guavaVersion" - implementation project(":xiangqi-core") - implementation project(":engine-api") - implementation "io.ktor:ktor-client-core:$kTorVersion" - implementation "io.ktor:ktor-client-cio:$kTorVersion" - implementation "io.ktor:ktor-client-logging:$kTorVersion" - implementation "io.ktor:ktor-client-content-negotiation:$kTorVersion" - implementation "com.opencsv:opencsv:$openCsvVersion" - implementation "com.sun.mail:javax.mail:$javaxMailVersion" - implementation "org.apache.logging.log4j:log4j-core:2.26.0" - implementation "org.mockito.kotlin:mockito-kotlin:$mockitoVersion" - } - - tasks.register('minify', JavaExec) { - group = 'deployment' - description = 'Minify JS and CSS assets' - mainClass = 'io.elephantchess.scripts.minification.MinifyKt' - classpath = sourceSets.main.runtimeClasspath - workingDir = rootProject.projectDir - } - - tasks.register('removeMinified', JavaExec) { - group = 'deployment' - description = 'Remove all minified JS and CSS files' - mainClass = 'io.elephantchess.scripts.minification.RemoveMinifiedKt' - classpath = sourceSets.main.runtimeClasspath - workingDir = rootProject.projectDir - } - - tasks.register('liquibaseGeneration', JavaExec) { - group = 'deployment' - description = 'Generate liquibase changelog for DB code generation' - mainClass = 'io.elephantchess.scripts.LiquibaseGenerationKt' - classpath = sourceSets.main.runtimeClasspath - workingDir = rootProject.projectDir - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..63521bbd3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,393 @@ +import liquibase.Contexts +import liquibase.Liquibase +import liquibase.database.core.H2Database +import liquibase.database.jvm.JdbcConnection +import liquibase.resource.DirectoryResourceAccessor +import org.gradle.jvm.tasks.Jar +import org.h2.Driver +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jooq.codegen.GenerationTool +import org.jooq.meta.jaxb.* +import org.jooq.meta.jaxb.Target +import java.sql.Connection + +fun DependencyHandlerScope.api(dependencyNotation: Any) = add("api", dependencyNotation) + +fun DependencyHandlerScope.implementation(dependencyNotation: Any) = add("implementation", dependencyNotation) + +fun DependencyHandlerScope.testImplementation(dependencyNotation: Any) = add("testImplementation", dependencyNotation) + +val rootLibs = libs + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath(libs.jooq.codegen) + classpath(libs.h2) + classpath(libs.liquibase.core) + } +} + +plugins { + alias(libs.plugins.versions) + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.test.logger) apply false + alias(libs.plugins.kotlin.serialization) apply false +} + +subprojects { + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "com.adarshr.test-logger") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + + extensions.configure { + jvmToolchain(21) + } + + repositories { + mavenCentral() + mavenLocal() + } + + dependencies { + implementation(rootLibs.kotlin.logging) + implementation(rootLibs.kotlin.stdlib) + implementation(rootLibs.coroutines.core) + implementation(rootLibs.logback.classic) + testImplementation(rootLibs.kotlin.test) + testImplementation(rootLibs.coroutines.test) + testImplementation(rootLibs.mockito.kotlin) + testImplementation(rootLibs.junit.jupiter.params) + } + + val nettyVersion = "4.2.12.Final" + configurations.configureEach { + resolutionStrategy { + force("org.apache.commons:commons-lang3:${rootLibs.versions.commonsLang3.get()}") + force("org.checkerframework:checker-qual:${rootLibs.versions.checkerQual.get()}") + force("io.netty:netty-buffer:$nettyVersion") + force("io.netty:netty-codec:$nettyVersion") + force("io.netty:netty-codec-base:$nettyVersion") + force("io.netty:netty-codec-compression:$nettyVersion") + force("io.netty:netty-codec-dns:$nettyVersion") + force("io.netty:netty-codec-http:$nettyVersion") + force("io.netty:netty-codec-http2:$nettyVersion") + force("io.netty:netty-codec-socks:$nettyVersion") + force("io.netty:netty-common:$nettyVersion") + force("io.netty:netty-handler:$nettyVersion") + force("io.netty:netty-handler-proxy:$nettyVersion") + force("io.netty:netty-resolver:$nettyVersion") + force("io.netty:netty-resolver-dns:$nettyVersion") + force("io.netty:netty-transport:$nettyVersion") + force("io.netty:netty-transport-classes-epoll:$nettyVersion") + force("io.netty:netty-transport-classes-kqueue:$nettyVersion") + force("io.netty:netty-transport-native-epoll:$nettyVersion") + force("io.netty:netty-transport-native-kqueue:$nettyVersion") + force("io.netty:netty-transport-native-unix-common:$nettyVersion") + } + } + + tasks.withType().configureEach { + useJUnitPlatform() + minHeapSize = "1G" + maxHeapSize = "1G" + failOnNoDiscoveredTests = false + } +} + +val daoCodeGen = tasks.register("dao-code-gen") { + doLast { + val conn: Connection = Driver().connect("jdbc:h2:mem:test", null) + + conn.createStatement().use { stmt -> + stmt.execute("drop all OBJECTS") + stmt.execute("set schema PUBLIC") + } + + val resourceAccessor = DirectoryResourceAccessor( + project.layout.projectDirectory.dir("webapp-dao-migration/src/main/resources").asFile, + ) + val db = H2Database().apply { + setConnection(JdbcConnection(conn)) + } + + val liquibase = Liquibase("liquibase-changelog-generation.xml", resourceAccessor, db) + liquibase.update(Contexts()) + conn.commit() + + val jdbc = Jdbc() + .withDriver("org.h2.Driver") + .withUrl("jdbc:h2:mem:test") + + val forcedTypeMap = linkedMapOf() + file("jooq-type-mappings.txt").forEachLine { line -> + val parts = line.split(",") + val columnName = parts[0].trim() + val typeName = parts[1].trim() + forcedTypeMap[columnName] = typeName + } + + val forcedTypes = forcedTypeMap + .map { (key, value) -> + ForcedType() + .withIncludeExpression(key) + .withUserType(value) + .withEnumConverter(true) + } + .toMutableList() + + forcedTypes += ForcedType() + .withIncludeTypes("(?i:TIMESTAMP\\s+WITH\\s+TIME\\s+ZONE|TIMESTAMP\\([0-9]+\\)\\s+WITH\\s+TIME\\s+ZONE|TIMESTAMPTZ)") + .withUserType("kotlin.time.Instant") + .withConverter("io.elephantchess.db.codegen.OffsetDateTimeInstantConverter") + + val database = Database() + .withExcludes("DATABASECHANGELOG|DATABASECHANGELOGLOCK") + .withInputSchema("PUBLIC") + .withForcedTypes(forcedTypes) + + val generate = Generate() + .withPojos(true) + .withDaos(true) + + val target = Target() + .withPackageName("io.elephantchess.db.dao.codegen") + .withDirectory("${project.projectDir}/webapp-dao/build/jooq") + + val generator = Generator() + .withDatabase(database) + .withGenerate(generate) + .withTarget(target) + + GenerationTool.generate( + Configuration() + .withJdbc(jdbc) + .withGenerator(generator), + ) + + conn.close() + } +} + +project(":webapp-dao").tasks.named("compileKotlin") { + dependsOn(daoCodeGen) +} + +val publishableModules = listOf( + "engine-api", + "xiangqi-core", + "xiangqi-core-test-utils", + "seven-kingdoms-core", + "seven-kingdoms-core-test-utils", +) + +configure(publishableModules.map { project(":$it") }) { + apply(plugin = "maven-publish") + + group = "io.elephantchess" + version = rootProject.findProperty("publishVersion") as String? ?: "1.0.0-SNAPSHOT" + + extensions.configure { + withJavadocJar() + withSourcesJar() + } + + extensions.configure { + publications { + create("maven") { + from(components["java"]) + } + } + } +} + +project(":engine-api") { + dependencies { + implementation(rootLibs.coroutines.core) + implementation(project(":xiangqi-core")) + testImplementation(project(":xiangqi-core")) + } +} + +project(":xiangqi-core") { + dependencies { + testImplementation(project(":xiangqi-core-test-utils")) + } +} + +project(":seven-kingdoms-core") { + dependencies { + testImplementation(project(":seven-kingdoms-core-test-utils")) + } +} + +project(":seven-kingdoms-core-test-utils") { + dependencies { + implementation(project(":seven-kingdoms-core")) + api(rootLibs.jackson.datatype.jsr310) + api(rootLibs.jackson.module.kotlin) + } +} + +project(":webapp-dao-migration") { + dependencies { + implementation(rootLibs.liquibase.core) + implementation(rootLibs.postgresql) + } +} + +project(":webapp-dao") { + the()["main"].java.srcDir(layout.buildDirectory.dir("jooq")) + + dependencies { + implementation(project(":utils")) + implementation(project(":webapp-dao-migration")) + implementation(project(":seven-kingdoms-core")) + implementation(project(":webapp-config")) + implementation(project(":webapp-model-common")) + implementation(project(":xiangqi-core")) + implementation(project(":engine-api")) + implementation(rootLibs.jooq) + implementation(rootLibs.jooq.kotlin) + implementation(rootLibs.jooq.kotlin.coroutines) + implementation(rootLibs.liquibase.core) + implementation(rootLibs.r2dbc.postgresql) + implementation(rootLibs.r2dbc.pool) + } +} + +project(":webapp-html-renderer") { + dependencies { + implementation(project(":utils")) + implementation(rootLibs.ktor.server.html.builder) + implementation(rootLibs.jsoup) + } +} + +project(":webapp-service-layer") { + dependencies { + api(project(":utils")) + api(project(":webapp-config")) + api(project(":webapp-model-common")) + api(project(":webapp-html-renderer")) + implementation(project(":seven-kingdoms-core")) + implementation(project(":webapp-dao")) + api(rootLibs.koin.core) + api(project(":xiangqi-core")) + api(project(":engine-api")) + api(rootLibs.commons.lang3) + implementation(rootLibs.jsoup) + implementation(rootLibs.commons.validator) + implementation(rootLibs.java.jwt) + implementation(rootLibs.javax.mail) + implementation(rootLibs.cache4k) + implementation(rootLibs.kubernetes.client) + implementation(rootLibs.jooq) + implementation(rootLibs.ktor.client.core) + implementation(rootLibs.ktor.client.cio) + implementation(rootLibs.ktor.client.content.negotiation) + implementation(rootLibs.ktor.client.logging) + implementation(rootLibs.ktor.serialization.jackson) + implementation(rootLibs.ktor.serialization.kotlinx.json) + implementation(rootLibs.ktor.server.html.builder) + testImplementation(project(":xiangqi-core-test-utils")) + testImplementation(project(":seven-kingdoms-core-test-utils")) + testImplementation(rootLibs.commons.rng.simple) + testImplementation(rootLibs.commons.text) + testImplementation(rootLibs.h2) + testImplementation(rootLibs.liquibase.core) + testImplementation(rootLibs.testcontainers.postgresql) + } +} + +project(":webapp-config") { + dependencies { + implementation(rootLibs.commons.cli) + } +} + +project(":webapp") { + dependencies { + implementation(project(":webapp-service-layer")) + implementation(rootLibs.ktor.server.core) + implementation(rootLibs.ktor.server.netty) + implementation(rootLibs.ktor.server.status.pages) + implementation(rootLibs.ktor.server.default.headers) + implementation(rootLibs.ktor.server.content.negotiation) + implementation(rootLibs.ktor.server.caching.headers) + implementation(rootLibs.ktor.server.html.builder) + implementation(rootLibs.ktor.serialization.jackson) + implementation(rootLibs.ktor.serialization.kotlinx.json) + implementation(rootLibs.ktor.server.websockets) + implementation(rootLibs.ktor.server.compression) + implementation(rootLibs.jackson.datatype.jsr310) + implementation(rootLibs.jackson.module.kotlin) + implementation(rootLibs.cache4k) + } + + tasks.named("jar") { + manifest { + attributes["Main-Class"] = "io.elephantchess.webapp.MainKt" + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + from( + configurations["runtimeClasspath"].map { + if (it.isDirectory) it else zipTree(it) + }, + ) + } +} + +project(":scripts") { + dependencies { + implementation(project(":utils")) + implementation(project(":seven-kingdoms-core")) + implementation(project(":seven-kingdoms-core-test-utils")) + implementation(project(":webapp-model-common")) + implementation(project(":webapp-dao")) + implementation(project(":webapp-service-layer")) + implementation(project(":webapp-config")) + implementation(project(":webapp")) + implementation(rootLibs.guava) + implementation(project(":xiangqi-core")) + implementation(project(":engine-api")) + implementation(rootLibs.ktor.client.core) + implementation(rootLibs.ktor.client.cio) + implementation(rootLibs.ktor.client.logging) + implementation(rootLibs.ktor.client.content.negotiation) + implementation(rootLibs.opencsv) + implementation(rootLibs.javax.mail) + implementation(rootLibs.log4j.core) + implementation(rootLibs.mockito.kotlin) + } + + val sourceSets = the() + + tasks.register("minify") { + group = "deployment" + description = "Minify JS and CSS assets" + mainClass.set("io.elephantchess.scripts.minification.MinifyKt") + classpath = sourceSets["main"].runtimeClasspath + workingDir = rootProject.projectDir + } + + tasks.register("removeMinified") { + group = "deployment" + description = "Remove all minified JS and CSS files" + mainClass.set("io.elephantchess.scripts.minification.RemoveMinifiedKt") + classpath = sourceSets["main"].runtimeClasspath + workingDir = rootProject.projectDir + } + + tasks.register("liquibaseGeneration") { + group = "deployment" + description = "Generate liquibase changelog for DB code generation" + mainClass.set("io.elephantchess.scripts.LiquibaseGenerationKt") + classpath = sourceSets["main"].runtimeClasspath + workingDir = rootProject.projectDir + } +} diff --git a/engine-api/src/main/kotlin/io/elephantchess/engines/EnginePool.kt b/engine-api/src/main/kotlin/io/elephantchess/engines/EnginePool.kt index 18138a354..242a415df 100644 --- a/engine-api/src/main/kotlin/io/elephantchess/engines/EnginePool.kt +++ b/engine-api/src/main/kotlin/io/elephantchess/engines/EnginePool.kt @@ -5,6 +5,7 @@ import io.elephantchess.engines.protocol.commands.EngineProcessLocator import io.elephantchess.engines.protocol.commands.LocalProcessLocator import io.elephantchess.engines.protocol.model.InfoLinesResult import io.elephantchess.engines.utils.EngineUtils.waitForCondition +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -43,9 +44,15 @@ class EnginePool( } } - suspend fun queryForDepth(fen: String, engineId: EngineId, depth: Int, timeout: Long = 20_000): InfoLinesResult? { + suspend fun queryForDepth( + fen: String, + engineId: EngineId, + depth: Int, + timeout: Long = 20_000, + variant: Variant = Variant.XIANGQI + ): InfoLinesResult? { return acquireAndExecute(engineId, timeout) { lockableEngineProcess -> - lockableEngineProcess.queryForBestMove(fen, depth) + lockableEngineProcess.queryForBestMove(fen, depth, variant) } } @@ -109,8 +116,8 @@ class EnginePool( engineProcess.quit() } - suspend fun queryForBestMove(fen: String, depth: Int): InfoLinesResult { - return engineProcess.queryForBestMove(fen, depth) + suspend fun queryForBestMove(fen: String, depth: Int, variant: Variant): InfoLinesResult { + return engineProcess.queryForBestMove(fen, depth, variant) } } diff --git a/engine-api/src/main/kotlin/io/elephantchess/engines/process/EngineProcess.kt b/engine-api/src/main/kotlin/io/elephantchess/engines/process/EngineProcess.kt index 253a6996d..e991e31c3 100644 --- a/engine-api/src/main/kotlin/io/elephantchess/engines/process/EngineProcess.kt +++ b/engine-api/src/main/kotlin/io/elephantchess/engines/process/EngineProcess.kt @@ -6,6 +6,7 @@ import io.elephantchess.engines.protocol.LineListener import io.elephantchess.engines.protocol.commands.EngineProcessLocator import io.elephantchess.engines.protocol.model.InfoLinesResult import io.elephantchess.engines.utils.EngineUtils.waitForCondition +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KLogger import kotlinx.coroutines.Runnable import kotlinx.coroutines.runBlocking @@ -45,12 +46,14 @@ abstract class EngineProcess( suspend fun queryForBestMove( fen: String, maxDepth: Int, + variant: Variant = Variant.XIANGQI, maxDelay: Long = DEFAULT_MAX_DELAY, checkPeriod: Long = DEFAULT_CHECK_PERIOD, ): InfoLinesResult { val infoListener = InfoLineListener() lineListener = infoListener + setVariant(variant) inputCommand("position fen $fen") inputCommand("go depth $maxDepth") waitForCondition(maxDelay, checkPeriod) { @@ -60,6 +63,12 @@ abstract class EngineProcess( return infoListener.getResult() } + /** + * Override in engine implementations to set the variant before queries. + * Default implementation does nothing (for engines that do not support variants). + */ + open fun setVariant(variant: Variant) {} + fun inputCommandAndWaitBlocking( command: String, maxDelay: Long = DEFAULT_MAX_DELAY, diff --git a/engine-api/src/main/kotlin/io/elephantchess/engines/process/FairyStockfishEngineProcess.kt b/engine-api/src/main/kotlin/io/elephantchess/engines/process/FairyStockfishEngineProcess.kt index 3868abc65..bef13e014 100644 --- a/engine-api/src/main/kotlin/io/elephantchess/engines/process/FairyStockfishEngineProcess.kt +++ b/engine-api/src/main/kotlin/io/elephantchess/engines/process/FairyStockfishEngineProcess.kt @@ -2,6 +2,7 @@ package io.elephantchess.engines.process import io.elephantchess.engines.protocol.commands.EngineProcessLocator import io.elephantchess.engines.protocol.commands.LocalProcessLocator +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KotlinLogging class FairyStockfishEngineProcess( @@ -13,9 +14,18 @@ class FairyStockfishEngineProcess( override val logger = KotlinLogging.logger {} + private var currentVariant: Variant? = null + override fun initEngine() { inputCommand("ucci") inputCommand("setoption Threads $numberOfThreads") } + override fun setVariant(variant: Variant) { + if (variant != currentVariant) { + inputCommand("setoption UCI_Variant ${variant.fairyStockfishVariantName()}") + currentVariant = variant + } + } + } diff --git a/engine-api/src/main/kotlin/io/elephantchess/engines/process/PikafishEngineProcess.kt b/engine-api/src/main/kotlin/io/elephantchess/engines/process/PikafishEngineProcess.kt index 288018e20..e017affc7 100644 --- a/engine-api/src/main/kotlin/io/elephantchess/engines/process/PikafishEngineProcess.kt +++ b/engine-api/src/main/kotlin/io/elephantchess/engines/process/PikafishEngineProcess.kt @@ -2,6 +2,7 @@ package io.elephantchess.engines.process import io.elephantchess.engines.protocol.commands.EngineProcessLocator import io.elephantchess.engines.protocol.commands.LocalProcessLocator +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KotlinLogging class PikafishEngineProcess( @@ -17,4 +18,10 @@ class PikafishEngineProcess( inputCommand("setoption name Threads value $numberOfThreads") } + override fun setVariant(variant: Variant) { + if (variant != Variant.XIANGQI) { + logger.error { "Pikafish does not support variants, ignoring setVariant command" } + } + } + } diff --git a/gradle.properties b/gradle.properties index 8aea7c703..6a3b8bfe5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,19 +1 @@ publishVersion=1.1.4-SNAPSHOT -kotlinVersion=2.3.21 -coroutineVersion=1.10.2 -kTorVersion=3.4.3 -koinCoreVersion=4.2.1 -postgresVersion=42.7.11 -liquibaseVersion=5.0.2 -jooqVersion=3.21.2 -h2Version=2.4.240 -jacksonVersion=2.21.2 -jsoupVersion=1.22.2 -mockitoVersion=6.3.0 -cache4kVersion=0.14.0 -commonLang3Version=3.14.0 -javaxMailVersion=1.6.2 -openCsvVersion=5.12.0 -testContainerVersion=1.21.4 -guavaVersion=33.6.0-jre -logbackVersion=1.5.32 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..20bdc42d7 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,90 @@ +[versions] +kotlin = "2.3.21" +coroutines = "1.11.0" +ktor = "3.5.0" +koinCore = "4.2.1" +postgresql = "42.7.11" +liquibase = "5.0.3" +jooq = "3.21.4" +h2 = "2.4.240" +jackson = "2.21.3" +jsoup = "1.22.2" +mockitoKotlin = "6.3.0" +cache4k = "0.14.0" +commonsLang3 = "3.20.0" +javaxMail = "1.6.2" +opencsv = "5.12.0" +testcontainers = "1.21.4" +guava = "33.6.0-jre" +logback = "1.5.32" +kotlinLogging = "8.0.03" +junitJupiterParams = "6.1.0" +checkerQual = "4.1.0" +r2dbcPostgresql = "1.1.1.RELEASE" +r2dbcPool = "1.0.2.RELEASE" +commonsValidator = "1.10.1" +javaJwt = "4.5.2" +kubernetesClient = "7.7.0" +commonsRngSimple = "1.7" +commonsText = "1.15.0" +commonsCli = "1.11.0" +log4jCore = "2.26.0" +versionsPlugin = "0.54.0" +testLoggerPlugin = "4.0.0" + +[libraries] +cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" } +commons-cli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" } +commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" } +commons-rng-simple = { module = "org.apache.commons:commons-rng-simple", version.ref = "commonsRngSimple" } +commons-text = { module = "org.apache.commons:commons-text", version.ref = "commonsText" } +commons-validator = { module = "commons-validator:commons-validator", version.ref = "commonsValidator" } +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +h2 = { module = "com.h2database:h2", version.ref = "h2" } +jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } +jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } +java-jwt = { module = "com.auth0:java-jwt", version.ref = "javaJwt" } +javax-mail = { module = "com.sun.mail:javax.mail", version.ref = "javaxMail" } +jooq = { module = "org.jooq:jooq", version.ref = "jooq" } +jooq-codegen = { module = "org.jooq:jooq-codegen", version.ref = "jooq" } +jooq-kotlin = { module = "org.jooq:jooq-kotlin", version.ref = "jooq" } +jooq-kotlin-coroutines = { module = "org.jooq:jooq-kotlin-coroutines", version.ref = "jooq" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junitJupiterParams" } +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } +kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlinLogging" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kubernetes-client = { module = "io.fabric8:kubernetes-client", version.ref = "kubernetesClient" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-serialization-jackson = { module = "io.ktor:ktor-serialization-jackson", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers", version.ref = "ktor" } +ktor-server-compression = { module = "io.ktor:ktor-server-compression", version.ref = "ktor" } +ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } +ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } +ktor-server-default-headers = { module = "io.ktor:ktor-server-default-headers", version.ref = "ktor" } +ktor-server-html-builder = { module = "io.ktor:ktor-server-html-builder", version.ref = "ktor" } +ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" } +ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" } +ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" } +liquibase-core = { module = "org.liquibase:liquibase-core", version.ref = "liquibase" } +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4jCore" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } +opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" } +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } +r2dbc-pool = { module = "io.r2dbc:r2dbc-pool", version.ref = "r2dbcPool" } +r2dbc-postgresql = { module = "org.postgresql:r2dbc-postgresql", version.ref = "r2dbcPostgresql" } +jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } +testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } + +[plugins] +versions = { id = "com.github.ben-manes.versions", version.ref = "versionsPlugin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +test-logger = { id = "com.adarshr.test-logger", version.ref = "testLoggerPlugin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b52fb7e71..df6a6ad76 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/jooq-type-mappings.txt b/jooq-type-mappings.txt index 096ec1791..a972ef370 100644 --- a/jooq-type-mappings.txt +++ b/jooq-type-mappings.txt @@ -13,10 +13,12 @@ game.status, io.elephantchess.model.GameEventType game.time_control_category, io.elephantchess.model.TimeControlCategory game.time_control_mode, io.elephantchess.model.TimeControlMode game.join_source, io.elephantchess.model.GameJoinSource +game.variant, io.elephantchess.xiangqi.Variant bot_game.user_color, io.elephantchess.xiangqi.Color bot_game.outcome, io.elephantchess.model.Outcome bot_game.game_status, io.elephantchess.model.GameEventType bot_game.engine, io.elephantchess.model.Engine +bot_game.variant, io.elephantchess.xiangqi.Variant bot_game_status_event.event_type, io.elephantchess.model.GameEventType game.analysis_status, io.elephantchess.model.AnalysisStatus bot_game.analysis_status, io.elephantchess.model.AnalysisStatus diff --git a/scripts/src/main/kotlin/io/elephantchess/scripts/CreateVietnameseTestUsers.kt b/scripts/src/main/kotlin/io/elephantchess/scripts/CreateVietnameseTestUsers.kt new file mode 100644 index 000000000..290470a2a --- /dev/null +++ b/scripts/src/main/kotlin/io/elephantchess/scripts/CreateVietnameseTestUsers.kt @@ -0,0 +1,60 @@ +package io.elephantchess.scripts + +import io.elephantchess.db.services.UserDaoService +import io.elephantchess.servicelayer.dto.user.SignUpRequest +import io.elephantchess.servicelayer.services.UserService +import kotlinx.coroutines.runBlocking +import org.koin.core.component.inject +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +object CreateVietnameseTestUsers : KoinScriptInit() { + + private const val LOCALHOST_BASE_URL = "http://localhost:8080" + private const val DEFAULT_PASSWORD = "password" + + private val userService by inject() + private val userDaoService by inject() + + private data class UserSeed( + val username: String, + val email: String, + ) + + private val usersToCreate = listOf( + UserSeed(username = "nguyễn", email = "vietuser1@example.com"), + UserSeed(username = "trần", email = "vietuser2@example.com"), + UserSeed(username = "Đặng_123", email = "vietuser3@example.com"), + UserSeed(username = "phạm-văn", email = "vietuser4@example.com"), + ) + + @JvmStatic + fun main(args: Array) { + runBlocking { + usersToCreate.forEach { user -> + val exists = userDaoService.existsForUsername(user.username) + if (!exists) { + val either = userService.signUp(SignUpRequest(user.username, user.email, DEFAULT_PASSWORD)) + if (either.isRight()) { + println("Created user ${user.username}") + println(profileUrl(user.username)) + } else { + val errorMessage = either.left().errors + .ifEmpty { listOf("User creation failed with no validation errors provided") } + .joinToString("; ") + println("ERROR: Failed to create user ${user.username}: $errorMessage") + } + } else { + println("User ${user.username} already exists") + println(profileUrl(user.username)) + } + } + } + } + + private fun profileUrl(username: String): String { +// val encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8).replace("+", "%20") + return "$LOCALHOST_BASE_URL/@/$username" + } + +} diff --git a/scripts/src/main/kotlin/io/elephantchess/scripts/game/CreateListOfGamesToJoin.kt b/scripts/src/main/kotlin/io/elephantchess/scripts/game/CreateListOfGamesToJoin.kt index 9406d67e4..64d982c0f 100644 --- a/scripts/src/main/kotlin/io/elephantchess/scripts/game/CreateListOfGamesToJoin.kt +++ b/scripts/src/main/kotlin/io/elephantchess/scripts/game/CreateListOfGamesToJoin.kt @@ -9,9 +9,10 @@ import io.elephantchess.servicelayer.dto.game.CreateGameRequest import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.services.PlayerVsPlayerGameService import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant import kotlinx.coroutines.runBlocking import org.koin.core.component.inject -import java.util.* +import kotlin.random.Random import kotlin.system.exitProcess import kotlin.time.Duration import kotlin.time.Duration.Companion.days @@ -20,6 +21,70 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds private const val testUsers = 10 +private const val weirdTimeControlsCount = 20 + +private val standardTimeControls: List> = + listOf( + 1.minutes to null, + 1.minutes to 1.seconds, + 2.minutes to 1.seconds, + 3.minutes to null, + 3.minutes to 2.seconds, + 5.minutes to 2.seconds, + 5.minutes to 5.seconds, + 5.minutes to null, + 10.minutes to null, + 10.minutes to 5.seconds, + 15.minutes to 10.seconds, + 20.minutes to 10.seconds, + 30.minutes to null, + 30.minutes to 20.seconds, + 45.minutes to 15.seconds, + 1.hours to null, + 1.hours to 30.seconds, + 1.hours to 1.minutes, + 2.hours to null, + 1.days to null, + 2.days to null, + 3.days to null, + 7.days to null, + ) + +private fun Random.generateWeirdTimeControls(): List> = + List(weirdTimeControlsCount) { + if (nextBoolean()) { + randomWeirdLiveTimeControl() + } else { + randomWeirdCorrespondenceTimeControl() + } + } + +private fun Random.randomWeirdLiveTimeControl(): Pair { + val base = + when (nextInt(4)) { + 0 -> nextInt(45, 180).seconds + 1 -> nextInt(3, 25).minutes + nextInt(60).seconds + 2 -> nextInt(25, 90).minutes + nextInt(60).seconds + else -> nextInt(1, 6).hours + nextInt(60).minutes + nextInt(60).seconds + } + + val increment = + when (nextInt(6)) { + 0 -> null + 1 -> nextInt(1, 4).seconds + 2 -> nextInt(4, 11).seconds + 3 -> nextInt(11, 31).seconds + 4 -> nextInt(31, 90).seconds + else -> nextInt(1, 4).minutes + } + + return base to increment +} + +private fun Random.randomWeirdCorrespondenceTimeControl(): Pair { + val base = nextInt(1, 15).days + nextInt(24).hours + nextInt(60).minutes + return base to null +} object CreateListOfGamesToJoin : KoinScriptInit() { @@ -28,43 +93,27 @@ object CreateListOfGamesToJoin : KoinScriptInit() { @JvmStatic fun main(args: Array): Unit = runBlocking { - // set up users (1..testUsers).forEach { i -> createTestUserIfNotExists(i) } - val timeControls: List> = - listOf( - 1.minutes to null, - 1.minutes to 1.seconds, - 2.minutes to 1.seconds, - 3.minutes to null, - 5.minutes to 2.seconds, - 5.minutes to null, - 10.minutes to null, - 15.minutes to 10.seconds, - 30.minutes to null, - 1.hours to null, - 1.hours to 1.minutes, - 2.hours to null, - 1.days to null, - 3.days to null, - 7.days to null, - ) - // create games - val rnd = Random() - repeat(30) { - val i = Random().nextInt(testUsers) + 1 - val user = userDaoService.findByUserName("test$i")!! + val rnd = Random.Default + val weirdTimeControls = rnd.generateWeirdTimeControls() + val timeControls = standardTimeControls + weirdTimeControls + + repeat(50) { + val i = rnd.nextInt(testUsers) + 1 + val user = userDaoService.findByEmail("test$i@protonmail.com")!! val hasColor = rnd.nextBoolean() val color = if (hasColor) null else Color.random() - val timeControl = timeControls.random() + val timeControl = timeControls.random(rnd) val timeControlMode = if (timeControl.first.inWholeSeconds < 1.days.inWholeSeconds) { TimeControlMode.GAME_TIME } else { TimeControlMode.MOVE_TIME } + val variant = if (rnd.nextInt(100) < 20) Variant.MANCHU else Variant.XIANGQI val request = CreateGameRequest( @@ -75,7 +124,8 @@ object CreateListOfGamesToJoin : KoinScriptInit() { timeControlMode = timeControlMode, allowGuests = true, alwaysVisibleInLobby = true, - privateInvite = false + privateInvite = false, + variant = variant ) val createGameResponse = pvpGameService.createGame(UserId(AUTHENTICATED, user.id), request) diff --git a/scripts/src/main/kotlin/io/elephantchess/scripts/game/Utils.kt b/scripts/src/main/kotlin/io/elephantchess/scripts/game/Utils.kt index 53c1b6261..d21c5fb0c 100644 --- a/scripts/src/main/kotlin/io/elephantchess/scripts/game/Utils.kt +++ b/scripts/src/main/kotlin/io/elephantchess/scripts/game/Utils.kt @@ -6,6 +6,7 @@ import io.elephantchess.db.utils.awaitExecute import io.elephantchess.db.utils.fixed import io.elephantchess.servicelayer.dto.user.SignUpRequest import io.elephantchess.servicelayer.services.UserService +import io.elephantchess.utils.safeRandomAlphaNumericString import org.jooq.DSLContext import org.jooq.impl.DSL import org.jooq.kotlin.coroutines.transactionCoroutine @@ -20,8 +21,8 @@ object Utils : KoinComponent { private val dslContext by inject() suspend fun createTestUserIfNotExists(i: Int, randomizeRatings: Boolean = true) { - val username = "test$i" - val email = "test$i@gmail.com" + val username = "test$i-pretty-long-name-${safeRandomAlphaNumericString(5, 10)}".take(30) + val email = "test$i@protonmail.com" val password = "password$i" val userExists = userDaoService.existsForEmail(email) @@ -47,6 +48,12 @@ object Utils : KoinComponent { .set(USER.GAME_RATING_CLASSICAL.fixed(), nextInt(500, 2500)) .set(USER.GAME_RATING_SEVERAL_DAYS.fixed(), nextInt(500, 2500)) .set(USER.GAME_RATING_CORRESPONDENCE.fixed(), nextInt(500, 2500)) + .set(USER.GAME_RATING_MANCHU_BULLET.fixed(), nextInt(500, 2500)) + .set(USER.GAME_RATING_MANCHU_BLITZ.fixed(), nextInt(500, 2500)) + .set(USER.GAME_RATING_MANCHU_RAPID.fixed(), nextInt(500, 2500)) + .set(USER.GAME_RATING_MANCHU_CLASSICAL.fixed(), nextInt(500, 2500)) + .set(USER.GAME_RATING_MANCHU_SEVERAL_DAYS.fixed(), nextInt(500, 2500)) + .set(USER.GAME_RATING_MANCHU_CORRESPONDENCE.fixed(), nextInt(500, 2500)) .where(USER.ID.eq(userId)) .awaitExecute() } diff --git a/scripts/src/main/kotlin/io/elephantchess/scripts/manchu/GenerateManchuGames.kt b/scripts/src/main/kotlin/io/elephantchess/scripts/manchu/GenerateManchuGames.kt new file mode 100644 index 000000000..6e632dad4 --- /dev/null +++ b/scripts/src/main/kotlin/io/elephantchess/scripts/manchu/GenerateManchuGames.kt @@ -0,0 +1,144 @@ +/* + * 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 . + */ + +package io.elephantchess.scripts.manchu + +import io.elephantchess.xiangqi.Board +import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.HalfMove +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import java.io.File +import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger + +object GenerateManchuGames { + + private const val TOTAL_GAMES = 20_000 + private const val GAMES_TO_KEEP = 1_000 + private const val MIN_PER_RESULT_TYPE = 100 + private const val MAX_MOVES_PER_GAME = 400 + + private enum class GameResult { + RED_WINS_CHECKMATE, + BLACK_WINS_CHECKMATE, + RED_WINS_STALEMATE, + BLACK_WINS_STALEMATE, + TOO_LONG, + } + + private data class GeneratedGame( + val id: String, + val moves: List, + val result: GameResult, + ) + + private fun movesKey(game: GeneratedGame): String = game.moves.joinToString(",") + + private fun generateGame(): GeneratedGame { + val board = Board(Board.MANCHU_START_FEN) + val moves = mutableListOf() + + while (moves.size < MAX_MOVES_PER_GAME) { + val colorToPlay = board.colorToPlay() + + if (board.isCheckmated()) { + // colorToPlay is the one that is checkmated, so opponent wins + val result = + if (colorToPlay == Color.RED) GameResult.BLACK_WINS_CHECKMATE else GameResult.RED_WINS_CHECKMATE + return GeneratedGame(randomId(), moves.map { it.toUci() }, result) + } + + if (board.isStalemated()) { + // colorToPlay is the one that is stalemated, so opponent wins (Xiangqi rules) + val result = + if (colorToPlay == Color.RED) GameResult.BLACK_WINS_STALEMATE else GameResult.RED_WINS_STALEMATE + return GeneratedGame(randomId(), moves.map { it.toUci() }, result) + } + + val legalMoves = board.listLegalMoves(colorToPlay) + if (legalMoves.isEmpty()) break + + val move = legalMoves.random() + board.registerMove(move) + moves.add(move) + } + + return GeneratedGame(randomId(), moves.map { it.toUci() }, GameResult.TOO_LONG) + } + + private fun randomId(): String = UUID.randomUUID().toString().replace("-", "").take(8) + + @JvmStatic + fun main(args: Array): Unit = runBlocking { + println("Generating $TOTAL_GAMES Manchu games in parallel...") + + val startedAt = System.currentTimeMillis() + val completed = AtomicInteger(0) + + val allGames = (1..TOTAL_GAMES) + .map { + async(Dispatchers.Default) { + val game = generateGame() + val done = completed.incrementAndGet() + if (done % 500 == 0 || done == TOTAL_GAMES) { + val elapsedSeconds = (System.currentTimeMillis() - startedAt) / 1000 + println("Progress: $done/$TOTAL_GAMES (${done * 100 / TOTAL_GAMES}%) - ${elapsedSeconds}s elapsed") + } + game + } + } + .awaitAll() + + val uniqueGames = allGames.distinctBy { movesKey(it) } + println("Deduplicated ${allGames.size - uniqueGames.size} duplicate games") + + val byResult = uniqueGames.groupBy { it.result }.mapValues { (_, games) -> games.sortedBy { it.moves.size } } + println("Generated: ${byResult.mapValues { it.value.size }}") + + val completedGames = uniqueGames.filter { it.result != GameResult.TOO_LONG } + + // Pick a minimum from each terminal result type for variety + val selectedByMoves = linkedMapOf() + GameResult.entries.filter { it != GameResult.TOO_LONG }.forEach { resultType -> + (byResult[resultType] ?: emptyList()) + .take(MIN_PER_RESULT_TYPE) + .forEach { game -> selectedByMoves.putIfAbsent(movesKey(game), game) } + } + + // Fill remaining slots with the shortest completed games + completedGames.sortedBy { it.moves.size }.forEach { game -> + if (selectedByMoves.size < GAMES_TO_KEEP) { + selectedByMoves.putIfAbsent(movesKey(game), game) + } + } + + val finalGames = selectedByMoves.values.sortedBy { it.moves.size }.take(GAMES_TO_KEEP) + println("Keeping ${finalGames.size} games: ${finalGames.groupBy { it.result }.mapValues { it.value.size }}") + + val outputPath = "xiangqi-core-test-utils/src/main/resources/manchu.txt" + File(outputPath).writeText( + finalGames.joinToString("\n") { game -> "${game.id};${game.moves.joinToString(",")}" } + ) + println("Written to $outputPath") + } + +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 110df0412..000000000 --- a/settings.gradle +++ /dev/null @@ -1,16 +0,0 @@ -rootProject.name = 'elephantchess.io' - -include('utils') -include('engine-api') -include('xiangqi-core') -include('xiangqi-core-test-utils') -include('seven-kingdoms-core') -include('seven-kingdoms-core-test-utils') -include('webapp-config') -include('webapp-dao') -include('webapp-dao-migration') -include('webapp-html-renderer') -include('webapp-model-common') -include('webapp-service-layer') -include('webapp') -include('scripts') diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..57a23e193 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +rootProject.name = "elephantchess.io" + +include("utils") +include("engine-api") +include("xiangqi-core") +include("xiangqi-core-test-utils") +include("seven-kingdoms-core") +include("seven-kingdoms-core-test-utils") +include("webapp-config") +include("webapp-dao") +include("webapp-dao-migration") +include("webapp-html-renderer") +include("webapp-model-common") +include("webapp-service-layer") +include("webapp") +include("scripts") diff --git a/utils/src/main/resources/bad-words.txt b/utils/src/main/resources/bad-words.txt index 2f2a81ccc..5590f9ce2 100644 --- a/utils/src/main/resources/bad-words.txt +++ b/utils/src/main/resources/bad-words.txt @@ -1,4 +1,6 @@ # found: https://www.cs.cmu.edu/~biglou/resources/bad-words.txt +# obviously not all of those words are offensive per se, +# but it's better to avoid them in generated guest id abbo abo abortion diff --git a/webapp-dao-migration/src/main/resources/liquibase-changelog-generation.xml b/webapp-dao-migration/src/main/resources/liquibase-changelog-generation.xml index a05271e6e..c5bd195a5 100644 --- a/webapp-dao-migration/src/main/resources/liquibase-changelog-generation.xml +++ b/webapp-dao-migration/src/main/resources/liquibase-changelog-generation.xml @@ -2208,4 +2208,108 @@ + + + + + + + update_time IS NULL + + + + + + + + + Delete persisted "empty" searches (all criteria null/blank) that were stored before the + empty-criteria guard was added. + + + + (player_name IS NULL OR player_name = '') + AND player_id IS NULL + AND player_color IS NULL + AND (event_name IS NULL OR event_name = '') + AND event_id IS NULL + AND search_start IS NULL + AND search_end IS NULL + AND (fen IS NULL OR fen = '') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add email confirmation fields to user table (separate from automated email_verification) + + + + + + + + Add variant column to game and bot_game tables for Manchu variant support + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapp-dao-migration/src/main/resources/liquibase-changelog.xml b/webapp-dao-migration/src/main/resources/liquibase-changelog.xml index 0a203b71c..c0b0e4c16 100644 --- a/webapp-dao-migration/src/main/resources/liquibase-changelog.xml +++ b/webapp-dao-migration/src/main/resources/liquibase-changelog.xml @@ -2360,5 +2360,129 @@ + DATE '2024-04-28'; + + -- Pikafish games created on 2024-04-28 keep engine_version = NULL (unknown). + ]]> + + + + + + + + update_time IS NULL + + + + + + + + + Delete persisted "empty" searches (all criteria null/blank) that were stored before the + empty-criteria guard was added. + + + + (player_name IS NULL OR player_name = '') + AND player_id IS NULL + AND player_color IS NULL + AND (event_name IS NULL OR event_name = '') + AND event_id IS NULL + AND search_start IS NULL + AND search_end IS NULL + AND (fen IS NULL OR fen = '') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add email confirmation fields to user table (separate from automated email_verification) + + + + + + + + Add variant column to game and bot_game tables for Manchu variant support + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/codegen/OffsetDateTimeInstantConverter.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/codegen/OffsetDateTimeInstantConverter.kt index 28529703d..5d1bd695c 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/codegen/OffsetDateTimeInstantConverter.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/codegen/OffsetDateTimeInstantConverter.kt @@ -15,7 +15,7 @@ import kotlin.time.toKotlinInstant * pins the absolute instant, so no zone assumption is needed. We always write back at * [ZoneOffset.UTC] for consistency. * - * Wired by `build.gradle`'s `dao-code-gen` task as a forced type for every `timestamptz` column. + * Wired by `build.gradle.kts`'s `dao-code-gen` task as a forced type for every `timestamptz` column. */ class OffsetDateTimeInstantConverter : AbstractConverter( OffsetDateTime::class.java, diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/model/UserSessionRecord.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/model/UserSessionRecord.kt index 8b77db566..809296c95 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/model/UserSessionRecord.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/model/UserSessionRecord.kt @@ -3,6 +3,7 @@ package io.elephantchess.db.model import kotlin.time.Instant data class UserSessionRecord( + val id: Int? = null, val userId: String?, val remoteAddress: String, val userAgent: String, diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/GameChatTypingStatusDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/GameChatTypingStatusDaoService.kt new file mode 100644 index 000000000..5d7f29d73 --- /dev/null +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/GameChatTypingStatusDaoService.kt @@ -0,0 +1,50 @@ +package io.elephantchess.db.services + +import io.elephantchess.db.dao.codegen.Tables.GAME_CHAT_TYPING_STATUS +import io.elephantchess.db.dao.codegen.tables.pojos.GameChatTypingStatus +import io.elephantchess.db.utils.awaitExecute +import io.elephantchess.db.utils.awaitMappedRecords +import io.elephantchess.db.utils.fixed +import io.elephantchess.db.utils.isAfter +import org.jooq.DSLContext +import org.jooq.impl.DSL +import kotlin.time.Clock +import kotlin.time.Instant + +class GameChatTypingStatusDaoService(private val dslContext: DSLContext) { + + suspend fun upsertTypingStatus(gameId: String, userId: String) { + val now = Clock.System.now() + + // insert, or update on conflict + dslContext + .insertInto(GAME_CHAT_TYPING_STATUS.fixed()) + .set(GAME_CHAT_TYPING_STATUS.GAME_ID.fixed(), gameId) + .set(GAME_CHAT_TYPING_STATUS.USER_ID.fixed(), userId) + .set(GAME_CHAT_TYPING_STATUS.TYPED_AT.fixed(), now) + .onConflict(GAME_CHAT_TYPING_STATUS.GAME_ID, GAME_CHAT_TYPING_STATUS.USER_ID) + .doUpdate() + .set(GAME_CHAT_TYPING_STATUS.TYPED_AT, DSL.excluded(GAME_CHAT_TYPING_STATUS.TYPED_AT)) + .awaitExecute() + } + + /** + * Returns a map keyed by gameId where each value is the list of [GameChatTypingStatus] + * for that game. Only entries newer than [cutOff] (i.e. typed after that + * instant) are returned, so stale typing events never leak to callers. + */ + suspend fun fetchForGameIds( + gameIds: List, + cutOff: Instant, + ): Map> { + if (gameIds.isEmpty()) return emptyMap() + + return dslContext + .selectFrom(GAME_CHAT_TYPING_STATUS) + .where(GAME_CHAT_TYPING_STATUS.GAME_ID.`in`(gameIds)) + .and(GAME_CHAT_TYPING_STATUS.TYPED_AT.isAfter(cutOff)) + .awaitMappedRecords() + .groupBy { it.gameId!! } + } + +} diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/MoveAnalysisDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/MoveAnalysisDaoService.kt index 46963eb79..3a901b5ac 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/MoveAnalysisDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/MoveAnalysisDaoService.kt @@ -1,5 +1,6 @@ package io.elephantchess.db.services +import io.elephantchess.db.codegen.OffsetDateTimeInstantConverter import io.elephantchess.db.dao.codegen.Tables.* import io.elephantchess.db.dao.codegen.tables.daos.MoveAnalysisDao import io.elephantchess.db.dao.codegen.tables.pojos.MoveAnalysis @@ -12,6 +13,7 @@ import io.elephantchess.model.GameType import io.elephantchess.model.GameType.* import org.jooq.* import org.jooq.impl.DSL +import org.jooq.impl.SQLDataType import org.jooq.kotlin.coroutines.transactionCoroutine import kotlin.time.Clock import kotlin.time.Instant @@ -141,7 +143,12 @@ class MoveAnalysisDaoService(private val dslContext: DSLContext) { val COALESCED_VIEW = DSL.table("move_analysis_coalesced") val GAME_TYPE = DSL.field("game_type", String::class.java) val GAME_ID = DSL.field("game_data_id", String::class.java) - val ENTRY_CREATION = DSL.field("entry_creation", Instant::class.java) + + // jOOQ's DefaultDataType registry does not know about kotlin.time.Instant, + // so we attach the same converter the codegen uses for timestamptz columns. + val INSTANT_TYPE: DataType = + SQLDataType.TIMESTAMPWITHTIMEZONE(6).asConvertedDataType(OffsetDateTimeInstantConverter()) + val ENTRY_CREATION = DSL.field("entry_creation", INSTANT_TYPE) fun moveAnalysisTableIdCondition(gameId: GameId): Condition { return moveAnalysisTableIdField(gameId.type).eq(gameId.id) diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsBotGameDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsBotGameDaoService.kt index 92e2a5d3d..dc871f84f 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsBotGameDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsBotGameDaoService.kt @@ -19,7 +19,9 @@ import io.elephantchess.model.GameEventType import io.elephantchess.model.GameEventType.* import io.elephantchess.model.Outcome import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KotlinLogging +import org.jooq.Condition import org.jooq.DSLContext import org.jooq.impl.DSL import org.jooq.kotlin.coroutines.transactionCoroutine @@ -64,29 +66,55 @@ class PlayerVsBotGameDaoService(private val dslContext: DSLContext) { .awaitMappedRecords() } - suspend fun listLastGamesByIdentifiedUsers( + suspend fun listLatestGamesByIdentifiedUsers( limit: Int, minMoveIndex: Int? = null, - beforeTs: Long? = null + beforeTs: Long? = null, + excludeAutoResigned: Boolean = false, + distinctByUsers: Boolean = false ): List { - var query = - dslContext - .select() - .from(BOT_GAME) - .where(BOT_GAME.USER_ID.isNotNull) + val conditions = mutableListOf() + conditions += BOT_GAME.USER_ID.isNotNull if (minMoveIndex != null) { - query = query.and(BOT_GAME.CURRENT_HALF_MOVE_INDEX.ge(minMoveIndex)) + conditions += BOT_GAME.CURRENT_HALF_MOVE_INDEX.ge(minMoveIndex) } if (beforeTs != null) { - query = query.and(BOT_GAME.LAST_UPDATED.isBeforeEpochMillis(beforeTs)) + conditions += BOT_GAME.LAST_UPDATED.isBeforeEpochMillis(beforeTs) } - return query - .orderBy(BOT_GAME.LAST_UPDATED.desc()) - .limit(limit) - .awaitMappedRecords() + if (excludeAutoResigned) { + conditions += BOT_GAME.GAME_STATUS.ne(AUTO_RESIGNED) + } + + return if (distinctByUsers) { + val rn = DSL.rowNumber() + .over(DSL.partitionBy(BOT_GAME.USER_ID).orderBy(BOT_GAME.LAST_UPDATED.desc())) + .`as`("rn") + + val sub = dslContext + .select(BOT_GAME.asterisk(), rn) + .from(BOT_GAME) + .where(conditions) + .asTable("t") + + dslContext + .select(sub.asterisk()) + .from(sub) + .where(sub.field("rn", Int::class.java)!!.eq(1)) + .orderBy(sub.field(BOT_GAME.LAST_UPDATED)!!.desc()) + .limit(limit) + .awaitMappedRecords() + } else { + dslContext + .select() + .from(BOT_GAME) + .where(conditions) + .orderBy(BOT_GAME.LAST_UPDATED.desc()) + .limit(limit) + .awaitMappedRecords() + } } suspend fun listPreAnalysisToDelete(limit: Duration): List> { @@ -244,7 +272,19 @@ class PlayerVsBotGameDaoService(private val dslContext: DSLContext) { return dslContext .selectCount() .from(BOT_GAME) - .where(BOT_GAME.CURRENT_HALF_MOVE_INDEX.ge(minIndex)) + .where(BOT_GAME.CURRENT_HALF_MOVE_INDEX.ge(minIndex).or(BOT_GAME.GAME_STATUS.`in`(CHECKMATED, STALEMATED))) + .awaitSingleValue()!! + } + + suspend fun countManchuGames(minIndex: Int): Int { + val countCondition = + BOT_GAME.CURRENT_HALF_MOVE_INDEX.ge(minIndex) + .or(BOT_GAME.GAME_STATUS.`in`(CHECKMATED, STALEMATED)) + + return dslContext + .selectCount() + .from(BOT_GAME) + .where(countCondition.and(BOT_GAME.VARIANT.eq(Variant.MANCHU))) .awaitSingleValue()!! } @@ -405,4 +445,13 @@ class PlayerVsBotGameDaoService(private val dslContext: DSLContext) { } } + suspend fun transferFromGuestToUser(guestUserId: String, newUserId: String) { + dslContext + .update(BOT_GAME) + .set(BOT_GAME.USER_ID, newUserId) + .set(BOT_GAME.GUEST_USER_ID, guestUserId) + .where(BOT_GAME.USER_ID.eq(guestUserId)) + .awaitExecute() + } + } diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt index 7dcdc968e..214bbf81d 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt @@ -23,6 +23,7 @@ import io.elephantchess.model.GameEventType.Companion.gameEndedStatuses import io.elephantchess.model.GameEventType.Companion.inProgressStatuses import io.elephantchess.utils.TryEither import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.reactor.awaitSingle import org.jooq.Condition @@ -59,6 +60,7 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { timeControlIncrement: Int?, userType: UserType, userId: String, + variant: Variant, ): List { var query = dslContext @@ -70,6 +72,7 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { .and(GAME.IS_RATED.eq(isRated)) .and(GAME.INVITER.notEqual(userId)) .and(GAME.PRIVATE_INVITE.eq(false)) + .and(GAME.VARIANT.eq(variant)) query = when (timeControlIncrement) { null -> query.and(GAME.TIME_CONTROL_INCREMENT.isNull) @@ -121,6 +124,24 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { .awaitMappedRecords() } + suspend fun listLastGamesByUserId(userId: String, limit: Int, minMoveIndex: Int, beforeTs: Long?): List { + var sql = dslContext + .select() + .from(GAME) + .where(isPlaying(userId)) + .and(GAME.CURRENT_HALF_MOVE_INDEX.ge(minMoveIndex)) + .and(GAME.CONTAINS_ERRORS.eq(false)) + + if (beforeTs != null) { + sql = sql.and(GAME.LAST_UPDATED.isBeforeEpochMillis(beforeTs)) + } + + return sql + .orderBy(GAME.LAST_UPDATED.desc()) + .limit(limit) + .awaitMappedRecords() + } + suspend fun listPotentiallyFlaggedGames(): List { val now = Clock.System.now() val games = mutableListOf() @@ -275,7 +296,19 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { return dslContext .selectCount() .from(GAME) - .where(GAME.CURRENT_HALF_MOVE_INDEX.ge(minMoveIndex)) + .where(GAME.CURRENT_HALF_MOVE_INDEX.ge(minMoveIndex).or(GAME.GAME_STATUS.`in`(CHECKMATED, STALEMATED))) + .awaitSingleValue()!! + } + + suspend fun countManchuGames(minMoveIndex: Int): Int { + val countCondition = + GAME.CURRENT_HALF_MOVE_INDEX.ge(minMoveIndex) + .or(GAME.GAME_STATUS.`in`(CHECKMATED, STALEMATED)) + + return dslContext + .selectCount() + .from(GAME) + .where(countCondition.and(GAME.VARIANT.eq(Variant.MANCHU))) .awaitSingleValue()!! } @@ -365,17 +398,18 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { } } - suspend fun fetchRatingForUser(userId: String, timeControlCategory: TimeControlCategory): Int? { - return fetchRatingForUser(dslContext, userId, timeControlCategory) + suspend fun fetchRatingForUser(userId: String, timeControlCategory: TimeControlCategory, variant: Variant): Int? { + return fetchRatingForUser(dslContext, userId, timeControlCategory, variant) } private suspend fun fetchRatingForUser( context: DSLContext, userId: String, timeControlCategory: TimeControlCategory, + variant: Variant, ): Int? { return context - .select(findRatingField(timeControlCategory)) + .select(findRatingField(timeControlCategory, variant)) .from(USER) .where(USER.ID.eq((userId))) .awaitSingleValue() @@ -435,6 +469,7 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { } suspend fun fetchGameStates(gameIds: List): Map { + // TODO: Flux.from() -> can we not use our shortcut thingy? return Flux.from( dslContext .select( @@ -530,6 +565,14 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { .awaitSingleValue() } + suspend fun fetchVariant(gameId: String): Variant? { + return dslContext + .select(GAME.VARIANT) + .from(GAME) + .where(GAME.ID.eq(gameId)) + .awaitSingleValue() + } + /** * Returns email if we should send a notification, null otherwise */ @@ -759,7 +802,7 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { gameId: String, move: String, playMoveCallback: (Game) -> PlayMoveCallbackResult, - hasViolatedPerpetualCheckingRule: (List) -> PerpetualCheckingCallbackResult?, + hasViolatedPerpetualCheckingRule: (Game, List) -> PerpetualCheckingCallbackResult?, updateRatingsCallback: (Game, Outcome, Int, Int) -> UpdateRatingsCallbackResult?, ): TryEither { return dslContext.transactionalContextTry { transactional -> @@ -799,7 +842,7 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { } else if (playMoveResult.mustCheckPerpetualChecking) { // check if violated perpetual checking rule val moves = listMoves(transactional, gameId) - hasViolatedPerpetualCheckingRule(moves)?.let { perpetualCheckingResult -> + hasViolatedPerpetualCheckingRule(gameRecord, moves)?.let { perpetualCheckingResult -> persistVictory(perpetualCheckingResult.newGameEventType, perpetualCheckingResult.outcome) playMoveResult = playMoveResult.copy( newGameEventType = perpetualCheckingResult.newGameEventType, @@ -856,10 +899,11 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { suspend fun persistRatingUpdate() { val timeControlCategory = gameRecord.timeControlCategory!! - val ratingField = findRatingField(timeControlCategory) + val variant = gameRecord.variant + val ratingField = findRatingField(timeControlCategory, variant) // we use the current rating instead of the rating the user had at the beginning of the game - val inviterRating = fetchRatingForUser(transactional, gameRecord.inviter, timeControlCategory) - val inviteeRating = fetchRatingForUser(transactional, gameRecord.invitee, timeControlCategory) + val inviterRating = fetchRatingForUser(transactional, gameRecord.inviter, timeControlCategory, variant) + val inviteeRating = fetchRatingForUser(transactional, gameRecord.invitee, timeControlCategory, variant) val updatedRatings = updateRatingsCallback(gameRecord, outcome, inviterRating!!, inviteeRating!!) suspend fun updateUserRating(userId: String, rating: Int) { @@ -943,14 +987,25 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { return field } - fun findRatingField(timeControlCategory: TimeControlCategory): TableField { - return when (timeControlCategory) { - TimeControlCategory.BULLET -> return USER.GAME_RATING_BULLET - TimeControlCategory.BLITZ -> return USER.GAME_RATING_BLITZ - TimeControlCategory.RAPID -> return USER.GAME_RATING_RAPID - TimeControlCategory.CLASSICAL -> return USER.GAME_RATING_CLASSICAL - TimeControlCategory.SEVERAL_DAYS -> USER.GAME_RATING_SEVERAL_DAYS - TimeControlCategory.CORRESPONDENCE -> USER.GAME_RATING_CORRESPONDENCE + fun findRatingField(timeControlCategory: TimeControlCategory, variant: Variant): TableField { + return when (variant) { + Variant.XIANGQI -> when (timeControlCategory) { + TimeControlCategory.BULLET -> USER.GAME_RATING_BULLET + TimeControlCategory.BLITZ -> USER.GAME_RATING_BLITZ + TimeControlCategory.RAPID -> USER.GAME_RATING_RAPID + TimeControlCategory.CLASSICAL -> USER.GAME_RATING_CLASSICAL + TimeControlCategory.SEVERAL_DAYS -> USER.GAME_RATING_SEVERAL_DAYS + TimeControlCategory.CORRESPONDENCE -> USER.GAME_RATING_CORRESPONDENCE + } + + Variant.MANCHU -> when (timeControlCategory) { + TimeControlCategory.BULLET -> USER.GAME_RATING_MANCHU_BULLET + TimeControlCategory.BLITZ -> USER.GAME_RATING_MANCHU_BLITZ + TimeControlCategory.RAPID -> USER.GAME_RATING_MANCHU_RAPID + TimeControlCategory.CLASSICAL -> USER.GAME_RATING_MANCHU_CLASSICAL + TimeControlCategory.SEVERAL_DAYS -> USER.GAME_RATING_MANCHU_SEVERAL_DAYS + TimeControlCategory.CORRESPONDENCE -> USER.GAME_RATING_MANCHU_CORRESPONDENCE + } } } @@ -1009,4 +1064,22 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { } } + suspend fun transferFromGuestToUser(guestUserId: String, newUserId: String) { + dslContext.transactionCoroutine { cfg -> + val transaction = DSL.using(cfg) + + transaction.update(GAME) + .set(GAME.INVITER, newUserId) + .set(GAME.GUEST_USER_ID, guestUserId) + .where(GAME.INVITER.eq(guestUserId)) + .awaitExecute() + + transaction.update(GAME) + .set(GAME.INVITEE, newUserId) + .set(GAME.GUEST_USER_ID, guestUserId) + .where(GAME.INVITEE.eq(guestUserId)) + .awaitExecute() + } + } + } diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PuzzleResultDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PuzzleResultDaoService.kt index fca122c06..1fdfb9d56 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PuzzleResultDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PuzzleResultDaoService.kt @@ -3,7 +3,6 @@ package io.elephantchess.db.services import io.elephantchess.db.dao.codegen.Tables.* import io.elephantchess.db.dao.codegen.tables.daos.PuzzleResultDao import io.elephantchess.db.dao.codegen.tables.pojos.PuzzleResult -import io.elephantchess.db.dao.codegen.tables.pojos.PuzzleResultAnonymous import io.elephantchess.db.model.PlayedPuzzleRecord import io.elephantchess.db.utils.* import io.elephantchess.model.PuzzleOutcome @@ -275,4 +274,13 @@ class PuzzleResultDaoService(private val dslContext: DSLContext) { } + suspend fun transferFromGuestToUser(guestUserId: String, newUserId: String) { + dslContext + .update(PUZZLE_RESULT) + .set(PUZZLE_RESULT.USER_ID, newUserId) + .set(PUZZLE_RESULT.GUEST_USER_ID, guestUserId) + .where(PUZZLE_RESULT.USER_ID.eq(guestUserId)) + .awaitExecute() + } + } diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferenceGameDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferenceGameDaoService.kt index 610c41ed6..5869e1775 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferenceGameDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferenceGameDaoService.kt @@ -61,7 +61,7 @@ class ReferenceGameDaoService(private val dslContext: DSLContext) { .awaitRecords() } - suspend fun findByEventId(eventId: String) : List { + suspend fun findByEventId(eventId: String): List { return dslContext .select() .from(REFERENCE_GAME) @@ -365,25 +365,103 @@ class ReferenceGameDaoService(private val dslContext: DSLContext) { limit: Int, numberOfResults: Int ) { - val record = ReferenceGameSearchQuery() - record.queryId = generateId() - record.userId = userId - record.searchStart = searchStart - record.searchEnd = searchEnd - record.playerName = playerName - record.playerId = playerId - record.playerColor = playerColor - record.eventName = eventName - record.eventId = eventId - record.fen = fen - record.offset = offset - record.limit = limit - record.numberOfResults = numberOfResults - record.queryTime = Clock.System.now() + // Only track the first page (offset null or 0) to avoid duplicate entries for pagination + if (offset != null && offset > 0) { + return + } - dslContext.transactionCoroutine { cfg -> - ReferenceGameSearchQueryDao(cfg).insertReactive(record) + val now = Clock.System.now() + + // Sanitize player name the same way the JS client does: strip parenthetical substrings + // (e.g. Chinese characters like "(陈松顺)") so "Chen Songshun (陈松顺)" and + // "Chen Songshun" are treated as the same search entry. + val sanitizedPlayerName = sanitizePlayerName(playerName) + + val existingId = dslContext + .select(REFERENCE_GAME_SEARCH_QUERY.QUERY_ID) + .from(REFERENCE_GAME_SEARCH_QUERY) + .where(REFERENCE_GAME_SEARCH_QUERY.USER_ID.eq(userId)) + .and( + if (sanitizedPlayerName != null) { + lower(REFERENCE_GAME_SEARCH_QUERY.PLAYER_NAME).eq(sanitizedPlayerName.lowercase()) + } else { + REFERENCE_GAME_SEARCH_QUERY.PLAYER_NAME.isNull + } + ) + .and( + if (playerId != null) REFERENCE_GAME_SEARCH_QUERY.PLAYER_ID.eq(playerId) + else REFERENCE_GAME_SEARCH_QUERY.PLAYER_ID.isNull + ) + .and( + if (playerColor != null) REFERENCE_GAME_SEARCH_QUERY.PLAYER_COLOR.eq(playerColor) + else REFERENCE_GAME_SEARCH_QUERY.PLAYER_COLOR.isNull + ) + .and( + if (eventName != null) REFERENCE_GAME_SEARCH_QUERY.EVENT_NAME.eq(eventName) + else REFERENCE_GAME_SEARCH_QUERY.EVENT_NAME.isNull + ) + .and( + if (eventId != null) REFERENCE_GAME_SEARCH_QUERY.EVENT_ID.eq(eventId) + else REFERENCE_GAME_SEARCH_QUERY.EVENT_ID.isNull + ) + .and( + if (searchStart != null) REFERENCE_GAME_SEARCH_QUERY.SEARCH_START.eq(searchStart) + else REFERENCE_GAME_SEARCH_QUERY.SEARCH_START.isNull + ) + .and( + if (searchEnd != null) REFERENCE_GAME_SEARCH_QUERY.SEARCH_END.eq(searchEnd) + else REFERENCE_GAME_SEARCH_QUERY.SEARCH_END.isNull + ) + .and( + if (fen != null) REFERENCE_GAME_SEARCH_QUERY.FEN.eq(fen) + else REFERENCE_GAME_SEARCH_QUERY.FEN.isNull + ) + .awaitSingleValue() + + if (existingId != null) { + dslContext + .update(REFERENCE_GAME_SEARCH_QUERY) + .set(REFERENCE_GAME_SEARCH_QUERY.UPDATE_TIME.fixed(), now) + .set(REFERENCE_GAME_SEARCH_QUERY.NUMBER_OF_RESULTS.fixed(), numberOfResults) + .where(REFERENCE_GAME_SEARCH_QUERY.QUERY_ID.eq(existingId)) + .awaitExecute() + return } + + val queryId = generateId() + dslContext + .insertInto(REFERENCE_GAME_SEARCH_QUERY.fixed()) + .set(REFERENCE_GAME_SEARCH_QUERY.QUERY_ID.fixed(), queryId) + .set(REFERENCE_GAME_SEARCH_QUERY.USER_ID.fixed(), userId) + .set(REFERENCE_GAME_SEARCH_QUERY.QUERY_TIME.fixed(), now) + .set(REFERENCE_GAME_SEARCH_QUERY.UPDATE_TIME.fixed(), now) + .set(REFERENCE_GAME_SEARCH_QUERY.SEARCH_START.fixed(), searchStart) + .set(REFERENCE_GAME_SEARCH_QUERY.SEARCH_END.fixed(), searchEnd) + .set(REFERENCE_GAME_SEARCH_QUERY.PLAYER_NAME.fixed(), sanitizedPlayerName) + .set(REFERENCE_GAME_SEARCH_QUERY.PLAYER_ID.fixed(), playerId) + .set(REFERENCE_GAME_SEARCH_QUERY.PLAYER_COLOR.fixed(), playerColor) + .set(REFERENCE_GAME_SEARCH_QUERY.EVENT_NAME.fixed(), eventName) + .set(REFERENCE_GAME_SEARCH_QUERY.EVENT_ID.fixed(), eventId) + .set(REFERENCE_GAME_SEARCH_QUERY.FEN.fixed(), fen) + .set(REFERENCE_GAME_SEARCH_QUERY.OFFSET.fixed(), offset) + .set(REFERENCE_GAME_SEARCH_QUERY.LIMIT.fixed(), limit) + .set(REFERENCE_GAME_SEARCH_QUERY.NUMBER_OF_RESULTS.fixed(), numberOfResults) + .awaitExecute() + } + + suspend fun listUserSearches(userId: String, beforeTime: Instant?, limit: Int): List { + val condition = REFERENCE_GAME_SEARCH_QUERY.USER_ID.eq(userId) + .let { cond -> + if (beforeTime != null) cond.and(REFERENCE_GAME_SEARCH_QUERY.UPDATE_TIME.lessThan(beforeTime)) + else cond + } + + return dslContext + .selectFrom(REFERENCE_GAME_SEARCH_QUERY) + .where(condition) + .orderBy(REFERENCE_GAME_SEARCH_QUERY.UPDATE_TIME.desc()) + .limit(limit) + .awaitMappedRecords() } suspend fun listLatestDatabaseSearches(limit: Int): List { @@ -412,4 +490,22 @@ class ReferenceGameDaoService(private val dslContext: DSLContext) { } } + suspend fun transferFromGuestToUser(guestUserId: String, newUserId: String) { + dslContext + .update(REFERENCE_GAME_SEARCH_QUERY) + .set(REFERENCE_GAME_SEARCH_QUERY.USER_ID, newUserId) + .set(REFERENCE_GAME_SEARCH_QUERY.GUEST_USER_ID, guestUserId) + .where(REFERENCE_GAME_SEARCH_QUERY.USER_ID.eq(guestUserId)) + .awaitExecute() + } + + companion object { + private val PARENTHETICAL_REGEX = Regex("""\s*\([^)]*\)\s*""") + private val WHITESPACE_REGEX = Regex("""\s+""") + + fun sanitizePlayerName(playerName: String?): String? = + playerName?.replace(PARENTHETICAL_REGEX, "")?.replace(WHITESPACE_REGEX, " ")?.trim() + ?.takeIf { it.isNotEmpty() } + } + } diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferencePlayerDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferencePlayerDaoService.kt index 7cf07102f..fc3e6c0bd 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferencePlayerDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/ReferencePlayerDaoService.kt @@ -476,7 +476,10 @@ class ReferencePlayerDaoService(private val dslContext: DSLContext) { REFERENCE_PLAYER.CHINESE_NAME ) .from(REFERENCE_PLAYER) - .where(lower(REFERENCE_PLAYER.CANONICAL_NAME).startsWith(startsWith.lowercase())) + .where( + lower(REFERENCE_PLAYER.CANONICAL_NAME).startsWith(startsWith.lowercase()) + .or(REFERENCE_PLAYER.CHINESE_NAME.startsWith(startsWith)) + ) .and(REFERENCE_PLAYER.IS_VISIBLE.eq(true)) .orderBy(REFERENCE_PLAYER.CANONICAL_NAME) .limit(limit) @@ -493,7 +496,10 @@ class ReferencePlayerDaoService(private val dslContext: DSLContext) { REFERENCE_PLAYER.CHINESE_NAME ) .from(REFERENCE_PLAYER) - .where(lower(REFERENCE_PLAYER.CANONICAL_NAME).contains(contains.lowercase())) + .where( + lower(REFERENCE_PLAYER.CANONICAL_NAME).contains(contains.lowercase()) + .or(REFERENCE_PLAYER.CHINESE_NAME.contains(contains)) + ) .and(REFERENCE_PLAYER.IS_VISIBLE.eq(true)) .orderBy(REFERENCE_PLAYER.CANONICAL_NAME) .limit(limit) diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserDaoService.kt index 398d7a957..783cf48f2 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserDaoService.kt @@ -12,6 +12,7 @@ import io.elephantchess.model.TimeControlCategory import io.elephantchess.model.UserType import io.elephantchess.model.UserType.AUTHENTICATED import io.elephantchess.utils.safeRandomAlphaNumericString +import io.github.oshai.kotlinlogging.KLogger import org.jooq.DSLContext import org.jooq.Record2 import org.jooq.TableField @@ -23,7 +24,7 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant -class UserDaoService(private val dslContext: DSLContext) { +class UserDaoService(private val dslContext: DSLContext, val logger: KLogger) { suspend fun save(user: User): String { dslContext.transactionCoroutine { cfg -> @@ -88,7 +89,7 @@ class UserDaoService(private val dslContext: DSLContext) { .firstOrNull() } - suspend fun updateProfileSettings(userId: String, description: String, country: String) { + suspend fun updateProfileSettings(userId: String, description: String, country: String?) { dslContext.transactionCoroutine { cfg -> DSL .using(cfg) @@ -303,6 +304,46 @@ class UserDaoService(private val dslContext: DSLContext) { .awaitSingleMappedRecord() } + suspend fun listManuallyConfirmedEmailAddresses(): List { + return dslContext + .select(USER.EMAIL) + .from(USER) + .where(USER.EMAIL.isNotNull) + .and(USER.EMAIL_CONFIRMED_AT.isNotNull) + .awaitMappedRecords() + } + + suspend fun findByEmailConfirmationCode(code: String): User? { + return dslContext + .select() + .from(USER) + .where(USER.EMAIL_CONFIRMATION_CODE.eq(code)) + .awaitSingleMappedRecord() + } + + suspend fun markEmailConfirmed(userId: String, confirmedAt: Instant) { + dslContext.transactionCoroutine { cfg -> + DSL + .using(cfg) + .update(USER) + .set(USER.EMAIL_CONFIRMED_AT.fixed(), confirmedAt) + .where(USER.ID.eq(userId)) + .awaitExecute() + } + } + + suspend fun updateEmailConfirmationCode(userId: String, code: String, createdAt: Instant) { + dslContext.transactionCoroutine { cfg -> + DSL + .using(cfg) + .update(USER) + .set(USER.EMAIL_CONFIRMATION_CODE, code) + .set(USER.EMAIL_CONFIRMATION_CODE_CREATED_AT.fixed(), createdAt) + .where(USER.ID.eq(userId)) + .awaitExecute() + } + } + suspend fun existsById(userId: String): Boolean { return dslContext .selectCount() @@ -341,6 +382,33 @@ class UserDaoService(private val dslContext: DSLContext) { } } + suspend fun transferRatingsFromGuest(guestUserId: String, newUserId: String) { + val guestUser = findById(guestUserId) + if (guestUser == null) { + logger.warn { "transferRatingsFromGuest: guest user $guestUserId not found — ratings not transferred to $newUserId" } + return + } + dslContext.transactionCoroutine { cfg -> + DSL.using(cfg) + .update(USER) + .set(USER.PUZZLE_RATING, guestUser.puzzleRating) + .set(USER.GAME_RATING_BULLET, guestUser.gameRatingBullet) + .set(USER.GAME_RATING_BLITZ, guestUser.gameRatingBlitz) + .set(USER.GAME_RATING_RAPID, guestUser.gameRatingRapid) + .set(USER.GAME_RATING_CLASSICAL, guestUser.gameRatingClassical) + .set(USER.GAME_RATING_SEVERAL_DAYS, guestUser.gameRatingSeveralDays) + .set(USER.GAME_RATING_CORRESPONDENCE, guestUser.gameRatingCorrespondence) + .set(USER.GAME_RATING_MANCHU_BULLET, guestUser.gameRatingManchuBullet) + .set(USER.GAME_RATING_MANCHU_BLITZ, guestUser.gameRatingManchuBlitz) + .set(USER.GAME_RATING_MANCHU_RAPID, guestUser.gameRatingManchuRapid) + .set(USER.GAME_RATING_MANCHU_CLASSICAL, guestUser.gameRatingManchuClassical) + .set(USER.GAME_RATING_MANCHU_SEVERAL_DAYS, guestUser.gameRatingManchuSeveralDays) + .set(USER.GAME_RATING_MANCHU_CORRESPONDENCE, guestUser.gameRatingManchuCorrespondence) + .where(USER.ID.eq(newUserId)) + .awaitExecute() + } + } + suspend fun updateLastOnline(userIds: List) { if (userIds.isNotEmpty()) { dslContext.transactionCoroutine { cfg -> @@ -546,6 +614,12 @@ class UserDaoService(private val dslContext: DSLContext) { fields += USER.GAME_RATING_CLASSICAL fields += USER.GAME_RATING_SEVERAL_DAYS fields += USER.GAME_RATING_CORRESPONDENCE + fields += USER.GAME_RATING_MANCHU_BULLET + fields += USER.GAME_RATING_MANCHU_BLITZ + fields += USER.GAME_RATING_MANCHU_RAPID + fields += USER.GAME_RATING_MANCHU_CLASSICAL + fields += USER.GAME_RATING_MANCHU_SEVERAL_DAYS + fields += USER.GAME_RATING_MANCHU_CORRESPONDENCE return fields } diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserSessionDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserSessionDaoService.kt index 2cf302f8e..54fb0c8f1 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserSessionDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/UserSessionDaoService.kt @@ -108,6 +108,45 @@ class UserSessionDaoService( .map { mapToRecord(it) } } + suspend fun listAuthenticatedSessionsForUser(userId: String, limit: Int, offset: Int = 0): List { + return dslContext + .select() + .from(USER_SESSION) + .where(USER_SESSION.USER_ID.eq(userId)) + .orderBy(USER_SESSION.LAST_UPDATED.desc()) + .limit(limit) + .offset(offset) + .awaitMappedRecords() + .map { mapToRecord(it) } + } + + suspend fun countAuthenticatedSessionsForUser(userId: String): Int { + return dslContext + .selectCount() + .from(USER_SESSION) + .where(USER_SESSION.USER_ID.eq(userId)) + .awaitSingleValue() ?: 0 + } + + suspend fun deleteAuthenticatedSessionsForUser(userId: String, sessionIds: Collection): Int { + if (sessionIds.isEmpty()) { + return 0 + } + + return dslContext + .deleteFrom(USER_SESSION) + .where(USER_SESSION.USER_ID.eq(userId)) + .and(USER_SESSION.ID.`in`(sessionIds)) + .awaitExecute() + } + + suspend fun deleteAllAuthenticatedSessionsForUser(userId: String): Int { + return dslContext + .deleteFrom(USER_SESSION) + .where(USER_SESSION.USER_ID.eq(userId)) + .awaitExecute() + } + suspend fun findByUserAgent(userAgent: String): List { val records = mutableListOf() @@ -162,6 +201,7 @@ class UserSessionDaoService( fun mapToRecord(userSession: UserSession): UserSessionRecord { return UserSessionRecord( + id = userSession.id, userId = userSession.userId, remoteAddress = userSession.remoteAddress, userAgent = userSession.userAgent, diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/DbUtils.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/DbUtils.kt index 1f71564b4..e59c19ace 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/DbUtils.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/DbUtils.kt @@ -12,7 +12,7 @@ import io.r2dbc.spi.ConnectionFactory import io.r2dbc.spi.ConnectionFactoryOptions import liquibase.resource.ClassLoaderResourceAccessor import liquibase.resource.ResourceAccessor -import org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric +import org.apache.commons.lang3.RandomStringUtils.insecure import org.jooq.DSLContext import org.jooq.SQLDialect.POSTGRES import org.jooq.conf.RenderNameCase.LOWER @@ -36,7 +36,7 @@ fun generateId(size: Int = 12): String { } var id = convertLongToBase62(System.nanoTime()) - id += randomAlphanumeric(size - id.length) + id += insecure().nextAlphanumeric(size - id.length) return id } diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/JooqOps.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/JooqOps.kt index 55ead2c5e..a2f98d2b1 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/JooqOps.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/JooqOps.kt @@ -24,7 +24,7 @@ fun Table.fixed(): Table { * Workaround for inserts and updates */ fun Field.fixed(): Field { - return DSL.field(name.lowercase()) + return DSL.field(DSL.quotedName(name.lowercase())) } fun Field.eqIgnoreCaseTrimmed(value: String): Condition = @@ -45,11 +45,11 @@ fun Field.isOlderThan(duration: Duration): Field { return isBefore(limit) } -fun Field.isBeforeEpochMillis(timestampMillis: Long): Field { +fun Field.isBeforeEpochMillis(timestampMillis: Long): Condition { return isBefore(Instant.fromEpochMilliseconds(timestampMillis)) } -fun Field.isWithin(duration: Duration): Field { +fun Field.isWithin(duration: Duration): Condition { val limit = Clock.System.now() - duration return isAfter(limit) } @@ -122,11 +122,11 @@ fun diffInSeconds(f1: Field, f2: Field): Field { return DSL.extract(f1, DatePart.EPOCH).minus(DSL.extract(f2, DatePart.EPOCH)) } -private fun Field.isAfter(timestamp: Instant): Field { +fun Field.isAfter(timestamp: Instant): Condition { return greaterThan(timestamp) } -private fun Field.isBefore(timestamp: Instant): Field { +fun Field.isBefore(timestamp: Instant): Condition { return lessThan(timestamp) } diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/RecordOps.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/RecordOps.kt index 7e3f120d4..f6901672c 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/RecordOps.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/utils/RecordOps.kt @@ -6,6 +6,7 @@ import io.elephantchess.db.model.TimeControlRecord import io.elephantchess.model.* import io.elephantchess.model.GameEventType.* import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant import org.apache.commons.lang3.BooleanUtils import io.elephantchess.sevenkingdoms.Color as Color7k @@ -28,14 +29,24 @@ val User.roles: List return roles.toList() } -fun User.rating(timeControlCategory: TimeControlCategory): Int { - return when (timeControlCategory) { - TimeControlCategory.BULLET -> gameRatingBullet - TimeControlCategory.BLITZ -> gameRatingBlitz - TimeControlCategory.RAPID -> gameRatingRapid - TimeControlCategory.CLASSICAL -> gameRatingClassical - TimeControlCategory.SEVERAL_DAYS -> gameRatingSeveralDays - TimeControlCategory.CORRESPONDENCE -> gameRatingCorrespondence +fun User.rating(timeControlCategory: TimeControlCategory, variant: Variant = Variant.XIANGQI): Int { + return when (variant) { + Variant.XIANGQI -> when (timeControlCategory) { + TimeControlCategory.BULLET -> gameRatingBullet + TimeControlCategory.BLITZ -> gameRatingBlitz + TimeControlCategory.RAPID -> gameRatingRapid + TimeControlCategory.CLASSICAL -> gameRatingClassical + TimeControlCategory.SEVERAL_DAYS -> gameRatingSeveralDays + TimeControlCategory.CORRESPONDENCE -> gameRatingCorrespondence + } + Variant.MANCHU -> when (timeControlCategory) { + TimeControlCategory.BULLET -> gameRatingManchuBullet + TimeControlCategory.BLITZ -> gameRatingManchuBlitz + TimeControlCategory.RAPID -> gameRatingManchuRapid + TimeControlCategory.CLASSICAL -> gameRatingManchuClassical + TimeControlCategory.SEVERAL_DAYS -> gameRatingManchuSeveralDays + TimeControlCategory.CORRESPONDENCE -> gameRatingManchuCorrespondence + } } } @@ -207,6 +218,10 @@ private fun Game.userIdByColor(color: Color): String? { } } +fun BotGame.prettyEngineName() : String { + return "$engine (${depth})" +} + fun SevenKingdomsGame.minColorPerPlayer(): Int { return when (minPlayers) { 4 -> 1 diff --git a/webapp-html-renderer/src/main/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolver.kt b/webapp-html-renderer/src/main/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolver.kt new file mode 100644 index 000000000..2b1df0d9b --- /dev/null +++ b/webapp-html-renderer/src/main/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolver.kt @@ -0,0 +1,20 @@ +package io.elephantchess.htmlrenderer + +import kotlinx.html.TagConsumer +import kotlinx.html.stream.appendHTML + +/** + * Resolves a template tag by rendering HTML content with the Ktor/kotlinx.html DSL. + */ +class KtorHtmlBuilderTagResolver( + override val tagName: String, + private val htmlBuilder: TagConsumer.() -> Unit, +) : TagResolver { + + override suspend fun resolveContent() = listOf( + buildString { + appendHTML().run(htmlBuilder) + } + ) + +} diff --git a/webapp-html-renderer/src/test/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolverTest.kt b/webapp-html-renderer/src/test/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolverTest.kt new file mode 100644 index 000000000..5a53e98ac --- /dev/null +++ b/webapp-html-renderer/src/test/kotlin/io/elephantchess/htmlrenderer/KtorHtmlBuilderTagResolverTest.kt @@ -0,0 +1,34 @@ +package io.elephantchess.htmlrenderer + +import kotlinx.coroutines.test.runTest +import kotlinx.html.a +import kotlinx.html.p +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class KtorHtmlBuilderTagResolverTest { + + @Test + fun `resolves html content from builder`() = runTest { + val url = "https://elephantchess.io/game?id=42" + val resolver = KtorHtmlBuilderTagResolver("game_link") { + a(href = url) { +url } + } + + val resolved = resolver.resolveContent() + assertEquals(listOf("""$url"""), resolved) + assertFalse(resolved.single().endsWith("\n")) + } + + @Test + fun `resolves non-anchor html fragments`() = runTest { + val resolver = KtorHtmlBuilderTagResolver("paragraph") { + p { +"Hello from builder" } + } + + // kotlinx.html keeps a trailing newline for this block-level tag. + assertEquals(listOf("

Hello from builder

\n"), resolver.resolveContent()) + } + +} diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/KoinModule.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/KoinModule.kt index 2b976fdf5..b6e29dc74 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/KoinModule.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/KoinModule.kt @@ -64,6 +64,7 @@ private fun daoModule() = module { singleAuto() singleAuto() singleAuto() + singleAuto() singleAuto() singleAuto() singleAuto() diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt index 8d607982f..a52b1fd23 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt @@ -5,6 +5,7 @@ import io.elephantchess.model.GameEventType import io.elephantchess.model.Outcome import io.elephantchess.model.UserType import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant data class ListBotGamesResponse( val entries: List, @@ -25,6 +26,7 @@ data class ListBotGamesResponse( val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, + val variant: Variant?, ) } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt index 78137f107..3ccf20d1b 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt @@ -2,6 +2,7 @@ package io.elephantchess.servicelayer.dto.admin import io.elephantchess.model.GameEventType import io.elephantchess.model.GameJoinSource +import io.elephantchess.xiangqi.Variant data class ListGamesResponse(val entries: List) { @@ -23,7 +24,8 @@ data class ListGamesResponse(val entries: List) { val winnerUserId: String?, val created: Long, val lastUpdated: Long, - val sourceType: GameJoinSource? + val sourceType: GameJoinSource?, + val variant: Variant?, ) } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/CreateBotGameRequest.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/CreateBotGameRequest.kt index b2d3219cc..8b829e34c 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/CreateBotGameRequest.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/CreateBotGameRequest.kt @@ -2,10 +2,12 @@ package io.elephantchess.servicelayer.dto.botgame import io.elephantchess.model.Engine import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant data class CreateBotGameRequest( val color: Color, val depth: Int, val engine: Engine, val startFen: String?, + val variant: Variant = Variant.XIANGQI, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/GetBotGameDataResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/GetBotGameDataResponse.kt index a952751ea..72e910c39 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/GetBotGameDataResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/GetBotGameDataResponse.kt @@ -5,6 +5,7 @@ import io.elephantchess.model.GameEventType import io.elephantchess.model.Outcome import io.elephantchess.model.UserType import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant data class GetBotGameDataResponse( val userId: String?, @@ -20,4 +21,5 @@ data class GetBotGameDataResponse( val created: Long, val lastUpdated: Long, val outcome: Outcome?, + val variant: Variant ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt index 7045503c6..ef725a4c8 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt @@ -4,6 +4,7 @@ import io.elephantchess.model.Engine import io.elephantchess.model.GameEventType import io.elephantchess.model.Outcome import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant data class ListUserBotGamesDto(val entries: List) { @@ -20,6 +21,7 @@ data class ListUserBotGamesDto(val entries: List) { val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, + val variant: Variant, ) } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabaseGameSummary.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabaseGameSummary.kt new file mode 100644 index 000000000..a9e7bd10f --- /dev/null +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabaseGameSummary.kt @@ -0,0 +1,24 @@ +package io.elephantchess.servicelayer.dto.database + +import io.elephantchess.model.AnalysisStatus +import io.elephantchess.model.Outcome +import java.time.LocalDate + +/** + * Lightweight summary of a database (reference) game, used to populate + * the title / meta description / server-rendered content of the database + * game viewer page. + */ +data class DatabaseGameSummary( + val gameId: String, + val redPlayerCanonicalName: String?, + val redPlayerChineseName: String?, + val blackPlayerCanonicalName: String?, + val blackPlayerChineseName: String?, + val eventId: String?, + val eventName: String?, + val date: LocalDate?, + val outcome: Outcome?, + val finalFen: String?, + val analysisStatus: AnalysisStatus?, +) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/MyDbSearchesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/MyDbSearchesResponse.kt new file mode 100644 index 000000000..bd7744da6 --- /dev/null +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/MyDbSearchesResponse.kt @@ -0,0 +1,19 @@ +package io.elephantchess.servicelayer.dto.database + +import io.elephantchess.xiangqi.Color + +data class MyDbSearchesResponse(val entries: List) { + + data class Entry( + val queryId: String, + val updateTime: Long, + val playerName: String?, + val playerColor: Color?, + val eventName: String?, + val searchStart: String?, + val searchEnd: String?, + val fen: String?, + val numberOfResults: Int, + ) + +} diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/CreateGameRequest.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/CreateGameRequest.kt index 36cd826a5..06a89449e 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/CreateGameRequest.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/CreateGameRequest.kt @@ -2,6 +2,7 @@ package io.elephantchess.servicelayer.dto.game import io.elephantchess.model.TimeControlMode import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant data class CreateGameRequest( val inviterColor: Color?, @@ -12,4 +13,5 @@ data class CreateGameRequest( val allowGuests: Boolean, val alwaysVisibleInLobby: Boolean, val privateInvite: Boolean, + val variant: Variant = Variant.XIANGQI, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/GetGameDataResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/GetGameDataResponse.kt index ba946eb25..9253e011f 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/GetGameDataResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/GetGameDataResponse.kt @@ -2,6 +2,7 @@ package io.elephantchess.servicelayer.dto.game import io.elephantchess.model.* import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant // TODO: restructure the 2 users data class GetGameDataResponse( @@ -29,4 +30,5 @@ data class GetGameDataResponse( val outcome: Outcome?, val ratingUpdate: RatingUpdate?, val drawPropositionUser: String?, + val variant: Variant ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt index fee050f0f..1895298c2 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt @@ -5,6 +5,7 @@ import io.elephantchess.model.TimeControlCategory import io.elephantchess.model.UserOutcome import io.elephantchess.model.UserType import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant data class ListUserGamesResponse(val entries: List) { @@ -17,6 +18,8 @@ data class ListUserGamesResponse(val entries: List) { val color: Color?, val isRated: Boolean, val timeControlCategory: TimeControlCategory, + val timeControlBase: Int?, + val timeControlIncrement: Int?, val opponentUserType: UserType?, val opponentUserId: String?, val opponentUsername: String?, @@ -26,7 +29,8 @@ data class ListUserGamesResponse(val entries: List) { val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, - val numberOfMessages: Int + val numberOfMessages: Int, + val variant: Variant, ) } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/gamedata/GameMetadataDto.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/gamedata/GameMetadataDto.kt index 30f88c941..e076edcb0 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/gamedata/GameMetadataDto.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/gamedata/GameMetadataDto.kt @@ -2,6 +2,7 @@ package io.elephantchess.servicelayer.dto.gamedata import io.elephantchess.model.* import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant data class GameMetadataDto( val gameId: GameId, @@ -27,4 +28,5 @@ data class GameMetadataDto( val depth: Int? = null, val lastUpdated: Long? = null, val paginationOffset: Int? = null, + val variant: Variant ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/global/GlobalGameStatsResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/global/GlobalGameStatsResponse.kt index 9af977155..248929278 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/global/GlobalGameStatsResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/global/GlobalGameStatsResponse.kt @@ -7,4 +7,5 @@ data class GlobalGameStatsResponse( val totalMoves : Int, // half moves val totalInAppMoves: Int, + val totalManchuGames: Int, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsRequest.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsRequest.kt new file mode 100644 index 000000000..9d12286c9 --- /dev/null +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsRequest.kt @@ -0,0 +1,3 @@ +package io.elephantchess.servicelayer.dto.user + +data class DeleteUserSessionsRequest(val sessionIds: List) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsResponse.kt new file mode 100644 index 000000000..8bb516665 --- /dev/null +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/DeleteUserSessionsResponse.kt @@ -0,0 +1,3 @@ +package io.elephantchess.servicelayer.dto.user + +data class DeleteUserSessionsResponse(val deletedCount: Int) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailAddressSettingsResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailAddressSettingsResponse.kt index 0cd17cd9e..f0e126239 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailAddressSettingsResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailAddressSettingsResponse.kt @@ -2,5 +2,5 @@ package io.elephantchess.servicelayer.dto.user data class EmailAddressSettingsResponse( val email: String, - val isValid: Boolean?, + val validityStatus: EmailValidityStatus, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailValidityStatus.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailValidityStatus.kt new file mode 100644 index 000000000..d6bc5d290 --- /dev/null +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/EmailValidityStatus.kt @@ -0,0 +1,43 @@ +package io.elephantchess.servicelayer.dto.user + +/** + * Describes how the email address of a user has been (or has not been) validated. + * + * The check is layered: + * 1. If the user clicked the confirmation link sent at signup, the address is considered + * [MANUALLY_CONFIRMED] (the strongest signal). + * 2. Otherwise, we fall back to the automated verification (external service / bounce list) + * and report one of [AUTOMATED_VALID], [AUTOMATED_BOUNCED] or [AUTOMATED_INVALID]. + * 3. If we have no information at all, the status is [UNKNOWN]. + */ +enum class EmailValidityStatus { + + /** + * The user clicked the confirmation link we sent them at signup. + */ + MANUALLY_CONFIRMED, + + /** + * The external automated verification reported the address as valid. + */ + AUTOMATED_VALID, + + /** + * An email previously sent to this address bounced. + */ + AUTOMATED_BOUNCED, + + /** + * The external automated verification reported the address as invalid (other than a bounce). + */ + AUTOMATED_INVALID, + + /** + * No information available yet. + */ + UNKNOWN; + + val isValid: Boolean + get() = this == MANUALLY_CONFIRMED || this == AUTOMATED_VALID + +} diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/SignUpRequest.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/SignUpRequest.kt index 77a2ce0dd..5f73342b0 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/SignUpRequest.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/SignUpRequest.kt @@ -3,5 +3,6 @@ package io.elephantchess.servicelayer.dto.user data class SignUpRequest( val username: String, val email: String, - val password: String + val password: String, + val transferGuestData: Boolean = false, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/UserSessionsSettingsResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/UserSessionsSettingsResponse.kt new file mode 100644 index 000000000..65f5e8e03 --- /dev/null +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/user/UserSessionsSettingsResponse.kt @@ -0,0 +1,19 @@ +package io.elephantchess.servicelayer.dto.user + +data class UserSessionsSettingsResponse( + val entries: List, + val total: Int, +) { + data class Entry( + val id: Int, + val os: String, + val agentName: String, + val countryCode: String?, + val countryName: String?, + val region: String?, + val city: String?, + val remoteAddress: String, + val created: Long, + val updated: Long, + ) +} diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/GameToPlay.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/GameToPlay.kt index bd49b2fde..61f8481a6 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/GameToPlay.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/GameToPlay.kt @@ -3,6 +3,7 @@ package io.elephantchess.servicelayer.dto.ws import io.elephantchess.model.TimeControlCategory import io.elephantchess.model.UserType import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant /** * To join or where it's user's turn to play @@ -20,5 +21,6 @@ data class GameToPlay( val timeControlBase: Int?, val timeControlIncrement: Int?, val allowGuests: Boolean, - val lastUpdated: Long + val lastUpdated: Long, + val variant: Variant ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerInput.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerInput.kt index 38ca7a6b6..c848a35d6 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerInput.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerInput.kt @@ -1,5 +1,6 @@ package io.elephantchess.servicelayer.dto.ws data class PlayerVsPlayerInput( - val message: String, + val message: String? = null, + val isTyping: Boolean = false, ) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerUpdate.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerUpdate.kt index 7b7b9a303..c14cb8e88 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerUpdate.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/ws/PlayerVsPlayerUpdate.kt @@ -14,6 +14,13 @@ data class PlayerVsPlayerUpdate( val ratingUpdate: RatingUpdate? = null, val timeRemaining: TimeRemaining? = null, val chatMessages: List = emptyList(), + val typingUsers: List = emptyList(), +) + +data class TypingUser( + val userId: String, + val username: String, + val typedAt: Long, ) data class HasJoined( diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/DatabaseService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/DatabaseService.kt index 623381710..83fe92237 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/DatabaseService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/DatabaseService.kt @@ -22,6 +22,7 @@ import io.elephantchess.utils.generateNameVariations import io.elephantchess.utils.isChineseText import io.elephantchess.xiangqi.Board.Companion.validateFen import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KLogger import io.github.reactivecircus.cache4k.Cache import org.apache.commons.lang3.StringUtils @@ -96,6 +97,7 @@ class DatabaseService( } } + suspend fun search( dateStart: String?, dateEnd: String?, @@ -178,24 +180,36 @@ class DatabaseService( finalFen = record.finalFen, outcome = record.outcome, analysisStatus = record.analysisStatus, - paginationOffset = (offset ?: 1) + i + paginationOffset = (offset ?: 1) + i, + variant = Variant.XIANGQI ) } - referenceGameDaoService.persistSearch( - userId = userId, - searchStart = dateStartParsed, - searchEnd = dateEndParsed, - playerName = playerName, - playerId = playerIds.firstOrNull(), - playerColor = playerColor, - eventName = eventName, - eventId = eventIds.firstOrNull(), - fen = fen, - offset = offset, - limit = SEARCH_RESULT_LIMIT, - numberOfResults = entries.size - ) + val hasAnyCriteria = dateStartParsed != null || + dateEndParsed != null || + !playerName.isNullOrBlank() || + resolvedPlayerIds.isNotEmpty() || + playerColor != null || + !eventName.isNullOrBlank() || + resolvedEventIds.isNotEmpty() || + !abridgedFen.isNullOrBlank() + + if (hasAnyCriteria) { + referenceGameDaoService.persistSearch( + userId = userId, + searchStart = dateStartParsed, + searchEnd = dateEndParsed, + playerName = playerName, + playerId = playerIds.firstOrNull(), + playerColor = playerColor, + eventName = eventName, + eventId = eventIds.firstOrNull(), + fen = fen, + offset = offset, + limit = SEARCH_RESULT_LIMIT, + numberOfResults = entries.size + ) + } return ReferenceGameSearchResult(entries) } @@ -482,6 +496,25 @@ class DatabaseService( return referencePlayerDaoService.searchByNames(names = nameVariations, excludedPlayerId = player.id) } + suspend fun fetchGameSummary(gameId: String): DatabaseGameSummary? { + val record = referenceGameDaoService.findById(gameId) ?: return null + val redPlayer = record.redPlayer?.let { referencePlayerDaoService.findPlayer(it) } + val blackPlayer = record.blackPlayer?.let { referencePlayerDaoService.findPlayer(it) } + return DatabaseGameSummary( + gameId = record.id, + redPlayerCanonicalName = redPlayer?.canonicalName, + redPlayerChineseName = redPlayer?.chineseName, + blackPlayerCanonicalName = blackPlayer?.canonicalName, + blackPlayerChineseName = blackPlayer?.chineseName, + eventId = record.event, + eventName = idToEventName(record.event), + date = record.date, + outcome = record.outcome, + finalFen = record.finalFen, + analysisStatus = record.analysisStatus, + ) + } + suspend fun fetchEventName(eventId: String): String? { return referenceEventDaoService.findEventName(eventId) } @@ -627,6 +660,39 @@ class DatabaseService( return ValidatedResponse.Valid(Unit) } + suspend fun listUserSearches(userId: String, beforeTime: Long?): MyDbSearchesResponse { + val beforeInstant = beforeTime?.let { kotlin.time.Instant.fromEpochMilliseconds(it) } + val records = referenceGameDaoService.listUserSearches(userId, beforeInstant, 30) + val resolvedPlayers = records + .mapNotNull { entry -> entry.playerName } + .distinct() + .mapNotNull { name -> resolvePlayerByName(name)?.let { name to it } } + .toMap() + + return records + .map { record -> + val prettyName = + record.playerName?.let { name -> + resolvedPlayers[name] + ?.let { player -> formatWithChineseName(player.canonicalName, player.chineseName) } + ?: name + } + + MyDbSearchesResponse.Entry( + queryId = record.queryId, + updateTime = record.updateTime.toEpochMilliseconds(), + playerName = prettyName, + playerColor = record.playerColor, + eventName = record.eventName, + searchStart = record.searchStart?.toString(), + searchEnd = record.searchEnd?.toString(), + fen = record.fen, + numberOfResults = record.numberOfResults ?: 0, + ) + } + .let { mapped -> MyDbSearchesResponse(mapped) } + } + suspend fun findEditedPlayersLatestVersions(userId: String): ListUserEdits { return referencePlayerDaoService .findEditedPlayersByEditorId(userId) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GameDataService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GameDataService.kt index ac2b8334c..cf2203568 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GameDataService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GameDataService.kt @@ -30,11 +30,11 @@ import io.elephantchess.xiangqi.Board.Companion.DEFAULT_START_FEN import io.elephantchess.xiangqi.Board.Companion.resetFullMoveCount import io.elephantchess.xiangqi.Color.BLACK import io.elephantchess.xiangqi.Color.RED +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.time.Clock import kotlin.time.Duration import kotlin.time.Instant @@ -53,6 +53,7 @@ class GameDataService( private val referencePlayerDaoService: ReferencePlayerDaoService, private val referenceEventDaoService: ReferenceEventDaoService, private val userDaoService: UserDaoService, + private val userService: UserService, private val userCache: UserCache, private val logger: KLogger, ) { @@ -72,6 +73,11 @@ class GameDataService( pvpGameDaoService.countTotalGames(MIN_MOVE_INDEX) } + suspend fun countTotalManchuGames(): Int { + return pvbGameDaoService.countManchuGames(MIN_MOVE_INDEX) + + pvpGameDaoService.countManchuGames(MIN_MOVE_INDEX) + } + /** * Half moves or plies */ @@ -104,14 +110,15 @@ class GameDataService( GameMetadataDto( gameId = gameId, - lastUpdated = record.created.toEpochMilliseconds(), + lastUpdated = record.lastUpdated.toEpochMilliseconds(), redPlayerId = redPlayerId, redPlayerName = redPlayerName, blackPlayerId = blackPlayerId, blackPlayerName = blackPlayerName, finalFen = record.currentFen, outcome = record.outcome, - analysisStatus = record.analysisStatus + analysisStatus = record.analysisStatus, + variant = record.variant, ) } } @@ -122,10 +129,30 @@ class GameDataService( ?.let { record -> val userId = record.userId val userName = fetchUserName(userId) - val redPlayerId = if (record.userColor == RED) userId else null - val redPlayerName = if (record.userColor == RED) userName else null - val blackPlayerId = if (record.userColor == BLACK) userId else null - val blackPlayerName = if (record.userColor == BLACK) userName else null + var blackPlayerId: String? = null + var blackPlayerName: String? = null + var redPlayerId: String? = null + var redPlayerName: String? = null + val engineName = record.prettyEngineName() + + when (record.userColor) { + RED -> { + redPlayerId = userId + redPlayerName = userName + blackPlayerName = engineName + } + + BLACK -> { + redPlayerName = engineName + blackPlayerId = userId + blackPlayerName = userName + } + + else -> { + // should not happen, but in case of data inconsistency, we don't want to fail the whole request + logger.warn { "Game $gameId has invalid user color ${record.userColor}" } + } + } GameMetadataDto( gameId = gameId, @@ -139,7 +166,8 @@ class GameDataService( outcome = record.outcome, analysisStatus = record.analysisStatus, engine = record.engine, - depth = record.depth + depth = record.depth, + variant = record.variant ) } } @@ -174,7 +202,8 @@ class GameDataService( outcome = record.outcome, analysisStatus = record.analysisStatus, eventId = event?.id, - eventName = event?.name + eventName = event?.name, + variant = Variant.XIANGQI ) } } @@ -206,11 +235,15 @@ class GameDataService( if (shouldStart) { when (gameId.type) { PVP -> { - val gameStatus = pvpGameDaoService.fetchGameStatus(gameId.id) + val gameRecord = pvpGameDaoService.fetchById(gameId.id) ?: throw NotFoundException("Game $gameId not found") - if (gameStatus.isInProgress()) { - throw stillInProgressException(gameId, gameStatus) + if (gameRecord.variant == Variant.MANCHU) { + throw BadRequestException("Analysis is not supported for Manchu variant games") + } + + if (gameRecord.gameStatus.isInProgress()) { + throw stillInProgressException(gameId, gameRecord.gameStatus) } } @@ -218,6 +251,11 @@ class GameDataService( val statusRecord = pvbGameDaoService.fetchGameStatus(gameId.id) ?: throw NotFoundException("Game $gameId not found") + val pvbRecord = pvbGameDaoService.fetchById(gameId.id) + if (pvbRecord != null && (pvbRecord.variant == Variant.MANCHU)) { + throw BadRequestException("Analysis is not supported for Manchu variant games") + } + if (statusRecord.isInProgress()) { throw stillInProgressException(gameId, statusRecord.status) } @@ -495,60 +533,94 @@ class GameDataService( ) val userIds = games.flatMap { game -> listOf(game.inviter, game.invitee) }.distinct().filterNotNull() - val lastOnlineByUserId = userDaoService.fetchLastOnline(userIds) - val isOnlineLimit = Clock.System.now().minusSeconds(15L) + val onlineUserIds = userService.areOnline(userIds).onlineUserIds val selectedGames = if (distinctByUsers) distinctByUserId(games) else games return selectedGames .take(actualLimit) - .map { gameRecord -> - val redUserId = gameRecord.redUserId()!! - val blackUserId = gameRecord.blackUserId()!! - - GameMetadataDto( - gameId = GameId(PVP, gameRecord.id), - redPlayerId = redUserId, - redPlayerName = fetchUserName(redUserId), - redPlayerRating = gameRecord.redPlayerRating(), - redUserType = userCache.fetchUserType(redUserId), - isRedOnline = lastOnlineByUserId[redUserId]?.isAfter(isOnlineLimit) == true, - blackPlayerId = blackUserId, - blackPlayerName = fetchUserName(blackUserId), - blackPlayerRating = gameRecord.blackPlayerRating(), - blackUserType = userCache.fetchUserType(blackUserId), - isBlackOnline = lastOnlineByUserId[blackUserId]?.isAfter(isOnlineLimit) == true, - userColor = null, - finalFen = gameRecord.currentFen, - status = gameRecord.gameStatus, - outcome = gameRecord.outcome, - lastUpdated = gameRecord.lastUpdated.toEpochMilliseconds() - ) + .map { mapPvpGameToDto(it, onlineUserIds) } + .let { entries -> + ListLastGamesResponse(entries) } + } + + suspend fun listLastPvpGamesByUsername( + username: String, + requestedLimit: Int, + beforeTs: Long? + ): ListLastGamesResponse { + val user = userDaoService.findByUserName(username) + ?: throw NotFoundException("User $username could not be found") + return listLastPvpGamesByUserId( + userId = user.id, + requestedLimit = requestedLimit, + beforeTs = beforeTs + ) + } + + suspend fun listLastPvpGamesByUserId( + userId: String, + requestedLimit: Int, + beforeTs: Long? + ): ListLastGamesResponse { + val games = pvpGameDaoService.listLastGamesByUserId( + userId = userId, + limit = requestedLimit, + minMoveIndex = MIN_MOVE_INDEX, + beforeTs = beforeTs + ) + + val userIds = games.flatMap { game -> listOf(game.inviter, game.invitee) }.distinct().filterNotNull() + val onlineUserIds = userService.areOnline(userIds).onlineUserIds + + return games + .map { mapPvpGameToDto(it, onlineUserIds) } .let { entries -> ListLastGamesResponse(entries) } } - suspend fun listLastPvbGames( + private suspend fun mapPvpGameToDto(gameRecord: Game, onlineUserIds: Set): GameMetadataDto { + val redUserId = gameRecord.redUserId()!! + val blackUserId = gameRecord.blackUserId()!! + return GameMetadataDto( + gameId = GameId(PVP, gameRecord.id), + redPlayerId = redUserId, + redPlayerName = fetchUserName(redUserId), + redPlayerRating = gameRecord.redPlayerRating(), + redUserType = userCache.fetchUserType(redUserId), + isRedOnline = onlineUserIds.contains(redUserId), + blackPlayerId = blackUserId, + blackPlayerName = fetchUserName(blackUserId), + blackPlayerRating = gameRecord.blackPlayerRating(), + blackUserType = userCache.fetchUserType(blackUserId), + isBlackOnline = onlineUserIds.contains(blackUserId), + userColor = null, + finalFen = gameRecord.currentFen, + status = gameRecord.gameStatus, + outcome = gameRecord.outcome, + lastUpdated = gameRecord.lastUpdated.toEpochMilliseconds(), + variant = gameRecord.variant + ) + } + + suspend fun listLatestPvbGames( requestedLimit: Int, distinctByUsers: Boolean = true, - beforeTs: Long? = null + beforeTs: Long? = null, + excludeAutoResigned: Boolean ): ListLastGamesResponse { - val actualLimit = if (distinctByUsers) requestedLimit * 20 else requestedLimit val gameRecords = pvbGameDaoService - .listLastGamesByIdentifiedUsers(actualLimit, minMoveIndex = MIN_MOVE_INDEX, beforeTs = beforeTs) - .let { games -> - if (distinctByUsers) { - games.distinctBy { game -> game.userId } - } else { - games - } - } - .sortedByDescending { game -> game.lastUpdated } + .listLatestGamesByIdentifiedUsers( + limit = requestedLimit, + minMoveIndex = MIN_MOVE_INDEX, + beforeTs = beforeTs, + excludeAutoResigned = excludeAutoResigned, + distinctByUsers = distinctByUsers + ) val userIds = gameRecords.map { game -> game.userId }.distinct().filterNotNull() - val lastOnlineByUserId = userDaoService.fetchLastOnline(userIds) - val isOnlineLimit = Clock.System.now().minusSeconds(15L) + val onlineUserIds = userService.areOnline(userIds).onlineUserIds return gameRecords .take(requestedLimit) @@ -561,7 +633,7 @@ class GameDataService( val blackPlayerName: String? val blackPlayerRating: Int? val blackUserType: UserType? - val isUserOnline = lastOnlineByUserId[record.userId]?.isAfter(isOnlineLimit) == true + val isUserOnline = onlineUserIds.contains(record.userId) if (record.userColor == RED) { redUserId = record.userId @@ -569,13 +641,13 @@ class GameDataService( redPlayerRating = null redUserType = userCache.fetchUserType(record.userId) blackUserId = null - blackPlayerName = record.engine.toString() - blackPlayerRating = record.depth + blackPlayerName = record.prettyEngineName() + blackPlayerRating = null blackUserType = null } else { redUserId = null - redPlayerName = record.engine.toString() - redPlayerRating = record.depth + redPlayerName = record.prettyEngineName() + redPlayerRating = null redUserType = null blackUserId = record.userId blackPlayerName = fetchUserName(record.userId) @@ -599,7 +671,8 @@ class GameDataService( finalFen = record.currentFen, status = record.gameStatus, outcome = record.outcome, - lastUpdated = record.lastUpdated.toEpochMilliseconds() + lastUpdated = record.lastUpdated.toEpochMilliseconds(), + variant = record.variant, ) } .let { entries -> @@ -654,7 +727,8 @@ class GameDataService( finalFen = game.finalFen ?: DEFAULT_START_FEN, outcome = game.outcome, lastUpdated = game.date?.atStartOfDay()?.toUtcInstant()?.toEpochMilliseconds(), - paginationOffset = (offset ?: 0) + i + paginationOffset = (offset ?: 0) + i, + variant = Variant.XIANGQI ) } } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GlobalAnalyticsService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GlobalAnalyticsService.kt index 521f6cbf3..45964b81c 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GlobalAnalyticsService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/GlobalAnalyticsService.kt @@ -151,7 +151,8 @@ class GlobalAnalyticsService( totalGames = totalGames, totalInAppGames = totalInAppGames, totalMoves = totalMoves, - totalInAppMoves = totalInAppMovesMap + totalInAppMoves = totalInAppMovesMap, + totalManchuGames = gameDataService.countTotalManchuGames() ) } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/MailService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/MailService.kt index c159ea5ef..4913f1b6a 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/MailService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/MailService.kt @@ -11,10 +11,12 @@ import io.elephantchess.htmlrenderer.SimpleValueTagResolver import io.elephantchess.htmlrenderer.TagResolver import io.elephantchess.model.EmailVerifierService.EMAIL_LIST_VERIFY import io.elephantchess.servicelayer.clients.EmailListVerifyClient +import io.elephantchess.servicelayer.dto.user.EmailValidityStatus import io.elephantchess.servicelayer.model.Email import io.elephantchess.servicelayer.model.MatchingEmail import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.services.UserService.Companion.PASSWORD_RECOVERY_TIMEOUT_HOURS +import io.elephantchess.servicelayer.services.resolvers.EmailConfirmationLinkTagResolver import io.elephantchess.servicelayer.services.resolvers.GameLinkTagResolver import io.elephantchess.servicelayer.services.resolvers.RecoveryLinkTagResolver import io.elephantchess.servicelayer.services.resolvers.UnsubscribeTagResolver @@ -58,25 +60,38 @@ class MailService( private val mailScope by lazy { CoroutineScope(Dispatchers.IO) } suspend fun isEmailAddressValid(email: String): Boolean { - return getEmailValidityStatus(email) == true + return getEmailValidityDetails(email).isValid } /** - * null means not checked yet + * Returns the full [EmailValidityStatus] for the given email. + * + * The check is layered: if a user owns this address and has clicked the confirmation + * link sent at signup, the address is considered [EmailValidityStatus.MANUALLY_CONFIRMED] + * (the strongest signal). Otherwise, we fall back to the automated verification. */ - suspend fun getEmailValidityStatus(email: String): Boolean? { + suspend fun getEmailValidityDetails(email: String): EmailValidityStatus { + val user = userDaoService.findByEmail(email) + if (user?.emailConfirmedAt != null) { + return EmailValidityStatus.MANUALLY_CONFIRMED + } + if (emailVerificationDaoService.hasEmailBounced(email)) { - return false + return EmailValidityStatus.AUTOMATED_BOUNCED } val automatedVerifications = emailVerificationDaoService.findAutomatedVerifications(email, emailValidityDuration) return when (automatedVerifications.size) { - 0 -> null + 0 -> EmailValidityStatus.UNKNOWN else -> { val result = automatedVerifications.last().result - result == "ok" || result == "ok_for_all" + if (result == "ok" || result == "ok_for_all") { + EmailValidityStatus.AUTOMATED_VALID + } else { + EmailValidityStatus.AUTOMATED_INVALID + } } } } @@ -145,22 +160,27 @@ class MailService( /** * List all emails that: - * - have been validated automatically by the external service + * - have a valid status (manually confirmed by the user, or automatically validated by the + * external service) * - have not bounced * - consent to receive the newsletter (implicit) */ suspend fun listNewsLetterRecipientEmails(): List { val automaticallyValidated = emailVerificationDaoService.listAllAutomaticallyValidatedEmails(emailValidityDuration) + val manuallyConfirmed = userDaoService.listManuallyConfirmedEmailAddresses().toSet() val bounced = emailVerificationDaoService.listBouncedEmails() val newsletterRecipients = userDaoService .listNewsletterEmailAddresses() - .filter { emailAddress -> automaticallyValidated.contains(emailAddress) } + .filter { emailAddress -> + manuallyConfirmed.contains(emailAddress) || automaticallyValidated.contains(emailAddress) + } .filterNot { emailAddress -> bounced.contains(emailAddress) } .map { emailAddress -> emailAddress.trim() } logger.info { - "found ${automaticallyValidated.size} valid e-mail addresses, " + + "found ${automaticallyValidated.size} automatically validated e-mail addresses, " + + "${manuallyConfirmed.size} manually confirmed e-mail addresses, " + "${bounced.size} bounced e-mail addresses, " + "${newsletterRecipients.size} total newsletter recipients" } @@ -209,15 +229,37 @@ class MailService( ) } - suspend fun sendNewUserNotification(user: User) { + suspend fun sendNewUserNotification(user: User, guestTransferred: Boolean) { resolveAndSend( recipient = ADMIN_GMAIL_EMAIL, subject = "new user", templateName = "new_user_notification", resolvers = listOf( SimpleValueTagResolver("username", user.handle), - SimpleValueTagResolver("email", user.email) - ) + SimpleValueTagResolver("email", user.email), + SimpleValueTagResolver("guest_transferred", if (guestTransferred) "yes" else "no") + ), + skipRecipientValidityCheck = true + ) + } + + suspend fun sendEmailConfirmation(recipient: String, code: String, showWelcomeMessage: Boolean) { + val subject = if (showWelcomeMessage) { + "Welcome to elephantchess - email address confirmation" + } else { + "elephantchess - email address confirmation" + } + + resolveAndSend( + recipient = recipient, + subject = subject, + templateName = "email_confirmation", + resolvers = listOf( + EmailConfirmationLinkTagResolver(webHost, code) + ), + // The whole point of this email is to flip the recipient's status to MANUALLY_CONFIRMED, + // so we must not gate it on the address already being known-valid (it never is at signup). + skipRecipientValidityCheck = true, ) } @@ -326,6 +368,7 @@ class MailService( templateName: String, resolvers: List = listOf(), copyToAdmin: Boolean = false, + skipRecipientValidityCheck: Boolean = false, ) { fun sendSafeAsync(email: Email) { mailScope.launch { @@ -338,11 +381,11 @@ class MailService( } if (!sendMailNotifications) { - logger.debug { "not sending e-mail '$subject' because emails are disabled" } + logger.info { "not sending e-mail '$subject' because emails are disabled" } return } - if (!isEmailAddressValid(recipient)) { + if (!skipRecipientValidityCheck && !isEmailAddressValid(recipient)) { logger.debug { "not sending e-mail '$subject' because recipient $recipient is not valid" } return } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt index 4da992fa9..fa4159d7d 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt @@ -46,6 +46,7 @@ import io.elephantchess.xiangqi.Board.Companion.validateFen import io.elephantchess.xiangqi.Color import io.elephantchess.xiangqi.Color.BLACK import io.elephantchess.xiangqi.Color.RED +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.ChannelResult @@ -125,6 +126,11 @@ class PlayerVsBotGameService( throw BadRequestException("You must be logged in to play with depth greater than 6") } + // Manchu variant requires Fairy Stockfish + if (request.variant == Variant.MANCHU && request.engine == Engine.PIKAFISH) { + throw BadRequestException("Pikafish does not support the Manchu variant. Please use Fairy Stockfish.") + } + request.startFen?.let { fen -> try { validateFen(fen) @@ -142,17 +148,16 @@ class PlayerVsBotGameService( val now = Clock.System.now() val gameId = generateId() - val actualStartFen = request.startFen ?: DEFAULT_START_FEN - val usesDefaultStartFen = actualStartFen == DEFAULT_START_FEN + val actualStartFen = request.startFen ?: Board.defaultStartFen(request.variant) + val usesDefaultStartFen = actualStartFen == Board.defaultStartFen(request.variant) + // Manchu variant requires Fairy Stockfish. // If Pikafish is requested but the start FEN is non-standard, safeQueryForDepth will // use Fairy Stockfish instead, so we persist the effective engine/version that will be used. - val effectiveEngine = if (request.engine == Engine.PIKAFISH && isNonStandardFen(actualStartFen)) { - Engine.FAIRYSTOCKFISH - } else { - request.engine - } + val isVariant = request.variant == Variant.MANCHU + val isUnsupportedByPikafish = isNonStandardFen(actualStartFen) + val effectiveEngine = if (isVariant || isUnsupportedByPikafish) Engine.FAIRYSTOCKFISH else request.engine val engineVersion = when (effectiveEngine) { Engine.PIKAFISH -> pikafishVersion Engine.FAIRYSTOCKFISH -> fairyStockfishVersion @@ -171,6 +176,7 @@ class PlayerVsBotGameService( gameRecord.currentHalfMoveIndex = 0 gameRecord.created = now gameRecord.lastUpdated = now + gameRecord.variant = request.variant val statusRecord = BotGameStatusEvent() statusRecord.botGameId = gameId @@ -189,7 +195,8 @@ class PlayerVsBotGameService( position = 0, engine = effectiveEngine, depth = request.depth, - usesDefaultStartFen = usesDefaultStartFen + usesDefaultStartFen = usesDefaultStartFen, + variant = request.variant, ) if (botMove != null) { @@ -220,6 +227,7 @@ class PlayerVsBotGameService( isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), + variant = record.variant ?: Variant.XIANGQI, ) } .let { entries -> @@ -239,13 +247,14 @@ class PlayerVsBotGameService( userColor = game.userColor, engine = game.engine, depth = game.depth, - startFen = game.startFen ?: DEFAULT_START_FEN, + startFen = game.startFen ?: Board.defaultStartFen(game.variant ?: Variant.XIANGQI), status = game.gameStatus, moveIndex = game.currentHalfMoveIndex, fen = game.currentFen, created = game.created.toEpochMilliseconds(), lastUpdated = game.lastUpdated.toEpochMilliseconds(), - outcome = game.outcome + outcome = game.outcome, + variant = game.variant, ) } } @@ -262,6 +271,7 @@ class PlayerVsBotGameService( // TODO: use same pattern as in GameService with TryEither pvbGameDaoService.saveUserMoveResult(request.gameId, request.move) { gameRecord, userMove -> + val gameVariant = gameRecord.variant val userColor = gameRecord.userColor val botColor = userColor.reverse() @@ -286,27 +296,33 @@ class PlayerVsBotGameService( botColor = botColor, userMove = userMove, fen = board.outputFen(), - startFen = gameRecord.startFen ?: DEFAULT_START_FEN, + startFen = gameRecord.startFen ?: Board.defaultStartFen(gameVariant), position = gameRecord.currentHalfMoveIndex, engine = gameRecord.engine, depth = gameRecord.depth, - usesDefaultStartFen = gameRecord.startFen == null + usesDefaultStartFen = gameRecord.startFen == null, + variant = gameVariant, ) if (botMove != null) { - board.registerMove(botMove.uci) - logger.debug { "FEN after bot move: ${board.outputFen()}" } - result = result.copy(botMove = botMove) - - val isUserCheckmated = board.isCheckmated(userColor) - val isUserStalemated = board.isStalemated(userColor) + if (!Board.isMoveLegal(board.outputFen(), botMove.uci)) { + logger.error { "[${request.gameId}] bot returned illegal move ${botMove.uci} for FEN ${board.outputFen()}" } + result = result.addError(BOT_MOVE_NOT_FOUND) + } else { + board.registerMove(botMove.uci) + logger.debug { "FEN after bot move: ${board.outputFen()}" } + result = result.copy(botMove = botMove) + + val isUserCheckmated = board.isCheckmated(userColor) + val isUserStalemated = board.isStalemated(userColor) + + if (isUserCheckmated || isUserStalemated) { + // bot has won against the player + result = result.updateForBotVictory(botColor, isUserCheckmated, isUserStalemated) + } - if (isUserCheckmated || isUserStalemated) { - // bot has won against the player - result = result.updateForBotVictory(botColor, isUserCheckmated, isUserStalemated) + result = result.copy(newPosition = gameRecord.currentHalfMoveIndex + 2) } - - result = result.copy(newPosition = gameRecord.currentHalfMoveIndex + 2) } else { result = result.addError(BOT_MOVE_NOT_FOUND) } @@ -346,10 +362,16 @@ class PlayerVsBotGameService( engine: Engine, depth: Int, usesDefaultStartFen: Boolean, + variant: Variant = Variant.XIANGQI, ): BotMove? { - suspend fun playWithEngine() = playWithEngine(gameId, botColor, startFen, fen, position, engine, depth) + suspend fun playWithEngine(): BotMove? { + return playWithEngine(gameId, botColor, startFen, fen, position, engine, depth, variant) + } + + val canUseOpeningRepository = + usesDefaultStartFen && position <= REPO_MAX_POSITION_INDEX && variant == Variant.XIANGQI - return if (usesDefaultStartFen && position <= REPO_MAX_POSITION_INDEX) { + return if (canUseOpeningRepository) { playFromOpeningRepository(gameId, userMove) ?: playWithEngine() } else { playWithEngine() @@ -379,9 +401,10 @@ class PlayerVsBotGameService( position: Int, engine: Engine, depth: Int, + variant: Variant, ): BotMove? { suspend fun queryEngine(fenToEngine: String): InfoLinesResult? { - return enginesPool.safeQueryForDepth(fenToEngine, modelToProcess(engine), depth, 15_000) + return enginesPool.safeQueryForDepth(fenToEngine, modelToProcess(engine), depth, 15_000, variant) } suspend fun findAlternativeMove(bestMove: String): String? { diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt index 82de39db1..6f8d3e685 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt @@ -7,6 +7,7 @@ import io.elephantchess.db.dao.codegen.tables.pojos.Game import io.elephantchess.db.dao.codegen.tables.pojos.GameChatMessage import io.elephantchess.db.model.TimeControlRecord import io.elephantchess.db.services.ChatMessageDaoService +import io.elephantchess.db.services.GameChatTypingStatusDaoService import io.elephantchess.db.services.PlayerVsPlayerGameDaoService import io.elephantchess.db.services.UserDaoService import io.elephantchess.db.utils.* @@ -32,7 +33,6 @@ import io.elephantchess.servicelayer.utils.ops.ratingUpdate import io.elephantchess.utils.EloCalculator.calculateElo import io.elephantchess.utils.TryEither import io.elephantchess.xiangqi.Board -import io.elephantchess.xiangqi.Board.Companion.DEFAULT_START_FEN import io.elephantchess.xiangqi.Board.Companion.calculateNewFen import io.elephantchess.xiangqi.Board.Companion.isCheckmated import io.elephantchess.xiangqi.Board.Companion.isInCheck @@ -43,6 +43,7 @@ import io.elephantchess.xiangqi.Color.BLACK import io.elephantchess.xiangqi.Color.RED import io.elephantchess.xiangqi.HalfMove.Companion.parseMoveFromUci import io.elephantchess.xiangqi.PerpetualCheckingRule.Companion.defaultPerpetualCheckingRules +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.ChannelResult @@ -62,13 +63,14 @@ class PlayerVsPlayerGameService( private val mailService: MailService, private val pvpGameDaoService: PlayerVsPlayerGameDaoService, private val chatMessageDaoService: ChatMessageDaoService, + private val gameChatTypingStatusDaoService: GameChatTypingStatusDaoService, private val userCache: UserCache, private val discordService: DiscordService, private val logger: KLogger, refresherScope: CoroutineScope ) { - private val sessionsRefresh = 2.seconds + private val sessionsRefresh = 1.seconds private val perpetualCheckRules by lazy { defaultPerpetualCheckingRules } private val gamesToPlaySessions = mutableListOf() @@ -140,9 +142,14 @@ class PlayerVsPlayerGameService( private suspend fun refreshPlayerVsPlayerSessions() { val allGameIds = playerVsPlayerSessions.map { session -> session.gameId }.distinct() val stateMap = pvpGameDaoService.fetchGameStates(allGameIds) + + // chat val chatIndexes = chatMessageDaoService.currentIndexes(allGameIds) + val chatTypingStatusCutOff = Clock.System.now() - TYPING_FRESHNESS_WINDOW + val chatTypingStatusMap = gameChatTypingStatusDaoService.fetchForGameIds(allGameIds, chatTypingStatusCutOff) - // not very optimized: 2 sessions about the same game -> some information will be fetched 2x from the db + // TODO: not very optimized: 2 sessions about the same game -> some information will be fetched 2x from the db + // DAO access could be cached in Map and stuff playerVsPlayerSessions .forEach { session -> val gameId = session.gameId @@ -191,12 +198,28 @@ class PlayerVsPlayerGameService( ) } + // typing: collect all fresh typing events in this game from other users. + // Stale entries are filtered out at the DAO layer (see + // [TypingStatusDaoService.fetchTypingStatuses]), and clients are + // responsible for de-duplicating / hiding the indicator on their side. + val typingUsers = chatTypingStatusMap[gameId] + ?.filter { entry -> entry.userId != session.userId.id } + ?.map { entry -> + TypingUser( + userId = entry.userId, + username = userCache.fetchUsernameOrDefault(entry.userId), + typedAt = entry.typedAt.toEpochMilliseconds(), + ) + } + .orEmpty() + val shouldUpdate = session.currentStatus() != status || newMove != null || hasJoinedEvent != null || timeRemaining != null || - chatMessages.isNotEmpty() + chatMessages.isNotEmpty() || + typingUsers.isNotEmpty() // update session if (shouldUpdate) { @@ -214,6 +237,7 @@ class PlayerVsPlayerGameService( ratingUpdate = fetchRatingUpdateIfNecessaryWs(session.gameId, status), timeRemaining = timeRemaining, chatMessages = chatMessages, + typingUsers = typingUsers, ) ) } @@ -266,7 +290,7 @@ class PlayerVsPlayerGameService( val allowGuests = userId.userType == UserType.GUEST || request.allowGuests val timeControlCategory = TimeControlCategory.fromSeconds(request.timeControlBase) - val userRating = getUserRating(userId.id, timeControlCategory) + val userRating = getUserRating(userId.id, timeControlCategory, request.variant) // if such a game already exists, join it instead of creating a new one if (!request.privateInvite) { @@ -283,13 +307,14 @@ class PlayerVsPlayerGameService( game.id = generateId() game.inviter = userId.id game.inviterColor = request.inviterColor - game.currentFen = DEFAULT_START_FEN + game.currentFen = Board.defaultStartFen(request.variant) game.gameStatus = CREATED game.currentHalfMoveIndex = 0 game.allowGuestsToJoin = allowGuests game.alwaysVisibleInLobby = !request.privateInvite && request.alwaysVisibleInLobby game.privateInvite = request.privateInvite game.containsErrors = false + game.variant = request.variant val now = Clock.System.now() game.created = now @@ -318,16 +343,16 @@ class PlayerVsPlayerGameService( userRating: Int, request: CreateGameRequest, ): Game? { - return pvpGameDaoService - .listCompatibleGames( - inviterColor = request.inviterColor, - isRated = request.isRated, - timeControlMode = request.timeControlMode, - timeControlBase = request.timeControlBase, - timeControlIncrement = request.timeControlIncrement, - userType = userId.userType, - userId = userId.id - ) + return pvpGameDaoService.listCompatibleGames( + inviterColor = request.inviterColor, + isRated = request.isRated, + timeControlMode = request.timeControlMode, + timeControlBase = request.timeControlBase, + timeControlIncrement = request.timeControlIncrement, + userType = userId.userType, + userId = userId.id, + variant = request.variant + ) .filter { gameRecord -> isOnline(gameRecord.inviter) } .minByOrNull { gameRecord -> abs(gameRecord.inviterRatingFrom - userRating) } } @@ -371,6 +396,8 @@ class PlayerVsPlayerGameService( color = color, isRated = gameRecord.isRated, timeControlCategory = gameRecord.timeControlCategory, + timeControlBase = gameRecord.timeControlBase, + timeControlIncrement = gameRecord.timeControlIncrement, opponentUserType = opponentUserType, opponentUserId = opponentUserId, opponentUsername = opponentUsername, @@ -380,7 +407,8 @@ class PlayerVsPlayerGameService( isPreAnalyzed = gameRecord.analysisStatus == AnalysisStatus.COMPLETED, created = gameRecord.created.toEpochMilliseconds(), lastUpdated = gameRecord.lastUpdated.toEpochMilliseconds(), - numberOfMessages = numberOfMessages + numberOfMessages = numberOfMessages, + variant = gameRecord.variant, ) } .let { entries -> @@ -419,6 +447,7 @@ class PlayerVsPlayerGameService( val session = PlayerVsPlayerWebSocketSession( gameId = gameId, + userId = userId, status = gameState.gameEventType, moveIndex = gameState.index, chatIndex = chatMessageDaoService.currentIndex(gameId), @@ -435,11 +464,15 @@ class PlayerVsPlayerGameService( val isAllowedToChat = gamePlayersStatus.status == CREATED || gamePlayersStatus.isPlaying(userId.id) if (isAllowedToChat) { - chatMessageDaoService.insertChat( - gameId = gameId, - userId = userId.id, - content = input.message.trim().take(MESSAGE_LENGTH_LIMIT) - ) + if (input.message != null) { + chatMessageDaoService.insertChat( + gameId = gameId, + userId = userId.id, + content = input.message.trim().take(MESSAGE_LENGTH_LIMIT) + ) + } else if (input.isTyping) { + gameChatTypingStatusDaoService.upsertTypingStatus(gameId, userId.id) + } } else { logger.warn { "user $userId is not allowed to send message in game $gameId" } } @@ -492,7 +525,8 @@ class PlayerVsPlayerGameService( ratingUpdate = gameRecord.ratingUpdate(), gameEventType = gameRecord.gameStatus, outcome = gameRecord.outcome, - drawPropositionUser = gameRecord.drawPropositionUser + drawPropositionUser = gameRecord.drawPropositionUser, + variant = gameRecord.variant, ) } @@ -651,6 +685,7 @@ class PlayerVsPlayerGameService( throw BadRequestException("Guest users are not allowed to join this game") } else { val timeControlCategory = pvpGameDaoService.fetchTimeControlCategory(gameId)!! + val variant = pvpGameDaoService.fetchVariant(gameId)!! if (userId.userType == UserType.GUEST && timeControlCategory == TimeControlCategory.CORRESPONDENCE) { throw BadRequestException("Guest users are not allowed to join correspondence games") @@ -666,7 +701,7 @@ class PlayerVsPlayerGameService( inviteeColor = inviterColor.reverse() logger.debug { "[$gameId] inviter had selected $inviterColor, invitee receives $inviteeColor" } } - val userRating = getUserRating(userId.id, timeControlCategory) + val userRating = getUserRating(userId.id, timeControlCategory, variant) val timeControlRecord = pvpGameDaoService.fetchTimeControl(gameId) pvpGameDaoService.joinGame( userId = userId.id, @@ -854,7 +889,9 @@ class PlayerVsPlayerGameService( gameId = request.gameId, move = request.move, playMoveCallback = playMoveCallback, - hasViolatedPerpetualCheckingRule = { moves -> hasViolatedPerpetualCheckingRuleCallback(moves) }, + hasViolatedPerpetualCheckingRule = { game, moves -> + hasViolatedPerpetualCheckingRuleCallback(game, moves) + }, updateRatingsCallback = updateRatingsCallback ) @@ -903,8 +940,12 @@ class PlayerVsPlayerGameService( /** * Moves must include last one played */ - private fun hasViolatedPerpetualCheckingRuleCallback(moves: List): PerpetualCheckingCallbackResult? { - val board = Board(keepHistory = true) + private fun hasViolatedPerpetualCheckingRuleCallback( + game: Game, + moves: List + ): PerpetualCheckingCallbackResult? { + val startFen = Board.defaultStartFen(game.variant ?: Variant.XIANGQI) + val board = Board(startFen, keepHistory = true) board.registerMoves(moves.map { parseMoveFromUci(it) }) val playerColor = board.colorToPlay().reverse() val history = board.getHistory()!! @@ -929,11 +970,12 @@ class PlayerVsPlayerGameService( /** * Only fetch the necessary fields to proceed to the basic requests validation */ + // TODO: should we explicitly index the columns fetches in this call? private suspend fun fetchPlayersAndStatus(gameId: String) = pvpGameDaoService.fetchPlayersAndStatus(gameId) ?: throw NotFoundException("Game $gameId not found") - private suspend fun getUserRating(userId: String, timeControlCategory: TimeControlCategory) = - pvpGameDaoService.fetchRatingForUser(userId, timeControlCategory) ?: 0 + private suspend fun getUserRating(userId: String, timeControlCategory: TimeControlCategory, variant: Variant) = + pvpGameDaoService.fetchRatingForUser(userId, timeControlCategory, variant) ?: 0 private suspend fun fetchRatingUpdateIfNecessary(gameId: String, gameEventType: GameEventType): RatingUpdate? = if (gameEndedStatuses.contains(gameEventType)) { @@ -1040,7 +1082,8 @@ class PlayerVsPlayerGameService( timeControlBase = game.timeControlBase, timeControlIncrement = game.timeControlIncrement, allowGuests = game.allowGuestsToJoin, - lastUpdated = game.lastUpdated.toEpochMilliseconds() + lastUpdated = game.lastUpdated.toEpochMilliseconds(), + variant = game.variant ) } @@ -1062,6 +1105,14 @@ class PlayerVsPlayerGameService( val NOTIFICATIONS_OFFLINE_FOR = 2.minutes const val MESSAGE_LENGTH_LIMIT = 200 + /** + * Maximum age of a typing event we are willing to surface to a client. + * Older entries are considered stale (e.g. persisted in the DB from a + * previous session) and are filtered out so that, on refresh/reconnect, + * we do not re-trigger an "is typing…" indicator for stale events. + */ + val TYPING_FRESHNESS_WINDOW = 2.seconds + fun formatMillis(millis: Long): String { val hours = TimeUnit.MILLISECONDS.toHours(millis).toInt() val minutes = TimeUnit.MILLISECONDS.toMinutes(millis).toInt() - hours * 60 diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PuzzleService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PuzzleService.kt index 7ac39bd2b..bbde8e61e 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PuzzleService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PuzzleService.kt @@ -19,6 +19,7 @@ import io.elephantchess.servicelayer.exceptions.NotAcceptableException import io.elephantchess.servicelayer.exceptions.NotFoundException import io.elephantchess.servicelayer.utils.modelToProcess import io.elephantchess.utils.EloCalculator.calculateElo +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KLogger import kotlin.math.ceil import kotlin.time.Clock @@ -141,6 +142,7 @@ class PuzzleService( analysisStatus = puzzleRecords.map { record -> record.get(REFERENCE_GAME.ANALYSIS_STATUS) }.first(), outcome = outcome, lastUpdated = date?.atStartOfDay()?.toUtcInstant()?.toEpochMilliseconds(), + variant = Variant.XIANGQI, ) PuzzlesOriginalGameMetadataResponse.Entry(puzzleId, metadata) } else { diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/UserService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/UserService.kt index 0663faa08..baf4306e0 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/UserService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/UserService.kt @@ -3,8 +3,7 @@ package io.elephantchess.servicelayer.services import io.elephantchess.config.AppConfig import io.elephantchess.db.dao.codegen.tables.pojos.PasswordRecoveryAttempt import io.elephantchess.db.dao.codegen.tables.pojos.User -import io.elephantchess.db.services.PasswordRecoveryAttemptsDaoService -import io.elephantchess.db.services.UserDaoService +import io.elephantchess.db.services.* import io.elephantchess.db.utils.* import io.elephantchess.model.UserType import io.elephantchess.servicelayer.dto.ContactFormRequest @@ -17,12 +16,14 @@ import io.elephantchess.servicelayer.model.AuthenticatedToken import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.model.VerifiedToken import io.elephantchess.servicelayer.services.TokenManager.Companion.RENEW_SESSION_INTERVAL +import io.elephantchess.servicelayer.services.UserService.Companion.EMAIL_CONFIRMATION_CODE_EXPIRY_HOURS import io.elephantchess.servicelayer.utils.ops.launchAtFixedRateStartImmediately import io.elephantchess.utils.stripHtml import io.github.oshai.kotlinlogging.KLogger import kotlinx.coroutines.CoroutineScope -import org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric +import org.apache.commons.lang3.RandomStringUtils.insecure import java.time.LocalDate +import java.util.* import javax.crypto.SecretKeyFactory import javax.crypto.spec.PBEKeySpec import kotlin.time.Clock @@ -32,6 +33,11 @@ class UserService( appConfig: AppConfig, private val passwordRecoveryRequestDaoService: PasswordRecoveryAttemptsDaoService, private val userDaoService: UserDaoService, + private val userSessionDaoService: UserSessionDaoService, + private val playerVsPlayerGameDaoService: PlayerVsPlayerGameDaoService, + private val playerVsBotGameDaoService: PlayerVsBotGameDaoService, + private val puzzleResultDaoService: PuzzleResultDaoService, + private val referenceGameDaoService: ReferenceGameDaoService, private val userSessionService: UserSessionService, private val tokenManager: TokenManager, private val mailService: MailService, @@ -40,6 +46,11 @@ class UserService( private val logger: KLogger, ) { + private fun normalizeCountry(country: String?): String? = + country + ?.trim() + ?.takeUnless { it.isBlank() || it.equals("none", ignoreCase = true) } + @Volatile private var onlineUserIds: Set = emptySet() @@ -83,7 +94,7 @@ class UserService( } } - suspend fun signUp(request: SignUpRequest): ValidatedResponse { + suspend fun signUp(request: SignUpRequest, guestUserId: String? = null): ValidatedResponse { val errors = validateSignUpRequest(request) val now = Clock.System.now() @@ -98,9 +109,23 @@ class UserService( user.lastOnline = now user.userType = UserType.AUTHENTICATED user.puzzleRating = PUZZLE_START_RATING + user.emailConfirmationCode = UUID.randomUUID().toString() + user.emailConfirmationCodeCreatedAt = now userDaoService.save(user) - mailService.sendNewUserNotification(user) + val guestTransferred = + if (guestUserId != null) { + transferGuestData(guestUserId, user.id) + true + } else { + false + } + + // email + mailService.sendNewUserNotification(user, guestTransferred) + mailService.sendEmailConfirmation(user.email, user.emailConfirmationCode, showWelcomeMessage = true) mailService.verifyEmailAddressAsync(user.email) + + // result ValidatedResponse.Valid( SignUpResponse( userId = user.id, @@ -116,6 +141,51 @@ class UserService( } } + /** + * Confirms the email address associated with the given code, which was sent to the user by email at signup. + * Returns true if the code matches a user (whether it was already confirmed or not) and has not expired. + * + * The code is valid for [EMAIL_CONFIRMATION_CODE_EXPIRY_HOURS] hour(s) after it was generated; after that the + * user must request a new one from the settings page. Already-confirmed users remain confirmed regardless + * of code expiration (the check is idempotent). + * + * The result is stored in [User.emailConfirmedAt], which is separate from the automated verification + * results stored in the `email_verification` table. + */ + suspend fun confirmEmail(code: String): Boolean { + if (code.isBlank()) { + return false + } + val user = userDaoService.findByEmailConfirmationCode(code) ?: return false + if (user.emailConfirmedAt == null) { + val createdAt = user.emailConfirmationCodeCreatedAt + if (createdAt == null || createdAt.plusHours(EMAIL_CONFIRMATION_CODE_EXPIRY_HOURS) + .isBefore(Clock.System.now()) + ) { + logger.info { "email confirmation code expired for user ${user.id}" } + return false + } + userDaoService.markEmailConfirmed(user.id, Clock.System.now()) + logger.info { "email confirmed for user ${user.id}" } + } + return true + } + + /** + * Regenerates the email confirmation code (with a fresh creation timestamp) and re-sends the + * confirmation email. No-op for users whose email is already manually confirmed. + */ + suspend fun resendEmailConfirmation(userId: String) { + val user = userDaoService.findById(userId) ?: throw NotFoundException("User not found: $userId") + if (user.emailConfirmedAt != null) { + logger.debug { "email already confirmed for user $userId, skipping resend" } + return + } + val newCode = UUID.randomUUID().toString() + userDaoService.updateEmailConfirmationCode(userId, newCode, Clock.System.now()) + mailService.sendEmailConfirmation(user.email, newCode, showWelcomeMessage = false) + } + suspend fun obtainGuestUserToken(): ObtainAnonymousTokenResponse { val id = userDaoService.createGuestUser() logger.debug { "created guest #$id" } @@ -192,7 +262,7 @@ class UserService( if (mailService.isEmailAddressValid(request.email)) { val attemptRecord = PasswordRecoveryAttempt() attemptRecord.emailProvided = request.email - attemptRecord.recoveryCode = randomAlphanumeric(64) + attemptRecord.recoveryCode = insecure().nextAlphanumeric(64) attemptRecord.matchingUserId = user.id passwordRecoveryRequestDaoService.save(attemptRecord) @@ -235,6 +305,16 @@ class UserService( } } + /** + * Throws NotFoundException if user with given username does not exist + */ + suspend fun validateUserExists(username: String) { + val userExists = userDaoService.existsForUsername(username) + if (!userExists) { + throw NotFoundException("User $username could not be found") + } + } + suspend fun fetchProfile(username: String): UserProfile { // TODO: only fetch relevant fields val user = userDaoService.findByUserName(username) @@ -244,7 +324,7 @@ class UserService( UserProfile( userId = user.id, username = user.handle, - country = user.country, + country = normalizeCountry(user.country), profileDescription = user.description, puzzleRating = user.puzzleRating ) @@ -263,9 +343,6 @@ class UserService( } } - suspend fun fetchDescriptionByUserName(username: String) = - userDaoService.fetchDescriptionByUsername(username) - suspend fun updateProfileSettings(userId: String, request: ProfileSettingsDto) { // max 2 consecutive line breaks fun removeSuperfluousLineBreaks(input: String): String { @@ -278,7 +355,8 @@ class UserService( } val description = stripHtml(removeSuperfluousLineBreaks(request.description)) - userDaoService.updateProfileSettings(userId, description, request.country) + val country = normalizeCountry(request.country) + userDaoService.updateProfileSettings(userId, description, country) } suspend fun fetchNotificationsSettings(userId: String): NotificationsSettingsDto { @@ -315,10 +393,51 @@ class UserService( return EmailAddressSettingsResponse( email = email, - isValid = mailService.getEmailValidityStatus(email), + validityStatus = mailService.getEmailValidityDetails(email), + ) + } + + suspend fun fetchUserSessions(userId: String, limit: Int, offset: Int = 0): UserSessionsSettingsResponse { + val total = userSessionDaoService.countAuthenticatedSessionsForUser(userId) + val entries = + userSessionDaoService + .listAuthenticatedSessionsForUser(userId, limit, offset) + .map { record -> + UserSessionsSettingsResponse.Entry( + id = record.id!!, + os = record.operatingSystemName, + agentName = record.agentName, + countryCode = record.countryCode, + countryName = record.countryName, + region = record.region, + city = record.city, + remoteAddress = record.remoteAddress, + created = record.created!!.toEpochMilliseconds(), + updated = record.lastUpdated!!.toEpochMilliseconds(), + ) + } + + return UserSessionsSettingsResponse( + entries = entries, + total = total, ) } + suspend fun deleteUserSessions(userId: String, request: DeleteUserSessionsRequest): DeleteUserSessionsResponse { + val deletedCount = + userSessionDaoService.deleteAuthenticatedSessionsForUser( + userId = userId, + sessionIds = request.sessionIds.toSet() + ) + + return DeleteUserSessionsResponse(deletedCount) + } + + suspend fun deleteAllUserSessions(userId: String): DeleteUserSessionsResponse { + val deletedCount = userSessionDaoService.deleteAllAuthenticatedSessionsForUser(userId) + return DeleteUserSessionsResponse(deletedCount) + } + fun isOnline(userId: String): Boolean { return onlineUserIds.contains(userId) } @@ -356,7 +475,7 @@ class UserService( } if (!isValidUsername(request.username)) { - errors += "Username must be alphanumeric (can also include _ or -)" + errors += "Username must contain only letters, numbers, _ or -" } if (!isEmailFormatValid(request.email)) { @@ -377,6 +496,15 @@ class UserService( return errors } + private suspend fun transferGuestData(guestUserId: String, newUserId: String) { + logger.debug { "transferring data from guest $guestUserId to new user $newUserId" } + playerVsPlayerGameDaoService.transferFromGuestToUser(guestUserId, newUserId) + playerVsBotGameDaoService.transferFromGuestToUser(guestUserId, newUserId) + puzzleResultDaoService.transferFromGuestToUser(guestUserId, newUserId) + referenceGameDaoService.transferFromGuestToUser(guestUserId, newUserId) + userDaoService.transferRatingsFromGuest(guestUserId, newUserId) + } + private fun hash(password: CharArray): ByteArray { val spec = PBEKeySpec(password, salt, 65536, 128) return secretKeyFactory.generateSecret(spec).encoded!! @@ -393,6 +521,7 @@ class UserService( companion object { const val PASSWORD_RECOVERY_TIMEOUT_HOURS = 1L + const val EMAIL_CONFIRMATION_CODE_EXPIRY_HOURS = 1L const val SALT_ALGO = "PBKDF2WithHmacSHA1" const val USERNAME_MIN_LENGTH = 4 @@ -416,10 +545,10 @@ class UserService( } /** - * Allowed characters: alphanumeric, underscore and dashes + * Allowed characters: latin letters (including combining marks), numbers, underscore and dashes */ private fun isValidUsername(chars: String): Boolean = - chars.matches("^[a-zA-Z0-9_-]*$".toRegex()) + chars.matches("^[\\p{IsLatin}\\p{M}\\p{N}_-]*$".toRegex()) private fun isEmailFormatValid(chars: String): Boolean = chars.matches(EMAIL_REGEX) diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt index 5d246a2cb..b8241fd69 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt @@ -92,7 +92,8 @@ class AdminFeedService( winnerUserId = record.winnerUserId(), created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), - sourceType = record.joinSource + sourceType = record.joinSource, + variant = record.variant ) } .let { entries -> @@ -103,7 +104,7 @@ class AdminFeedService( suspend fun listLastBotGames(): ListBotGamesResponse { val entries = pvbGameDaoService - .listLastGamesByIdentifiedUsers(80) + .listLatestGamesByIdentifiedUsers(80) .map { gameRecord -> mapBotGameToDto(gameRecord) } return ListBotGamesResponse(entries) @@ -125,6 +126,7 @@ class AdminFeedService( isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), + variant = record.variant, ) } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/EmailConfirmationLinkTagResolver.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/EmailConfirmationLinkTagResolver.kt new file mode 100644 index 000000000..7ce64f2f9 --- /dev/null +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/EmailConfirmationLinkTagResolver.kt @@ -0,0 +1,19 @@ +package io.elephantchess.servicelayer.services.resolvers + +import io.elephantchess.htmlrenderer.TagResolver +import java.net.URLEncoder + +class EmailConfirmationLinkTagResolver( + private val webHost: String, + private val code: String, +) : TagResolver { + + override val tagName = "email_confirmation_link" + + override suspend fun resolveContent(): List { + val encodedCode = URLEncoder.encode(code, "UTF-8") + val url = "${webHost}/email/confirm?code=$encodedCode" + return listOf(makeAnchor(url)) + } + +} diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtils.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtils.kt index bed4ae2cf..384748205 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtils.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtils.kt @@ -1,5 +1,9 @@ package io.elephantchess.servicelayer.services.resolvers +import kotlinx.html.a +import kotlinx.html.stream.createHTML + +/** Builds an HTML anchor where both href and label are [url]. */ fun makeAnchor(url: String): String { - return "$url" + return createHTML().a(href = url) { +url } } diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/ws/PlayerVsPlayerWebSocketSession.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/ws/PlayerVsPlayerWebSocketSession.kt index dcc88923f..0eae2a9ed 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/ws/PlayerVsPlayerWebSocketSession.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/ws/PlayerVsPlayerWebSocketSession.kt @@ -2,6 +2,7 @@ package io.elephantchess.servicelayer.services.ws import io.elephantchess.model.GameEventType import io.elephantchess.servicelayer.dto.ws.PlayerVsPlayerUpdate +import io.elephantchess.servicelayer.model.UserId import kotlinx.coroutines.channels.ChannelResult import kotlin.time.Clock import kotlin.time.Duration.Companion.seconds @@ -9,6 +10,7 @@ import kotlin.time.Instant class PlayerVsPlayerWebSocketSession( val gameId: String, + val userId: UserId, private var status: GameEventType, private var moveIndex: Int, private var chatIndex: Int, @@ -64,7 +66,11 @@ class PlayerVsPlayerWebSocketSession( mustUpdate = true } - // push update + if (update.typingUsers.isNotEmpty()) { + mustUpdate = true + } + + // push update if (mustUpdate) { val result = sendCb(update) if (result.isClosed) { diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt index eaf27f9eb..e7b0bc777 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/utils/ops/Ops.kt @@ -11,6 +11,7 @@ import io.elephantchess.servicelayer.dto.game.RatingUpdate import io.elephantchess.xiangqi.AbstractPieceType import io.elephantchess.xiangqi.Board import io.elephantchess.xiangqi.Color +import io.elephantchess.xiangqi.Variant import io.github.oshai.kotlinlogging.KotlinLogging private val logger = KotlinLogging.logger("Ops") @@ -51,16 +52,18 @@ suspend fun EnginePool.safeQueryForDepth( engineId: EngineId, depth: Int, timeout: Long = 60_000, + variant: Variant = Variant.XIANGQI, ): InfoLinesResult? { var safeEngineId = engineId - if (engineId == PikafishEngineId && isNonStandardFen(fen)) { - logger.warn { "non-standard FEN detected, forcing use of Fairy Stockfish instead of Pikafish $fen" } + if (engineId == PikafishEngineId && (variant == Variant.MANCHU || isNonStandardFen(fen))) { + logger.warn { "non-standard FEN or Manchu variant detected, forcing use of Fairy Stockfish instead of Pikafish $fen" } safeEngineId = FairyStockfishEngineId } return queryForDepth( fen = fen, engineId = safeEngineId, depth = depth, - timeout = timeout + timeout = timeout, + variant = variant, ) } diff --git a/webapp-service-layer/src/main/resources/mail_templates/email_confirmation.html b/webapp-service-layer/src/main/resources/mail_templates/email_confirmation.html new file mode 100644 index 000000000..bfbaa437e --- /dev/null +++ b/webapp-service-layer/src/main/resources/mail_templates/email_confirmation.html @@ -0,0 +1,11 @@ +{{email_css}} +Thanks for signing up and welcome to elephantchess.io! +

+Please confirm your email address by clicking the following link: {{email_confirmation_link}}. +The link is valid for 1 hour. +

+You don't have to do it, but it might help for example if you need to reset your password in the future. +

+{{update_mail_settings}} +

+If you did not sign up, please contact elephantchess: {{contact_link}} diff --git a/webapp-service-layer/src/main/resources/mail_templates/new_user_notification.html b/webapp-service-layer/src/main/resources/mail_templates/new_user_notification.html index 141792e01..7508eaf5e 100644 --- a/webapp-service-layer/src/main/resources/mail_templates/new_user_notification.html +++ b/webapp-service-layer/src/main/resources/mail_templates/new_user_notification.html @@ -1,3 +1,4 @@ {{email_css}} new user: {{username}}
-email: {{email}} +email: {{email}}
+guest transferred: {{guest_transferred}} diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index f3d99889e..73f88f0f1 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -21,6 +21,7 @@ import io.elephantchess.servicelayer.model.UserId import io.elephantchess.xiangqi.Color import io.elephantchess.xiangqi.Color.BLACK import io.elephantchess.xiangqi.Color.RED +import io.elephantchess.xiangqi.Variant import io.elephantchess.xiangqi.testutils.Ops.endsInCheckmate import kotlinx.coroutines.test.runTest import org.jooq.DSLContext @@ -286,6 +287,40 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertEquals(2, countGameByStatus(CREATED)) } + /** + * A Manchu game and a Xiangqi game with compatible colors should NOT be matched together — + * variant must be part of the matching criteria. + */ + @Test + fun joinMatchingGameIncompatibleTest07() = runTest { + val manchuRequest = CreateGameRequest( + inviterColor = RED, + isRated = true, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = true, + alwaysVisibleInLobby = false, + privateInvite = false, + variant = Variant.MANCHU + ) + + val xiangqiRequest = manchuRequest.copy(inviterColor = BLACK, variant = Variant.XIANGQI) + + val response1 = pvpGameService.createGame(userId1, manchuRequest) + val response2 = pvpGameService.createGame(userId2, xiangqiRequest) + + assertEquals(CREATED, response1.eventType) + assertEquals(RED, response1.color) + + assertEquals(CREATED, response2.eventType) + assertEquals(BLACK, response2.color) + + assertEquals(0, countGameByStatus(JOINED)) + assertEquals(2, countGameByStatus(CREATED)) + } + + @Test fun happyPathTest01() = runTest { val gameId = createAndJoinGame(RED) @@ -336,10 +371,12 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { val expectedLoserRating = 1_000 - 8 val inviterWins = - (gameData.outcome == RED_WINS && inviterColor == RED) || (gameData.outcome == BLACK_WINS && inviterColor == BLACK) + (gameData.outcome == RED_WINS && inviterColor == RED) || + (gameData.outcome == BLACK_WINS && inviterColor == BLACK) val inviteeWins = - (gameData.outcome == RED_WINS && inviterColor == BLACK) || (gameData.outcome == BLACK_WINS && inviterColor == RED) + (gameData.outcome == RED_WINS && inviterColor == BLACK) || + (gameData.outcome == BLACK_WINS && inviterColor == RED) when { inviterWins -> { @@ -352,6 +389,37 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertEquals(expectedWinnerRating, ratingUpdate.inviteeRatingTo) } } + + // Xiangqi RAPID DB ratings must reflect the exact +8 / -8 change + val xiangqiRating1 = fetchXiangqiRapidRating(userId1.id) + val xiangqiRating2 = fetchXiangqiRapidRating(userId2.id) + + val userId1IsInviter = gameData.inviterId == userId1.id + when { + inviterWins -> { + if (userId1IsInviter) { + assertEquals(expectedWinnerRating, xiangqiRating1) + assertEquals(expectedLoserRating, xiangqiRating2) + } else { + assertEquals(expectedLoserRating, xiangqiRating1) + assertEquals(expectedWinnerRating, xiangqiRating2) + } + } + + inviteeWins -> { + if (userId1IsInviter) { + assertEquals(expectedLoserRating, xiangqiRating1) + assertEquals(expectedWinnerRating, xiangqiRating2) + } else { + assertEquals(expectedWinnerRating, xiangqiRating1) + assertEquals(expectedLoserRating, xiangqiRating2) + } + } + } + + // Manchu RAPID rating must remain unchanged at 1000 + assertEquals(1_000, fetchManchuRapidRating(userId1.id)) + assertEquals(1_000, fetchManchuRapidRating(userId2.id)) } @Test @@ -542,6 +610,151 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertTrue(afterUpdateEntry.isPreAnalyzed) } + /** + * Two Manchu games with compatible colors should be matched together. + */ + @Test + fun joinMatchingGameManchuTest01() = runTest { + val request1 = CreateGameRequest( + inviterColor = RED, + isRated = true, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = true, + alwaysVisibleInLobby = false, + privateInvite = false, + variant = Variant.MANCHU + ) + + val request2 = request1.copy(inviterColor = BLACK) + + val response1 = pvpGameService.createGame(userId1, request1) + val response2 = pvpGameService.createGame(userId2, request2) + + assertEquals(CREATED, response1.eventType) + assertEquals(RED, response1.color) + + assertEquals(JOINED, response2.eventType) + assertEquals(BLACK, response2.color) + + assertEquals(1, countGameByStatus(JOINED)) + assertEquals(0, countGameByStatus(CREATED)) + } + + /** + * Play a rated Manchu game to completion and verify that only the Manchu rating columns + * are updated — Xiangqi ratings must remain at the default 1000. + */ + @Test + fun happyPathManchuTest01() = runTest { + val gameId = createAndJoinManchuGame(RED) + val manchuMoves = manchuGameMovesCache.listAll().random() + + manchuMoves.uciMoves.dropLast(1).forEachIndexed { i, move -> + val result = pvpGameService.playMove( + userId = userIdToPlay(gameId), + request = PlayMoveRequest(gameId, move) + ) + assertEquals(i + 1, result.updatedIndex) + assertNull(result.gameEventType) + assertNull(result.ratingUpdate) + } + + val lastMoveResult = pvpGameService.playMove( + userId = userIdToPlay(gameId), + request = PlayMoveRequest(gameId, manchuMoves.uciMoves.last()) + ) + + assertNotNull(lastMoveResult.gameEventType) + val ratingUpdate = lastMoveResult.ratingUpdate!! + assertEquals(true, ratingUpdate.isRated) + assertEquals(1_000, ratingUpdate.inviterRatingFrom) + assertEquals(1_000, ratingUpdate.inviteeRatingFrom) + + val gameData = pvpGameService.fetchGame(gameId) + assertNotNull(gameData.outcome) + assertNotEquals(Outcome.DRAW, gameData.outcome) + + val inviterColor = gameData.inviterColor!! + val expectedWinnerRating = 1_000 + 8 + val expectedLoserRating = 1_000 - 8 + + val inviterWins = + (gameData.outcome == RED_WINS && inviterColor == RED) || + (gameData.outcome == BLACK_WINS && inviterColor == BLACK) + + val inviteeWins = + (gameData.outcome == RED_WINS && inviterColor == BLACK) || + (gameData.outcome == BLACK_WINS && inviterColor == RED) + + when { + inviterWins -> { + assertEquals(expectedWinnerRating, ratingUpdate.inviterRatingTo) + assertEquals(expectedLoserRating, ratingUpdate.inviteeRatingTo) + } + + inviteeWins -> { + assertEquals(expectedLoserRating, ratingUpdate.inviterRatingTo) + assertEquals(expectedWinnerRating, ratingUpdate.inviteeRatingTo) + } + } + + // Xiangqi RAPID rating must remain unchanged at 1000 + assertEquals(1_000, fetchXiangqiRapidRating(userId1.id)) + assertEquals(1_000, fetchXiangqiRapidRating(userId2.id)) + + // Manchu RAPID ratings must reflect the exact +8 / -8 change + val manchuRating1 = fetchManchuRapidRating(userId1.id) + val manchuRating2 = fetchManchuRapidRating(userId2.id) + + val userId1IsInviter = gameData.inviterId == userId1.id + when { + inviterWins -> { + if (userId1IsInviter) { + assertEquals(expectedWinnerRating, manchuRating1) + assertEquals(expectedLoserRating, manchuRating2) + } else { + assertEquals(expectedLoserRating, manchuRating1) + assertEquals(expectedWinnerRating, manchuRating2) + } + } + + inviteeWins -> { + if (userId1IsInviter) { + assertEquals(expectedLoserRating, manchuRating1) + assertEquals(expectedWinnerRating, manchuRating2) + } else { + assertEquals(expectedWinnerRating, manchuRating1) + assertEquals(expectedLoserRating, manchuRating2) + } + } + } + } + + private suspend fun createManchuGame(inviterColor: Color): String { + val request = CreateGameRequest( + inviterColor = inviterColor, + isRated = true, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = true, + alwaysVisibleInLobby = false, + privateInvite = false, + variant = Variant.MANCHU + ) + + val response = pvpGameService.createGame(userId1, request) + return response.gameId + } + + private suspend fun createAndJoinManchuGame(inviterColor: Color): String { + val gameId = createManchuGame(inviterColor) + pvpGameService.joinGame(userId2, JoinGameRequest(gameId)) + return gameId + } + private suspend fun createGame(inviterColor: Color): String { val request = CreateGameRequest( inviterColor = inviterColor, @@ -574,11 +787,25 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { private fun colorToPlay(currentHalfMoveIndex: Int) = if (currentHalfMoveIndex % 2 == 0) RED else BLACK - private suspend fun countGameByStatus(status: GameEventType) = + private suspend fun countGameByStatus(status: GameEventType): Int = dslContext .selectCount() .from(GAME) .where(GAME.GAME_STATUS.eq(status)) - .awaitSingleValue()!! + .awaitSingleValue()!! + + private suspend fun fetchXiangqiRapidRating(userId: String): Int = + dslContext + .select(USER.GAME_RATING_RAPID) + .from(USER) + .where(USER.ID.eq(userId)) + .awaitSingleValue()!! + + private suspend fun fetchManchuRapidRating(userId: String): Int = + dslContext + .select(USER.GAME_RATING_MANCHU_RAPID) + .from(USER) + .where(USER.ID.eq(userId)) + .awaitSingleValue()!! } diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PostgresTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PostgresTest.kt index 40736c83c..ce2310ff1 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PostgresTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PostgresTest.kt @@ -1,6 +1,10 @@ package io.elephantchess.servicelayer.services +import io.elephantchess.db.utils.awaitSingleValue import io.github.oshai.kotlinlogging.KotlinLogging +import org.jooq.Condition +import org.jooq.DSLContext +import org.jooq.TableField import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance @@ -12,11 +16,14 @@ abstract class PostgresTest { private val logger = KotlinLogging.logger {} + protected val dbUser = "postgres" + protected val dbPassword = "postgres" + protected val container: PostgreSQLContainer<*> by lazy { PostgreSQLContainer("postgres:17.6") .withDatabaseName("xiangqi") - .withUsername("postgres") - .withPassword("postgres") + .withUsername(dbUser) + .withPassword(dbPassword) } @BeforeAll @@ -30,4 +37,20 @@ abstract class PostgresTest { container.stop() } + /** + * Shortcut for `select(field).from(field.table).where(condition).awaitSingleValue()`. + * + * Useful in tests / one-off lookups where a single column value needs to be fetched + * by a simple where condition. + */ + suspend inline fun DSLContext.fetchValueAsync( + field: TableField<*, T>, + condition: Condition, + ): T? { + return select(field) + .from(field.table) + .where(condition) + .awaitSingleValue() + } + } diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt index 320471fff..51e419635 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt @@ -6,9 +6,10 @@ import io.elephantchess.db.utils.getDslContext import io.elephantchess.servicelayer.dto.user.SignUpRequest import io.elephantchess.servicelayer.serviceLayerModule import io.elephantchess.xiangqi.testutils.GameMovesDtoCache +import io.elephantchess.xiangqi.testutils.ManchuGameMovesDtoCache import io.github.oshai.kotlinlogging.KotlinLogging import liquibase.resource.ClassLoaderResourceAccessor -import org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric +import org.apache.commons.lang3.RandomStringUtils.insecure import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.koin.core.component.KoinComponent @@ -21,6 +22,7 @@ abstract class ServiceTest : PostgresTest(), KoinComponent { protected val logger = KotlinLogging.logger {} protected val gameMovesCache by lazy { GameMovesDtoCache() } + protected val manchuGameMovesCache by lazy { ManchuGameMovesDtoCache() } protected val userService by inject() @BeforeAll @@ -34,8 +36,8 @@ abstract class ServiceTest : PostgresTest(), KoinComponent { val dbConfig = DbConfig( dbName = "postgres", url = container.jdbcUrl, - user = "postgres", - password = "postgres", + user = dbUser, + password = dbPassword, ) getDslContext( @@ -54,11 +56,20 @@ abstract class ServiceTest : PostgresTest(), KoinComponent { super.afterAll() } - suspend fun signUpTestUser(i: Int = RandomUtils.nextInt(1_000, 1_000_000)): Pair { - val password = randomAlphanumeric(10) - val testUserIdentifier = "$i-${randomAlphanumeric(6)}" - val request = SignUpRequest("test$testUserIdentifier", "test$testUserIdentifier@gmail.com", password) - val either = userService.signUp(request) + suspend fun signUpTestUser( + i: Int = RandomUtils.nextInt(1_000, 1_000_000), + transferGuestData: Boolean = false, + guestUserId: String? = null, + ): Pair { + val password = insecure().nextAlphanumeric(10) + val testUserIdentifier = "$i-${insecure().nextAlphanumeric(6)}" + val request = SignUpRequest( + username = "test$testUserIdentifier", + email = "test$testUserIdentifier@gmail.com", + password = password, + transferGuestData = transferGuestData, + ) + val either = userService.signUp(request, guestUserId = guestUserId) return request to either.right().userId } diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ShardingTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ShardingTest.kt index a39ee2e7e..c4f4f5b8c 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ShardingTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ShardingTest.kt @@ -2,7 +2,7 @@ package io.elephantchess.servicelayer.services import io.elephantchess.servicelayer.model.Pod import io.github.oshai.kotlinlogging.KotlinLogging -import org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric +import org.apache.commons.lang3.RandomStringUtils.insecure import kotlin.test.Test import kotlin.test.assertEquals @@ -28,7 +28,7 @@ class ShardingTest { logger.debug { "margin: $margin" } repeat(n) { - val id = randomAlphanumeric(12) + val id = insecure().nextAlphanumeric(12) val pods = map.keys.filter { pod -> pod.mustProcess(id) } assertEquals(1, pods.size) diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/UserServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/UserServiceTest.kt index 9b02424bd..ee50609bb 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/UserServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/UserServiceTest.kt @@ -1,18 +1,55 @@ package io.elephantchess.servicelayer.services +import io.elephantchess.db.dao.codegen.Tables.* +import io.elephantchess.db.dao.codegen.tables.pojos.BotGame +import io.elephantchess.db.dao.codegen.tables.pojos.BotGameStatusEvent +import io.elephantchess.db.model.UserSessionRecord +import io.elephantchess.db.services.PlayerVsBotGameDaoService +import io.elephantchess.db.services.UserDaoService +import io.elephantchess.db.services.UserSessionDaoService +import io.elephantchess.db.utils.awaitExecute +import io.elephantchess.db.utils.minusHours +import io.elephantchess.model.* +import io.elephantchess.model.UserType.GUEST import io.elephantchess.servicelayer.dto.ValidatedResponse -import io.elephantchess.servicelayer.dto.user.SignUpRequest -import io.elephantchess.servicelayer.dto.user.UserLoginRequest +import io.elephantchess.servicelayer.dto.game.CreateGameRequest +import io.elephantchess.servicelayer.dto.user.* import io.elephantchess.servicelayer.exceptions.UnauthorizedException +import io.elephantchess.servicelayer.model.UserId import io.elephantchess.servicelayer.model.VerifiedToken +import io.elephantchess.xiangqi.Board.Companion.DEFAULT_START_FEN +import io.elephantchess.xiangqi.Color.RED import kotlinx.coroutines.test.runTest -import org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric +import org.apache.commons.lang3.RandomStringUtils.insecure +import org.jooq.DSLContext import org.koin.core.component.inject import kotlin.test.* +import kotlin.time.Clock +import kotlin.time.Duration.Companion.minutes class UserServiceTest : ServiceTest() { private val tokenManager by inject() + private val userDaoService by inject() + private val userSessionDaoService by inject() + private val pvpGameService by inject() + private val pvbGameDaoService by inject() + private val dslContext by inject() + + @AfterTest + fun afterEach() = runTest { + listOf( + GAME_MOVE, GAME_STATUS_EVENT, GAME, + BOT_GAME_MOVE, BOT_GAME_STATUS_EVENT, BOT_GAME, + REFERENCE_GAME_SEARCH_QUERY, + USER_SESSION, PUZZLE_RESULT, USER, PUZZLE + ) + .forEach { table -> + dslContext + .deleteFrom(table) + .awaitExecute() + } + } @Test fun hashTest01() = runTest { @@ -31,11 +68,15 @@ class UserServiceTest : ServiceTest() { // user cannot log in with wrong password assertFailsWith { - userService.login(UserLoginRequest(email, randomAlphanumeric(10))) + userService.login( + UserLoginRequest(email, insecure().nextAlphanumeric(10)) + ) } assertFailsWith { - userService.login(UserLoginRequest(username, randomAlphanumeric(10))) + userService.login( + UserLoginRequest(username, insecure().nextAlphanumeric(10)) + ) } // userId can be extracted from token @@ -56,13 +97,16 @@ class UserServiceTest : ServiceTest() { for (email in invalidEmails) { val request = SignUpRequest( - username = "testuser${randomAlphanumeric(5)}", + username = "testuser${insecure().nextAlphanumeric(5)}", email = email, password = "validPassword123" ) val result = userService.validateSignUp(request) assertIs>(result, "Email '$email' should be rejected") - assertTrue(result.left().errors.contains("Invalid email format"), "Should contain 'Invalid email format' error for '$email'") + assertTrue( + result.left().errors.contains("Invalid email format"), + "Should contain 'Invalid email format' error for '$email'" + ) } } @@ -77,13 +121,16 @@ class UserServiceTest : ServiceTest() { for (email in invalidEmails) { val request = SignUpRequest( - username = "testuser${randomAlphanumeric(5)}", + username = "testuser${insecure().nextAlphanumeric(5)}", email = email, password = "validPassword123" ) val result = userService.validateSignUp(request) assertIs>(result, "Email '$email' should be rejected") - assertTrue(result.left().errors.contains("Invalid email format"), "Should contain 'Invalid email format' error for '$email'") + assertTrue( + result.left().errors.contains("Invalid email format"), + "Should contain 'Invalid email format' error for '$email'" + ) } } @@ -98,7 +145,7 @@ class UserServiceTest : ServiceTest() { for (email in validEmails) { val request = SignUpRequest( - username = "testuser${randomAlphanumeric(5)}", + username = "testuser${insecure().nextAlphanumeric(5)}", email = email, password = "validPassword123" ) @@ -111,24 +158,30 @@ class UserServiceTest : ServiceTest() { fun `signUp should reject username that is too short`() = runTest { val request = SignUpRequest( username = "abc", // 3 chars, minimum is 4 - email = "valid${randomAlphanumeric(5)}@example.com", + email = "valid${insecure().nextAlphanumeric(5)}@example.com", password = "validPassword123" ) val result = userService.validateSignUp(request) assertIs>(result, "Username 'abc' should be rejected (too short)") - assertTrue(result.left().errors.contains("Username must be between 4 and 30 char."), "Should contain username length error") + assertTrue( + result.left().errors.contains("Username must be between 4 and 30 char."), + "Should contain username length error" + ) } @Test fun `signUp should reject username that is too long`() = runTest { val request = SignUpRequest( username = "a".repeat(31), // 31 chars, maximum is 30 - email = "valid${randomAlphanumeric(5)}@example.com", + email = "valid${insecure().nextAlphanumeric(5)}@example.com", password = "validPassword123" ) val result = userService.validateSignUp(request) assertIs>(result, "Username with 31 chars should be rejected (too long)") - assertTrue(result.left().errors.contains("Username must be between 4 and 30 char."), "Should contain username length error") + assertTrue( + result.left().errors.contains("Username must be between 4 and 30 char."), + "Should contain username length error" + ) } @Test @@ -138,18 +191,22 @@ class UserServiceTest : ServiceTest() { "user@name", // @ symbol "user.name", // dot "user!name", // exclamation mark - "用户名称", // non-ASCII characters + "用户名称", // Chinese characters + "user🙂name", // emoji ) for (username in invalidUsernames) { val request = SignUpRequest( username = username, - email = "valid${randomAlphanumeric(5)}@example.com", + email = "valid${insecure().nextAlphanumeric(5)}@example.com", password = "validPassword123" ) val result = userService.validateSignUp(request) assertIs>(result, "Username '$username' should be rejected") - assertTrue(result.left().errors.contains("Username must be alphanumeric (can also include _ or -)"), "Should contain invalid characters error for '$username'") + assertTrue( + result.left().errors.contains("Username must contain only letters, numbers, _ or -"), + "Should contain invalid characters error for '$username'" + ) } } @@ -161,13 +218,16 @@ class UserServiceTest : ServiceTest() { "user_name", // with underscore "user-name", // with dash "User_Name-123", // mixed case with underscore and dash + "nguyễn", // vietnamese letters + "trần", // vietnamese letters with diacritics + "Đặng_123", // vietnamese letters with underscore and digits "a".repeat(30), // maximum length ) for (username in validUsernames) { val request = SignUpRequest( username = username, - email = "valid${randomAlphanumeric(5)}@example.com", + email = "valid${insecure().nextAlphanumeric(5)}@example.com", password = "validPassword123" ) val result = userService.validateSignUp(request) @@ -178,25 +238,31 @@ class UserServiceTest : ServiceTest() { @Test fun `signUp should reject password that is too short`() = runTest { val request = SignUpRequest( - username = "validuser${randomAlphanumeric(5)}", - email = "valid${randomAlphanumeric(5)}@example.com", + username = "validuser${insecure().nextAlphanumeric(5)}", + email = "valid${insecure().nextAlphanumeric(5)}@example.com", password = "abc" // 3 chars, minimum is 4 ) val result = userService.validateSignUp(request) assertIs>(result, "Password 'abc' should be rejected (too short)") - assertTrue(result.left().errors.contains("Password must be between 4 and 50 char."), "Should contain password length error") + assertTrue( + result.left().errors.contains("Password must be between 4 and 50 char."), + "Should contain password length error" + ) } @Test fun `signUp should reject password that is too long`() = runTest { val request = SignUpRequest( - username = "validuser${randomAlphanumeric(5)}", - email = "valid${randomAlphanumeric(5)}@example.com", + username = "validuser${insecure().nextAlphanumeric(5)}", + email = "valid${insecure().nextAlphanumeric(5)}@example.com", password = "a".repeat(51) // 51 chars, maximum is 50 ) val result = userService.validateSignUp(request) assertIs>(result, "Password with 51 chars should be rejected (too long)") - assertTrue(result.left().errors.contains("Password must be between 4 and 50 char."), "Should contain password length error") + assertTrue( + result.left().errors.contains("Password must be between 4 and 50 char."), + "Should contain password length error" + ) } @Test @@ -209,8 +275,8 @@ class UserServiceTest : ServiceTest() { for (password in validPasswords) { val request = SignUpRequest( - username = "validuser${randomAlphanumeric(5)}", - email = "valid${randomAlphanumeric(5)}@example.com", + username = "validuser${insecure().nextAlphanumeric(5)}", + email = "valid${insecure().nextAlphanumeric(5)}@example.com", password = password ) val result = userService.validateSignUp(request) @@ -218,4 +284,355 @@ class UserServiceTest : ServiceTest() { } } + @Test + fun `fetchEmailAddressSettings should reflect the email validity status`() = runTest { + val (request, userId) = signUpTestUser() + + // Before confirmation, no automated check has run, so status is UNKNOWN. + val before = userService.fetchEmailAddressSettings(userId) + assertEquals(request.email, before.email) + assertEquals(EmailValidityStatus.UNKNOWN, before.validityStatus) + + // After the user clicks the confirmation link, the email is MANUALLY_CONFIRMED. + val code = userDaoService.findById(userId)!!.emailConfirmationCode + assertTrue(userService.confirmEmail(code)) + + val after = userService.fetchEmailAddressSettings(userId) + assertEquals(EmailValidityStatus.MANUALLY_CONFIRMED, after.validityStatus) + } + + @Test + fun `updateProfileSettings should unset country when none is selected`() = runTest { + val (request, userId) = signUpTestUser() + + userService.updateProfileSettings(userId, ProfileSettingsDto(description = "", country = "be")) + userService.updateProfileSettings(userId, ProfileSettingsDto(description = "", country = "none")) + + val profile = userService.fetchProfile(request.username) + assertNull(profile.country) + + val storedCountry = dslContext.fetchValueAsync(USER.COUNTRY, USER.ID.eq(userId)) + assertNull(storedCountry) + } + + @Test + fun `signUp should generate an email confirmation code and confirmEmail should mark the email as confirmed`() = + runTest { + val (_, userId) = signUpTestUser() + + // a confirmation code is generated at signup and the email is not yet confirmed + val userAfterSignup = userDaoService.findById(userId)!! + assertNotNull(userAfterSignup.emailConfirmationCode, "Confirmation code should be generated at signup") + assertNull(userAfterSignup.emailConfirmedAt, "Email should not be confirmed yet") + + // confirming with an unknown code does nothing + assertFalse(userService.confirmEmail("unknown-code")) + assertNull(userDaoService.findById(userId)!!.emailConfirmedAt) + + // confirming with a blank code does nothing + assertFalse(userService.confirmEmail("")) + assertNull(userDaoService.findById(userId)!!.emailConfirmedAt) + + // confirming with the right code marks the email as confirmed + assertTrue(userService.confirmEmail(userAfterSignup.emailConfirmationCode)) + val userAfterConfirmation = userDaoService.findById(userId)!! + assertNotNull(userAfterConfirmation.emailConfirmedAt, "Email should be confirmed") + + // confirming again is idempotent + val firstConfirmedAt = userAfterConfirmation.emailConfirmedAt + assertTrue(userService.confirmEmail(userAfterSignup.emailConfirmationCode)) + assertEquals(firstConfirmedAt, userDaoService.findById(userId)!!.emailConfirmedAt) + } + + @Test + fun `confirmEmail should reject an expired confirmation code`() = runTest { + val (_, userId) = signUpTestUser() + val userAfterSignup = userDaoService.findById(userId)!! + val code = userAfterSignup.emailConfirmationCode + + // simulate that the code was generated more than 1h ago + userDaoService.updateEmailConfirmationCode(userId, code, Clock.System.now().minusHours(2L)) + + assertFalse(userService.confirmEmail(code), "Expired code should be rejected") + assertNull(userDaoService.findById(userId)!!.emailConfirmedAt) + + // after a resend, a new code is generated with a fresh timestamp and confirmation works again + userService.resendEmailConfirmation(userId) + val refreshed = userDaoService.findById(userId)!! + assertNotEquals(code, refreshed.emailConfirmationCode, "Resend should generate a new code") + assertTrue(userService.confirmEmail(refreshed.emailConfirmationCode)) + assertNotNull(userDaoService.findById(userId)!!.emailConfirmedAt) + } + + @Test + fun `resendEmailConfirmation should be a no-op for already-confirmed users`() = runTest { + val (_, userId) = signUpTestUser() + val originalCode = userDaoService.findById(userId)!!.emailConfirmationCode + assertTrue(userService.confirmEmail(originalCode)) + + userService.resendEmailConfirmation(userId) + + // code is unchanged since the user is already confirmed + assertEquals(originalCode, userDaoService.findById(userId)!!.emailConfirmationCode) + } + + @Test + fun `fetchUserSessions should return latest sessions with total count`() = runTest { + val userId = signUpTestUser().second + + repeat(6) { i -> + userSessionDaoService.createOrUpdate( + UserSessionRecord( + userId = userId, + remoteAddress = "1.2.3.${i + 1}", + userAgent = "agent-$i", + operatingSystemName = "os-$i", + agentName = "browser-$i", + ) + ) + } + + val result = userService.fetchUserSessions(userId, limit = 5) + + assertEquals(6, result.total) + assertEquals(5, result.entries.size) + } + + @Test + fun `deleteUserSessions should only delete selected sessions`() = runTest { + val userId = signUpTestUser().second + + repeat(3) { i -> + userSessionDaoService.createOrUpdate( + UserSessionRecord( + userId = userId, + remoteAddress = "2.3.4.${i + 1}", + userAgent = "del-agent-$i", + operatingSystemName = "del-os-$i", + agentName = "del-browser-$i", + ) + ) + } + + val sessionsBeforeDelete = userService.fetchUserSessions(userId, limit = 10) + val selectedSessionIds = sessionsBeforeDelete.entries.take(2).map { it.id } + + val deleteResult = userService.deleteUserSessions(userId, DeleteUserSessionsRequest(selectedSessionIds)) + + assertEquals(2, deleteResult.deletedCount) + assertEquals(1, userService.fetchUserSessions(userId, limit = 10).entries.size) + } + + @Test + fun `signUp with guestUserId should transfer PvP games to new user`() = runTest { + val guestId = UserId(GUEST, userService.obtainGuestUserToken().id) + userService.refreshIsOnlineCache() + + val createGameRequest = CreateGameRequest( + inviterColor = RED, + isRated = false, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = true, + alwaysVisibleInLobby = false, + privateInvite = false + ) + val gameResponse = pvpGameService.createGame(guestId, createGameRequest) + val gameId = gameResponse.gameId + + // Verify the game is initially owned by the guest + val inviterBefore = dslContext.fetchValueAsync(GAME.INVITER, GAME.ID.eq(gameId)) + assertEquals(guestId.id, inviterBefore) + + // Sign up and pass the verified guest ID (simulating routing-level token extraction) + val newUserId = signUpTestUser(transferGuestData = true, guestUserId = guestId.id).second + + // The PvP game should now be owned by the new authenticated user + val inviterAfter = dslContext.fetchValueAsync(GAME.INVITER, GAME.ID.eq(gameId)) + assertEquals(newUserId, inviterAfter) + + // The original guest user ID should be recorded on the game row + val guestUserIdAfter = dslContext.fetchValueAsync(GAME.GUEST_USER_ID, GAME.ID.eq(gameId)) + assertEquals(guestId.id, guestUserIdAfter) + } + + @Test + fun `signUp with guestUserId should transfer PvB games to new user`() = runTest { + val guestId = UserId(GUEST, userService.obtainGuestUserToken().id) + + // Create a PvB game owned by the guest directly via the DAO (avoids running the engine) + val gameId = insecure().nextAlphanumeric(12) + val now = Clock.System.now() + + val gameRecord = BotGame().apply { + id = gameId + userId = guestId.id + userColor = RED + engine = Engine.FAIRYSTOCKFISH + engineVersion = "11.2" + depth = 4 + startFen = null + gameStatus = GameEventType.CREATED + currentFen = DEFAULT_START_FEN + currentHalfMoveIndex = 0 + created = now + lastUpdated = now + } + val statusRecord = BotGameStatusEvent().apply { + botGameId = gameId + eventType = GameEventType.CREATED + eventTime = now + } + pvbGameDaoService.insertGame(gameRecord, statusRecord) + + // Verify the bot game is initially owned by the guest + val userIdBefore = dslContext.fetchValueAsync(BOT_GAME.USER_ID, BOT_GAME.ID.eq(gameId)) + assertEquals(guestId.id, userIdBefore) + + // Sign up and pass the verified guest ID + val newUserId = signUpTestUser(transferGuestData = true, guestUserId = guestId.id).second + + // The PvB game should now be owned by the new authenticated user + val userIdAfter = dslContext.fetchValueAsync(BOT_GAME.USER_ID, BOT_GAME.ID.eq(gameId)) + assertEquals(newUserId, userIdAfter) + + // The original guest user ID should be recorded on the bot game row + val guestUserIdAfter = dslContext.fetchValueAsync(BOT_GAME.GUEST_USER_ID, BOT_GAME.ID.eq(gameId)) + assertEquals(guestId.id, guestUserIdAfter) + } + + @Test + fun `signUp without guestUserId should not transfer any data`() = runTest { + val guestId = UserId(GUEST, userService.obtainGuestUserToken().id) + userService.refreshIsOnlineCache() + + val createGameRequest = CreateGameRequest( + inviterColor = RED, + isRated = false, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = true, + alwaysVisibleInLobby = false, + privateInvite = false + ) + val gameResponse = pvpGameService.createGame(guestId, createGameRequest) + val gameId = gameResponse.gameId + + // Sign up without passing a guest user ID (no transfer) + signUpTestUser() + + // The game should still belong to the guest + val inviterAfter = dslContext.fetchValueAsync(GAME.INVITER, GAME.ID.eq(gameId)) + assertEquals(guestId.id, inviterAfter) + + val guestUserId = dslContext.fetchValueAsync(GAME.GUEST_USER_ID, GAME.ID.eq(gameId)) + assertNull(guestUserId, "Guest user ID should be null since no transfer occurred") + } + + @Test + fun `signUp with guestUserId should transfer puzzle rating to new user`() = runTest { + val guestTokenResponse = userService.obtainGuestUserToken() + val guestId = UserId(GUEST, guestTokenResponse.id) + + // Verify the guest starts with the default puzzle rating + val guestRatingBefore = dslContext.fetchValueAsync(USER.PUZZLE_RATING, USER.ID.eq(guestId.id)) + assertEquals(800, guestRatingBefore) + + // Simulate the guest improving their puzzle rating + dslContext.update(USER) + .set(USER.PUZZLE_RATING, 950) + .where(USER.ID.eq(guestId.id)) + .awaitExecute() + + // Sign up and transfer guest data + val newUserId = signUpTestUser(transferGuestData = true, guestUserId = guestId.id).second + + // The new user should have the guest's puzzle rating + val newUserRating = dslContext.fetchValueAsync(USER.PUZZLE_RATING, USER.ID.eq(newUserId)) + assertEquals(950, newUserRating) + } + + @Test + fun `signUp with guestUserId should transfer puzzle results to new user`() = runTest { + val guestId = UserId(GUEST, userService.obtainGuestUserToken().id) + + // Insert a minimal puzzle row (required by the puzzle_result FK) + val puzzleId = "testpzl1" + dslContext.insertInto(PUZZLE) + .set(PUZZLE.ID, puzzleId) + .set(PUZZLE.REF_GAME_SOURCE, "test") + .set(PUZZLE.REF_GAME_SOURCE_ID, "testSource1") + .set(PUZZLE.ALGORITHM, PuzzleAlgo.FIND_PATH_TO_MATE) + .set(PUZZLE.PLAYER_COLOR, RED) + .set(PUZZLE.START_FEN, DEFAULT_START_FEN) + .set(PUZZLE.INITIAL_RATING, 1000) + .set(PUZZLE.RATING, 1000) + .awaitExecute() + + // Insert a puzzle result for the guest + dslContext.insertInto(PUZZLE_RESULT) + .set(PUZZLE_RESULT.PUZZLE_ID, puzzleId) + .set(PUZZLE_RESULT.USER_ID, guestId.id) + .set(PUZZLE_RESULT.OUTCOME, PuzzleOutcome.SOLVED) + .set(PUZZLE_RESULT.PUZZLE_RATING_FROM, 1000) + .set(PUZZLE_RESULT.PUZZLE_RATING_TO, 1005) + .set(PUZZLE_RESULT.PLAYER_RATING_FROM, 800) + .set(PUZZLE_RESULT.PLAYER_RATING_TO, 810) + .awaitExecute() + + // Sign up and transfer guest data + val newUserId = signUpTestUser(transferGuestData = true, guestUserId = guestId.id).second + + // The puzzle result should now belong to the new user + val userIdAfter = dslContext.fetchValueAsync( + PUZZLE_RESULT.USER_ID, + PUZZLE_RESULT.PUZZLE_ID.eq(puzzleId) + ) + assertEquals(newUserId, userIdAfter) + + // The original guest user ID should be recorded on the puzzle result row + val guestUserIdAfter = dslContext.fetchValueAsync( + PUZZLE_RESULT.GUEST_USER_ID, + PUZZLE_RESULT.PUZZLE_ID.eq(puzzleId) + ) + assertEquals(guestId.id, guestUserIdAfter) + } + + @Test + fun `signUp with guestUserId should transfer reference game searches to new user`() = runTest { + val guestId = UserId(GUEST, userService.obtainGuestUserToken().id) + + // Insert a reference game search query owned by the guest + val queryId = insecure().nextAlphanumeric(12) + val now = Clock.System.now() + dslContext.insertInto(REFERENCE_GAME_SEARCH_QUERY) + .set(REFERENCE_GAME_SEARCH_QUERY.QUERY_ID, queryId) + .set(REFERENCE_GAME_SEARCH_QUERY.USER_ID, guestId.id) + .set(REFERENCE_GAME_SEARCH_QUERY.QUERY_TIME, now) + .set(REFERENCE_GAME_SEARCH_QUERY.UPDATE_TIME, now) + .set(REFERENCE_GAME_SEARCH_QUERY.PLAYER_NAME, "Hu Ronghua") + .set(REFERENCE_GAME_SEARCH_QUERY.LIMIT, 20) + .set(REFERENCE_GAME_SEARCH_QUERY.NUMBER_OF_RESULTS, 5) + .awaitExecute() + + // Sign up and transfer guest data + val newUserId = signUpTestUser(transferGuestData = true, guestUserId = guestId.id).second + + // The search query should now belong to the new user + val userIdAfter = dslContext.fetchValueAsync( + REFERENCE_GAME_SEARCH_QUERY.USER_ID, + REFERENCE_GAME_SEARCH_QUERY.QUERY_ID.eq(queryId), + ) + assertEquals(newUserId, userIdAfter) + + // The original guest user ID should be recorded on the search query row + val guestUserIdAfter = dslContext.fetchValueAsync( + REFERENCE_GAME_SEARCH_QUERY.GUEST_USER_ID, + REFERENCE_GAME_SEARCH_QUERY.QUERY_ID.eq(queryId), + ) + assertEquals(guestId.id, guestUserIdAfter) + } + } diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtilsTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtilsTest.kt new file mode 100644 index 000000000..cf93d5f07 --- /dev/null +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/resolvers/ResolverUtilsTest.kt @@ -0,0 +1,17 @@ +package io.elephantchess.servicelayer.services.resolvers + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class ResolverUtilsTest { + + @Test + fun `makeAnchor renders anchor using html builder`() { + val url = "https://elephantchess.io/game?id=42" + val anchor = makeAnchor(url) + assertEquals("""$url""", anchor) + assertFalse(anchor.endsWith("\n")) + } + +} diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/WebAppKoinModule.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/WebAppKoinModule.kt index 2564f9d81..1265ec15b 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/WebAppKoinModule.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/WebAppKoinModule.kt @@ -73,10 +73,13 @@ private fun htmlRendering() = module { private fun pageRendererModule(eagerAllowed: Boolean) = module { singleAuto(eager = eagerAllowed) + singleAuto(eager = eagerAllowed) singleAuto(eager = eagerAllowed) singleAuto(eager = eagerAllowed) singleAuto(eager = eagerAllowed) singleAuto(eager = eagerAllowed) + singleAuto(eager = eagerAllowed) + singleAuto(eager = eagerAllowed) } private fun listFragmentTags(): List { diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/ops/ValidationRoutingOps.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/ops/ValidationRoutingOps.kt index 095c306a9..d10a76481 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/ops/ValidationRoutingOps.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/ops/ValidationRoutingOps.kt @@ -8,6 +8,7 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +// FIXME: what's the point of this beside naming? suspend inline fun RoutingContext.handleValidatedResponse( handler: (T) -> ValidatedResponse ) { @@ -25,3 +26,11 @@ suspend inline fun RoutingCo call.respond(NotAcceptable, either.left()) } } + +suspend inline fun RoutingContext.handleEither(either: GenericEither) { + if (either.isRight()) { + call.respond(Created, either.right()) + } else { + call.respond(NotAcceptable, either.left()) + } +} diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/BoardGuiExampleRenderer.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/BoardGuiExampleRenderer.kt index 94a4391b2..68cf47db2 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/BoardGuiExampleRenderer.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/BoardGuiExampleRenderer.kt @@ -37,11 +37,6 @@ class BoardGuiExampleRenderer(private val simplePageRenderer: SimplePageRenderer .let(::escapeHtml) } - private fun escapeHtml(s: String): String = - s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - private fun boardGuiAssetsTagResolver(useCdn: Boolean): TagResolver = CallbackTagResolver("board_gui_assets") { var distPath = "/dist/$DIST_VERSION" @@ -57,7 +52,7 @@ class BoardGuiExampleRenderer(private val simplePageRenderer: SimplePageRenderer companion object { - const val DIST_VERSION = "0.0.2" + const val DIST_VERSION = "0.1.0" } } diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/ChangelogPageRenderer.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/ChangelogPageRenderer.kt new file mode 100644 index 000000000..ccfad06b8 --- /dev/null +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/ChangelogPageRenderer.kt @@ -0,0 +1,58 @@ +package io.elephantchess.webapp.rendering + +import io.elephantchess.htmlrenderer.TagResolver +import io.elephantchess.utils.ResourceUtils.resourceAsString + +class ChangelogPageRenderer(private val simplePageRenderer: SimplePageRenderer) { + + suspend fun renderChangelogPage(): String = + simplePageRenderer.renderTemplate(CHANGELOG_TEMPLATE_NAME, listOf(changelogTocTagResolver())) + + private fun changelogTocTagResolver(): TagResolver = + CallbackTagResolver("changelog_toc") { + val html = resourceAsString(CHANGELOG_TEMPLATE_PATH) + ?: throw IllegalStateException("Changelog template not found at $CHANGELOG_TEMPLATE_PATH") + + renderChangelogToc(html) + } + + companion object { + + const val CHANGELOG_TEMPLATE_NAME = "about/changelog" + const val CHANGELOG_TEMPLATE_PATH = "/templates/$CHANGELOG_TEMPLATE_NAME.html" + + // Match `

...

`, capturing the year-month id. + val H1_MONTH_WITH_ID_REGEX = + Regex("""]*\bid\s*=\s*"(\d{4})-(\d{2})"[^>]*>[\s\S]*?""", RegexOption.IGNORE_CASE) + + /** + * Parses the changelog HTML and returns the table of content as an HTML fragment, grouped + * by quarter. Each TOC entry is a quarter label (e.g. "Q2 2026") that links to the first + * month section belonging to that quarter in document order (i.e. the most recent month + * of the quarter, since the changelog is reverse-chronological). + */ + fun renderChangelogToc(html: String): String { + val quarters = linkedMapOf, String>() // (year, quarter) -> first month id + H1_MONTH_WITH_ID_REGEX.findAll(html).forEach { match -> + val year = match.groupValues[1].toInt() + val month = match.groupValues[2].toInt() + if (month in 1..12) { + val quarter = (month - 1) / 3 + 1 + val key = year to quarter + val id = "${match.groupValues[1]}-${match.groupValues[2]}" + quarters.putIfAbsent(key, id) + } + } + + if (quarters.isEmpty()) return "" + val items = quarters.entries.joinToString("\n") { (key, id) -> + val (year, quarter) = key + val label = "Q$quarter $year" + """
  • ${escapeHtml(label)}
  • """ + } + return """""" + } + + } + +} diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt index 8e1ea10e0..4cf970d16 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt @@ -1,12 +1,22 @@ package io.elephantchess.webapp.rendering import io.elephantchess.htmlrenderer.HtmlRenderer +import io.elephantchess.htmlrenderer.KtorHtmlBuilderTagResolver import io.elephantchess.htmlrenderer.SimpleValueTagResolver import io.elephantchess.htmlrenderer.TagResolver +import io.elephantchess.model.AnalysisStatus +import io.elephantchess.model.Outcome import io.elephantchess.servicelayer.dto.database.* import io.elephantchess.utils.cropToFirstNWords import io.elephantchess.utils.formatWithChineseName import io.github.reactivecircus.cache4k.Cache +import io.ktor.http.* +import kotlinx.html.a +import kotlinx.html.li +import kotlinx.html.meta +import kotlinx.html.p +import kotlinx.html.style +import kotlinx.html.unsafe import kotlin.time.Duration.Companion.hours class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) { @@ -30,36 +40,43 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) { fetchEditorsUsername: suspend () -> List, ): String { val description = edit.profileText + val editors = fetchEditorsUsername() val playerNameEncodedResolver = SimpleValueTagResolver("player_name_encoded", databasePlayer.urlName) val playerIdResolver = SimpleValueTagResolver("player_id", databasePlayer.id) - val descriptionResolver = CallbackTagResolver("player_profile_description") { - description?.let { formatNewLinesToHtmlParagraphs(it) } + val descriptionResolver = KtorHtmlBuilderTagResolver("player_profile_description") { + description?.toParagraphs()?.forEach { p { unsafe { +it } } } } - val sourcesResolver = CallbackTagResolver("player_profile_sources") { - edit.sources - .sortedBy { it.index } - .joinToString("") { source -> - """
  • ${source.title}
  • """ + val sourcesResolver = KtorHtmlBuilderTagResolver("player_profile_sources") { + edit.sources.sortedBy { it.index }.forEach { source -> + li { + a(href = source.url, target = "_blank") { + rel = "noopener noreferrer" + +source.title + } } + } } - val styleResolver = CallbackTagResolver("player_profile_description_style") { + val styleResolver = KtorHtmlBuilderTagResolver("player_profile_description_style") { if (description == null) { - "" - } else { - "" + style { + unsafe { + +"#player-profile-description { display: none; }" + } + } } } - val authorMeta = CallbackTagResolver("author_meta") { - val editors = fetchEditorsUsername() + val authorMeta = KtorHtmlBuilderTagResolver("author_meta") { if (editors.isNotEmpty()) { - meta("author", editors.sorted().joinToString(", ")) - } else { - "" + val names = editors.sortedBy { it.lowercase() }.joinToString(", ") + meta { + name = "author" + content = names + } } } @@ -255,8 +272,137 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) { } } + suspend fun renderGamePage(summary: DatabaseGameSummary, orientation: String? = null): String { + fun formatName(canonicalName: String?, chineseName: String?): String { + return if (!canonicalName.isNullOrBlank()) { + formatWithChineseName(canonicalName, chineseName) + } else { + "" + } + } + + val redDisplay = formatName(summary.redPlayerCanonicalName, summary.redPlayerChineseName) + val blackDisplay = formatName(summary.blackPlayerCanonicalName, summary.blackPlayerChineseName) + + val title = "$redDisplay vs. $blackDisplay" + + val outcomeText = when (summary.outcome) { + Outcome.RED_WINS -> "$redDisplay won (red)" + Outcome.BLACK_WINS -> "$blackDisplay won (black)" + Outcome.DRAW -> "draw" + null -> null + } + + val description = buildString { + append("Chinese chess (Xiangqi) game between $redDisplay and $blackDisplay") + if (!summary.eventName.isNullOrBlank()) append(" at ${summary.eventName}") + summary.date?.let { append(", played on $it") } + append(".") + outcomeText?.let { append(" Outcome: $it.") } + when (summary.analysisStatus) { + AnalysisStatus.COMPLETED -> + append(" Full engine analysis available.") + + AnalysisStatus.PARTIALLY_COMPLETED -> + append(" Partial engine analysis available.") + + else -> { + // not analyzed yet: don't mention it + } + } + } + + fun playerLink(canonicalName: String?, displayName: String, colorClass: String): String { + val escapedName = escapeHtml(displayName) + return if (canonicalName.isNullOrBlank()) { + """$escapedName""" + } else { + val urlName = canonicalName.replace(" ", "_").encodeURLPath() + """$escapedName""" + } + } + + val winnerStar = + """winner""" + val redWinnerSuffix = if (summary.outcome == Outcome.RED_WINS) " $winnerStar" else "" + val blackWinnerSuffix = if (summary.outcome == Outcome.BLACK_WINS) " $winnerStar" else "" + + val playersInfoHtml = buildString { + append( + """
    ${ + playerLink( + summary.redPlayerCanonicalName, + redDisplay, + "red-color" + ) + }$redWinnerSuffix
    """ + ) + append( + """
    ${ + playerLink( + summary.blackPlayerCanonicalName, + blackDisplay, + "black-color" + ) + }$blackWinnerSuffix
    """ + ) + } + + val dateInfoHtml = summary.date?.toString() ?: "" + + val eventInfoHtml = summary.eventName?.let { name -> + val escapedName = escapeHtml(name) + val eventId = summary.eventId + if (!eventId.isNullOrBlank()) { + """$escapedName""" + } else { + escapedName + } + } ?: "" + + // PGN-friendly fields (also surfaced as body data-* attributes so the JS + // does not need to fetch metadata to render the board or build the PGN download). + // Each resolver below renders the full `data-x="..."` attribute when the value + // is present, or an empty string when it isn't, so we don't pollute the body + // tag with empty attributes like `data-pgn-date=""`. + val pgnResult = when (summary.outcome) { + Outcome.RED_WINS -> "1-0" + Outcome.BLACK_WINS -> "0-1" + Outcome.DRAW -> "1/2-1/2" + null -> null + } + val pgnDate = summary.date?.let { d -> + "%04d.%02d.%02d".format(d.year, d.monthValue, d.dayOfMonth) + } + + return htmlRenderer.renderHtml( + templatePath = "/templates/database/database_game_viewer.html", + specificTagResolvers = listOf( + SimpleValueTagResolver("page_title", title), + SimpleValueTagResolver("description_meta", descriptionMeta(description)), + SimpleValueTagResolver("players_info", playersInfoHtml), + SimpleValueTagResolver("game_date_info", dateInfoHtml), + SimpleValueTagResolver("game_event_info", eventInfoHtml), + SimpleValueTagResolver("body_data_attrs", buildString { + append(dataAttr("game-id", summary.gameId)) + append(dataAttr("orientation", orientation)) + append(dataAttr("final-fen", summary.finalFen)) + append(dataAttr("pgn-red-player", summary.redPlayerCanonicalName)) + append(dataAttr("pgn-black-player", summary.blackPlayerCanonicalName)) + append(dataAttr("pgn-event", summary.eventName)) + append(dataAttr("pgn-date", pgnDate)) + append(dataAttr("pgn-result", pgnResult)) + }), + ), + canonicalPath = "/database/game?id=${summary.gameId}" + ) + } + private companion object { + fun dataAttr(name: String, value: String?): String = + if (value.isNullOrBlank()) "" else """ data-$name="${escapeHtmlAttr(value)}"""" + fun descriptionMeta(edit: DatabasePlayerProfileEdit): TagResolver { return CallbackTagResolver("description_meta") { val profileText = edit.profileText diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabaseTagResolvers.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabaseTagResolvers.kt index 46f3ad06b..6f2ab9329 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabaseTagResolvers.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabaseTagResolvers.kt @@ -23,7 +23,7 @@ fun eventsTableTagResolver( } buildString { - append("""""") + append("""
    """) append("") append("") append("") @@ -48,7 +48,7 @@ fun playersTableTagResolver( playerStandings: PlayersListResponse ) = CallbackTagResolver("players_table") { buildString { - append("""
    EventDate
    """) + append("""
    """) append("") append("") append("") @@ -117,7 +117,7 @@ fun eventGamesListTagResolver( ) val lines = mutableListOf() - lines += """
    PlayerW
    """ + lines += """
    """ lines += "" for (game in sortedGames) { lines += """() lines += """

    Standings

    """ - lines += """
    redoutcomeblack
    """ + lines += """
    """ lines += "" for ((index, entry) in sortedScores.withIndex()) { val (triple, stats) = entry diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/FaqPageRenderer.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/FaqPageRenderer.kt new file mode 100644 index 000000000..825186693 --- /dev/null +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/FaqPageRenderer.kt @@ -0,0 +1,55 @@ +package io.elephantchess.webapp.rendering + +import io.elephantchess.htmlrenderer.TagResolver +import io.elephantchess.utils.ResourceUtils.resourceAsString + +class FaqPageRenderer(private val simplePageRenderer: SimplePageRenderer) { + + suspend fun renderFaqPage(): String = + simplePageRenderer.renderTemplate(FAQ_TEMPLATE_NAME, listOf(faqTocTagResolver())) + + private fun faqTocTagResolver(): TagResolver = + CallbackTagResolver("faq_toc") { + val html = resourceAsString(FAQ_TEMPLATE_PATH) + ?: throw IllegalStateException("FAQ template not found at $FAQ_TEMPLATE_PATH") + + renderFaqToc(html) + } + + private companion object { + + const val FAQ_TEMPLATE_NAME = "about/faq" + const val FAQ_TEMPLATE_PATH = "/templates/$FAQ_TEMPLATE_NAME.html" + + // Match `

    inner

    ` across lines, capturing id and inner content. + val H1_WITH_ID_REGEX = Regex("""]*\bid\s*=\s*"([^"]+)"[^>]*>([\s\S]*?)""", RegexOption.IGNORE_CASE) + val INNER_TAG_REGEX = Regex("<[^>]+>") + val WHITESPACE_REGEX = Regex("\\s+") + + /** + * Parses the FAQ HTML and returns the table of content as an HTML fragment. + */ + fun renderFaqToc(html: String): String { + val entries = H1_WITH_ID_REGEX.findAll(html) + .map { match -> + val id = match.groupValues[1].trim() + val title = match.groupValues[2] + .replace(INNER_TAG_REGEX, "") + .replace(WHITESPACE_REGEX, " ") + .trim() + + id to title + } + .filter { (id, title) -> id.isNotEmpty() && title.isNotEmpty() } + .toList() + + if (entries.isEmpty()) return "" + val items = entries.joinToString("\n") { (id, title) -> + """
  • ${escapeHtml(title)}
  • """ + } + return """""" + } + + } + +} diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/GamePageRenderer.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/GamePageRenderer.kt new file mode 100644 index 000000000..5e2812420 --- /dev/null +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/GamePageRenderer.kt @@ -0,0 +1,104 @@ +package io.elephantchess.webapp.rendering + +import io.elephantchess.htmlrenderer.HtmlRenderer +import io.elephantchess.htmlrenderer.SimpleValueTagResolver +import io.elephantchess.model.Engine +import io.elephantchess.servicelayer.services.KofiService +import io.elephantchess.servicelayer.services.PlayerVsBotGameService +import io.elephantchess.servicelayer.services.PlayerVsPlayerGameService +import io.elephantchess.xiangqi.Color + +class GamePageRenderer( + private val htmlRenderer: HtmlRenderer, + private val kofiService: KofiService, + private val pvpGameService: PlayerVsPlayerGameService, + private val pvbGameService: PlayerVsBotGameService +) { + + suspend fun renderPvpGamePage(gameId: String): String { + val title = fetchPvpTitle(gameId) + + return htmlRenderer.renderHtml( + templatePath = "/templates/player_vs_player_game.html", + specificTagResolvers = listOf( + SimpleValueTagResolver("page_title", title) + ), + canonicalPath = "/game?id=$gameId" + ) + } + + suspend fun renderPvbGamePage(gameId: String): String { + val title = fetchPvbTitle(gameId) + val latestSupporter = kofiService.fetchLatestSupporter() + + return htmlRenderer.renderHtml( + templatePath = "/templates/player_vs_bot.html", + specificTagResolvers = listOf( + SimpleValueTagResolver("page_title", title), + latestSupporterTagResolver(latestSupporter) + ), + canonicalPath = "/playbot?id=$gameId" + ) + } + + private suspend fun fetchPvpTitle(gameId: String): String { + val title = gameId.let { id -> + runCatching { + val game = pvpGameService.fetchGame(id) + formatPvpPageTitle(game.inviterUsername, game.inviteeUsername, game.inviterColor) + }.getOrDefault(DEFAULT_PVP_TITLE) + } + + return escapeHtml(title) + } + + private suspend fun fetchPvbTitle(gameId: String): String { + val title = gameId.let { id -> + runCatching { + val game = pvbGameService.fetchGameData(id) + pvbPageTitle(game.username, game.userColor, game.engine, game.depth) + }.getOrDefault(DEFAULT_PVB_TITLE) + } + + return escapeHtml(title) + } + + companion object { + internal const val DEFAULT_PVP_TITLE = "Game" + internal const val DEFAULT_PVB_TITLE = "Play vs. Bot" + + internal fun formatPvpPageTitle( + inviterUsername: String, + inviteeUsername: String?, + inviterColor: Color? + ): String { + if (inviterColor == null) { + return "new game by $inviterUsername" + } + + val invitee = inviteeUsername ?: "" + return when (inviterColor) { + Color.RED -> "$inviterUsername vs. $invitee" + Color.BLACK -> "$invitee vs. $inviterUsername" + } + } + + internal fun pvbPageTitle(username: String?, userColor: Color, engine: Engine, depth: Int): String { + val humanPlayer = username ?: "anonymous" + val botPlayer = "${formatEngineName(engine)} ($depth)" + + return when (userColor) { + Color.RED -> "$humanPlayer vs. $botPlayer" + Color.BLACK -> "$botPlayer vs. $humanPlayer" + } + } + + private fun formatEngineName(engine: Engine): String { + return when (engine) { + Engine.PIKAFISH -> "Pikafish" + Engine.FAIRYSTOCKFISH -> "Fairy Stockfish" + } + } + } + +} diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/TagResolvers.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/TagResolvers.kt index 4226e69be..69c07bdd9 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/TagResolvers.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/TagResolvers.kt @@ -8,27 +8,43 @@ import java.time.LocalDate import java.time.ZoneOffset.UTC import java.time.format.DateTimeFormatter.ofPattern import java.util.Locale +import kotlinx.html.meta +import kotlinx.html.stream.createHTML import kotlin.math.floor private const val COST_IN_EUR_PER_DAY = 4 private val supportedCurrencies = setOf("USD", "EUR", "GBP") -fun formatNewLinesToHtmlParagraphs(str: String): String { - return str +/** + * HTML-escapes a string so it can be safely inlined as text content. + */ +fun escapeHtml(s: String): String = + s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + +/** + * HTML-escapes a string so it can be safely used as an attribute value. + */ +fun escapeHtmlAttr(s: String): String = + escapeHtml(s).replace("\"", """) + +fun String.toParagraphs(): List { + return this .split("\n\n") - .filter { it.trim().isNotEmpty() } - .joinToString("") { "

    ${it.trim()}

    " } + .map { it.trim() } + .filter { it.isNotEmpty() } } -fun meta(name: String, content: String) = - """""" - fun descriptionMeta(description: String): String { - return meta( - "description", description - .replace("\n", " ") - .replace("\r", "") - ) + return createHTML().meta { + name = "description" + content = stripHtml( + description + .replace("\n", " ") + .replace("\r", "") + ) + } } fun latestSupporterTagResolver(latestSupporter: LatestSupporter?): TagResolver { diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/UserProfilePageRenderer.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/UserProfilePageRenderer.kt index 298332b7d..cb03a0302 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/UserProfilePageRenderer.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/UserProfilePageRenderer.kt @@ -1,10 +1,16 @@ package io.elephantchess.webapp.rendering import io.elephantchess.htmlrenderer.HtmlRenderer +import io.elephantchess.htmlrenderer.KtorHtmlBuilderTagResolver import io.elephantchess.htmlrenderer.SimpleValueTagResolver import io.elephantchess.htmlrenderer.TagResolver import io.elephantchess.servicelayer.dto.user.UserProfile import io.elephantchess.utils.cropToFirstNWords +import io.ktor.http.encodeURLPath +import kotlinx.html.div +import kotlinx.html.id +import kotlinx.html.img +import kotlinx.html.p class UserProfilePageRenderer(private val htmlRenderer: HtmlRenderer) { @@ -15,6 +21,7 @@ class UserProfilePageRenderer(private val htmlRenderer: HtmlRenderer) { return htmlRenderer.renderHtml( templatePath = "/templates/user_profile.html", + canonicalPath = "/@/${username.encodeURLPath()}", specificTagResolvers = listOf( noIndexMeta(description), SimpleValueTagResolver("user_id", userProfile.userId), @@ -48,33 +55,40 @@ class UserProfilePageRenderer(private val htmlRenderer: HtmlRenderer) { } private fun flagPanelTagResolver(countryCode: String?): TagResolver { - return CallbackTagResolver("flag_header_panel") { - if (countryCode != null) { - """
    - |$countryCode - |
    """.trimMargin() - } else { - "" + return KtorHtmlBuilderTagResolver("flag_header_panel") { + if (!countryCode.isNullOrBlank() && !countryCode.equals("none", ignoreCase = true)) { + div("profile-header-panel") { + id = "flag-header-panel" + attributes["data-country-code"] = countryCode + img(alt = countryCode, src = "/images/flags/$countryCode.svg", classes = "flag-icons") { + id = "profile-flag" + } + } } } } private fun descriptionDivTagResolver(username: String, description: String?): TagResolver { - return CallbackTagResolver("user_profile_description") { - if (description != null) { - buildString { - append("""
    """) - append(formatNewLinesToHtmlParagraphs(description)) - append("""
    """) + return KtorHtmlBuilderTagResolver("user_profile_description") { + if (!description.isNullOrBlank()) { + div { + id = "profile-description" + description.toParagraphs().forEach { p { +it } } } } else { - buildString { - append("""
    """) - append("$username has not filled their description yet.") - append("""
    """) + div("empty-block-placeholder") { + id = "profile-description" + +"$username has not filled their description yet." } } } } + suspend fun renderUserBrowsePvpGames(username: String): String { + return htmlRenderer.renderHtml( + "/templates/user_browse_pvp_games.html", + specificTagResolvers = listOf(SimpleValueTagResolver("username", username)) + ) + } + } diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/EmailNotificationRoutes.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/EmailSettingRoutes.kt similarity index 69% rename from webapp/src/main/kotlin/io/elephantchess/webapp/routing/EmailNotificationRoutes.kt rename to webapp/src/main/kotlin/io/elephantchess/webapp/routing/EmailSettingRoutes.kt index 588edae05..fcb6cb5be 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/EmailNotificationRoutes.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/EmailSettingRoutes.kt @@ -3,6 +3,7 @@ package io.elephantchess.webapp.routing import io.elephantchess.htmlrenderer.SimpleValueTagResolver import io.elephantchess.servicelayer.exceptions.BadRequestException import io.elephantchess.servicelayer.services.MailService +import io.elephantchess.servicelayer.services.UserService import io.elephantchess.servicelayer.utils.obfuscateEmail import io.elephantchess.servicelayer.utils.ops.koin import io.elephantchess.webapp.rendering.SimplePageRenderer @@ -12,11 +13,33 @@ import io.ktor.server.response.* import io.ktor.server.routing.* private val simplePageRenderer by koin() -private val mailService by koin() + +fun Application.emailSettingUpdatePages() { + emailConfirmationRoutes() + newsletterUnsubscriptionRoutes() +} + +// renders a public page when the user clicks the confirmation link sent to them by email at signup. +private fun Application.emailConfirmationRoutes() { + val userService by koin() + routing { + get("/email/confirm") { + val code = call.parameters["code"].orEmpty() + val confirmed = userService.confirmEmail(code) + val template = if (confirmed) { + "email_confirmation/email_confirmed" + } else { + "email_confirmation/email_confirmation_error" + } + call.respondText(simplePageRenderer.renderTemplateNoCache(template), ContentType.Text.Html) + } + } +} + // when sending the newsletter, there is a link to unsubscribe, which contains a unique code that identifies // the user and the type of unsubscription (newsletter only or all notifications). -fun Application.newsletterUnsubscriptionRoutes() { +private fun Application.newsletterUnsubscriptionRoutes() { routing { get("/unsubscribe") { val code = call.parameters["code"] ?: throw BadRequestException("code not provided") @@ -26,6 +49,8 @@ fun Application.newsletterUnsubscriptionRoutes() { } private suspend fun processAndRenderUnsubscription(code: String): String { + val mailService by koin() + return when (val matching = mailService.unsubscribeFromEmailNotifications(code)) { null -> simplePageRenderer.renderTemplateNoCache("unsubscribe/unsubscription_error") else -> { diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/DatabaseRouting.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/DatabaseRouting.kt index 903859142..8cff108f7 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/DatabaseRouting.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/DatabaseRouting.kt @@ -77,6 +77,12 @@ fun Route.databaseRoutes() { } } } + get("/list-user-searches") { + val continuation = call.request.queryParameters["continuation"]?.toLongOrNull() + requireIdentification { verifiedToken -> + databaseService.listUserSearches(verifiedToken.userId, continuation) + } + } get("/search") { requireIdentification { verifiedToken -> val dateStart = call.request.queryParameters["dateStart"] diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/GameDataRouting.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/GameDataRouting.kt index 9f74b80b0..f5b22dc76 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/GameDataRouting.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/GameDataRouting.kt @@ -45,12 +45,29 @@ fun Route.gameDataRoutes() { ) } } + get("/list-latest-pvp-games-by-user") { + // distinctByUsers is ignored here: filtering by username already scopes results to one user + paginationParams { limit, continuation, _ -> + val username = call.parameters["username"] + ?: throw BadRequestException("username parameter is required") + + gameDataService.listLastPvpGamesByUsername( + username = username, + requestedLimit = limit, + beforeTs = continuation + ) + } + } get("/list-latest-pvb-games") { paginationParams { limit, continuation, distinctByUsers -> - gameDataService.listLastPvbGames( + // false by default + val excludeAutoResigned = (call.parameters["excludeAutoResigned"] ?: "false").toBoolean() + + gameDataService.listLatestPvbGames( requestedLimit = limit, distinctByUsers = distinctByUsers, - beforeTs = continuation + beforeTs = continuation, + excludeAutoResigned = excludeAutoResigned ) } } diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/UserRouting.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/UserRouting.kt index e585964c0..0e4f70144 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/UserRouting.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/api/UserRouting.kt @@ -2,6 +2,7 @@ package io.elephantchess.webapp.routing.api import io.elephantchess.servicelayer.dto.ContactFormRequest import io.elephantchess.servicelayer.dto.user.* +import io.elephantchess.servicelayer.model.GuestToken import io.elephantchess.servicelayer.services.GlobalAnalyticsService import io.elephantchess.servicelayer.services.UserProfileAnalyticsService import io.elephantchess.servicelayer.services.UserService @@ -41,8 +42,15 @@ private fun Route.loginAndSignUpRoutes() { } } post("/signup") { - handleValidatedResponse { request -> - userService.signUp(request) + requireIdentificationWithBody { verifiedToken, request -> + val guestUserId = + if (request.transferGuestData) { + (verifiedToken as? GuestToken)?.userId + } else { + null + } + + handleEither(userService.signUp(request, guestUserId)) } } get("/obtain-guest-user-token") { @@ -143,6 +151,37 @@ private fun Route.userSettingsRoutes() { userService.fetchEmailAddressSettings(verifiedToken.userId) } } + post("/email-address/resend-confirmation") { + requireAuthentication { verifiedToken -> + userService.resendEmailConfirmation(verifiedToken.userId) + } + } + get("/sessions") { + requireAuthentication { verifiedToken -> + val limit = + call.request.queryParameters["limit"] + ?.toIntOrNull() + ?.coerceAtLeast(1) + ?: 10 + val offset = + call.request.queryParameters["offset"] + ?.toIntOrNull() + ?.coerceAtLeast(0) + ?: 0 + + userService.fetchUserSessions(verifiedToken.userId, limit, offset) + } + } + post("/sessions/delete") { + requireAuthenticationWithBody { verifiedToken, request -> + userService.deleteUserSessions(verifiedToken.userId, request) + } + } + post("/sessions/delete-all") { + requireAuthentication { verifiedToken -> + userService.deleteAllUserSessions(verifiedToken.userId) + } + } } } diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlDatabaseRouting.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlDatabaseRouting.kt index 01562a76d..a9ba0360f 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlDatabaseRouting.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlDatabaseRouting.kt @@ -112,6 +112,12 @@ internal fun Routing.databasePages() { call.respondHtml(databasePageRenderer.renderBrowseEventGamesPage(eventId, eventName, round)) } + get("/database/game") { + val gameId = call.parameters["id"] ?: throw BadRequestException("id query parameter not provided") + val summary = databaseService.fetchGameSummary(gameId) ?: throw NotFoundException("Game not found") + val orientation = call.request.queryParameters["orientation"] + call.respondHtml(databasePageRenderer.renderGamePage(summary, orientation)) + } } private suspend fun RoutingContext.resolveDatabasePlayer(render: suspend (DatabasePlayer) -> String) { diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlRouting.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlRouting.kt index e21d3dd9c..c1f6a47d1 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlRouting.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/routing/html/HtmlRouting.kt @@ -5,13 +5,13 @@ import io.elephantchess.servicelayer.services.KofiService import io.elephantchess.servicelayer.services.UserService import io.elephantchess.servicelayer.utils.ops.koin import io.elephantchess.webapp.ops.* -import io.elephantchess.webapp.rendering.ModalRenderer -import io.elephantchess.webapp.rendering.SimplePageRenderer -import io.elephantchess.webapp.rendering.UserProfilePageRenderer -import io.elephantchess.webapp.rendering.latestSupporterTagResolver +import io.elephantchess.webapp.rendering.* +import io.elephantchess.webapp.routing.emailSettingUpdatePages import io.github.oshai.kotlinlogging.KotlinLogging.logger +import io.ktor.http.ContentType.Text.Html import io.ktor.http.HttpStatusCode.Companion.BadRequest import io.ktor.http.HttpStatusCode.Companion.NotFound +import io.ktor.http.content.TextContent import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* @@ -20,16 +20,14 @@ private val logger = logger {} internal val simplePageRenderer by koin() -private val publicPageMapping = mapOf( +private val simplePublicPageMapping = mapOf( "/401" to "401", "/403" to "403", "/404" to "404", - "/game" to "player_vs_player_game", "/puzzles" to "puzzles", - "/board" to "simple_board", + "/board" to "test_board", "/database" to "database/database_search", "/database/search" to "database/database_search", - "/database/game" to "database/database_game_viewer", "/browse/player-vs-player" to "browse_pvp_games", "/browse/player-vs-bot" to "browse_pvb_games", "/analysis" to "analysis_board", @@ -37,17 +35,15 @@ private val publicPageMapping = mapOf( "/recovery" to "password_recovery1", "/recovery/finalize" to "password_recovery2", "/about" to "about/about", - "/about/changelog" to "about/changelog", - "/about/faq" to "about/faq", "/contact" to "contact_form", + "/how-to-play-xiangqi" to "how_to_play_xiangqi", "/7k/game" to "seven_kingdoms/seven_kingdoms_game", "/7k/playground" to "seven_kingdoms/seven_kingdoms_playground", "/7k/about" to "seven_kingdoms/seven_kingdoms_about", ) -private val publicPageMappingWithSupporterBanner = mapOf( +private val simplePublicPageMappingWithSupporterBanner = mapOf( "/" to "lobby", - "/playbot" to "player_vs_bot", ) private val publicPageRedirection = mapOf( @@ -60,12 +56,14 @@ private val identificationRequiredPagesMapping = mapOf( "/userdata/games" to "userdata/my_games", "/userdata/botgames" to "userdata/my_bot_games", "/userdata/analysis" to "userdata/my_analysis", - "/userdata/puzzles" to "userdata/my_played_puzzles" + "/userdata/puzzles" to "userdata/my_played_puzzles", + "/userdata/db-searches" to "userdata/my_db_searches", ) // only available authenticated users private val authenticatedRequiredPagesMapping = mapOf( "/user/settings" to "user_settings", + "/user/settings/sessions" to "user_sessions", ) private val adminPagesMapping = mapOf( @@ -94,15 +92,40 @@ fun Application.htmlRoutingModule() { routing { simpleMappings() simpleMappingsWithSupporterBanner() + gamePages() boardGuiExample() userProfile() modals() databasePages() + aboutPages() + emailSettingUpdatePages() + } +} + +private fun Route.gamePages() { + val gamePageRenderer by koin() + + get("/game") { + val gameId = call.parameters["id"] + if (gameId == null) { + call.respond(TextContent(simplePageRenderer.renderTemplate("404"), Html)) + } else { + call.respondHtml(gamePageRenderer.renderPvpGamePage(gameId)) + } + } + + get("/playbot") { + val gameId = call.parameters["id"] + if (gameId == null) { + call.respond(TextContent(simplePageRenderer.renderTemplate("404"), Html)) + } else { + call.respondHtml(gamePageRenderer.renderPvbGamePage(gameId)) + } } } private fun Routing.simpleMappings() { - publicPageMapping.forEach { (path, templateName) -> + simplePublicPageMapping.forEach { (path, templateName) -> logger.info { "[public] mapping html file $templateName to path $path" } get(path) { call.respond(simplePageRenderer.renderTemplateHtml(templateName)) @@ -143,7 +166,7 @@ private fun Routing.simpleMappings() { private fun Routing.simpleMappingsWithSupporterBanner() { val kofiService by koin() - publicPageMappingWithSupporterBanner.forEach { (path, templateName) -> + simplePublicPageMappingWithSupporterBanner.forEach { (path, templateName) -> logger.info { "[public] mapping html file $templateName to path $path (with supporter banner)" } get(path) { val latestTipper = kofiService.fetchLatestSupporter() @@ -168,6 +191,25 @@ private fun Route.userProfile() { val userProfileResponse = userService.fetchProfile(username) call.respondHtml(renderer.renderUserProfile(userProfileResponse)) } + get("/@/{username}/browse-pvp-games") { + val username = call.parameters["username"] + ?: throw BadRequestException("username not provided") + + userService.validateUserExists(username) + call.respondHtml(renderer.renderUserBrowsePvpGames(username)) + } +} + +private fun Route.aboutPages() { + val changelogPageRenderer by koin() + val faqPageRenderer by koin() + + get("/about/faq") { + call.respondHtml(faqPageRenderer.renderFaqPage()) + } + get("/about/changelog") { + call.respondHtml(changelogPageRenderer.renderChangelogPage()) + } } private fun Route.modals() { diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/server/ApiServiceRoutingModule.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/server/ApiServiceRoutingModule.kt index e30c37c64..da01c650c 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/server/ApiServiceRoutingModule.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/server/ApiServiceRoutingModule.kt @@ -47,7 +47,6 @@ fun Application.apiServiceModule() { lobbyRoutes() supporterRoutes() adminConsoleRoutes() - newsletterUnsubscriptionRoutes() integrationRoutes() } } diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/sitemap/SiteMapService.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/sitemap/SiteMapService.kt index d3c091ffb..c356cb60b 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/sitemap/SiteMapService.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/sitemap/SiteMapService.kt @@ -23,6 +23,7 @@ class SiteMapService( ) { private val deployDay = LocalDate.now() + private val howToPlayLastPageChange = LocalDate.of(2026, 5, 24) private val sevenKingdomLastChange = LocalDate.of(2025, 12, 10) private val webHostTrimmed = appConfig.webHost.trimEnd('/') @@ -96,6 +97,12 @@ class SiteMapService( changefreq = MONTHLY, priority = 0.8 ), + SitemapEntry( + loc = "$webHostTrimmed/how-to-play-xiangqi", + lastmod = howToPlayLastPageChange, + changefreq = MONTHLY, + priority = 0.8 + ), SitemapEntry( loc = "$webHostTrimmed/7k/about", lastmod = sevenKingdomLastChange, diff --git a/webapp/src/main/resources/logback.xml b/webapp/src/main/resources/logback.xml index 0c70e5a4f..b9a49c160 100644 --- a/webapp/src/main/resources/logback.xml +++ b/webapp/src/main/resources/logback.xml @@ -14,6 +14,7 @@ + diff --git a/webapp/src/main/resources/modals/import-moves.html b/webapp/src/main/resources/modals/import-moves.html index 57e01c4a7..91e614bb6 100644 --- a/webapp/src/main/resources/modals/import-moves.html +++ b/webapp/src/main/resources/modals/import-moves.html @@ -4,7 +4,7 @@ about -

    Import PGN, WXF, UCI or Algebraic

    +

    Import PGN, WXF, UCI, ICCS or Algebraic

    diff --git a/webapp/src/main/resources/modals/new-game.html b/webapp/src/main/resources/modals/new-game.html index ced74109c..628634849 100644 --- a/webapp/src/main/resources/modals/new-game.html +++ b/webapp/src/main/resources/modals/new-game.html @@ -6,6 +6,28 @@

    New Game

    Create a new game that other players will be able to join. You can wait for another player to join your or share the link with a friend. +
    #PlayerWDLScore
    + + + + + + +
    +
    + variant +
    +
    +
    + 象 Xiangqi +
    +
    +
    + 统 Manchu +
    +
    + + + + diff --git a/webapp/src/main/resources/templates/player_vs_player_game.html b/webapp/src/main/resources/templates/player_vs_player_game.html index b819fde7d..c6ba7be36 100644 --- a/webapp/src/main/resources/templates/player_vs_player_game.html +++ b/webapp/src/main/resources/templates/player_vs_player_game.html @@ -1,9 +1,10 @@ {{header_init}} - Game + {{page_title}} {{meta_no_index}} + {{apex_charts}} {{board_js}} @@ -16,6 +17,10 @@ {{body_init}} +
    +
    --:--
    +
    --:--
    +
    @@ -81,6 +86,12 @@
    + + + +
    diff --git a/webapp/src/main/resources/modals/play-bot.html b/webapp/src/main/resources/modals/play-bot.html index 97481cdc3..6789c1783 100644 --- a/webapp/src/main/resources/modals/play-bot.html +++ b/webapp/src/main/resources/modals/play-bot.html @@ -1,145 +1,106 @@ + + diff --git a/webapp/src/main/resources/modals/signup.html b/webapp/src/main/resources/modals/signup.html index 086fbc939..960ad419b 100644 --- a/webapp/src/main/resources/modals/signup.html +++ b/webapp/src/main/resources/modals/signup.html @@ -33,6 +33,12 @@

    Sign Up

    +
    Depth
    Outcome
    Status diff --git a/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_about.html b/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_about.html index 2cf8c3a3d..67075816e 100644 --- a/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_about.html +++ b/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_about.html @@ -6,7 +6,6 @@ - @@ -325,5 +324,6 @@

    Emperor

    {{footer}} +{{kofi_floating_widget}} diff --git a/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_game.html b/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_game.html index 05c586773..f302946f1 100644 --- a/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_game.html +++ b/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_game.html @@ -7,7 +7,6 @@ {{meta_no_index}} - diff --git a/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_playground.html b/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_playground.html index 8ca3189bc..0144b775c 100644 --- a/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_playground.html +++ b/webapp/src/main/resources/templates/seven_kingdoms/seven_kingdoms_playground.html @@ -6,7 +6,6 @@ - @@ -59,5 +58,6 @@ {{logged_as_guest_message}} +{{kofi_floating_widget}} diff --git a/webapp/src/main/resources/templates/simple_board.html b/webapp/src/main/resources/templates/simple_board.html deleted file mode 100644 index 56a509c43..000000000 --- a/webapp/src/main/resources/templates/simple_board.html +++ /dev/null @@ -1,39 +0,0 @@ - - - {{header_init}} - Chinese Chess (Xiangqi) Simple Board – elephantchess.io - - {{board_js}} - - - -{{body_init}} -
    -
    -
    - - - - - - - -
    - chess - - Just a simple board tool to test positions or play IRL on a tablet. -
    -
    -
    - -
    -
    -
    - -
    - {{settings_info_box}} -
    -
    -{{logged_as_guest_message}} - - diff --git a/webapp/src/main/resources/templates/test_board.html b/webapp/src/main/resources/templates/test_board.html new file mode 100644 index 000000000..6cf7c1ba2 --- /dev/null +++ b/webapp/src/main/resources/templates/test_board.html @@ -0,0 +1,66 @@ + + + {{header_init}} + Chinese Chess (Xiangqi) Test Board – elephantchess.io + + {{board_js}} + + + + +{{body_init}} +
    +
    + +
    + + + + + + + +
    + chess + + Just a simple test board tool to test positions or play IRL on a tablet. +
    +
    +
    +

    + In Manchu chess (also called Yitong): Black's pieces are set up and move the same as in xiangqi, but + horses, cannons, and one of the chariots are absent for Red. The remaining chariot has the combined + powers of the chariot, horse, and cannon. +

    +

    + Although Black appears to have the advantage, the lethality of the red chariot can easily lead to a + losing endgame if Black does not play cautiously. +

    +

    + The red chariot is believed to be the representation of Solon soldiers who were brave and + battle-hardened during the Manchu conquest of China. +

    +

    + This special red chariot is also called "super chariot" and represented as M in the FEN notation. +

    +

    + https://en.wikipedia.org/wiki/Manchu_chess +

    +
    +
    + +
    +
    +
    + +
    + {{settings_info_box}} +
    + +
    +
    +{{logged_as_guest_message}} +{{kofi_floating_widget}} + + diff --git a/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_all.html b/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_all.html index e37a3673d..e5bf6589e 100644 --- a/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_all.html +++ b/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_all.html @@ -8,7 +8,7 @@ {{body_init}}
    - {{emailAddress}} has been successfully unsubscribed from all email notifications +

    {{emailAddress}} has been successfully unsubscribed from all email notifications

    {{footer}} diff --git a/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_newsletter.html b/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_newsletter.html index 28baa5629..e34a072bb 100644 --- a/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_newsletter.html +++ b/webapp/src/main/resources/templates/unsubscribe/unsubscribed_from_newsletter.html @@ -8,7 +8,7 @@ {{body_init}}
    - {{emailAddress}} has been successfully unsubscribed from the newsletter +

    {{emailAddress}} has been successfully unsubscribed from the newsletter

    {{footer}} diff --git a/webapp/src/main/resources/templates/user_browse_pvp_games.html b/webapp/src/main/resources/templates/user_browse_pvp_games.html new file mode 100644 index 000000000..571121d07 --- /dev/null +++ b/webapp/src/main/resources/templates/user_browse_pvp_games.html @@ -0,0 +1,32 @@ + + + {{header_init}} + {{username}}'s Player-vs-Player Games + + + + + + + + + +{{body_init}} +
    +

    {{username}}'s Player-vs-Player Games

    +
    +
    + {{game_thumb}}[[iterations:6; {gameType: 'pvp', parentClass: 'pvp-game-thumb'}]] +
    + + + +
    + + diff --git a/webapp/src/main/resources/templates/user_profile.html b/webapp/src/main/resources/templates/user_profile.html index f5f94e397..54a83989f 100644 --- a/webapp/src/main/resources/templates/user_profile.html +++ b/webapp/src/main/resources/templates/user_profile.html @@ -5,15 +5,19 @@ {{meta_no_index_conditional}} {{description_meta}} + {{apex_charts}} + + + - + {{body_init}}
    @@ -79,6 +83,19 @@

    Game Stats

    +
    +

    Puzzle Stats

    diff --git a/webapp/src/main/resources/templates/user_sessions.html b/webapp/src/main/resources/templates/user_sessions.html new file mode 100644 index 000000000..7e7828351 --- /dev/null +++ b/webapp/src/main/resources/templates/user_sessions.html @@ -0,0 +1,50 @@ + + + {{header_init}} + Sessions + {{meta_no_index}} + + + + + + + + +{{body_init}} +
    +

    Sessions

    +
    + +
    + + + + + + + + + + + + + + + + +
    osagentcountryregioncityIPcreatedupdated
    +
    + + + +
    + + diff --git a/webapp/src/main/resources/templates/user_settings.html b/webapp/src/main/resources/templates/user_settings.html index 8adf76d69..1ff9212dd 100644 --- a/webapp/src/main/resources/templates/user_settings.html +++ b/webapp/src/main/resources/templates/user_settings.html @@ -3,14 +3,16 @@ {{header_init}} Settings {{meta_no_index}} + + {{body_init}} -
    +

    Profile Settings


    Description

    @@ -30,22 +32,39 @@

    Email Notifications Settings

    Email Address


    - diff --git a/webapp/src/main/resources/templates/userdata/my_analysis.html b/webapp/src/main/resources/templates/userdata/my_analysis.html index 55bceacf0..6abc3b41c 100644 --- a/webapp/src/main/resources/templates/userdata/my_analysis.html +++ b/webapp/src/main/resources/templates/userdata/my_analysis.html @@ -5,7 +5,6 @@ {{meta_no_index}} - diff --git a/webapp/src/main/resources/templates/userdata/my_bot_games.html b/webapp/src/main/resources/templates/userdata/my_bot_games.html index 42cb5c148..cdcf22fb0 100644 --- a/webapp/src/main/resources/templates/userdata/my_bot_games.html +++ b/webapp/src/main/resources/templates/userdata/my_bot_games.html @@ -5,7 +5,6 @@ {{meta_no_index}} - diff --git a/webapp/src/main/resources/templates/userdata/my_db_searches.html b/webapp/src/main/resources/templates/userdata/my_db_searches.html new file mode 100644 index 000000000..5ed31ed33 --- /dev/null +++ b/webapp/src/main/resources/templates/userdata/my_db_searches.html @@ -0,0 +1,32 @@ + + + {{header_init}} + My Database Searches + {{meta_no_index}} + + + + + + + + +{{body_init}} +
    +

    My Database Searches

    +
    +
    +
    +
    + Loading more searches... +
    +
    + No more searches to load. +
    +
    + You haven't searched the database yet. Go to the database to start searching. +
    + {{logged_as_guest_message}} +
    + + diff --git a/webapp/src/main/resources/templates/userdata/my_games.html b/webapp/src/main/resources/templates/userdata/my_games.html index 86908d7bb..668338885 100644 --- a/webapp/src/main/resources/templates/userdata/my_games.html +++ b/webapp/src/main/resources/templates/userdata/my_games.html @@ -5,7 +5,6 @@ {{meta_no_index}} - diff --git a/webapp/src/main/resources/templates/userdata/my_played_puzzles.html b/webapp/src/main/resources/templates/userdata/my_played_puzzles.html index 26a50dcba..877f1234f 100644 --- a/webapp/src/main/resources/templates/userdata/my_played_puzzles.html +++ b/webapp/src/main/resources/templates/userdata/my_played_puzzles.html @@ -5,7 +5,6 @@ {{meta_no_index}} - diff --git a/webapp/src/main/resources/web_fragments/analysis_summary_report.html b/webapp/src/main/resources/web_fragments/analysis_summary_report.html index 314c3ccea..b12a8f4b1 100644 --- a/webapp/src/main/resources/web_fragments/analysis_summary_report.html +++ b/webapp/src/main/resources/web_fragments/analysis_summary_report.html @@ -4,6 +4,7 @@ about
    +
    diff --git a/webapp/src/main/resources/web_fragments/base_js.html b/webapp/src/main/resources/web_fragments/base_js.html index f749b3f34..d21a5e3e7 100644 --- a/webapp/src/main/resources/web_fragments/base_js.html +++ b/webapp/src/main/resources/web_fragments/base_js.html @@ -17,6 +17,7 @@ + diff --git a/webapp/src/main/resources/web_fragments/board_js.html b/webapp/src/main/resources/web_fragments/board_js.html index c7fe2305e..24b4ad745 100644 --- a/webapp/src/main/resources/web_fragments/board_js.html +++ b/webapp/src/main/resources/web_fragments/board_js.html @@ -2,6 +2,6 @@ - + diff --git a/webapp/src/main/resources/web_fragments/chat_box.html b/webapp/src/main/resources/web_fragments/chat_box.html index 8ec422903..5058ce653 100644 --- a/webapp/src/main/resources/web_fragments/chat_box.html +++ b/webapp/src/main/resources/web_fragments/chat_box.html @@ -1,6 +1,7 @@
    +
    diff --git a/webapp/src/main/resources/web_fragments/faq_import_moves_formats_examples.html b/webapp/src/main/resources/web_fragments/faq_import_moves_formats_examples.html index c6abda231..d59a47e96 100644 --- a/webapp/src/main/resources/web_fragments/faq_import_moves_formats_examples.html +++ b/webapp/src/main/resources/web_fragments/faq_import_moves_formats_examples.html @@ -82,6 +82,27 @@

    UCI

    38. c0a2 e4f4 39. i4i5 e7f7 40. d7f6 d8f8 41. a4a5 f8f6
    +

    ICCS

    +

    + Internet Chinese Chess Server coordinate notation. Similar to UCI, but uses uppercase files separated by a dash. +

    +
    +
    +[Game "Chinese Chess"]
    +[Event "Chinese National Champion"]
    +[Format "ICCS"]
    +1. H2-E2 B9-C7
    +2. H0-G2 H9-G7
    +3. I0-H0 I9-H9
    +4. B0-A2 G6-G5
    +5. B2-C2 A9-B9
    +6. A0-B0 B7-B3
    +7. H0-H4 H7-I7
    +8. H4-F4 H9-H8
    +9. A3-A4 H8-B8
    +10. G3-G4 G5-G4
    +    
    +

    WXF

    The World Xiangqi Federation move notation uses specific letters to represent each piece. This notation is widely diff --git a/webapp/src/main/resources/web_fragments/footer.html b/webapp/src/main/resources/web_fragments/footer.html index 7cf1d1b66..429c6574f 100644 --- a/webapp/src/main/resources/web_fragments/footer.html +++ b/webapp/src/main/resources/web_fragments/footer.html @@ -4,9 +4,10 @@

    About

    -
    -
    - - - - -
    +
    +
    - - - - +
    + + + + +
    +
    + + + + +
    diff --git a/webapp/src/main/resources/web_fragments/head_description.txt b/webapp/src/main/resources/web_fragments/head_description.txt index eaa7afe9f..53b4f0826 100644 --- a/webapp/src/main/resources/web_fragments/head_description.txt +++ b/webapp/src/main/resources/web_fragments/head_description.txt @@ -1 +1 @@ -Play Chinese chess (Xiangqi) online for free. Create a game and share the link to play with a friend, join a game from the lobby, challenge a bot, solve puzzles, analyze your games and learn from your mistakes. No download. +Play Chinese chess (Xiangqi) and Manchu chess (Yitong) online for free. Create a game and share the link to play with a friend, join a game from the lobby, challenge a bot, solve puzzles, analyze your games and learn from your mistakes. Colorblind mode available. diff --git a/webapp/src/main/resources/web_fragments/head_keywords.txt b/webapp/src/main/resources/web_fragments/head_keywords.txt index b7d0439fc..6c9dbda8c 100644 --- a/webapp/src/main/resources/web_fragments/head_keywords.txt +++ b/webapp/src/main/resources/web_fragments/head_keywords.txt @@ -36,3 +36,9 @@ xiangqi online with friend xiangqi online 2 players with friend chinese chess online with friend chinese chess online 2 players with friend +manchu chess +yitong chess +一统棋 +满洲棋 +manchu chess online +xiangqi variant diff --git a/webapp/src/main/resources/web_fragments/logged_as_guest_message.html b/webapp/src/main/resources/web_fragments/logged_as_guest_message.html index 2cdee0139..5468c3fcb 100644 --- a/webapp/src/main/resources/web_fragments/logged_as_guest_message.html +++ b/webapp/src/main/resources/web_fragments/logged_as_guest_message.html @@ -5,14 +5,19 @@

    You are currently logged in as guest #. This is a 30 days temporary session attached to your device. To be able to authenticate across devices and - enjoy all the features, you can sign up to create an - account or log in if you already have one. + enjoy all the features, you can sign up + to create an account or log in if you + already have one.

    The website is totally free to use and - sign up is quick. + sign up is quick. Read more on the FAQ.

    +

    + You have the possibility to transfer the games played as guest on + sign up. +

    If you forgot your password, you can reset it.

    diff --git a/webapp/src/main/resources/web_fragments/settings_info_box.html b/webapp/src/main/resources/web_fragments/settings_info_box.html index 0ddbeecd1..42da57c8d 100644 --- a/webapp/src/main/resources/web_fragments/settings_info_box.html +++ b/webapp/src/main/resources/web_fragments/settings_info_box.html @@ -14,22 +14,6 @@ loading="lazy">
    -
    -
    - Select move format -
    -
    -
    -
    - Show or hide board coordinates -
    -
    + + advanced + +
    + +
    + Move format setting icon +
    +
    +
    Move format
    +
    Notation shown in the move list.
    +
    +
    + + + + +
    +
    +
    +
    + Show coordinates setting icon +
    +
    +
    Show coordinates
    +
    Display board coordinates around the board edges.
    +
    +
    + + +
    +
    +
    +
    + Coordinates style setting icon +
    +
    +
    Coordinates
    +
    Numeral system used for board coordinates.
    +
    +
    + + + + + + + +
    + +
    +
    +
    + Play sounds setting icon +
    +
    +
    Play sounds
    +
    + Play board sounds when opponent joined the game, play a move, etc. +
    +
    +
    + + +
    +
    +
    +
    + Colorblind black pieces setting icon +
    +
    +
    Colorblind black pieces
    +
    Increase contrast for black pieces.
    +
    +
    + + +
    +
    +
    +
    diff --git a/webapp/src/main/resources/web_fragments/top_bar.html b/webapp/src/main/resources/web_fragments/top_bar.html index 0ea4a120b..a91813810 100644 --- a/webapp/src/main/resources/web_fragments/top_bar.html +++ b/webapp/src/main/resources/web_fragments/top_bar.html @@ -81,7 +81,7 @@
    - profile + menu
    diff --git a/webapp/src/test/kotlin/io/elephantchess/webapp/rendering/GamePageRendererTest.kt b/webapp/src/test/kotlin/io/elephantchess/webapp/rendering/GamePageRendererTest.kt new file mode 100644 index 000000000..39f912db4 --- /dev/null +++ b/webapp/src/test/kotlin/io/elephantchess/webapp/rendering/GamePageRendererTest.kt @@ -0,0 +1,72 @@ +package io.elephantchess.webapp.rendering + +import io.elephantchess.model.Engine +import io.elephantchess.xiangqi.Color +import kotlin.test.Test +import kotlin.test.assertEquals + +class GamePageRendererTest { + + @Test + fun `default titles remain backward compatible`() { + assertEquals("Game", GamePageRenderer.DEFAULT_PVP_TITLE) + assertEquals("Play vs. Bot", GamePageRenderer.DEFAULT_PVB_TITLE) + } + + @Test + fun `pvp page title contains both player names`() { + assertEquals( + "alice vs. bob", + GamePageRenderer.formatPvpPageTitle("alice", "bob", Color.RED) + ) + } + + @Test + fun `pvp page title keeps red first when inviter is black`() { + assertEquals( + "bob vs. alice", + GamePageRenderer.formatPvpPageTitle("alice", "bob", Color.BLACK) + ) + } + + @Test + fun `pvp page title uses opponent fallback with red first when invitee is missing`() { + assertEquals( + " vs. alice", + GamePageRenderer.formatPvpPageTitle("alice", null, Color.BLACK) + ) + } + + @Test + fun `pvp page title uses new game label when colors are not picked yet`() { + assertEquals( + "new game by alice", + GamePageRenderer.formatPvpPageTitle("alice", null, null) + ) + } + + @Test + fun `pvb page title puts human first when playing red`() { + assertEquals( + "alice vs. Fairy Stockfish (5)", + GamePageRenderer.pvbPageTitle("alice", Color.RED, Engine.FAIRYSTOCKFISH, 5) + ) + } + + @Test + fun `pvb page title puts bot first when human plays black`() { + assertEquals( + "Pikafish (12) vs. alice", + GamePageRenderer.pvbPageTitle("alice", Color.BLACK, Engine.PIKAFISH, 12) + ) + } + + @Test + fun `pvb page title uses anonymous fallback`() { + assertEquals( + "anonymous vs. Pikafish (8)", + GamePageRenderer.pvbPageTitle(null, Color.RED, Engine.PIKAFISH, 8) + ) + } + +} diff --git a/xiangqi-core-test-utils/src/main/kotlin/io/elephantchess/xiangqi/testutils/ManchuGameMovesDtoCache.kt b/xiangqi-core-test-utils/src/main/kotlin/io/elephantchess/xiangqi/testutils/ManchuGameMovesDtoCache.kt new file mode 100644 index 000000000..72b874037 --- /dev/null +++ b/xiangqi-core-test-utils/src/main/kotlin/io/elephantchess/xiangqi/testutils/ManchuGameMovesDtoCache.kt @@ -0,0 +1,26 @@ +package io.elephantchess.xiangqi.testutils + +class ManchuGameMovesDtoCache { + + fun listAll(): List = cache.toList() + + fun findByGameId(gameId: String) = cache.find { it.gameId == gameId } + ?: error("No Manchu game found with id '$gameId'") + + private companion object { + + val cache by lazy { loadAll() } + + fun loadAll(): List { + return ResourcesUtils.getResourceAsText("/manchu.txt") + .split("\n") + .filterNot { line -> line.isBlank() } + .map { line -> + val (gameId, moves) = line.split(";") + GameMovesDto(gameId, moves.split(",")) + } + } + + } + +} diff --git a/xiangqi-core-test-utils/src/main/resources/manchu.txt b/xiangqi-core-test-utils/src/main/resources/manchu.txt new file mode 100644 index 000000000..5787936d9 --- /dev/null +++ b/xiangqi-core-test-utils/src/main/resources/manchu.txt @@ -0,0 +1,1000 @@ +4ae27048;a0a6,b7g7,a6e6 +bed562f4;a0a6,h7h5,a6e6 +8fe2dff2;a0a6,i9i7,a6e6 +8fb4ebaa;a0a6,b7b1,a6e6 +e274c393;a0a6,h7h2,a6e6 +cc8524b6;a0a6,h7h1,a6e6 +cead682d;a0a6,h9i7,a6e6 +f2af94eb;a0a6,a9a8,a6e6 +5782e756;a0a6,c9a7,a6e6 +f773867c;a0a6,b7f7,a6e6 +0f8bec33;a0a6,c6c5,a6e6 +682958b1;a0a6,b7d7,a6e6 +d29cfb04;a0a6,h7h0,a6e6 +b6f25406;a0a6,h7h4,a6e6 +94c88d5b;a0a6,h7h3,a6e6 +36b59664;a0a6,b7b0,a6e6 +57bf8632;a0a6,b7c7,a6e6 +68ec3a30;a0a6,g6g5,a6e6 +ba455098;a0a6,h7d7,a6e6 +fb8f9a5b;a0a6,i9i8,a6e6 +b61dd696;a0a6,b7b5,a6e6 +fe4a1631;a0a6,h7h8,a6e6 +3fa93485;a0a6,g9i7,a6e6 +5b58bf78;a0a6,e9e8,a6e6 +d7d9d9e0;a0a2,h7h3,a2e2,h3h8,e2e6 +f0087073;e0e1,h7h4,a0a6,b7h7,a6e6 +844ef661;a0a1,b7d7,a1a6,d7d8,a6e6 +4511f1a6;a0a6,b7b5,a6b6,h7h1,b6c8 +ba7c9f30;a0a6,h7h1,a6c6,h1h7,c6c8 +9906d070;a0b0,b7a7,b0b7,d9e8,b7c9 +db3cfd32;a0a6,a9a8,a6b8,a8a7,b8c8 +a70c4395;a0a6,b7e7,a6a8,h7h5,a8c8 +60b36c74;a0a2,i6i5,a2a6,a9a8,a6e6 +fb334562;g3g4,b7b4,a0a6,h7f7,a6e6 +5ce522e9;a0c1,d9e8,c1c6,i9i8,c6c9 +e9b688c1;a0a6,h7h2,a6c7,a9a4,c7e6 +925b026f;a0a6,b7b2,a6b8,b2g2,b8c8 +ce643936;a0a6,b7b6,f0e1,b6b4,a6e6 +ca31f113;a0a6,b7g7,e3e4,h7h8,a6e6 +c5912038;a0a6,h7h1,a3a4,b7b0,a6e6 +aa03cf9a;a0a6,c9a7,g3g4,h7h1,a6e6 +5430f075;a0c1,b7b1,c1e2,b1g1,e2e6 +b979dbb2;a0b0,b7a7,b0b8,c6c5,b8c8 +f69ccb1c;a0c1,b7c7,c1e2,g6g5,e2e6 +084b557a;a3a4,h7i7,a0a6,b7a7,a6e6 +d915c492;a0b2,h7i7,b2e2,i9i8,e2e6 +e55f2cdd;a0b2,h7i7,b2b5,e6e5,b5e5 +57f608a8;a0b2,c6c5,b2b6,a9a8,b6e6 +0cf27ac4;a0c1,i9i7,c1c6,f9e8,c6c9 +a026e975;a0a6,b7a7,a6b6,h7i7,b6e6 +65194b57;a0a6,c9a7,a6c5,h7c7,c5e6 +29c3021e;a0a6,i9i7,g3g4,b7g7,a6e6 +1544853f;c3c4,h7g7,a0a6,c9a7,a6e6 +34d28128;a0a6,b7b3,a6a8,h7h3,a8c8 +1e43bd98;e3e4,c6c5,a0a6,h7i7,a6e6 +5658a924;a0b2,b7b6,b2d3,e6e5,d3e5 +c5d77168;e0e1,h7h3,a0a6,b7b5,a6e6 +5cfdd613;a0a6,e9e8,i3i4,b7c7,a6e6 +7a3b7a87;a0a6,g6g5,a6c6,b7b8,c6c8 +48b5bccc;g0e2,h7h6,a0a6,g6g5,a6e6 +937cc03c;a0a6,b7b4,a6a8,b4a4,a8g8 +bcb100d6;g0e2,h7c7,a0a6,c7i7,a6e6 +e7d06402;a0a6,b7f7,a3a4,f7f5,a6e6 +dcdf5bb5;d0e1,i9i8,a0a6,b7b5,a6e6 +86f3d526;a0a6,c6c5,g0i2,h7h1,a6e6 +61b68232;a0a6,i9i7,a6a8,h7c7,a8c8 +a76b3bb5;a0a1,h7g7,a1e1,g7f7,e1e6 +f15d9af0;a0a6,i6i5,a6c6,a9a8,c6e6 +b61fc902;a0c1,h7f7,c1e1,e6e5,e1e5 +477d3d60;d0e1,b7d7,a0a6,g9i7,a6e6 +3cd01245;a0c1,i9i7,c1e1,b7b8,e1e6 +8c3c7472;a0a6,c9a7,i3i4,i9i8,a6e6 +f3d06e47;a0a6,i6i5,a6a8,b7g7,a8c8 +ab488771;a0a6,h7g7,a6c7,a9a8,c7e6 +a248ddcd;a0b2,g6g5,b2e2,h9i7,e2e6 +622c5f5c;a0a6,h7h4,c0a2,a9a8,a6e6 +7c64610a;a0a6,b7b5,c0e2,h7h4,a6e6 +d2b2637b;a0a6,b7b6,a6a7,h7e7,a7c8 +d27b5648;a0a6,b7b2,a6c6,b2b3,c6c8 +a4a7d7e0;a0a6,h7d7,a6c6,b7b2,c3c4,d7d8,c6e6 +f4aa8e01;a0a6,b7c7,a6b8,g9i7,d0e1,h7h5,b8c8 +6bcc951c;a0c1,b7d7,c1c6,a9a7,g0i2,d9e8,c6c9 +da8a0971;d0e1,h7c7,c3c4,b7b4,a0a6,g6g5,a6e6 +4e050b4e;a0a6,h9g7,c0a2,h7i7,e3e4,g7i8,a6e6 +6a336db3;a0a6,b7b3,f0e1,a9a7,e3e4,b3g3,a6e6 +bcbc7a5f;g3g4,b7b5,a0a6,b5b7,f0e1,i6i5,a6e6 +4e6c3398;e0e1,h7h3,e3e4,b7f7,a0a6,h3h1,a6e6 +52a1cc8e;c0e2,a9a8,a0a2,h7i7,a2a6,i7i3,a6e6 +724aeed6;a0a2,h7c7,a2c2,c7f7,c2e1,c6c5,e1e6 +eb482bc2;a0a6,h7e7,a6a7,b7b3,a7c6,a9a5,c6c8 +f6bfdd38;a0a2,b7b3,a2f2,h7h1,f2e2,a9a7,e2e6 +cb06ff79;g3g4,c6c5,a0a1,h7i7,a1a6,b7d7,a6e6 +de517f73;a0a1,h7h5,g3g4,h5h7,a1e1,h7h1,e1e6 +8ecbdf15;c3c4,h7h2,a0a6,h2h7,a6b6,h7h8,b6e6 +bee35ad1;a0a6,b7g7,c0a2,g7c7,a6a8,c7e7,a8g8 +876cae37;f0e1,b7a7,a0a6,g6g5,c0a2,h7f7,a6e6 +855d2e23;e3e4,b9c7,a0a6,c6c5,a6c7,b7b0,c7c8 +028b8191;a0a2,h7h3,g0i2,b7a7,a2a6,a7d7,a6e6 +b64375a7;a0a6,i6i5,f0e1,i9i6,c0a2,h7f7,a6e6 +01105072;a0a6,b7a7,a6c5,h7h2,a3a4,h2h1,c5e6 +e6e70ce0;a0a1,h7d7,a1h1,d7d1,h1h6,c9e7,h6g8 +3abb5f73;a0c1,g6g5,f0e1,h7h5,c1e2,a9a7,e2e6 +d9135d03;i3i4,e6e5,a0c1,b7b8,c1d3,h7a7,d3e5 +77177ae0;a0c1,h7c7,e3e4,a9a7,c1c6,f9e8,c6c9 +a042f77b;a0b2,b7b3,b2f2,e6e5,f2g4,h7d7,g4e5 +c61f4393;c0e2,b9a7,a0a6,g6g5,d0e1,g9i7,a6e6 +efaa2773;a0a6,b9c7,a6a8,h7h8,a8f8,c9a7,f8g8 +735fe9fb;a0a6,h7h6,i3i4,b7b1,g0e2,g6g5,a6e6 +98d0997f;a0b2,h7h0,b2g2,b7d7,g2g6,i9i7,g6e6 +9cc42b8a;a0a2,h7h6,a2c2,h6h1,c2e2,h1h2,e2e6 +e33923d2;a0a6,b7b0,a6a8,h7a7,f0e1,a7h7,a8c8 +9ef4ee18;a0a2,h7h8,a3a4,h8b8,a2a6,b7f7,a6e6 +83961fea;a0a6,b7b5,a6c7,b5b4,c3c4,c6c5,c7c8 +994a8e68;d0e1,c6c5,a0c1,h7i7,c1c5,i7g7,c5e6 +f2ef243c;a0a6,g9e7,c0e2,h7i7,c3c4,e7g9,a6e6 +d90f9ee7;a0a6,b7b2,a6c6,a9a4,a3a4,b2d2,c6e6 +0ea8cea4;g0e2,h7d7,a0c1,d7d6,c1c6,d6d4,c6e6 +eb2b1dca;a0a6,h7d7,a3a4,g9i7,a6c6,a9a4,c6e6 +ffe689a8;c3c4,i6i5,a0c1,b7b5,c1c6,h7h4,c6c8 +0bb409d0;c0a2,h7h5,a0c1,i6i5,c1c6,i5i4,c6e6 +0699ba7a;e3e4,h7d7,a0a2,d7i7,a2e2,b7b4,e2e6 +bd479632;f0e1,b7b3,a0c1,b3b4,c1e2,i9i7,e2e6 +258fb090;a0b0,e6e5,b0b4,b7b8,b4f4,c6c5,f4e6 +66739f64;c0a2,b7e7,a0b2,h7h8,b2b7,h8e8,b7c9 +44c450de;a0a6,e6e5,a6c6,b7b5,f0e1,c9a7,c6e5 +d7f0e104;g0e2,b7g7,e3e4,h7h8,a0a6,c6c5,a6e6 +380974f8;g0i2,g6g5,a0a6,h7g7,e3e4,b7c7,a6e6 +427cbe8c;a0b2,e6e5,b2b7,e9e8,b7c9,i9i8,c9c8 +e1a830f6;a0a6,g9e7,a6c7,h7h4,g3g4,a9a4,c7c8 +b8c54ee5;c0e2,i9i8,a0a6,h7i7,a6b6,i8i9,b6e6 +3e112a99;e0e1,b7b0,a0a6,h7h1,a6a8,b0b5,a8g8 +bc430ec2;a0b0,h7f7,a3a4,b7c7,b0b8,f7g7,b8g8 +4ed8dc50;a0a6,g6g5,a6a9,f9e8,e3e4,b7f7,a9c9 +e81c529c;a0a6,h7f7,a6a8,g6g5,e3e4,b7e7,a8c8 +6ac2f9cd;a0a6,b9a7,a6a9,a7b5,e0e1,i9i7,a9c8 +286e3e5d;a0b2,a9a7,b2a2,h7f7,a2a6,h9g7,a6e6 +8e8b6913;g0e2,h7h4,a0a6,g6g5,e3e4,a9a8,a6e6 +93b717c3;a0a6,e6e5,a6a7,h7h5,a7a5,e5e4,a5e5 +e817b57b;a0b0,h7e7,b0b7,e7c7,b7c7,c9a7,c7e6 +30f4f24e;a0c1,b7b8,c1c6,a6a5,g0e2,g9e7,c6c8 +729d03d1;e3e4,b7b0,e4e5,h7h4,a0a6,h4g4,a6e6 +617a521c;a0a6,b7b5,a6a8,b5i5,a8b6,i5g5,b6e6 +d94b8e9b;a0a6,b7a7,a6c7,h7h8,g0i2,c6c5,c7e6 +14141eb2;a0a6,h7g7,g3g4,b7b3,a6a8,h9i7,a8c8 +c133ebe7;g0i2,b7b8,e3e4,e9e8,a0a6,a9a7,a6e6 +9508b42d;a0c1,b7b3,c1c6,b3b5,c0a2,e9e8,c6c8 +1ee15f9a;a0c1,h9i7,c1e1,h7h3,c3c4,b9a7,e1e6 +8c105cdd;a0b0,b7c7,c0a2,h7i7,b0b6,g9e7,b6c8 +b2bb2852;c0e2,h7h0,a0a6,b7b3,a6c7,e6e5,c7c8 +09a96f62;c0a2,b7c7,a0c1,h7h2,c1c6,h2h7,c6d6,c7b7,d6c8 +dc0e96c3;a0a6,i9i8,a6c6,h9g7,c3c4,g7h9,c0a2,c9a7,c6e6 +5a19a9a9;i3i4,c6c5,a0a1,g9i7,a1e1,b7g7,e3e4,h7h3,e1e6 +1b8bac0e;a0a6,a9a7,e0e1,b7b5,g0e2,b5i5,e2c4,g9i7,a6e6 +a9ea36a5;e0e1,e9e8,a0a1,i9i8,a1a6,c9a7,a3a4,b7d7,a6e6 +b27f9193;a0a1,b7f7,c0a2,f7c7,a2c4,i6i5,a1e1,e6e5,e1e5 +741d33c0;g0i2,a9a7,i2g0,a7a8,a0a6,h7f7,e3e4,a8a7,a6e6 +f9f96a25;a0a6,b7b4,a6a7,h7g7,a7a6,b4b3,f0e1,b3b5,a6e6 +0b5e64b0;g0i2,h7h5,a0b2,b7b5,b2d3,b5e5,d3e1,e5c5,e1e6 +5b09b719;e0e1,g9i7,a0a6,a9a8,e1d1,h7h0,a6c5,b7b1,c5e6 +64e86cae;g0e2,e9e8,a0c1,b9a7,c1c6,b7b3,e2g0,b3b2,c6e6 +96fe9155;a0a1,h7g7,a1c1,b7b1,g0i2,g7f7,c1e1,a9a8,e1e6 +29f0ea44;i3i4,b7b3,a0a6,a9a7,e0e1,a7d7,c0a2,e9e8,a6e6 +d1f526b8;a0b0,h7h8,b0b2,b7g7,b2e2,h8h4,d0e1,c6c5,e2e6 +3d79b381;d0e1,a9a8,a0a6,b7f7,i3i4,a8h8,a6c7,h8b8,c7e6 +339a81fb;a0c1,h7f7,c3c4,i9i7,a3a4,b7b6,c1c6,h9g7,c6c8 +4132fea2;i3i4,g9i7,a0a6,b7b5,a6a8,h7f7,a8b8,b5b2,b8c8 +f6d5cb37;a0b2,b9c7,b2d2,b7a7,e3e4,h7h6,d2d8,a7a3,d8g8 +5d8ea0c7;a0a1,g6g5,a1a2,i9i7,a2e2,e6e5,c3c4,h7d7,e2e5 +1dc4cb82;d0e1,b7a7,e3e4,h7h8,g0i2,h8h2,a0a6,h9i7,a6e6 +7f54f028;g3g4,e6e5,i3i4,g9i7,a0a6,i7g5,a6c7,b7b0,c7c8 +d5904b23;a0b2,h7h8,b2a4,b7b0,i3i4,h8c8,a4d4,b0b4,d4e6 +8df02a94;i3i4,h7h5,a3a4,h5e5,c0e2,e5b5,a0a6,c6c5,a6e6 +027fc74c;c3c4,i6i5,a0c1,b7a7,c1e2,c9e7,e2e1,e7c9,e1e6 +951bb1c7;a0c1,b9a7,d0e1,i6i5,c1e2,h7f7,g3g4,b7b5,e2e6 +f7c443ff;a0a6,a9a8,e3e4,h7c7,c0a2,b7b5,a2c4,b5b3,a6e6 +7e494439;a0b0,i9i7,b0b7,h7h6,b7b3,g9e7,b3b6,e7g9,b6c8 +d2acb3fa;a0a1,h7h2,a1b3,h9i7,b3b6,a9a7,f0e1,h2f2,b6e6 +c59d1526;a0a6,g9i7,c0e2,i7g9,e0e1,h7f7,e1d1,i9i7,a6e6 +0db5799c;a0a6,c6c5,a3a4,c5c4,e0e1,b7b6,e3e4,c9a7,a6e6 +8936b601;a0a6,b7b6,c0a2,h7f7,a3a4,f7f6,i3i4,b6b8,a6e6 +8b2e7f64;e3e4,h7c7,i3i4,a6a5,a0c1,c7c3,c1e1,c3c2,e1e6 +1402423d;d0e1,i9i7,a0a6,h7h3,a6c6,h3e3,e1d0,e3a3,c6e6 +27dea500;a0c1,c6c5,g0i2,h7e7,c1c5,b7b3,e0e1,e7b7,c5e6 +b5bfe1fe;a0a6,g9i7,a6b8,i7g9,b8h8,h7f7,h8i8,b9c7,i8c8 +6af4b2c6;a0b0,a6a5,b0b9,h9g7,b9a9,i6i5,i3i4,g9e7,a9c8 +6736a0c0;a0c1,h7g7,a3a4,b7b2,c1b3,i9i7,b3b6,g7g3,b6c8 +0bf25cc3;a3a4,g9e7,a4a5,e7g9,a0b2,g9i7,b2e2,b7b0,e2e6 +adf46ffc;a3a4,b7f7,a0b2,f7c7,g0e2,i9i7,b2b6,a6a5,b6e6 +287a2ca7;g0i2,i9i7,a0a2,d9e8,a2c1,e8d9,c1c6,h7h4,c6e6 +06a89c6f;d0e1,c9a7,a0a6,h7d7,e1f2,a7c5,f2e1,b7b4,a6e6 +706ca6c3;c0a2,h7c7,g0e2,c7i7,a0b0,b7c7,b0b8,c7c3,b8g8 +622b4167;a0a6,g6g5,a3a4,h9i7,i3i4,h7d7,a6c5,i6i5,c5e6 +e937af1f;a0a6,b7b4,a6c6,b4b3,c0a2,h7g7,f0e1,b3b5,c6e6 +e0033541;a0c1,i6i5,c1c2,b9a7,c2e2,h7h2,f0e1,h2h1,e2e6 +b08458cb;e0e1,h7c7,a0a6,f9e8,i3i4,c7h7,a6a9,h7f7,a9c9 +e232eba1;a0a1,c9a7,a1b1,i9i8,b1b5,h7h1,b5c5,b7b1,c5e6 +8e003d0f;a0a1,h7c7,e3e4,c7c3,e0e1,h9i7,a1a6,c9a7,a6e6 +abcfe69b;g0e2,h7h5,g3g4,b7f7,c3c4,h5b5,a0a6,c6c5,a6e6 +39e79cf1;a0a6,b7g7,a6a8,g7g8,a8a7,i9i8,a7a6,g8c8,a6e6 +c4eb6455;a0b2,i9i7,b2g2,b7f7,e0e1,a6a5,g2g6,e6e5,g6e6 +a1dc8449;a0a6,g9i7,g0i2,c9a7,a6a5,h7h8,a5d5,h8g8,d5e7 +638fda6c;c3c4,h7h0,a0a6,h0h4,a6c6,b7b8,g3g4,b8e8,c6c9 +2e757a8a;c0e2,h7i7,a0b0,c9a7,b0b3,i7i8,b3c5,i8h8,c5e6 +36209616;a0c1,h7h1,c1g1,b9a7,g1g2,i6i5,g2e1,b7b0,e1e6 +d549b8af;a0a6,b7b6,a3a4,f9e8,f0e1,b6b0,a6c6,a9a6,c6c9 +cf131bac;g0i2,h7h2,a3a4,h2h8,a0a6,b7h7,a6c6,h7h5,c6e6 +7799135f;a0a6,i9i8,a6c6,i8i9,i3i4,h7h4,c6c9,e9e8,c9c8 +627b2b39;e3e4,b7b4,a0a6,b4b7,a6c6,b7b4,c6b8,b4d4,b8g8 +d94c666a;g0i2,h7c7,f0e1,c9a7,a0a6,b7b8,c0a2,b8h8,a6e6 +037e7dce;g0e2,e9e8,e3e4,h7h5,a0a1,h5i5,a1a6,b7h7,a6e6 +a2d78e3d;a0a6,i9i7,d0e1,b7b4,c0e2,h7h1,e3e4,c6c5,a6e6 +13c2bd42;g3g4,d9e8,f0e1,e8d9,a0c1,i6i5,c1e2,e6e5,e2e5 +f4a25f44;g0e2,b7b4,g3g4,b4b0,a0a6,b0b7,c3c4,g6g5,a6e6 +8433c1fd;a3a4,h7h5,g0i2,h5f5,a0a6,c6c5,a4a5,f5f7,a6e6 +07ad627e;c0e2,h7g7,a0a1,b7e7,a1a6,e9e8,a6b6,a9a5,b6c8 +1267c90c;c3c4,h7h3,a0b0,b7b2,g0e2,b2d2,b0b6,d2a2,b6c8 +fdb3ef56;c0e2,b7b3,e3e4,e9e8,d0e1,e8d8,a0a1,f9e8,a1d1 +7bf1f0a5;g0e2,b7c7,a0c1,h7h5,c1b3,e6e5,b3b5,c7d7,b5e5 +de8ae37f;e3e4,h9g7,a0a2,g7e8,a2d2,h7c7,d2d6,b7b4,d6c8 +bda85825;d0e1,h7g7,g0i2,a9a8,e1f2,i6i5,a0a6,b7f7,a6e6 +37ea5cff;a0a6,h9i7,a6b4,h7c7,c0a2,i6i5,b4a6,c7c8,a6e6 +3e3686d4;a0a1,b7b3,e0e1,h7g7,a1d1,a9a7,d1d3,e6e5,d3e5 +8eec96fa;a0a2,b7b8,a2c2,b8c8,c2c1,h7b7,c1e1,b7b4,e1e6 +aab7f84b;a0a1,h7h5,a1f1,b7b5,e0e1,b5d5,f1f6,h5h0,f6e6 +3d88a57c;f0e1,b7d7,e1f2,d7d1,a0a6,h7h8,e0e1,d1a1,a6e6 +abd5d9f6;a0a6,b7d7,a6b8,a9a5,e3e4,d7d8,b8d8,h9i7,d8c8 +decd15fd;a0b2,h9i7,b2b4,b9c7,b4d4,c6c5,d4d3,e6e5,d3e5 +ce1c4597;a0b0,h7h1,b0b1,h1e1,b1b5,e1h1,b5b6,h1h3,b6e6 +8a44f5c3;a0a6,b7a7,a6c7,h7h3,g0i2,a7b7,c0a2,h9i7,c7e6 +5bbd1431;a0b0,h7h3,b0b5,b7i7,b5b8,i7b7,g0e2,c6c5,b8c8 +9c6c85d8;a0c1,h7c7,c1c6,c7c8,f0e1,g9e7,g0i2,b7b8,c6c8 +0ecc6c78;a0b2,b7d7,b2b5,e9e8,c0e2,a6a5,b5c5,i9i7,c5e6 +011100a9;i3i4,h7h0,e3e4,b7a7,a0a6,i9i7,c0a2,h0h1,a6e6 +77c4ede0;d0e1,a9a8,a0c1,a8h8,c1b1,h8a8,b1b6,h7g7,b6e6 +ae5bc8be;a0a1,a9a7,a1c2,i9i7,c2g2,b7b8,g2e1,b8h8,e1e6 +6e3aa7d5;g0e2,b7b6,c0a2,b6b7,e2g4,e6e5,c3c4,h7h6,d0e1,h6h0 +5e085a1b;e0e1,c9e7,e1f1,h7f7,a0b2,b7b4,b2b9,e7c9,d0e1,b4f4 +2234ea22;g0i2,h7h1,a0a6,h1h4,i2g4,b9a7,a3a4,i9i8,c0e2,i8g8,a6e6 +1b7cd673;c0e2,b7c7,i3i4,c9a7,a0b2,h9g7,b2b6,c7c3,b6a6,g7h9,a6e6 +b44652dc;d0e1,a9a7,a0c1,h7h4,e1d0,g9i7,c1c6,b7b3,c6a6,h4h3,a6e6 +45c0d0e0;i3i4,b7b8,g0e2,h7h3,a0a2,b8f8,a2a6,f8f5,e0e1,i9i8,a6e6 +f8b06d7b;g0e2,h7d7,a0b0,d7d8,e2c4,d8h8,b0b7,h8i8,b7c7,a6a5,c7e6 +9363af00;e3e4,h7f7,e4e5,h9g7,a0a6,f7f2,a6a7,f2a2,a7a8,a2f2,a8c8 +bb464507;e0e1,a9a8,a0c1,i6i5,c1c6,a8f8,c3c4,f8f1,e1e0,i9i6,c6c8 +f3765215;g0i2,h9i7,a0a6,h7h1,a6a4,i7h9,f0e1,d9e8,a4a8,b7a7,a8c9 +2425f080;g0i2,h7f7,e0e1,b7b2,a3a4,h9i7,a0a6,b2b3,e1e2,f7d7,a6e6 +399002c8;a0a6,h7h3,a6a7,h9i7,i3i4,h3h1,a7a8,b7b2,g0e2,b2b6,a8c8 +55a44f32;a0a6,h7g7,g3g4,i6i5,a6b4,a9a7,b4f4,e9e8,f4f8,e8e9,f8g8 +a2b2378f;a0a1,h9g7,a1a6,h7h5,d0e1,e9e8,a6a9,b7b6,a9a8,e8e9,a8g8 +d71ffcef;a0b0,b7d7,b0a2,a9a7,a2a6,h7h2,g3g4,e9e8,g0e2,d7g7,a6e6 +6bd5b2f3;g0i2,b7b0,a3a4,b9a7,a0b2,e6e5,b2e2,b0b2,i3i4,h7h1,e2e5 +c5c4a039;a0b0,a6a5,c0a2,e9e8,b0b1,g6g5,b1g1,e8d8,g1e2,f9e8,e2d2 +c07806d2;c3c4,b7b3,g0e2,i9i7,a0a6,i7i8,e3e4,h9i7,f0e1,b3b7,a6e6 +2f6946e3;f0e1,i9i7,a0a6,c9e7,a6a9,h9g7,a9b7,g7e8,b7d8,b9c7,d8c8 +73d19acb;f0e1,b7b1,e3e4,h7h1,a0b2,a9a7,b2e2,h9i7,c3c4,b1d1,e2e6 +176f13f7;a0b2,h7i7,c0e2,b7b5,e2c0,b5e5,g0e2,h9g7,b2b8,g7h9,b8g8 +ed1ef943;a0b2,h7f7,b2b4,c9e7,f0e1,f7f1,b4c6,i9i7,c0e2,b9a7,c6c8 +cd2d35ed;i3i4,b7d7,g0i2,d7c7,a0a6,c7e7,a6b8,e7d7,b8f8,a9a6,f8g8 +ebeb4775;a0a2,b9a7,a2i2,h7h2,d0e1,b7b4,i2i6,b4e4,c0e2,g6g5,i6e6 +b4f9cd8d;c0a2,i9i7,a2c4,a9a7,a0a2,b7b4,d0e1,a7g7,a2e2,g7f7,e2e6 +44d3b475;g0e2,h7g7,a0c1,c6c5,c1b3,h9i7,b3b6,i7g8,e3e4,i9i7,b6e6 +5f50dcd5;a0a2,h9g7,a2b2,h7i7,b2b3,e9e8,i3i4,e8e7,b3a5,e7f7,a5f5 +1611fc46;c0a2,h7e7,a0c1,i9i8,c1d1,g6g5,d1d8,b7c7,d8b8,i8i7,b8g8 +291cb64a;c0a2,h7e7,a3a4,h9i7,a0c0,i6i5,f0e1,g6g5,c0c6,b9a7,c6c8 +8aac8d1f;a0a6,b7g7,a6b6,e9e8,c0e2,e8d8,e2c4,h9i7,c4a2,e6e5,b6b8 +7d0a94fb;a0a1,h7i7,a1a6,i7c7,a6c7,i9i8,c3c4,b7b3,c7c6,i8h8,c6e6 +6f41c23f;c3c4,h7h0,a0a6,h0h8,a6a5,b7a7,a5h5,a7d7,h5c5,e9e8,c5e6 +76b3bfa1;a0a6,h9g7,f0e1,i9i7,e1f2,b7b3,a6a5,h7h9,a5a8,i7h7,a8c8 +72639f84;g3g4,h9g7,i3i4,b7f7,d0e1,i9h9,c0e2,h7h0,a0a6,h9i9,a6e6 +3e97b11d;e3e4,h7d7,c3c4,b7b2,a0a1,d7d1,a1d1,b2c2,d1d6,h9g7,d6c8 +42b7fe9f;a0c1,a9a8,c1b1,a8b8,g3g4,i9i8,b1b3,i8c8,b3b6,g9i7,b6e6 +846eabb3;i3i4,h9g7,a0b0,h7i7,b0b6,b7c7,g0e2,g7i8,c3c4,c7g7,b6e6 +c9721049;c0a2,b7b2,a0b0,b2h2,b0b8,h2d2,b8i8,h7d7,i8g9,e9e8,g9g8 +210d51e9;a0b2,h7i7,b2a4,b7b0,a4d4,a9a7,d4g4,a7b7,g4h6,b7e7,h6g8 +9aad7502;i3i4,b7d7,a0a1,h7i7,a1b1,d7c7,b1b4,a6a5,b4c6,c7c8,c6c8 +0472e4fd;a0b0,a9a7,c0e2,g9e7,b0a0,e7g5,a0b2,h7h8,b2b6,h8h1,b6e6 +38e4c307;i3i4,i9i7,a0a6,b7b2,g0e2,h7h8,e2c4,i7a7,c0a2,h8h1,a6e6 +d323f2f8;d0e1,b7a7,a0a6,g6g5,g3g4,a9a8,g0e2,i9i7,a6c5,h7h1,c5e6 +856b9ef3;e0e1,f9e8,a0a6,a9a8,a6c6,b7c7,c3c4,a8a6,c0a2,i9i7,c6c9 +58905c16;c0a2,h7c7,a0b2,b9a7,b2b4,e9e8,b4e4,e8d8,e4f6,c7d7,f6g8 +b1c7393a;e0e1,b7b0,e1e2,h7i7,a0a6,i7h7,g0i2,b0b5,a6b6,g9i7,b6c8 +56156091;f0e1,h7h0,g0i2,b7b3,a0a2,b3b1,a2d2,h0f0,d2d8,f0i0,d8g8 +ffe931ff;e3e4,i9i7,f0e1,h7h8,a0a1,i7i8,a1a6,h8h0,g0i2,b7d7,a6e6 +ccc378be;d0e1,b7c7,i3i4,h7h4,a0a6,g9e7,e1d0,c7d7,a6b6,e7g9,b6e6 +b4126115;e0e1,h7f7,a0a6,f9e8,g0e2,f7f8,a3a4,i9i7,a6a9,f8h8,a9c9 +f7e21542;e0e1,b7b8,i3i4,b8b4,a0a6,a9a8,g0i2,b9a7,c0e2,b4f4,a6e6 +ffac5e78;g3g4,a9a7,c3c4,b7b1,g0i2,b1c1,a0b0,a7a9,b0b6,b9c7,b6c8 +8697e3b0;a3a4,h7c7,a0a3,c7f7,c0a2,f7d7,a3a6,b9c7,a6c5,i9i7,c5e6 +de7cf37c;g0e2,b7b5,a3a4,h7i7,a0a2,b5d5,a2a6,i7i8,d0e1,g6g5,a6e6 +96987670;g0e2,b7e7,a3a4,e7a7,f0e1,a7g7,a0a6,h7h8,g3g4,h8b8,a6e6 +7aba3a09;c3c4,e9e8,a0a6,i9i8,c0a2,e8d8,a6b6,b7f7,b6a6,f7d7,a6b8 +05f203e0;g0e2,b7a7,i3i4,h7h1,a0b0,h1g1,b0b6,g6g5,e2g4,g1f1,b6e6 +6d000393;a0a2,i9i8,f0e1,c6c5,g0e2,h7h1,a2a6,h1i1,a6b6,g6g5,b6e6 +9a5fb22e;a0c1,g6g5,c1d3,b7b1,d0e1,d9e8,e1f2,h7h1,d3c5,h1h4,c5c9 +20db86b9;a3a4,b7b3,a0a6,c9a7,e0e1,h7h8,e1e2,e6e5,a6c6,b3e3,c6e5 +18e1d06a;a0b0,h7d7,b0b3,d7c7,g0e2,i6i5,b3c5,h9i7,i3i4,i7g8,c5e6 +665e4948;a0b0,g9e7,b0b3,e7g5,b3b6,b7f7,b6b4,f7f6,b4a6,h7i7,a6e6 +46c2200a;c3c4,b7b8,i3i4,b8b6,a0a6,h7h4,f0e1,h4h2,a6a8,c6c5,a8c8 +2f6290f8;i3i4,b7f7,a0a6,f7f5,a6c5,h7i7,c0a2,f5i5,d0e1,i9i8,c5e6 +7c14c9d8;i3i4,h7d7,a0a2,b7a7,d0e1,i9i8,c3c4,g9i7,a2a6,a7a8,a6e6 +3b7b6281;a0a6,h7h2,a6a4,b7e7,g0i2,h2h1,a4b4,h9i7,b4b6,h1h5,b6c8 +1c6940df;a0a6,e9e8,a6b6,b7d7,f0e1,h7f7,e1f0,f7f1,d0e1,d7d2,b6c8 +24ae5cc1;a0a2,b7a7,a2a6,h7h3,c0e2,a7i7,a6c7,h3h0,d0e1,h0h2,c7c8 +c16b69a4;e0e1,h7c7,a0a6,c6c5,g0e2,g6g5,i3i4,c7h7,e1d1,h7h1,a6e6 +5965c1d8;a0a1,a9a8,c0e2,i6i5,c3c4,h7h6,a1a6,g6g5,d0e1,h6h5,a6e6 +cd9ba99c;a0b2,h7h6,e0e1,g6g5,b2c4,f9e8,e1f1,b7e7,c4c5,h6h4,c5c9 +6c3960b1;a0b0,h9i7,b0b4,b7g7,e3e4,g9e7,b4c6,e7c5,c6g6,h7h0,g6e6 +7e095ea2;a0a2,b7b3,i3i4,h7h4,a2h2,b3b7,h2h4,b7f7,h4i6,f7c7,i6e6 +4b80890a;a0a1,a9a7,g3g4,h7c7,d0e1,c6c5,c0e2,b7b6,a1a6,c7g7,a6e6 +e08f604c;e0e1,e9e8,c3c4,b7f7,e3e4,c6c5,a0a6,g9e7,a6d6,e8f8,d6d8 +ad6d0657;g3g4,a6a5,g4g5,e6e5,e0e1,g6g5,a0c1,b7b5,c1e2,h7f7,e2e5 +76b161a2;a0c1,i6i5,c1b1,h7h2,b1f1,h2b2,c3c4,b2a2,f1f6,i9i7,f6g8 +d1a00143;a0b0,h7h0,e3e4,i6i5,i3i4,d9e8,b0b4,h0h3,b4c4,e8f7,c4c9 +d1d23290;a0b0,e9e8,b0b3,h7h1,b3b7,g6g5,b7a5,c9e7,a5c6,a6a5,c6c8 +126d635a;a0b0,e9e8,i3i4,a6a5,d0e1,e8d8,a3a4,b7g7,b0b1,i6i5,b1b8 +d08e8ffd;a0a6,i6i5,f0e1,h7d7,a6c6,b7b0,c6d8,b9c7,d8f8,i9i8,f8e6 +39f8140f;a0a6,h7i7,a6c5,e6e5,c0a2,i7d7,d0e1,g9i7,c5c6,b7b6,c6e6 +b51a96e2;g0e2,b7b4,e0e1,b4a4,a0a4,i6i5,a4d4,i5i4,d4d6,h7c7,d6c8 +212398cc;c0a2,h7h4,e0e1,e9e8,a0c0,c6c5,a3a4,b7b4,c0c5,b4b1,c5e6 +1193e0ae;a0b0,b7b8,f0e1,g9i7,g0i2,h7h4,b0b9,a9a8,b9b8,a8a9,b8c8 +ae91108d;c0e2,e6e5,a0a6,h7f7,a6b8,b7e7,b8b6,b9c7,e3e4,h9i7,b6c8 +c5b38eff;i3i4,h7e7,g0i2,e9e8,a0a6,e7h7,a6a7,h7g7,a7g7,b7f7,g7g8 +de186f24;i3i4,i6i5,c3c4,e6e5,c4c5,e9e8,e3e4,h9g7,a0a6,e8d8,a6b8 +65fdbc87;c0e2,b7f7,e3e4,c6c5,d0e1,e9e8,a0a2,h7i7,a2b2,e8f8,b2b8 +7de0bdd7;i3i4,a9a7,a0b2,a7a8,b2a0,b7f7,i4i5,a8c8,a0a6,f7d7,a6e6 +6a7510ad;a0b0,a9a7,b0b3,b7g7,b3b8,a7e7,c3c4,e7f7,b8h8,g9e7,h8c8 +a4113053;a0a1,h7h3,a1f1,b7f7,c3c4,h3h1,f1f5,c9a7,f5f6,f7c7,f6e6 +92b3e39f;a0b2,h9i7,b2b9,h7h1,b9a9,h1f1,e3e4,e9e8,a9b9,e8d8,b9b8 +4eb8101d;a0a6,h7d7,c0a2,b7c7,d0e1,d7e7,e1d2,e7d7,a6c6,d7h7,c6e6 +4194cfd1;d0e1,a6a5,a0c1,b7b6,c1c6,i9i7,a3a4,h7h1,c6c5,b6b4,c5e6 +31550c6f;a0c1,b7f7,g0e2,h7h3,c1c6,h3h1,c0a2,e6e5,i3i4,f9e8,c6c9 +98c53bca;a0b2,b7a7,b2b5,h7i7,b5e5,i7e7,e5d3,h9g7,d3d8,g6g5,d8g8 +38688cac;f0e1,b7b6,a0a6,a9a7,c3c4,h7h5,e1f0,b6b0,a6b6,a7e7,b6c8 +bba98a0e;a3a4,b9c7,a0a6,a9a7,g0e2,g6g5,e0e1,b7b4,a6c7,b4b7,c7e6 +c0b95c28;g0e2,h7h8,f0e1,h8h1,a0a1,b7i7,a1a6,i7b7,i3i4,b7b2,a6e6 +b2543815;a0c1,b7b4,c1a0,b4g4,a0b0,h7h0,c0a2,h0h7,b0b6,i9i7,b6e6 +f2bf8d36;c0a2,i6i5,a0c1,b7b1,f0e1,h7h4,a3a4,e6e5,c1e2,h9g7,a2c4,b1b0 +4d2884d9;c0e2,g9i7,e2c0,e9e8,e3e4,b7g7,g0e2,h9f8,d0e1,c9a7,e2g4,h7h0 +a1da6bcd;f0e1,h7e7,c0e2,e7f7,e1f0,c6c5,c3c4,e6e5,a0a6,e5e4,a6a8,i9i7,a8c8 +ef583747;g0e2,b7b6,g3g4,h7c7,e2c4,c7h7,e3e4,a9a8,a0a6,b6b2,a6a8,h7a7,a8g8 +a254047a;a0b2,a9a7,b2a4,h7h1,a4f4,b7b4,i3i4,a7d7,f4f8,b4b5,f8f6,c9a7,f6g8 +de017332;e0e1,a9a8,e3e4,d9e8,a0b0,b7b3,b0a0,i6i5,a0b0,c9e7,g0e2,a8d8,b0b9 +e61845ed;i3i4,c6c5,c0e2,b7d7,a0a6,h7h0,a6a9,d7f7,a9a4,i6i5,a4b6,f7a7,b6e6 +8987f396;c0a2,b9a7,i3i4,h7f7,a0b2,f7f6,f0e1,b7g7,b2d1,f6f2,d1d4,f2f6,d4e6 +dc64c1fa;a0a6,b7b2,e3e4,b2i2,c0a2,i2d2,a3a4,c9a7,d0e1,a9a8,a6a8,a7c5,a8g8 +a63207f2;a0b0,h9i7,b0b1,e6e5,b1b2,a6a5,b2c4,b7c7,g0i2,c6c5,c0a2,c7b7,c4e5 +2f874935;a0a1,b7b2,a1d1,h7e7,d1d6,h9i7,e0e1,e7f7,c3c4,f7d7,e1e2,d7f7,d6c8 +b7eccf11;a0b2,i9i8,b2b7,h7c7,g0i2,i8i7,b7a7,i6i5,a7b9,i5i4,b9b8,c7h7,b8g8 +30e745bc;a0a6,g9e7,a3a4,e7g9,g0e2,b7b3,a6c6,h7h3,e0e1,g9e7,c6c7,e7g5,c7c8 +a4bdde6b;a0a6,h7f7,a6b8,i9i7,g0i2,f7h7,c0e2,b7b6,b8a8,h7h4,a3a4,h4h7,a8g8 +051a7229;e0e1,h7i7,e1d1,b7b3,d1d2,i7g7,d2e2,b3e3,e2d2,e3e5,a0a6,i9i8,a6e6 +00389d4d;a0c1,b7b6,c1a1,h7h8,e0e1,g6g5,a1a6,c9e7,a6a8,h8i8,a8b8,h9g7,b8g8 +9450b87d;a0a6,e9e8,a6a9,g9i7,f0e1,b7b5,a9a7,b5b0,c3c4,b0b5,i3i4,g6g5,a7c8 +d88f88e2;e3e4,b9a7,g0i2,h7h1,a0a6,i6i5,a6c6,b7b5,d0e1,h1h8,g3g4,e6e5,c6e6 +623e38bf;d0e1,b7b6,a0a6,c9a7,a3a4,h7h1,e1f2,h1i1,i3i4,b6b0,c0a2,i1i3,a6e6 +5a45fb00;e3e4,h7g7,a0c1,g6g5,c1b1,b7e7,e0e1,g7i7,b1b6,b9c7,e4e5,e7e5,b6c8 +684c3551;a0a6,h9i7,a6a8,i7h9,e0e1,g9i7,g0i2,b7b0,a8b6,h7e7,e1e2,e7d7,b6e6 +769eb949;d0e1,g6g5,a0a6,b7b8,g3g4,b8c8,g0i2,i6i5,a6c5,h7h4,g4g5,a9a8,c5e6 +807dcfda;a3a4,b7e7,e0e1,e7e3,a0a6,e3i3,e1e0,e9e8,a6a8,e8e9,f0e1,g9i7,a8c8 +e499771b;d0e1,h7i7,a0b2,a9a7,g0i2,a6a5,e1d2,a5a4,f0e1,d9e8,b2c4,b7b1,c4c9 +7c00424c;a0b0,h7h2,b0a0,b7g7,d0e1,g7i7,a0b2,h2h5,b2b6,h5g5,e1d2,i7h7,b6e6 +cb512bd8;a0b0,h7h4,b0c2,h4h5,i3i4,i9i7,c2c1,b7b1,i4i5,g9e7,c1e1,e7g9,e1e6 +20991333;c0e2,h7h0,a0c0,b7b8,i3i4,b8e8,c0c6,h0f0,c6c7,e8e3,e2g4,e3e5,c7e6 +ff619f4c;a0a2,b7c7,a2i2,e9e8,c0a2,c7g7,i2g2,h7h0,g2h2,g9e7,a3a4,e8f8,h2h8 +9b88dce5;g3g4,h7g7,a0a6,b7b1,f0e1,a9a7,g0e2,b1b5,e0f0,i9i8,c0a2,b5b8,a6e6 +3fdb2609;f0e1,h7c7,e1d2,e6e5,a0a6,b7a7,i3i4,i6i5,a6a4,c7i7,a4g4,a7a5,g4e5 +b317e44e;g0i2,h7d7,a3a4,b9a7,a0b0,e6e5,b0c2,i9i8,c3c4,i8e8,c2e2,e8d8,e2e5 +251cf347;c3c4,h7h4,e3e4,i6i5,a0b0,b7i7,d0e1,h4h6,e1f2,i7f7,b0b6,i5i4,b6c8 +01e89ce6;a0b0,h7g7,b0b1,g7h7,b1c1,g6g5,g0i2,b7b2,c1f1,b2c2,f1f8,h7e7,f8c8 +c9113637;e0e1,g6g5,c0e2,b7e7,e3e4,e7e4,e2g4,a6a5,a0c1,i9i7,c1e0,e4f4,e0e6 +57b3889c;a0c1,b7b1,c1d3,h7a7,d3d2,a7b7,d2f3,b1a1,c3c4,b7b1,f3f6,i9i8,f6e6 +e596a748;a0a1,h7c7,a1d1,b7b8,d1d3,e6e5,c0a2,c7e7,d3d8,c6c5,f0e1,h9i7,d8c8 +a8e5943d;a0a6,e9e8,g0e2,e6e5,e0e1,e8d8,a6a4,i9i8,a4g4,b7d7,g4c4,d7e7,c4d6 +9f374d81;e3e4,h7c7,g0i2,b9a7,i2g0,g9i7,e0e1,b7b5,a0c1,b5b6,c1c6,a9b9,c6e6 +5d675d58;f0e1,i6i5,c0a2,g9e7,e0f0,h7h3,a0b2,e6e5,b2b9,e5e4,b9a7,b7b3,a7c8 +119727d3;a0b2,h7e7,g0i2,b7b8,c3c4,e7c7,b2b6,b8c8,i2g0,i9i7,b6c6,a9a8,c6e6 +ced60bb3;a3a4,b7b6,d0e1,a9a7,e1d0,i6i5,e0e1,h7h2,a0a6,a7h7,a6b6,h2b2,b6e6 +2032a207;e3e4,b7e7,a0a6,a9a7,i3i4,a7c7,c3c4,e9e8,c0e2,c7d7,a6a9,e8d8,a9a8 +9ad838ad;g0e2,b7d7,e2c4,d7a7,a0a6,h7h8,c0a2,a7d7,e0e1,h8h0,a6a7,d7f7,a7c8 +2d431834;c0e2,g6g5,a0b2,b9a7,e2c0,b7b9,b2a4,b9b0,i3i4,e6e5,a4f4,h7c7,f4e6 +2997f032;c3c4,b7b5,c0a2,b5b8,a0b0,h7d7,b0b8,h9i7,b8c8,d7d8,c8c7,d8d1,c7e6 +b7065fc4;a0a6,b7b2,a6c5,b2b8,c5d5,b8e8,d5g5,e6e5,g5g6,b9a7,a3a4,a7b5,g6g9 +f9d4c04e;a0a6,d9e8,e3e4,b7b1,a6c5,h7c7,e0e1,b1b4,c5a4,b4b2,a4b6,e8d9,b6c8 +e82ae866;g0i2,b7b6,a0a6,i9i7,a6a8,g6g5,a8i8,e6e5,i8b8,h7h1,e3e4,h1b1,b8g8 +26c8f2f3;a0a1,h7h3,d0e1,h3h0,a1d1,e9e8,a3a4,h9g7,d1d5,b7b3,d5d4,g7h9,d4e6 +d6c78d2f;a0a6,b7b0,a6a9,i9i7,g0e2,b0b1,a9a7,i7i8,a7h7,b1g1,h7c7,g6g5,c7e6 +6800d01a;a0b0,a9a7,g3g4,b7f7,c0e2,g9i7,b0d1,f7f1,d1d6,f1h1,c3c4,a7d7,d6e6 +82997741;g0i2,g9e7,a0b2,h7h1,f0e1,h9i7,c0a2,h1h9,b2b6,h9h5,e0f0,b7b8,b6c8 +e62c140a;g0e2,b7b6,a0a1,h7d7,a1a2,d7h7,i3i4,a9a8,a2a6,g9i7,a3a4,b6b8,a6e6 +2e641267;i3i4,h7h3,g3g4,e6e5,a0a1,b7b3,a1b1,h3h1,d0e1,b3b5,b1b5,i9i8,b5e5 +f74c4d93;c3c4,b7g7,a0c1,c6c5,c4c5,h9i7,e3e4,g7d7,g0i2,d9e8,c0a2,d7d6,c1c9 +9a1de7f5;i3i4,i6i5,a0a6,b7c7,i4i5,a9a8,g0i2,h9i7,a6c6,c7g7,c6c7,a8f8,c7e6 +d25cf7e7;a0b0,b7b5,b0b3,b5b6,b3b6,e9e8,e3e4,h7a7,a3a4,e8e9,g0i2,i6i5,b6e6 +a4924325;i3i4,b9a7,f0e1,h7g7,a0c1,b7b3,c1c6,b3b7,c6a7,b7b1,a7g7,g9i7,g7e6 +b5984da1;a0b0,e6e5,c0a2,h7h3,b0b2,b7a7,b2b8,a7h7,b8i8,h3h2,a3a4,h2e2,i8c8 +e47ade88;a0b2,b7b8,d0e1,i9i8,b2c2,h7h1,c0e2,e6e5,e2c0,b8b1,c2c6,h1h2,c6e5 +5d5ce9bb;a0a6,h7h4,a6b4,h4i4,b4b0,b7c7,b0b8,i4i5,c0e2,i9i7,b8a6,c7d7,a6e6 +36063841;i3i4,b7f7,f0e1,f7f6,g3g4,e9e8,e1f0,h7f7,a0a6,f6f3,g4g5,e8f8,a6b8 +aab5e346;e3e4,h7h0,c0a2,b7g7,a0b2,g6g5,b2b4,g7g8,b4c6,h0h4,c6c4,g8g3,c4c8 +19cf18d5;c0e2,b7g7,d0e1,g7g8,e0d0,h7d7,e2c0,d7d5,d0d1,g8c8,a0a6,d5b5,a6e6 +1fa4b6f8;f0e1,a9a8,c3c4,b7b4,c4c5,a8a9,i3i4,b4b7,a0a6,i9i7,c5d5,h7g7,a6e6 +0cc39884;a0a1,a6a5,a1b1,h7h5,b1c1,h5h4,c3c4,h4h5,c1e2,b7g7,e2e1,h5b5,e1e6 +8c11bff1;g0i2,h7h8,i2g0,f9e8,a0b2,b7b4,b2a2,b4g4,a2a6,h8h2,a6a8,h2c2,a8c9 +ffe771f5;i3i4,h7h0,a0a6,h0f0,a6c6,b7c7,d0e1,a9a7,g0e2,a7a6,e1f2,f9e8,c6c9 +ed005d63;e3e4,b9c7,c0e2,i6i5,a0b2,h7h1,b2b7,h1g1,e2c0,c7e8,b7a7,h9i7,a7d7 +a59ec917;a3a4,h7i7,a0a3,i7i8,a3c4,i8b8,c0e2,d9e8,c4c5,b7b0,e2c0,a6a5,c5c9 +b125fb28;g0e2,e9e8,e2g4,h7e7,g4i2,b7d7,a0a6,e7g7,c0e2,d7d4,f0e1,d4i4,a6e6 +84b30f83;a0b2,h7h4,f0e1,b7e7,b2b7,b9c7,b7b3,c7e8,b3b8,i9i7,b8b7,i7i9,b7d7 +e7344c8e;a0a6,b7b3,e3e4,b3b8,i3i4,f9e8,a6a4,i9i8,a4a7,h7h1,a7h7,i8f8,h7g9 +ffdbd8a8;d0e1,h7h5,a0a6,b7d7,c0e2,g6g5,a3a4,d7f7,c3c4,h5h6,i3i4,h6h5,a6e6 +c03f4f78;a0c1,b7b5,c1a0,b5b7,a0a6,b7c7,a6c5,c7c8,e3e4,h7f7,e0e1,g9e7,c5c8 +a9123b64;a0b2,b7b4,b2e2,b4b8,e3e4,i9i7,e2f4,b8b7,f4f2,b7b0,f2e2,b0b5,e2e6 +68e8f0b5;a0b2,i6i5,b2d2,a6a5,d2c4,h7d7,c4d4,d7g7,c3c4,b7b5,d4e2,a9a8,e2e6 +e08178a4;c3c4,h7h5,a0b2,h5h0,b2b7,i6i5,c0e2,h0i0,e2c0,d9e8,i3i4,e8d7,b7c9 +5111ac61;g0e2,a9a8,a0a1,a8i8,a1a6,i8a8,c0a2,i9i7,a6b8,a8a9,c3c4,b7b2,b8g8 +48f8ce69;a0a1,h7h6,a1a6,h6h2,i3i4,b7b8,a6b4,h2c2,b4f4,c2d2,c3c4,b8h8,f4e6 +ffdca1a3;a0a6,h7h1,a6a5,b7f7,a3a4,h9i7,a5a8,h1h5,a8i8,f7g7,d0e1,g7c7,i8c8 +3858b374;g0i2,e9e8,e3e4,b7f7,f0e1,a6a5,a0b2,e8f8,i2g4,i9i7,e1d2,a9a7,b2b8 +08191846;a0a6,b7f7,a6c5,h7h0,c5h5,a9a7,h5f4,h0h8,f4f6,h8h0,i3i4,i9i8,f6e6 +cb049d5b;c3c4,b7b8,g0e2,h7h5,a3a4,h5d5,i3i4,i6i5,e3e4,b8b3,a0a6,b3f3,a6e6 +4d460e7d;i3i4,b7e7,a0a1,e7g7,a3a4,g7e7,a1b3,h7h2,b3b6,h2c2,f0e1,e7d7,b6e6 +bdce665e;c0a2,h7h8,a3a4,h8c8,a0b2,f9e8,b2b6,b7c7,g0i2,c6c5,a2c0,e8f9,b6e6 +cf1c3d3c;d0e1,b7g7,g3g4,i9i8,a0c1,i8d8,c1a2,g7f7,a2a6,f7a7,g0e2,h7h2,a6e6 +103050ca;g0i2,i9i8,d0e1,b7g7,a0b2,i8f8,b2a2,g7c7,a2a6,h7f7,a6c6,f8c8,c6e6 +dd03b453;d0e1,b7b5,a0a2,g6g5,a2f2,f9e8,f2g4,b5b4,g4c4,a6a5,c4g4,h7h4,g4g9 +ce8725db;a0a2,g9e7,e3e4,b7b8,a2c1,h7h5,g0i2,b8c8,c3c4,a6a5,c1c6,c8a8,c6c8 +1fb8ea30;g3g4,c6c5,a0a6,h7i7,a6a4,f9e8,a4f4,e6e5,d0e1,i9i8,f4g6,i7c7,g6g9 +0da741ac;i3i4,h7h8,e3e4,b7b8,a0a6,g6g5,a6c5,h8d8,g3g4,f9e8,c5c6,a9a3,c6c9 +491dc604;a0a6,f9e8,c0a2,b7e7,g0e2,c9a7,a2c4,b9d8,e2g0,e7c7,a6a9,a7c9,a9c9 +0bb83062;a0a2,b7c7,c3c4,b9a7,f0e1,c6c5,a2b2,a7c6,e1f2,i9i8,b2b6,i8i9,b6e6 +abe068aa;d0e1,c9e7,e1d2,h7h4,e0d0,h9i7,d0d1,e9e8,f0e1,g6g5,c3c4,h4h6,g0i2,b7d7 +1cdbf021;g3g4,i9i7,e0e1,h7h5,e1e0,i7g7,f0e1,a9a7,a0a2,h5c5,a2c1,e6e5,c1d3,c5c0 +e794752d;a0b0,c9a7,b0b9,a7c9,b9d9,e9d9,e3e4,d9d8,d0e1,b7b4,g0e2,h7h6,e2c4,h6h0 +ea1b47c0;a3a4,b7b4,g0e2,h7h2,e2g4,h2b2,a4a5,b9c7,d0e1,h9g7,c3c4,b2i2,c4c5,i2i0 +c91c69fd;g0e2,b7b4,i3i4,b4b1,a3a4,g9e7,a0a3,e6e5,e2c4,b9a7,a3a2,d9e8,d0e1,h7h0 +6fe5e5ed;i3i4,g9i7,f0e1,b7e7,e0f0,i7g9,a0a2,e7e3,a2c1,e9e8,e1f2,a9a7,c1c6,a7b7,c6e6 +5f2ad804;i3i4,h7d7,a0c1,b7c7,c1c6,d7d4,c0a2,c7i7,c6c9,e9e8,g3g4,d4c4,a2c4,g9e7,c9c8 +cacb9c37;e0e1,b7b6,a0b2,h7d7,b2d3,h9g7,d3d6,d7e7,e1e2,g9i7,d0e1,g7h9,d6d8,e6e5,d8c8 +9357a801;i3i4,h7h1,e3e4,a9a7,c0a2,h1h5,a0b2,h5e5,b2e2,e5c5,e0e1,i6i5,a2c0,b7b3,e2e6 +ebbe97b9;a0a1,h7h5,a1b3,h5h0,d0e1,h0h8,a3a4,a6a5,b3b6,g6g5,c3c4,h8h6,e1d2,h6h1,b6e6 +a7087840;a0b2,i9i8,b2d1,g9i7,c0a2,b7d7,a2c0,g6g5,d1b1,h7h4,c3c4,a9a8,b1e1,h4h5,e1e6 +7ec432ad;d0e1,h7h0,g3g4,b7b6,a0b2,h0i0,c3c4,a6a5,b2g2,b6b0,c0a2,b0b6,g2g6,c6c5,g6e6 +8acdf940;g3g4,b7b0,e3e4,b0b4,a0c1,c6c5,c1c5,h7h1,g4g5,b4b1,c5c6,h1e1,a3a4,b1a1,c6c8 +2f3bc814;a0b2,c9a7,b2d1,h7h8,d1d7,e9e8,g0i2,g9e7,c0e2,h8h4,e3e4,h4g4,d7a7,b7b6,a7c8 +82184fdc;d0e1,b9a7,g0i2,e9e8,a3a4,e8e7,a0b0,h9g7,e1d2,h7h5,i3i4,h5b5,b0b5,i9i7,b5c7 +118368c2;a0b2,g9i7,g0i2,i7g5,c0e2,h7d7,b2c4,b7b3,e0e1,g5e7,c4d6,d7d8,d6e4,b3b6,e4e7 +fec16180;g3g4,b7e7,a0a6,f9e8,e3e4,i9i7,a6b4,h7h8,b4b0,a9a8,b0b9,i7i8,b9c7,a8a4,c7c9 +89427cf0;a0b0,h9g7,b0b9,g6g5,c0a2,i9i8,b9d9,e9e8,g0i2,h7h6,d9a9,i8h8,a3a4,h8h9,a9c8 +ab3bd172;c0a2,c6c5,c3c4,b7a7,a2c0,e6e5,c4c5,a7b7,a0a1,e9e8,a1d1,h7g7,d1d8,e8e9,d8e6 +55f339d4;c0e2,h7h4,e0e1,b7h7,g0i2,g9e7,a0a6,h4a4,a6a5,b9a7,a5g5,e7g9,i3i4,g9i7,g5e6 +17ee5a20;a0a6,e6e5,a6a7,i9i8,e0e1,h7d7,e1d1,i8i7,g0i2,i7i8,f0e1,d7i7,a7a5,i6i5,a5e5 +36e3e2be;g0e2,h7e7,a0b0,h9i7,e2g4,b7b3,b0b9,e7e3,b9a7,e3e2,g4e2,c9e7,c0a2,b3b2,a7c8 +60948762;c3c4,b7c7,e3e4,b9a7,a0b0,g6g5,i3i4,e6e5,e0e1,g5g4,e1e0,h7h4,b0b5,a7b9,b5e5 +8db555a0;c3c4,b7b8,i3i4,b8e8,e3e4,h7f7,a0a6,f7a7,g0i2,e8e7,f0e1,e7d7,c0a2,d7g7,a6e6 +a98ca5c7;a0b0,g9i7,e0e1,i6i5,b0c2,i7g9,c0a2,h7e7,e3e4,i9i7,c2e2,b7b5,e4e5,e7e5,e2e6 +d0b30fa0;a0a6,a9a8,c0a2,h9i7,a3a4,a8c8,a6b6,h7h3,b6c6,b7b0,e0e1,b0b5,c6d4,c8h8,d4e6 +f094f61c;g0i2,h7d7,a0a6,d7d1,a6c7,b7b0,c7a8,d1e1,i2g4,e1f1,a8a6,e9e8,a3a4,f1a1,a6e6 +419b0dd9;a3a4,e6e5,c0e2,b7b1,a4a5,b1e1,a0c0,h7h8,c0c1,f9e8,c1c6,e9f9,a5a6,f9e9,c6c9 +f81926ca;c0e2,b7b6,f0e1,h7h1,e1d2,d9e8,a0c0,b6b3,e2c4,b3b8,c4a2,h1h3,c0c6,h3h5,c6c9 +8d3ec6da;a0c1,i9i8,c0e2,h7h2,c1a2,i6i5,a2h2,i8i6,h2h3,c9a7,h3h6,a7c5,e2c4,b9c7,h6g8 +752e840b;a0b2,h9i7,b2a4,a6a5,f0e1,b9a7,g3g4,a7b9,e0f0,h7h5,a4c4,b7b2,c4d6,b2b8,d6e6 +7eb372a6;a0c1,h9g7,c1c6,b7d7,c6c9,e9e8,i3i4,g7h9,c9c7,e8d8,c7a6,d7c7,g0e2,h7h2,a6b8 +d440bb1d;g0i2,h7i7,e3e4,b7b8,a3a4,c6c5,a0b0,e9e8,g3g4,i7f7,b0b8,e8e9,b8c6,a9a7,c6c8 +43b2390a;i3i4,h7i7,g0i2,g6g5,g3g4,b7f7,c3c4,i7h7,a0b0,e9e8,b0b6,a6a5,a3a4,h7h1,b6e6 +db3df924;a0b0,b9a7,i3i4,c9e7,b0b2,e7g5,b2b3,b7c7,f0e1,c7g7,b3c5,h9i7,c3c4,e6e5,c5e5 +0b818b1e;g0e2,h7h4,c3c4,b7b4,a0a6,g6g5,i3i4,i9i8,c0a2,i8i7,e2c0,a9a8,c0e2,b4b2,a6e6 +c462d165;a3a4,h9g7,g0e2,h7h8,e3e4,e9e8,a0a6,h8h1,e4e5,h1c1,a6a8,e8e9,a8i8,g7h9,i8c8 +6853b02c;e0e1,b7a7,e1d1,c6c5,a0a6,h7h0,g3g4,h0f0,e3e4,f0f6,a6c7,i9i8,c7a8,i8c8,a8c8 +fb9d07f7;a0a2,c9a7,a2b2,b7b3,c0a2,g9e7,b2b3,h7h4,b3b8,h9g7,e3e4,e7c9,i3i4,h4h1,b8g8 +4dae9cb4;a0b2,e6e5,f0e1,b7a7,g3g4,h7i7,b2c2,a6a5,c2b0,a7d7,e1d2,d7e7,b0b7,d9e8,b7c9 +fa65102e;g3g4,b7b0,a0b2,h7d7,f0e1,d7g7,b2f2,b0b5,i3i4,c6c5,f2f0,g7i7,f0f8,a6a5,f8g8 +a10f2129;a0a2,h7e7,a2e2,e7d7,f0e1,d7g7,c0a2,b9a7,e3e4,b7b9,e2f4,a6a5,a2c4,b9b5,f4e6 +0cb88a4e;a0a2,g9i7,e0e1,h7g7,a2h2,c9e7,h2f3,b9a7,f3g5,a6a5,g0e2,b7b5,e1f1,e7c5,g5e6 +ecbe9286;a3a4,b7g7,g3g4,g9i7,a0b0,i7g5,g0e2,c6c5,a4a5,b9a7,b0b8,g7b7,e2c4,g5i7,b8c8 +76234ae1;f0e1,c9a7,g0i2,h7h5,a0a2,h5a5,e1f2,b7h7,a2d2,e6e5,d2c2,h7h3,c2c6,e9e8,c6e6 +bb133d54;a0a2,h7h2,d0e1,b7b3,a2d2,b3b1,c0e2,h2h1,g3g4,h1g1,d2d6,b1b5,e2c4,g1g2,d6c8 +77654fc9;g0e2,h7h5,a0c1,b7i7,c1f1,i7i3,c3c4,h5h6,f1d2,h6h0,f0e1,i3h3,d2d8,h0f0,d8c8 +a9917f55;i3i4,c9e7,a0b0,b9a7,i4i5,b7b1,a3a4,b1d1,b0b4,d1d3,b4c4,a7c8,f0e1,e9e8,c4c8 +5afadc41;g0e2,h7c7,a0a2,c7c3,f0e1,g6g5,a2b0,a6a5,b0b7,c9a7,e2g4,c3c4,b7g7,g9e7,g7e7 +52c5165a;f0e1,b7a7,g0i2,h7c7,g3g4,c7b7,a0a2,b7h7,a2a6,a7g7,g4g5,g7g8,a6b6,a9a5,b6e6 +dd03d1ca;g0i2,h7h3,e0e1,b7d7,a0a1,d7i7,g3g4,i7e7,a1a6,c6c5,g4g5,h3h2,a6a8,h2h3,a8c8 +f131b084;a3a4,c6c5,i3i4,a6a5,c3c4,h7c7,c0e2,c7h7,a0a1,h7h5,a1c2,b7b3,c2c5,b3b1,c5e6 +040c3601;a0a6,h7f7,a6a9,b7b2,a3a4,b2f2,g3g4,f2b2,g4g5,b2b5,a9c9,e9e8,c3c4,g6g5,c9c8 +9945bfae;g0e2,e6e5,a0a6,h7h2,e3e4,g9i7,a6a8,b7b5,e2g0,h2e2,a8h8,b5b1,h8a8,c6c5,a8c8 +253c11fe;a0a1,b7a7,a1a6,h7h1,a3a4,f9e8,g0e2,a7i7,d0e1,i7e7,a6b4,e7b7,b4g4,b9a7,g4g9 +20887543;c0a2,a6a5,a0c0,e6e5,g3g4,i9i8,c0b2,e5e4,b2e2,b9a7,i3i4,i8b8,a2c4,a9b9,e2e4 +f5d981f9;d0e1,b7c7,g3g4,h7i7,a0c1,g6g5,g4g5,g9e7,g5f5,c7d7,c1a2,i9i8,a2a6,e7g5,a6e6 +4b6fb356;a0a1,b7c7,a1a6,h7h1,g3g4,h1g1,g0e2,c7f7,e2g0,g1b1,a6c6,f7a7,c6g6,a7d7,g6e6 +dd56636e;c3c4,h7e7,a0a2,c6c5,a2c1,e7h7,c1h1,h7e7,h1h4,e7g7,h4i4,b7c7,i4i9,d9e8,i9g9 +93403aff;c0e2,b7b6,a3a4,h7i7,d0e1,a9a8,a0b2,a6a5,b2b0,i7c7,b0b6,a8c8,e3e4,i6i5,b6e6 +156de1f0;i3i4,b7b2,e0e1,e6e5,a0a6,h7h4,c0e2,b9a7,e1e0,h4f4,e3e4,b2b9,a6c5,i9i7,c5e5 +f02b9a13;g0e2,b7b3,a0a2,h7d7,a2a6,d7d2,e3e4,d2d5,f0e1,d5a5,e1f2,g6g5,g3g4,i6i5,a6e6 +2fa745f6;d0e1,e6e5,a0a1,b7a7,i3i4,h7g7,g0i2,g7f7,a1c1,f7g7,c0a2,i9i8,c1c6,g7h7,c6e6 +d1b1ef2c;e0e1,c9a7,g3g4,h7g7,e1d1,g7c7,a0a6,i9i7,d1d2,c7g7,g0i2,b7b5,a6c6,g7d7,c6e6 +02926ea9;c0e2,b9c7,e2g4,h7i7,a0b2,i7d7,b2b4,d7d4,b4b5,d4a4,c3c4,d9e8,b5g5,c6c5,g5g9 +3bd44e5f;e3e4,h7f7,g3g4,b7d7,a3a4,d7b7,c3c4,g9i7,c4c5,f7h7,a0a1,h7h2,a1a6,i7g5,a6e6 +4680c909;a0b2,a9a7,b2b6,h7g7,b6b1,f9e8,b1b5,a7a9,b5d4,g7g8,d4d1,b7b3,d1d6,e8f9,d6e6 +09ca488e;a0b0,c9a7,b0b7,c6c5,b7a7,a9a8,a7c6,h7h4,c6a6,c5c4,a6g6,a8a5,g3g4,a5a4,g6e6 +5ad8cf34;e0e1,b7d7,a0a6,d9e8,e1d1,i9i8,g3g4,d7b7,a3a4,h7d7,a6c7,a9a4,c7b7,i8i7,b7c9 +b67bdde0;a0a1,b7g7,a1a6,i9i8,g0i2,h7h2,a3a4,e9e8,a4a5,h2h6,a6c5,g6g5,c3c4,i8i9,c5e6 +05efed70;i3i4,b9a7,a0c1,h7h6,i4i5,h6h1,c1c6,h1i1,c6b8,i1e1,i5i6,e1e2,i6h6,a7b9,b8g8 +344287f2;e0e1,h7h2,a0a1,b7g7,a1a2,h2i2,e1f1,g6g5,a2a1,i2i0,a1a6,i6i5,c0a2,i0i2,a6e6 +430183ad;g0e2,b7b2,a0a6,d9e8,e0e1,a9a8,e1d1,h7h2,a6b6,b2a2,b6b7,h2h4,c3c4,g9i7,b7c9 +e5ebff2e;e3e4,a6a5,e4e5,b7a7,a3a4,g6g5,a0a5,a7g7,e5d5,h7h5,e0e1,i9i7,a5a8,g7a7,a8c8 +a60d9507;d0e1,b7g7,c0a2,g7e7,g3g4,g6g5,a0b0,i9i8,i3i4,h9i7,b0b9,e7b7,b9c9,e9e8,c9c8 +c7b488c1;g3g4,h7h3,a0a2,b7g7,a2g2,i9i7,g2h4,i7i9,h4h8,g7i7,h8a8,h3f3,f0e1,c6c5,a8c8 +2f0b5d2c;g3g4,h9i7,a0a2,e9e8,a2c2,e8d8,c2f2,i6i5,e3e4,h7h2,f2f3,b7b2,f3f6,b2b6,f6f8 +3ce1ebd9;c0e2,b7a7,a0b0,a9a8,g3g4,a8a9,e3e4,h7i7,b0b6,i7b7,c3c4,a9a8,i3i4,a7a3,b6e6 +f1a90fb3;f0e1,b7b5,a0a1,b5g5,a1b1,g5a5,b1b3,i9i8,c3c4,h7h3,b3b6,a5a4,e0f0,i8i7,b6e6 +ca333c16;c0e2,b7c7,a3a4,e9e8,a0a1,c6c5,a1b3,h7h2,b3b9,g9e7,a4a5,h2i2,b9b6,i6i5,b6c8 +b414e998;a0a1,h7d7,g3g4,d7i7,e3e4,b7f7,a1f1,g9e7,f1f5,i7g7,c0e2,e7c5,f5g7,f7f1,g7e6 +78c8d52a;g0i2,g6g5,a0a6,b7g7,e0e1,a9a7,a6c6,h7h3,c6c4,h3h1,c0e2,g7f7,c4d6,g9i7,d6c8 +49e1c0f3;i3i4,g9e7,c0e2,h7h0,a0b0,b7b5,b0b9,b5b3,b9b3,e7g9,b3b6,h9i7,e2c4,e6e5,b6c8 +c0f84e29;a0a1,h7h5,a1a6,h5h4,g0i2,g9i7,a6c6,a9a4,c6b8,h4c4,b8d8,c4h4,i3i4,h4h2,d8e6 +cc8fa7cf;a0a6,h7h5,e3e4,h5h8,a6c6,b7b4,g3g4,h8h3,g0e2,b4b7,c0a2,b7b5,a2c4,b5h5,c6e6 +21463a5c;i3i4,h7d7,a0c1,d7c7,c1a2,c7e7,c3c4,b7b6,a2c1,e7b7,c1c6,b7c7,c6b6,c7c8,b6e6 +168f7dcb;g0e2,b7a7,e2g0,i9i8,g3g4,i8d8,c0e2,d8a8,a0a6,g6g5,g4g5,h7h5,e2c4,e9e8,a6e6 +f6f99d36;a0c1,d9e8,c1e2,b7b8,i3i4,h7h8,e2e6,g9e7,e6g7,h8f8,e3e4,i6i5,d0e1,i5i4,g7g9 +2f7d130a;a0a2,b7d7,a2g2,d7d1,g2f2,b9a7,f2f1,d1d3,f1d1,d3d7,d1d2,d7c7,d2d4,c7d7,d4e6 +80b8c035;a0b2,c9a7,g3g4,h7g7,b2b7,c6c5,c0a2,f9e8,b7g7,a7c9,g4g5,a6a5,f0e1,a5a4,g7g9 +1767333b;a0a6,h7f7,a6c5,a9a8,c5c4,a8a4,c4d4,e6e5,d4c6,a4g4,c6d4,f7i7,d4c6,g4h4,c6e5 +58afbe10;f0e1,a9a8,a0a1,h7g7,g3g4,d9e8,a1a2,b7b3,a2a1,b3b0,a1b1,g7e7,b1b6,c9a7,b6b9 +6c55c58b;e3e4,f9e8,a3a4,b7b0,a0a6,h7e7,e0e1,e7h7,a6a7,b0d0,a7c7,e8f9,e1d1,h9i7,c7c8 +e7689161;c0e2,b7b8,a0b2,b8a8,e2c0,h7g7,b2d3,a8c8,d3e1,c8c3,e1c2,c9a7,c2e1,a7c5,e1e6 +f3eac0bf;g0e2,c9e7,e2g0,e7c5,c0a2,i9i8,a0b2,h7h2,b2c2,f9e8,e0e1,h2f2,e1f1,f2f8,f0e1,b7f7 +74db75c1;c0a2,g6g5,a2c4,g9e7,f0e1,i9i8,a0a6,i8a8,a6b6,e9e8,b6a6,a8c8,a6a7,c8a8,e3e4,b7b0 +c85776fc;d0e1,c6c5,g0i2,h7h1,i2g4,b7d7,e0d0,c9e7,a0a1,g6g5,d0e0,h1h7,c0a2,d9e8,a1c0,h7h0 +da5d94e5;g0i2,h9i7,c0a2,c9e7,i2g4,h7h2,a0c0,e9e8,a2c4,i7g8,c4a2,b7b8,d0e1,h2h6,a2c4,h6h0 +50d910ed;g0e2,b7b5,i3i4,i9i8,a0a1,h7h8,e3e4,b5b1,e2g4,h8h2,d0e1,h2h3,a1a6,b1b2,e4e5,h3h0 +3986714d;a0c1,h7h8,g3g4,b9c7,c1e1,h8d8,e1g1,b7b3,g1g6,d8d5,f0e1,b3b0,g0e2,b0b6,g6i7,a9b9,i7g8 +23877464;a0b2,h7e7,b2b4,b7a7,f0e1,e7f7,i3i4,i9i7,b4b2,f7c7,b2a4,c9e7,g0e2,a7a8,a4a8,e7c5,a8g8 +a2f14779;a0a1,b7b6,a1i1,h7c7,g3g4,e6e5,e0e1,c7i7,c3c4,b6b4,i1i6,b4g4,e1e0,i7f7,i6c6,g9e7,c6c8 +0e8dfb5a;g0i2,b7b0,e0e1,b0b3,a0b2,h7h8,b2b0,h8g8,c3c4,g8i8,b0b9,b3d3,b9b3,d3d5,b3b6,a9a7,b6e6 +5d246121;a0a2,h7h1,a2e2,b7b8,g3g4,h1h7,d0e1,b8d8,g0i2,d8h8,g4g5,a9a8,g5f5,h7h2,e1d0,h2f2,e2e6 +36da8017;f0e1,c9a7,a0a2,e9e8,a2h2,h7d7,h2h0,d7g7,h0h1,g7d7,h1h8,e8e7,a3a4,b9d8,e0f0,a9c9,h8g8 +dc14c251;i3i4,b7e7,c3c4,e9e8,d0e1,e6e5,c0e2,b9c7,a0a1,e8d8,a1a0,e7e3,g0i2,a9b9,a0c1,f9e8,c1d3 +6b9b1dcb;g3g4,h9i7,d0e1,b7e7,e1f2,g6g5,f2e1,h7h8,e0d0,e7e3,a0a6,g5g4,c3c4,e3b3,a6c5,h8h7,c5e6 +6005f04d;i3i4,b9c7,a0b2,g9i7,b2c2,b7b1,c2d2,b1b8,i4i5,e6e5,d2d7,e9e8,d7i7,h7h3,d0e1,b8b1,i7g8 +36b3452b;a0c1,b9a7,c1a0,b7d7,a0b2,a9b9,b2d3,b9b1,g0i2,b1b3,d3e1,d7d8,e3e4,d8d1,i3i4,b3b1,e1e6 +acec1f12;i3i4,g9i7,f0e1,h9f8,c0a2,b7d7,g0e2,h7h9,e2g0,c9a7,e1d2,a9a8,a0b2,c6c5,d0e1,d9e8,b2b9 +6bc6013d;a0b2,h7c7,f0e1,c7g7,c0a2,b7b3,b2i2,b9c7,i2e2,c7a8,e0f0,g9i7,a3a4,g7g8,a2c0,b3b2,e2e6 +174636d6;c3c4,b7b6,a0b2,h7h1,b2b5,c9a7,b5b6,h1h0,b6c6,i6i5,c6a7,d9e8,a7d7,b9d8,d7a7,g9e7,a7a9 +0d7449b5;i3i4,h7i7,g3g4,c6c5,e0e1,b7g7,a0c1,i7i8,c1c5,h9i7,a3a4,g7g8,c0e2,g8f8,e1d1,f8b8,c5e6 +dbf21f19;a0a1,a9a7,a1i1,g6g5,g0e2,h7h4,g3g4,f9e8,i1e1,e8f9,e1g2,h9i7,g2f4,b7b8,f4d4,a7a8,d4e6 +c8eb5b42;c3c4,b7b4,a0b2,b4b5,b2b5,h7d7,b5c7,c6c5,f0e1,d7d3,c7h7,d3d5,h7h6,c9a7,h6g6,b9d8,g6g8 +b24d8b6b;a0a2,g6g5,g3g4,c6c5,a3a4,h7c7,a2i2,b7b8,i2e2,c7f7,e2g2,b8d8,g2c2,h9i7,c2e1,d8d1,e1e6 +80254e82;g0e2,h7d7,e0e1,d7i7,a0a1,g6g5,a1c2,i7f7,c0a2,b7d7,e2g4,c9a7,e1e2,h9i7,c2c6,a7c5,c6e6 +fa48d749;c3c4,i6i5,a0c1,b7b6,c4c5,c9a7,c1c6,h7i7,g3g4,b9d8,c6d6,a9c9,d6g6,i7i8,g6h8,d8b7,h8g8 +2cc29bcb;g3g4,h7g7,d0e1,g7d7,a0a2,b7b1,g4g5,b1b0,a2b0,d7d4,e1d2,d4f4,b0b5,f4f7,b5c5,e9e8,c5e6 +696a3656;e3e4,e6e5,i3i4,h7h8,a0b2,b7b5,b2d2,b5b7,d2d5,c6c5,d5d6,h8h7,g3g4,b7a7,g4g5,b9c7,d6c8 +36d81f80;a3a4,b7g7,a0a6,h7h5,a4a5,i9i8,c0a2,i8a8,a6a7,h5h0,a2c4,a8g8,a7c8,g8d8,c8d6,h0i0,d6e6 +153c0d82;a0b0,h7h2,b0b7,i9i7,b7a7,h2g2,e0e1,g2i2,a7h7,i2f2,h7h5,f9e8,h5d5,a9a8,d5c5,f2f4,c5c9 +be9f37cf;a0a2,b7b3,e0e1,h7e7,a2h2,b3b1,h2a2,b1b2,e1e0,e7e3,a2a6,e3d3,f0e1,d3i3,c3c4,b2b1,a6e6 +90dd49ca;a0a2,b7d7,a2e2,h7e7,c3c4,e9e8,e2d2,b9c7,g0e2,e8f8,d2d5,a6a5,d5d7,i9i7,d7d4,d9e8,d4f4 +1f9e10ab;i3i4,i6i5,c3c4,b7f7,a0a1,h7i7,a1a2,i7h7,e0e1,b9c7,e1e2,h7h0,a2d2,c7b9,d2d8,f7f5,d8c8 +e4497cb0;c0a2,b7b4,a2c4,b4b0,a0b0,d9e8,b0b4,h7g7,e0e1,i6i5,c4e2,e8d9,b4b3,g9e7,b3b7,c9a7,b7e7 +945c3d3b;c0e2,e6e5,a0a6,h7g7,f0e1,b7e7,a6c5,e7a7,c5a5,a7f7,a5c6,h9i7,e3e4,a9a8,a3a4,f7f0,c6e6 +7a311938;a0a6,h9g7,g3g4,i6i5,a6b6,c9a7,f0e1,b9c7,b6c6,c7e8,c0a2,b7e7,i3i4,e8c9,c6d6,h7h3,d6c8 +7db94499;f0e1,f9e8,a0a6,b7b1,i3i4,h7a7,c0e2,e6e5,e2c4,g9i7,a6a7,i7g9,e1d2,b1d1,a7h7,c6c5,h7g9 +7916781f;a0c1,c6c5,c1b3,h7h6,b3b2,h6h3,b2d2,i6i5,d2a2,f9e8,f0e1,e8f9,a2c1,b7b0,c1e2,b0b4,e2e6 +181115ca;f0e1,i9i7,e0f0,a9a7,g0e2,i7i8,e2c4,b7b2,a0a6,i8b8,c0e2,b2b3,a6a7,h7h4,a7a6,b8d8,a6e6 +fe88da06;a0a2,b7b1,a2f2,a9a8,f2h1,b1b6,e3e4,h7h4,c0a2,h9g7,h1c1,c6c5,c1f1,g7i8,f1e3,h4h3,e3e6 +e58b6bce;g0i2,h7d7,d0e1,h9g7,a0c1,g9e7,c1a1,b7b2,a1c2,b2b1,c2c6,a6a5,c3c4,e9e8,i2g4,e7g9,c6c8 +0f559ccc;e3e4,h7h3,a0a2,b7f7,a2e2,f7h7,e2e3,i9i8,e3f3,g9i7,f3d3,c9e7,g3g4,e7c5,d3e3,g6g5,e3e6 +091dea9d;i3i4,d9e8,g0e2,b7a7,f0e1,e8f7,e2g0,e9e8,a0a2,h7h3,a2a1,a7a8,a1b1,c6c5,b1b6,a8b8,b6e6 +711c48de;a0a6,b9a7,g3g4,b7e7,a6c5,e7e8,c5c6,e8e3,c6b6,e3e1,b6g6,e1c1,c0a2,h7e7,d0e1,a7b5,g6g8 +7f9e7be9;c0a2,h7h8,a2c4,b7a7,a0a1,a7d7,f0e1,i6i5,a1a2,h8f8,a2e2,f8f7,e2g2,g6g5,g2e2,i5i4,e2e6 +90b1dd00;a3a4,b7b3,g0e2,h7i7,a0b0,i7b7,e0e1,b7g7,i3i4,e9e8,b0b3,e8e9,e2g4,i9i8,b3c5,i8i9,c5e6 +d8a88ce6;a0c1,i6i5,c1c6,e6e5,c6c4,i9i6,d0e1,b7e7,e3e4,e7f7,c4a5,f7f5,a5c5,e5e4,c5a4,f5a5,a4e4 +d52e6c90;a0c1,b9a7,d0e1,h7h4,e1f2,h4d4,c1e2,b7b4,g3g4,b4b5,g0i2,h9i7,c3c4,b5b2,e0d0,b2b3,e2e6 +cb86e840;a0a6,b7b6,f0e1,e9e8,a3a4,i6i5,c0e2,b6b1,a6a5,b1b0,e2c0,e6e5,g0e2,a9a8,a5c6,b0b7,c6e6 +e0d7ceb8;e0e1,h7h0,c3c4,i9i7,c0e2,i7e7,e1d1,b9a7,a0b2,b7d7,b2b9,d7c7,i3i4,c7b7,b9c7,e7g7,c7c8 +a198434c;a0a2,e6e5,a2c2,a6a5,c2i2,h7h4,i2g2,h9i7,g2g1,g6g5,g1f3,h4h9,i3i4,h9h1,d0e1,a9a7,f3e5 +a348cf0a;a0a6,i6i5,g0e2,i5i4,e2g0,h7g7,a6a8,g7d7,a8d8,d7f7,c0e2,f7f4,d8d4,f4f2,e2c4,i9i7,d4e6 +606b2f01;a0a2,g6g5,i3i4,b7d7,a2c2,h7g7,c2d2,e9e8,d2h2,e8f8,h2h4,d7d8,h4h1,d8d4,f0e1,a9a8,h1h8 +2734a919;g0e2,b7b6,i3i4,b6b5,c3c4,g9i7,a0a6,b5b4,a6c7,h9g7,c7c9,e9e8,c9a9,h7h1,a9a6,g7h9,a6e6 +b64b96b1;a0c1,c9e7,c3c4,e7g5,c1c6,d9e8,c0a2,b7d7,c6c5,e6e5,c5e5,g5e7,e5b5,d7d5,a2c0,a9a8,b5b9 +f1995e39;a0b0,b7b6,f0e1,b6b2,e1f0,g6g5,b0b9,h9i7,b9b3,b2b1,b3b4,g5g4,b4b8,h7h6,b8f8,a9b9,f8c8 +a81f95c6;e0e1,h9i7,a0c1,h7c7,c1b1,c7f7,e1d1,a6a5,b1b9,a5a4,b9c9,e9e8,g0e2,i7g8,e3e4,f7i7,c9c8 +12f30abc;a0b2,b7b3,b2d3,g9i7,d3e5,f9e8,e5g6,h7h3,c0e2,i6i5,i3i4,h3e3,e2c4,e8d7,g6g5,e9e8,g5e6 +0ef3f304;g0e2,h9i7,e3e4,i9h9,e2c4,b9a7,a0a6,h7f7,a6c5,b7b5,c4a2,f7b7,a2c4,b5b1,g3g4,b7c7,c5e6 +b6539bf6;a0c1,b9a7,c1e1,b7b4,e1f1,b4i4,f1c1,i4f4,c1h1,f4f3,h1d1,h7h0,d1i1,i9i7,i1i6,f3i3,i6e6 +1d15ccf0;a0a1,h9i7,a1b3,b9c7,g0i2,b7a7,d0e1,a9a8,e3e4,h7h5,b3b2,a7a3,e0d0,e6e5,b2e2,h5f5,e2e5 +be5eda95;c0a2,b7b0,a2c0,c9a7,g3g4,h7h3,g4g5,h3h0,c3c4,b0b1,a0b2,h0h3,b2c2,c6c5,c2e1,b1b0,e1e6 +34f657d7;a0b2,h7h0,b2b1,b7b6,b1a1,h0i0,a1a2,g9i7,g3g4,a6a5,a2d2,b6b0,e0e1,i0f0,d2e2,i7g9,e2e6 +07cf9487;c0a2,h7h0,a2c0,h0h8,d0e1,b7f7,e1d2,f7i7,e3e4,i6i5,a0a6,e9e8,e0d0,i5i4,a6b6,a9a5,b6e6 +bf5846f8;g3g4,b7c7,a0b2,i9i8,e3e4,a9a8,f0e1,h7h3,b2a4,h3d3,c3c4,d3d2,e1d2,i8i7,a4a8,i7f7,a8c8 +0cce6238;e3e4,h7e7,a0b2,b7d7,b2a4,e6e5,e0e1,h9i7,e1e0,i7h9,a4a5,e7e8,a5c4,e5e4,d0e1,e8i8,c4e4 +e261008f;g0i2,b7a7,g3g4,h7e7,c0e2,e7e8,g4g5,i6i5,i2g4,a7g7,a0a6,b9a7,e2c0,e8f8,g5g6,g7g8,a6e6 +bc688167;a0a6,h7h6,a6c7,a9a7,g3g4,h9g7,e3e4,h6h5,c0e2,h5h9,d0e1,h9h8,g0i2,h8h3,c7c9,e9e8,c9c8 +3df3dc9f;c0e2,h7h2,c3c4,e9e8,a0c0,b7b6,c0b0,g9e7,b0d1,a9a7,g0i2,h2h3,d1d8,e8e9,f0e1,i6i5,d8c8 +25f43be2;g0i2,d9e8,d0e1,c6c5,a0b2,h9i7,b2b6,h7h6,i2g0,c5c4,e0d0,b7h7,a3a4,h7a7,e1f2,a7h7,b6c8 +e2a8c5ca;g0e2,i6i5,a0c1,g9i7,c1h1,h7h2,h1h2,c6c5,h2h3,e9e8,h3g5,b7d7,g5g6,d7d3,g6i7,e6e5,i7g8 +d97e7875;g0i2,b7b2,i2g4,g9i7,c3c4,h7h2,a0a6,b2b5,a6a7,b5b1,g4e2,b1b0,a7f7,h9f8,f7c7,h2h5,c7c8 +61607992;g0e2,h7f7,a0a1,a9a8,a1b3,f7h7,g3g4,a8a9,e2g0,c6c5,a3a4,h7h6,b3b7,f9e8,g0e2,h6h4,b7c9 +0c2fc8c5;f0e1,h7h4,c3c4,h4d4,c4c5,h9i7,i3i4,b7b6,a0a6,d4h4,a6b6,h4e4,b6a6,a9a7,c0e2,i7h9,a6e6 +49413a0c;e0e1,e9e8,a0b2,b7c7,g0i2,i9i8,i2g0,c7d7,c0e2,e8e9,g3g4,b9c7,e2c4,i8c8,b2d3,e6e5,d3e5 +a4372f42;g0e2,h7h4,a0b2,h9g7,i3i4,h4d4,e2c4,b7b8,b2a2,a6a5,a2i2,g7i8,i4i5,b8b1,i2e2,a5a4,e2e6 +1d8cc42b;a0b2,b7b3,b2i2,h7h1,i2g1,g6g5,g0e2,c9e7,f0e1,e6e5,e0f0,i9i7,g1f3,b9c7,f3f6,b3b2,f6g8 +66092fd1;a3a4,h7h1,c0a2,h1h4,a0a1,c9a7,a1g1,b7b0,a2c0,h9g7,g1g6,b0b6,g6i6,b6b3,i6g7,h4h8,g7g8 +38d005ce;g0e2,b7b8,e0e1,b8e8,e1e0,h7g7,d0e1,g7d7,e2g0,c6c5,e1d0,a6a5,a0b2,d7c7,b2b8,e8i8,b8g8 +40b5bf21;a0b0,b7b8,b0b3,b8d8,b3b5,d8d3,c3c4,d3d5,b5a5,i9i7,a5c5,d5d4,c0a2,h7d7,c5c9,e9e8,c9c8 +0804b843;g0e2,c6c5,e2c4,b7b2,c4e2,a9a8,a0c1,a8a9,c1f1,b9c7,i3i4,c7e8,f1f3,h7a7,f3f8,e6e5,f8f7 +bfe4bd3e;a0a2,a9a7,a2i2,h9g7,i2h2,e6e5,g0i2,b7b3,h2b2,b9c7,a3a4,a7a9,b2d1,h7h9,d1d8,i6i5,d8g8 +0c9dbf01;a3a4,e9e8,c0e2,h7h4,e3e4,g6g5,i3i4,i9i8,e0e1,e8f8,a0b0,h4f4,c3c4,h9i7,b0d1,f8f7,d1d7 +4e0de063;g0i2,h7h3,d0e1,b7b4,i2g0,b4d4,a0c1,e9e8,c0e2,h3h8,e1f2,d4b4,c1e1,h8h7,e2g4,h7c7,e1e6 +9718b0b6;c0e2,h7h3,e3e4,b9a7,a0a2,b7b2,a2b0,h3h4,b0c2,b2b6,c2e3,b6b4,e3c4,b4b5,c4c5,g9i7,c5e6 +1ef894fe;a0a2,h7g7,i3i4,g7e7,a2a6,i6i5,c0a2,e9e8,a6c6,b7b6,g3g4,a9a5,g0e2,e7c7,e2c4,b6b1,c6e6 +5f21649f;c3c4,b7e7,e3e4,h7h0,a0a6,h0f0,c0e2,e7b7,d0e1,c9e7,a6c6,h9g7,c6b8,d9e8,g3g4,a9a4,b8b9 +13d41907;g3g4,f9e8,e3e4,e6e5,g0i2,h7d7,a0a6,e8f9,a6c6,d7c7,d0e1,b7b3,i2g0,i6i5,e0d0,c7c8,c6c8 +343b90fa;g0e2,i9i8,e2g0,b7g7,i3i4,c6c5,e0e1,g7c7,c0e2,h7h2,a0c0,h2h1,c0c2,i8c8,c2c5,c7h7,c5c8 +976562d1;d0e1,b7c7,c3c4,h9i7,e1f2,a9a7,i3i4,c7c4,a0a2,e6e5,a2a6,h7e7,a6g6,d9e8,g6c6,c4h4,c6c9 +1abf355e;a0b2,g9i7,b2d2,i7g9,d2f3,b7b4,f3f7,e9e8,f7h7,b4b0,g3g4,c6c5,g4g5,c9a7,h7h8,e8e9,h8g8 +9742fbd0;a0c1,h7h4,e3e4,b7f7,c1b3,h4i4,c0a2,f7b7,e0e1,b7d7,b3b7,g6g5,e4e5,d7d3,b7c9,e9e8,c9c8 +40ad5618;c3c4,b9c7,g3g4,h7h1,a0b2,c9e7,d0e1,a6a5,b2a4,h1i1,a4a9,e7c9,e1f2,i6i5,i3i4,i1h1,a9c8 +cf98531c;c0a2,h7h0,a0b0,b7b4,i3i4,b4b5,b0b4,h0h8,b4f4,b5b4,f4b4,g6g5,b4c4,h8a8,c4d6,a6a5,d6c8 +ec27c2f5;a0a2,h7e7,a2c1,g6g5,c1a2,g5g4,e3e4,a9a7,a2i2,b7d7,i2h0,a7b7,h0h9,g4f4,h9i7,c6c5,i7g8 +dea70cb8;d0e1,b7b0,c0e2,h7h2,a3a4,i9i7,a0a2,e9e8,e2g4,h2h4,a2b4,e6e5,b4b3,h4h5,b3b8,e8e9,b8g8 +f02aa84a;a0a2,h7h5,a3a4,h5b5,a2a0,a9a8,e0e1,a8h8,c0e2,b7c7,e2c4,b5c5,g0e2,c5h5,a0a6,h8d8,a6e6 +7058a1bd;a0a2,b7b0,a2b2,i6i5,b2h2,e9e8,h2b2,g6g5,b2a0,h7d7,a0b0,d7a7,b0b7,g5g4,b7d8,e8e9,d8g8 +bf9681a5;e3e4,b7g7,e0e1,g6g5,a0b0,b9a7,b0b4,g7f7,b4b7,f7f8,c3c4,c6c5,c4c5,f8d8,b7c7,d8d4,c7e6 +e821cb08;a0b2,b7d7,d0e1,d9e8,b2b5,d7d0,a3a4,a6a5,g0e2,d0d7,b5d5,b9c7,d5a5,h7i7,a5b7,a9a4,b7c9 +65a34904;a0c1,e9e8,c1b1,h7h1,g3g4,h1h3,b1b2,e8d8,g0i2,c9a7,b2b3,d8d7,b3a5,h3h1,a5b5,g6g5,b5b7 +d4d33938;f0e1,h7h0,g0i2,h9g7,a0b0,g7e8,b0b9,a9a8,a3a4,h0h8,g3g4,a8d8,e1d2,i6i5,a4a5,h8h6,b9c9 +fe72f619;c0e2,h7h1,i3i4,b7b0,d0e1,h1h8,c3c4,b0b7,a3a4,e6e5,e2g4,h8h3,a0a6,b7b5,a6a5,h9i7,a5e5 +44ec1d93;e3e4,h7i7,a0a2,b9c7,a2b0,b7b8,b0b7,a9b9,e0e1,i7f7,b7b8,c7a8,e1d1,h9i7,b8d8,d9e8,d8c8 +7de89ebd;g0i2,i9i8,i3i4,e9e8,d0e1,h7e7,a0b2,i8i9,e1d2,b7b5,i2g0,b5g5,b2b8,e8e9,b8i8,g5h5,i8c8 +49ccccae;e3e4,h7h6,a0a2,e9e8,a2d2,b7b2,g0i2,b2b1,d2d4,h6h1,d4b5,e8f8,b5g5,d9e8,c0e2,b1a1,g5f3 +550a072d;e0e1,a6a5,a0a5,h7d7,g3g4,i9i8,a5g5,i8a8,c0a2,d7f7,g5i4,f7i7,a2c4,b7b2,i4i6,b2b0,i6e6 +0dcb8f8c;a0b2,h9g7,b2b0,i9h9,g3g4,h7h8,b0b9,h8h0,b9b7,h0h2,b7c9,e9e8,c3c4,i6i5,c0a2,a9a7,c9c8 +0f74d025;a0c1,b9c7,d0e1,h7h3,e1d0,h3h5,c1b3,h5d5,b3b7,d5f5,g3g4,f5f8,g0i2,c7b9,c0a2,c9a7,b7e7 +5d7c2d80;a0b2,g9e7,b2b6,i6i5,b6a6,a9a7,a6a7,h7i7,c0e2,e7g9,a7a4,b7d7,a4g4,d7d1,g4g6,i5i4,g6g8 +ae8fd1ef;e3e4,i6i5,c0a2,c9a7,a2c4,b7b0,d0e1,b0b8,a0a2,h7d7,g3g4,d7d0,g4g5,a7c5,a2a6,c5a7,a6e6 +91a762a2;c0e2,g6g5,d0e1,b7b3,g3g4,h7f7,g4g5,b3e3,a0d0,i6i5,d0d3,f7f5,g5g6,f5f3,d3d8,f3f1,d8e6 +95533f0b;g0e2,h7h5,a3a4,h5a5,c3c4,b7b4,c4c5,a9a8,a4a5,b4b6,a0a2,b6b8,a2b4,b8b5,b4f4,b5b3,f4e6 +2849fc72;a0b0,b7b1,e3e4,h7i7,b0b9,i7i8,c0e2,b1g1,b9b3,g1b1,a3a4,c6c5,g3g4,e6e5,b3c5,b1b5,c5e5 +0ed0fe70;a0a6,h7h3,a6c5,h3h8,c0e2,h8g8,a3a4,b7h7,c5i5,e9e8,i3i4,h7h8,i5g5,e8f8,e2c4,f8e8,g5g8 +be9bedcc;c0e2,b7b1,e2c0,h7h2,a3a4,h2h5,g0e2,h5d5,a0b2,d5d4,f0e1,d4e4,e2g4,e4c4,g4i2,i9i7,a4a5,c4c0 +8b967631;e0e1,f9e8,a0a1,h7h4,c3c4,b7b1,a1a0,b1b5,e1d1,h4d4,g0i2,c6c5,c4c5,b5b3,f0e1,b3c3,c5b5,c3d3 +cd900632;e0e1,f9e8,a0b0,h7h5,e1e0,h5h3,g0e2,i9i7,d0e1,b7c7,e3e4,i7d7,e2g4,e9f9,c0a2,d7i7,b0c2,h3h0 +b7f3baa0;c0a2,h7h8,g0e2,a9a8,e2g4,h8e8,g4e2,i6i5,a0c0,b7a7,d0e1,i9i8,c0b2,i8i7,e2c4,a7h7,b2d3,h7h0 +757b6377;a0a6,b7e7,a6c5,e7c7,c0e2,h9g7,c5e5,g7e8,e3e4,h7e7,e5c4,e7g7,g3g4,a9a7,d0e1,a7a3,c4d6,g7h7,d6c8 +6dbff811;a0a6,e6e5,c3c4,b7b3,a6c5,i9i8,c5b5,i8f8,b5c7,h7h4,c7i7,b3g3,c4c5,g6g5,i7i9,g3g1,i9i6,f8h8,i6e6 +3383a163;i3i4,b7d7,g3g4,d7d3,g0e2,d9e8,a0b2,h7a7,b2b5,a7h7,b5a5,e8d9,a5c5,h7c7,c5g5,i9i8,g5c5,d3a3,c5e6 +1dd1cf90;g0e2,h7d7,c3c4,d7d5,c0a2,d5b5,c4c5,i6i5,c5c6,b7d7,a0b0,d7d6,b0b5,h9i7,b5b3,i7g8,b3c5,d6d7,c5e6 +df47926f;d0e1,b7c7,e0d0,h7h5,d0e0,c6c5,a3a4,d9e8,c3c4,h9i7,a0b2,i7g8,b2c2,h5h1,c4c5,g8e7,c5b5,e6e5,c2c9 +6d0f248a;g0i2,b7b2,c0a2,b2f2,e3e4,f2c2,a0b2,h7h4,b2b9,g9i7,g3g4,h9g7,b9b4,h4e4,a2c4,i9i8,b4e4,c9e7,e4e7 +b0c5dd71;a3a4,h7h2,a4a5,b7g7,a0a3,g7h7,g3g4,h2d2,a3a6,a9a7,g0e2,h7h3,e0e1,a7f7,e3e4,d2d5,a5b5,f7f2,a6e6 +3bd0dee1;c0e2,b7b4,a3a4,h7h6,f0e1,b4g4,a0b2,h6h8,i3i4,g4c4,b2d2,h8c8,d2c4,e6e5,e0f0,c8i8,e3e4,f9e8,c4c9 +f38133f6;c3c4,h7i7,a0a1,i7f7,g3g4,b7b1,e3e4,c9a7,a1a6,e9e8,a6b4,e8d8,b4b9,a7c9,b9b6,e6e5,b6c6,d8e8,c6c8 +ccf1032b;c0a2,b7b8,a0c1,e9e8,f0e1,h7i7,c1d3,i7f7,d3d4,f7f5,e3e4,g9e7,d4c4,g6g5,c4b6,a9a7,e1d2,a7d7,b6c8 +b8508566;c0a2,e9e8,c3c4,a9a7,a0b2,b7g7,e3e4,g7g3,b2f2,e6e5,f2f1,c6c5,a3a4,h7g7,f1f4,c9e7,f4f8,e8e9,f8g8 +b507526e;g0e2,e9e8,c0a2,e8d8,e2g4,b7e7,a0c0,e7g7,a2c4,d8d7,c0c2,g9e7,c2e2,a6a5,e2c1,h7h2,c1b3,g7g9,b3b7 +5af8c7c0;d0e1,h7d7,e1d2,b7b1,c0e2,d7d8,a0b2,b1f1,i3i4,c9a7,b2b8,f1f7,b8b7,f7d7,b7a7,e9e8,a7a6,d7c7,a6e6 +8fdc709f;a0a2,b7b8,a2f2,a9a7,f2f4,b8h8,f4f8,h7c7,c3c4,a6a5,d0e1,h8i8,a3a4,e6e5,c4c5,c6c5,f8f6,i8i3,f6g8 +aac70183;f0e1,g9i7,e0f0,b9c7,a0a6,b7b1,i3i4,h7h5,a6c7,a9a5,c0e2,a5a8,a3a4,b1b5,e1f2,a8c8,c7a8,c9a7,a8c8 +8fa918af;g0e2,b7e7,a0c1,e7f7,c1d1,c9a7,d1d4,f7b7,d4c4,i9i7,c4a5,b7b4,a5a7,h7h2,a7a9,g6g5,a9a6,e9e8,a6e6 +79b2f753;a0c1,h7h6,c1c6,b7i7,f0e1,i7i3,g0i2,h6h1,c6c4,g6g5,e0f0,h1h6,g3g4,i9i7,i2g0,i3f3,g0i2,f3f7,c4c8 +84ce9b8f;i3i4,b7d7,a0b0,h7h6,i4i5,d7g7,b0b6,g7g8,b6c4,e6e5,c4c6,g8g3,g0e2,g3g4,c6d4,g4f4,d4d6,b9c7,d6c8 +05adacdc;a0a2,h7i7,a2i2,a9a8,i2a2,b7f7,d0e1,e6e5,e1d2,a8h8,a2a6,f7f2,c0e2,h8e8,e2c0,e8i8,a6c6,f2h2,c6e5 +97f4edd9;a0c1,c9a7,c0a2,h7h1,c1d1,a7c5,f0e1,a6a5,d1d4,b9d8,d4i4,b7a7,i4f4,a9b9,f4h5,i6i5,h5f6,b9c9,f6g8 +e7216b98;g0i2,h7h6,a0a1,h6h3,a1a6,h3h5,i3i4,h5e5,d0e1,b7i7,e0d0,d9e8,i2g0,c9a7,i4i5,b9c7,c3c4,i9i8,a6a9 +21b3767b;g3g4,h7h4,a0c1,h4h8,g0i2,e6e5,c1b1,a9a8,b1d2,h8h4,d2d4,a8c8,d4d8,b7b8,c0a2,b8b1,d8c6,h4h7,c6e6 +2af866d1;g0i2,a6a5,a3a4,b7b5,e3e4,a5a4,i2g4,b5h5,a0c1,g9i7,c1i1,a4b4,i1g1,h5b5,g1e1,b5d5,i3i4,i7g5,e1e6 +0ded1ba4;d0e1,h9g7,a3a4,b7b0,c0a2,c6c5,a4a5,b9c7,g3g4,b0b9,a0c1,h7h6,c1c0,b9b8,c0d2,b8d8,d2f1,g7e8,f1f7 +abf74ab0;e0e1,h7h6,e1e0,i6i5,a3a4,h6h2,c3c4,h2h8,g0i2,h8h1,a0b0,b7g7,b0a0,h1f1,c4c5,f1f6,a0a6,g7h7,a6e6 +ff99aa4b;g0e2,h7h3,a0c1,b7c7,i3i4,c7g7,c1i1,b9a7,i1i0,h3h6,i0i6,g7g3,i6h8,i9i5,h8g6,g3g2,e3e4,i5i9,g6e6 +4d851f5c;f0e1,h7i7,g3g4,b7e7,c0e2,e7h7,g0i2,i7i3,a0c1,g6g5,c3c4,i3g3,e1f0,i9i7,e2c0,h7h8,c1d3,e6e5,d3e5 +e3ddfd1b;g0i2,h7g7,a3a4,i9i7,a0a6,g7c7,a6c5,b7b0,c3c4,a9a7,c5h5,a7a4,h5g5,c7d7,g5g6,a4a9,i2g4,i7g7,g6e6 +d5b42d2e;g0i2,h7h3,i3i4,e6e5,a0a6,b7a7,e0e1,a7a3,a6a8,i9i7,a8a4,i7g7,e1f1,b9c7,c0a2,a9a8,a4d4,e5e4,d4e4 +b3994544;a0a1,b7b3,a1g1,e9e8,g1f1,h7h8,f1c1,a9a8,c0e2,a8a9,c1c6,b3b2,g0i2,b9c7,c6c9,h8h2,d0e1,c7d5,c9c8 +c0080801;g0i2,h7f7,e3e4,f7f5,a0a6,f5i5,a6a4,e6e5,i3i4,a9a6,f0e1,b7h7,e1d2,d9e8,a4c5,h9i7,c5c4,e8f7,c4c9 +9e0e3606;a0b0,b7c7,b0b4,h9i7,b4b0,g6g5,b0a0,e9e8,i3i4,c7c3,a0a6,h7h0,a6b6,h0h4,c0e2,i7g6,i4i5,i9i7,b6e6 +3037d149;d0e1,b7b4,e1d2,i9i7,a0a1,b4b0,c0e2,g6g5,a1a6,e6e5,f0e1,h7b7,a6a9,i7i9,a9a4,b7c7,a4g4,g9i7,g4e5 +62084464;g0i2,b7b4,i2g0,b9a7,a0a2,h7h5,f0e1,h5h2,e0f0,a7b9,a2a0,b4g4,a0c1,h9g7,e3e4,h2f2,c1c6,g7h9,c6e6 +a262162d;g0i2,h7h2,a0a1,g6g5,f0e1,h2h5,i2g4,b7b6,a1a6,i9i8,a6a5,h5i5,e3e4,b6b2,a5a6,i8i7,e1f0,i7b7,a6e6 +1d500592;a0a6,i9i8,a6a4,b7b5,a4h4,i8a8,g3g4,h7h8,h4i2,b5d5,i2b2,h8h6,b2c2,d5d7,c2c6,a8d8,c6d8,h6h8,d8g8 +68c1f152;a0c1,b7g7,c1b3,g7g8,g0e2,g8d8,b3b7,c6c5,b7b1,g6g5,b1e1,h7c7,c3c4,i9i7,a3a4,a6a5,e2g0,d8d5,e1e6 +432559a1;i3i4,i9i8,a0b2,h7i7,b2b0,c6c5,b0b1,e9e8,b1b4,e8d8,i4i5,i7e7,b4a6,i8i7,d0e1,g6g5,g0e2,i7h7,a6b8 +dfe76548;a3a4,b7b4,f0e1,h7a7,e1f0,a7a4,g3g4,g6g5,a0a6,a4a3,g4g5,a3a4,a6c6,a4a8,g0i2,b4b2,c6d8,a8c8,d8c8 +8afb6a7b;g3g4,h7h8,a0b2,b7h7,b2g2,h7c7,i3i4,a6a5,g2e2,i9i8,e2c2,h8h3,c2b0,i8a8,b0b6,c7a7,f0e1,h3i3,b6e6 +7ecf6e75;g3g4,h7h8,d0e1,b7e7,a0a1,e6e5,a1b1,h8e8,c3c4,c6c5,b1a1,g9i7,a1a6,e8b8,i3i4,e9e8,c0e2,e7e6,a6e6 +03f5b8d5;g0i2,g6g5,c3c4,h7h8,g3g4,g9e7,i2g0,e7g9,e0e1,b7i7,c0e2,h8h3,e3e4,e9e8,e1f1,h3h5,a0a6,b9a7,a6e6 +d360b756;a0c1,e6e5,c0e2,c6c5,c1e1,h7h6,g3g4,c9e7,e1g2,h6h8,g2g3,h8i8,g3g6,b9d8,g6g7,b7b3,g7i7,a9b9,i7g8 +fd9dd727;a0b2,h9i7,b2b3,e6e5,g0i2,a9a8,b3b7,h7h9,c0e2,h9h0,f0e1,b9c7,b7b3,h0i0,b3b9,i6i5,b9c7,a8f8,c7e6 +7a2d3c1d;a0a6,b7b5,e3e4,h7h4,g0e2,b5c5,a6c6,i6i5,c6g6,h4h6,d0e1,c5h5,g6h8,h5a5,e2g4,i5i4,h8f8,a5a8,f8e6 +28517526;g0i2,i9i8,a0c1,i8f8,i3i4,f8h8,c1b1,b7d7,b1f1,c6c5,i2g0,e9e8,g0i2,d7g7,e3e4,h8f8,f1f8,e8e9,f8e6 +c08fc555;a0b2,b7c7,b2a2,g6g5,f0e1,e6e5,e1f0,h7h6,e3e4,c7g7,a2e2,h9i7,e2g1,h6h9,f0e1,c9a7,g1f3,b9c7,f3e5 +7688daae;g0i2,c6c5,a0a6,b7b4,a6c6,h7d7,c6a6,d7f7,a6a9,g6g5,e0e1,b4a4,i2g4,f7f8,c0a2,f8g8,a9a6,g8a8,a6e6 +ff95a6f0;a0b0,b7a7,e0e1,d9e8,g0e2,a7c7,e1e0,c7g7,b0a0,h7h5,i3i4,h5h7,a0a2,g7f7,a2a6,h7h5,a6a9,i9i7,a9c9 +6474f910;c0a2,h7d7,g3g4,d7h7,a0c1,g6g5,c1e1,h7h0,e1b1,b7h7,b1b5,h9g7,b5e5,f9e8,a3a4,h0h2,e5c5,h7h6,c5c9 +51ded51e;g3g4,c9e7,i3i4,h7h4,g0i2,h4h7,a0c1,h7h5,g4g5,a9a7,c1c6,e9e8,c0e2,b7b0,e2c0,a6a5,e3e4,h5h4,c6c8 +0a66beae;g3g4,b7b6,i3i4,h7h4,d0e1,b6b7,a3a4,e9e8,e1f2,e8d8,g4g5,b7b8,f0e1,g6g5,a0b0,f9e8,b0b3,h4e4,b3b7 +fd6f0f0d;e3e4,h7h3,a3a4,h3h7,e0e1,g6g5,c3c4,b7a7,a0a6,a7a8,e1e2,h7h5,a6c6,b9a7,c6d6,h9i7,c0a2,a7b5,d6c8 +65cb3bda;g0e2,h9i7,e2g4,b7b8,a0b2,a9a7,f0e1,b8i8,b2b4,h7h6,b4a6,i8b8,e1d2,c6c5,a6g6,a7a9,c3c4,b8d8,g6e6 +b7ebf330;c0e2,b7b3,e2c4,b3b0,e0e1,b0f0,a0b2,c6c5,e1e0,h7b7,b2d3,b7a7,d3d6,c5c4,d6b6,a7h7,b6a6,f0f6,a6e6 +99224138;e3e4,b9c7,a3a4,h7h3,g3g4,e9e8,a4a5,e8f8,a0c1,h3f3,c1c2,f3g3,a5b5,g3g1,c2b2,f9e8,b2i2,b7b6,i2f2 +fdc311ef;a3a4,b7b3,a0a3,h7h1,e3e4,b3b8,c0e2,h1h0,a3b1,e6e5,b1b9,b8b1,b9b8,g6g5,b8a8,b1g1,c3c4,g1h1,a8g8 +05a95c1f;a0b0,b7b5,b0b2,h7f7,g3g4,b5f5,b2c4,f5c5,c4a5,f7e7,i3i4,e6e5,a5a4,a6a5,a4f4,e7c7,f4e2,e5e4,e2e4 +ede0411f;i3i4,b7b5,g3g4,b5f5,c3c4,h7h2,a3a4,f5i5,a0b0,h2h5,b0b4,a9a8,c0e2,h5e5,b4b6,g9i7,e3e4,a8c8,b6e6 +329d0ce7;d0e1,a6a5,e0d0,h7h1,a0c1,b7a7,d0e0,a7a3,g3g4,a3a1,g0e2,a1b1,c3c4,h1g1,c1c6,g1h1,g4g5,e9e8,c6e6 +23184a9a;c0e2,h7h4,a3a4,b7i7,c3c4,c9a7,a0b0,h4d4,g3g4,c6c5,b0b4,a6a5,b4d4,i6i5,d4d8,h9g7,d8h8,i9h9,h8g8 +f5ccab42;c0e2,b7c7,a0a6,e9e8,a6a7,i9i7,a3a4,h7h6,a7a6,e8f8,a6a7,i7i8,a7a5,h6h3,e0e1,i6i5,a5d5,i8i9,d5d8 +1cee2de3;g3g4,h7e7,e0e1,h9i7,a0a1,e7d7,e1f1,g6g5,g4g5,b7b8,a1b3,i9i8,g5f5,d7c7,b3c5,i8f8,c5c6,b8b1,c6e6 +36e15a70;e3e4,b7c7,e0e1,i9i7,a0a1,c9a7,a1d1,h7h1,a3a4,b9d8,g3g4,c7e7,e1e2,a7c5,d1d8,e7f7,e2e1,a9b9,d8c8 +2c34dd3f;e0e1,b7a7,i3i4,h9i7,g3g4,a7b7,a0b2,e9e8,b2h2,b7b3,c0e2,e8d8,h2h3,h7a7,h3f4,g6g5,g0i2,b3b0,f4f8 +1b207c93;a0a2,b7b3,g0e2,i9i7,c3c4,e9e8,a2a6,c6c5,e0e1,b3b5,c4c5,h7h5,i3i4,b5b7,e1d1,i7h7,a3a4,b7b2,a6e6 +ac5c5fb9;f0e1,h7e7,g0e2,e6e5,a0b0,e7h7,c3c4,a9a7,e3e4,b7b1,e1f2,b1b8,d0e1,a7d7,e1f0,h7g7,b0b8,d7d1,b8g8 +3951cdea;f0e1,b7b0,a0b2,f9e8,a3a4,i9i7,e0f0,e8d7,b2i2,a6a5,e1d2,a5a4,i2i6,d7e8,i6g7,b9a7,g0e2,a9a8,g7g9 +f91375fc;a0a1,e9e8,a1a0,b7b4,f0e1,b4c4,e0f0,g9e7,e3e4,b9c7,a3a4,a6a5,a0a5,e8e9,c3c4,c6c5,a5a7,h7f7,a7c8 +0aab3a8b;e3e4,h9g7,a0b0,h7h6,b0b2,g7h9,b2b1,b7i7,b1i1,c6c5,i1i6,i7i3,i6i8,e6e5,e4e5,i3i2,i8g9,e9e8,g9g8 +c4ec94ad;c3c4,b7b5,a3a4,h9i7,g3g4,h7c7,a0a3,b5g5,e3e4,g5a5,a3e3,e6e5,e3d3,a5c5,d3d5,c7c8,d0e1,g6g5,d5e5 +6cc08f7a;e3e4,b7e7,a0c1,h7h3,c1d1,e7b7,a3a4,h3h5,d1b1,b7b4,c0e2,i6i5,c3c4,h5b5,b1f1,b5a5,f1e3,a9a8,e3e6 +debb5fd5;g0e2,b7b0,e2g4,b0b7,a0b2,b7b4,b2d2,b4b8,d2d1,h7i7,d1d2,e6e5,d2d7,b8d8,d7b6,i7g7,b6b8,d8d4,b8g8 +d3944c3a;i3i4,h7h0,a0a2,a9a8,a2i2,b7b3,i2i3,i9i7,i3h1,b3b5,c3c4,g6g5,e3e4,c9e7,h1h6,e9e8,c0a2,a8c8,h6g8 +5a7e4edf;d0e1,e9e8,c3c4,h7g7,a0c1,b7c7,c1d3,g7e7,d3e5,i9i7,e5c5,i7g7,e1f2,e8e9,c5h5,g7h7,h5f4,e7f7,f4e6 +153d3931;c0e2,b9a7,f0e1,a7b9,a0a1,c9e7,a1a6,i6i5,e0f0,h7h8,a6e6,f9e8,f0f1,a9a3,e6c5,e7c9,e1d2,b7b6,c5c9 +987c3059;a0c1,i9i7,c3c4,b7b0,c1c6,g6g5,c6a6,h7c7,a3a4,e9e8,g0i2,e8e9,a6a9,b0b7,a9a5,b7b8,a5g5,b8h8,g5g8 +d101b903;a0c1,b7b1,c1h1,b9a7,h1h0,e6e5,h0h3,h7f7,h3i5,b1b4,i5i6,c9e7,g3g4,e7g5,i6g6,b4b2,g6c6,b2d2,c6e5 +b5e57145;a0c1,b7e7,e3e4,e7c7,c1a0,e9e8,c0a2,e8d8,g3g4,h7h6,a0b0,i6i5,g4g5,c7e7,b0b3,e7e9,b3b6,i9i8,b6b8 +a84298a8;a0c1,i6i5,c1b3,h7c7,c0a2,c9a7,g0i2,h9g7,b3b5,c7e7,b5b9,e9e8,b9b8,e8e9,b8a8,b7b4,g3g4,b4b2,a8c8 +825afc35;d0e1,e9e8,a0a1,h7h0,a3a4,a9a7,a1a0,e8e9,a0a6,e9e8,a6c6,b7b5,c6a5,b5b1,i3i4,a7e7,a5a6,e7d7,a6e6 +596af5dd;f0e1,b9a7,e0f0,e9e8,e1f2,h7f7,f0e0,c9e7,a0a6,f7f3,c3c4,c6c5,f2e1,f3f7,c4c5,b7b8,a6e6,e8d8,e6d6 +03718042;a0a2,h7h3,a2c1,h9g7,c1b1,h3h9,e0e1,h9h2,c0e2,h2h8,i3i4,b7d7,a3a4,d7d4,b1d1,d4h4,d1d8,i6i5,d8g8 +476fcc8a;c0a2,b7b1,a0b0,h7h1,d0e1,h1f1,i3i4,b9a7,g0i2,f1f6,b0d0,f6f2,d0d7,e9e8,a3a4,b1a1,d7d5,e8f8,d5f6 +07028e83;c0a2,h7h2,c3c4,h2h0,e3e4,b7h7,a3a4,h0h2,a0a1,h2e2,a1a0,e2f2,g0e2,c6c5,a0b0,h9i7,b0b6,h7h9,b6e6 +f0d9d9aa;d0e1,h7h4,e0d0,b7c7,e3e4,h4h3,i3i4,a9a8,a0a1,h3h2,c0e2,a6a5,a1b1,g9e7,b1b4,d9e8,e2g4,c9a7,b4b9 +e71a6de5;g0i2,b7c7,a0a2,a9a8,c0e2,a8a7,a2d2,h7h8,d2d3,c7e7,d3d8,h8h1,d8e6,d9e8,a3a4,a7b7,e6c7,b7b3,c7c9 +79f2ce5b;a0a6,h7h3,a6a7,i6i5,g0e2,h3h2,e2c4,h2h5,c4a2,e6e5,i3i4,e5e4,a3a4,e4e3,f0e1,b7h7,a7a5,h7d7,a5e5 +4edcb5cd;c3c4,h7h0,a0a6,i9i7,e0e1,i7e7,a6a8,e7g7,a8b6,b7e7,g0e2,a9a5,i3i4,a5f5,c0a2,e7f7,e3e4,c6c5,b6e6 +d633224e;g3g4,b7c7,a0b2,h7h0,b2b0,a6a5,b0a2,h0h3,a2e2,h3h7,e2h2,c9e7,h2h4,e7c9,h4g6,c7f7,g6f6,h7h0,f6e6 +d88c87fc;a0a2,b7b8,c3c4,h7h5,f0e1,b8h8,a2c3,g6g5,c3c2,h5h2,c2h2,h8f8,h2h7,c6c5,h7f8,h9i7,c4c5,c9a7,f8e6 +678c9247;g0i2,h7e7,i3i4,b7d7,a0b2,d7d1,b2b0,g6g5,b0c2,e7a7,c2g2,e9e8,g2e1,d1d5,e1e2,a7g7,e3e4,d5d2,e2e6 +e8b8ca95;a0a2,h7h5,a2c2,b7b3,c2a2,d9e8,e0e1,c9e7,c3c4,b3b7,c0e2,e8d7,e1f1,h5h3,a2a6,b7a7,a6a9,e7c9,a9c9 +38f71cdd;i3i4,b7d7,g0e2,h7g7,a0a6,a9a8,a6c6,d7d1,c6a7,d1i1,e2c4,b9a7,c0e2,i1a1,e3e4,e9e8,f0e1,g7c7,e2g4,a1a0 +3af409e9;i3i4,h7h0,a0b0,b7b3,b0b1,b3e3,b1a1,h0h1,a1a0,h1h2,c0a2,h2c2,a3a4,i6i5,a2c0,h9i7,a0c1,i9h9,c1a0,c2e2 +9713dadf;f0e1,b7b1,a3a4,h7h4,a4a5,h4c4,e1f0,a6a5,a0b0,e9e8,g0i2,i9i8,i2g4,b1b3,b0a0,e8f8,a0a9,g9i7,f0e1,c4c0 +6fa2e35f;c0e2,g9e7,c3c4,b7d7,i3i4,i9i7,a0b2,e7g9,d0e1,f9e8,b2b0,c9a7,a3a4,h7h5,b0b4,d7b7,e2c0,h5g5,e3e4,g5g0 +7c3cdd43;a0b2,e9e8,d0e1,b9c7,b2b1,h7h5,b1b5,i9i7,e1f2,b7b8,e0e1,h5h8,c0a2,c7b9,b5b3,h8g8,e1f1,g8i8,f0e1,i8f8 +0722a770;a0a6,h7e7,a6c6,b7b6,c6c5,b9a7,c5d3,b6b9,e3e4,a7b5,g3g4,e7c7,i3i4,b5a3,f0e1,h9g7,g0e2,g9i7,e2g0,c7c0 +6a9a348d;a0c1,h7c7,g0i2,c7d7,c0a2,d9e8,a2c0,e9d9,c0e2,b7b4,c1b1,b4b2,e2c0,d7d3,b1i1,d3a3,d0e1,b2b0,c0e2,a3a0 +9a266fb5;a0c1,b7c7,c1d1,h7h8,d0e1,c7h7,d1c1,h8a8,c1c6,a8b8,i3i4,h9g7,c6b6,e6e5,b6b9,i9i8,g0e2,a9b9,e2c4,h7h0 +b683f1dd;c0a2,a6a5,g0e2,i9i8,a3a4,b7a7,g3g4,c9e7,e2c4,h7h2,a0c0,a7b7,c0c2,i8f8,g4g5,f8g8,d0e1,h2h4,c2b2,h4h0 +822cb0ff;g0i2,b7a7,c0a2,a7g7,a0b2,a6a5,e0e1,a9a6,b2b0,c9a7,a2c0,e9e8,e1e0,h7h2,c0a2,e6e5,b0c2,e8d8,c2c6,d8e8,c6c8 +b58b46bb;a0a1,g9i7,g3g4,b7b5,c0e2,h7h2,a1b1,h2h1,b1g1,i7g9,g1f3,b5c5,f3h2,i6i5,g4g5,c5a5,h2f2,e9e8,g5g6,e8d8,f2f8 +8b34f58b;i3i4,b7b0,f0e1,h7h6,a0b2,g6g5,b2h2,h6h4,h2f2,c6c5,i4i5,b0b6,f2d2,b6b1,d2e2,h4h2,g0i2,h2h1,g3g4,b1b6,e2e6 +aa047733;a0a1,a9a8,a1e1,b7b0,e1g1,a8g8,g1a1,h7e7,a1d1,g8g7,d1d6,b0b4,e3e4,h9i7,d6a6,e7a7,g3g4,b4g4,a6c7,g9e7,c7c8 +180ec699;a0c1,i9i8,c1c6,i8b8,c6a5,e6e5,g0e2,b8d8,d0e1,d8d2,e1d2,a9a7,e3e4,e5e4,d2e1,h7h3,a5g5,h3h4,g5g4,b7b5,g4e4 +1fe31a5c;a3a4,b7c7,a4a5,h7h8,a5a6,e9e8,i3i4,h8h0,a0a3,c7i7,a6b6,i7g7,a3a8,e8e9,a8c9,e9e8,c9b9,g9e7,b9a9,c6c5,a9c8 +43b76928;i3i4,b7b0,a0a1,h7h0,a1c1,a6a5,i4i5,a9a8,c1a2,b9a7,a2g2,g6g5,g2i1,h0h8,i1i0,g5g4,i0i6,a8c8,i6f6,e6e5,f6e6 +0fe585e5;a0c1,h7h0,c1d1,h9i7,g3g4,b7h7,d1g1,h7h6,g1i1,h0i0,i1g2,c9a7,e0e1,i9h9,g2g3,i0f0,g3g6,f0f7,g6c6,h6h1,c6e6 +9427ee3d;c3c4,h7h6,a3a4,b7i7,f0e1,i7g7,i3i4,d9e8,a0b2,e8f7,c0e2,g6g5,b2c0,i6i5,c0c6,h9i7,e1d2,h6h4,e2g4,g7g6,c6c9 +73b8f64b;a0a2,b7g7,e0e1,i9i7,e1e0,g7d7,a2a1,d9e8,a1a2,h7h8,a2b2,a9a7,b2b9,d7d9,g0i2,h8h7,c3c4,a7e7,e0e1,h7h1,b9c9 +dabff7c2;g3g4,h7h8,c0e2,h8h5,a0a6,b7h7,a6b4,h5e5,b4d3,h7h2,d3c5,d9e8,i3i4,h2f2,e0e1,e8f7,a3a4,f2f6,e1d1,a9a5,c5e6 +0b4f729c;a0c1,h7e7,c1a1,e7e3,c0a2,c6c5,a2c4,b7d7,a1f1,d7f7,f1h1,e9e8,e0e1,a6a5,h1h3,f7g7,e1d1,e8e7,h3h9,e3e2,h9g7 +68179a85;a0a6,b7b1,a6b4,h7f7,b4h4,g9i7,h4h3,b1a1,f0e1,a9a7,e1d2,f7b7,g3g4,i6i5,h3g1,a1a0,g1f1,i7g9,f1f6,b7b3,f6g8 +c58850ab;a0b0,b9c7,a3a4,b7b9,b0b4,d9e8,b4h4,c9e7,h4h9,h7f7,c0a2,i9i7,h9h1,f7f5,h1h9,e7g5,e0e1,i7h7,g0e2,b9b5,h9g9 +f3dd9e9e;f0e1,h7h6,c0e2,i6i5,e0f0,b7d7,a0b2,g6g5,b2b9,d7g7,b9b7,g9i7,e2c0,c9a7,e1f2,h6h0,g0e2,a7c9,b7a5,e6e5,a5e5 +552e9df5;a0a2,c6c5,a2a6,b7b1,a6b8,c5c4,b8a8,b1b8,g0i2,i9i7,a8b8,c4c3,i3i4,h7h0,f0e1,h9g7,i2g4,h0h8,c0e2,h8c8,b8c8 +064e3b7b;i3i4,b9c7,a3a4,a9a7,g0i2,g9e7,a0a6,a7a9,a6a9,g6g5,i2g0,e7c5,e3e4,e6e5,a9a5,c7e8,a5e5,c5e7,c0a2,h7h6,e5d7 +27218af7;a0a1,h7g7,a1b1,e6e5,b1b4,e9e8,b4f4,b9c7,i3i4,g7g3,f4f6,e8d8,c0a2,g3g2,d0e1,h9i7,g0e2,b7b1,a3a4,b1b8,f6d6 +fd226fc1;c3c4,h7i7,a0a1,i7i8,f0e1,b7g7,g3g4,g6g5,a1c1,i8h8,e3e4,g5g4,c1a0,h8h3,a0b0,g7h7,b0b8,a6a5,b8h8,a5a4,h8g8 +2f96710c;c3c4,c9a7,i3i4,f9e8,d0e1,h7h1,a0a6,c6c5,e3e4,b9d8,a6c6,i9i7,e0d0,h1i1,c6a7,d8c6,g3g4,i7i8,a7b7,h9i7,b7a9 +a018dba3;g0i2,h7h8,c3c4,b7i7,a0a2,i7b7,a2c2,b7g7,c4c5,b9a7,c2c1,h8d8,c1d3,d8a8,f0e1,i9i8,d3d4,g7f7,i2g4,f7f5,d4e6 +50c6cc92;f0e1,b9c7,g3g4,g9e7,e3e4,h7h6,g0i2,b7b4,c0e2,b4b1,a0c1,e9e8,e1d2,e8d8,c3c4,b1b3,c1a2,c7a8,a2b2,f9e8,b2d3 +986326f4;i3i4,a6a5,i4i5,b7b6,a0a5,c6c5,a5c4,h7h6,c4a4,b6b3,g3g4,f9e8,i5i6,c9a7,a4c4,e8f9,c4e4,g9e7,f0e1,h6h7,e4e7 +2a268a30;e3e4,b7b3,a0a2,h7e7,g0i2,b3b4,c0e2,e7c7,e2g0,h9g7,a2d2,g9e7,f0e1,e7g9,d2c4,a9a7,c4c7,a7a9,g0e2,g9i7,c7c8 +fa19eef2;e3e4,g6g5,g0i2,b7e7,a0a1,e7e8,a1c1,i9i8,i2g0,e8a8,i3i4,i8b8,c1d3,h7f7,i4i5,f7b7,g0e2,e9e8,d3d6,e8e9,d6e6 +d1dfb362;g0i2,i9i8,i2g4,h7h6,a0a6,e9e8,a6b8,e8e9,c0e2,h9g7,b8b7,h6h9,b7b4,a9a4,b4f4,i8c8,f4f8,a4a9,f8h8,a9a6,h8c8 +a91694b8;a0a1,h7g7,a1g1,a9a8,g1i0,b7c7,i0g1,e6e5,g1a1,c7c8,a1c1,g7g3,c1b3,c6c5,e3e4,c8b8,b3c5,g3d3,g0e2,e9e8,c5e6 +f603251f;g3g4,h7g7,g0e2,h9i7,c0a2,g7f7,a0b2,c9a7,b2b5,b9c7,b5c7,f7f5,e0e1,d9e8,a2c4,f5f6,i3i4,a9a8,e1e0,b7b8,c7b9 +2b403935;e0e1,h7i7,a0a6,b7a7,g0e2,a7d7,a3a4,a9a8,i3i4,d7e7,a6a7,i7i8,a7b7,d9e8,b7a5,e7i7,e1e0,b9c7,a5g5,c7a6,g5g9 +4c436e46;c0e2,h9g7,e0e1,i9i8,a0a2,b7b3,e1f1,h7h3,i3i4,b3b0,a2a6,h3h4,g3g4,g6g5,a6a9,b0b8,e3e4,b8b7,a9c9,e9e8,c9c8 +abd4d1fa;i3i4,b7b5,i4i5,h7d7,g0e2,d7d5,a0b0,b5a5,i5h5,c6c5,b0b6,i6i5,b6b5,d5e5,b5c5,h9i7,e3e4,a9a7,e4e5,a5b5,c5e6 +d56ae7c9;a0a2,h7d7,a2c2,b7c7,c2e1,i6i5,e1d3,d7g7,d3e1,g7e7,e1f3,a9a7,f3e5,i5i4,e5c6,c7c3,g0e2,e7g7,c6g6,f9e8,g6g9 +c2c30843;a0a2,g6g5,a3a4,b7f7,a2c2,f7f8,c2a3,i9i8,g0e2,a9a8,d0e1,h7h5,c0a2,f8h8,i3i4,a8g8,a3c4,i8i7,c4c9,e9e8,c9c8 +07c99bed;a0a1,b7a7,d0e1,a6a5,a1b1,a5a4,b1d0,h7h6,e3e4,h9g7,d0d5,g7e8,d5a5,e8g7,a5c6,a7a3,c6g6,c9a7,g6f6,b9c7,f6g8 +6c0fc609;a0b2,b9c7,g0e2,h9g7,d0e1,c9e7,c0a2,e7g5,c3c4,g5i7,a2c0,h7h9,e1d0,g7i8,e2g4,a6a5,i3i4,c7e8,b2d1,g9e7,d1d7 +0a0fa5b6;c0a2,b7e7,a0c0,i9i7,f0e1,h7h4,e3e4,h4h1,a3a4,h1h7,c0c6,h7h0,g0i2,g6g5,c6e6,f9e8,i3i4,i7i8,e6c5,i8i9,c5c9 +3c3b6f15;a0a1,d9e8,i3i4,i9i7,a1a2,b9c7,a2a0,e6e5,a0b2,g9e7,b2b7,h7h4,i4i5,h4h0,b7b9,c7d9,b9b2,c6c5,b2b6,h0i0,b6c8 +656af52b;c3c4,b7b0,e0e1,b9a7,a0a6,h7h3,a6b4,g9e7,e1e2,a7b9,b4a6,c6c5,g0i2,e7g9,a6c7,b0b5,e3e4,b5b4,c7a7,h3h2,a7c8 +85664cde;i3i4,d9e8,a0a6,c9a7,g0i2,h7e7,i2g4,b7b8,a6a9,e8d9,a9b7,e7i7,c0a2,i7d7,b7c7,g6g5,g4i2,d7f7,i4i5,f7f8,c7e7 +dc5e0a25;a3a4,h7i7,d0e1,b7b0,c0e2,e6e5,e3e4,i6i5,a4a5,d9e8,a0a4,i7e7,a4c5,a9a7,e0d0,i5i4,e2g4,i9i6,e1f2,h9i7,c5c9 +294ea0cd;a0a1,h7h0,c0e2,i9i7,a1b1,h0h5,b1b3,h5g5,b3b6,i7c7,a3a4,c7d7,f0e1,g5b5,g0i2,d7f7,b6b7,f7d7,e2c4,d7d8,b7a9,b5b0 +7f19ffa0;i3i4,a9a7,e3e4,b7b1,f0e1,h7i7,e4e5,b1b4,i4i5,c9e7,a0b2,d9e8,b2d2,b4b5,d2f1,b5c5,i5h5,i6i5,f1f0,i7h7,f0f1,c5c0 +19558024;d0e1,b7b6,c0a2,h7a7,g3g4,c6c5,i3i4,b6b3,c3c4,h9g7,i4i5,g9i7,i5h5,b3b2,g0i2,a6a5,a0b0,a9a8,b0c2,b2i2,e3e4,i2i0 +4680b13c;e0e1,b7b2,c0e2,d9e8,e1e0,h7h2,e2c0,b2b5,g3g4,h2h5,g0i2,c9e7,i2g0,h5g5,a3a4,a9a7,d0e1,b9c7,i3i4,b5b0,a0b0,g5g0 +a55aa84f;g0i2,b7b6,f0e1,h7h3,a3a4,a9a7,a0a2,a7f7,a2a1,d9e8,a1a2,e8d7,e1f2,h9g7,e0f0,f7e7,g3g4,b6b8,f0f1,e9d9,d0e1,b8f8 +eca0b48c;a0a6,b7b3,a3a4,c9e7,d0e1,b3e3,e1d0,h9i7,a6c7,i9h9,c7a8,h7h3,a8h8,h3h1,h8h2,b9d8,h2h5,h1h2,i3i4,h9h8,c3c4,h2e2 +489f22ad;i3i4,h7h0,d0e1,b7g7,a0c1,h0h5,c1e2,h5a5,e2d2,b9c7,g3g4,a9a8,d2b2,c7b9,b2b1,a5g5,b1d1,a8a9,d1b2,g9i7,b2i2,g5g0 +474943c9;g0e2,b7b1,e3e4,b1b3,e4e5,h7h5,c3c4,d9e8,d0e1,h5h3,e1d2,b3b1,a0a1,b1b6,a1h1,b6b8,h1c1,c6c5,d2e1,b8b3,e2g4,h3h0 +b645618a;e3e4,a9a7,a0c1,h7f7,c1a1,f7d7,a1a0,i9i7,c0a2,d7h7,g0i2,a7a8,a0b0,f9e8,f0e1,b7b6,b0b9,e8f7,b9d9,e9d9,a2c4,a8g8,i2g4,b6b0 +88f01386;c0a2,i9i8,g3g4,b7c7,a2c0,i8i9,a0b2,i9i8,b2c4,i8i9,d0e1,h7h1,c4b6,h1h6,b6b4,h9i7,g4g5,a6a5,a3a4,c7c3,g0e2,c3c2,b4b2,e6e5,e2g4,h6h0 +9718905a;a0a6,h7h8,a6c7,b9a7,c0a2,h8c8,g0i2,i9i8,c7e8,e9e8,e0e1,c8c3,g3g4,c3d3,e1e2,i6i5,i2g0,e8d8,e2d2,a7b5,d0e1,a9a5,i3i4,c6c5,g0e2,b7d7 +4907705b;a0c1,e9e8,c1i1,b9a7,f0e1,b7b9,i3i4,b9b1,i1g2,a9a8,g2b2,a8c8,b2a2,h7h2,c0e2,e8d8,e2c4,h9g7,e3e4,h2i2,i4i5,d9e8,a2b2,e8d7,b2i2,b1b0 +eeee8615;c0e2,g6g5,g3g4,i9i8,e0e1,a9a8,g4g5,i8d8,g0i2,b7b3,e2g0,a6a5,a0a5,b3b7,e1e0,b7b6,a5a6,h7h5,g5f5,d8c8,f5f6,h5e5,f0e1,a8a9,a6a7,b6b0 +78186d4c;a0a2,b7b8,a2b0,b8b3,c0e2,c9e7,e0e1,e7c9,e1d1,b3b6,b0b2,b6b5,b2b0,h7i7,f0e1,h9g7,b0b3,b5c5,e1d2,e6e5,d0e1,g7e8,b3c1,g6g5,c1b1,c5d5 +06dab82d;f0e1,b7b2,a0b2,a9a8,i3i4,a8c8,b2d1,i9i7,d1d4,h7h8,e0f0,c8a8,d4d6,i7c7,d6d9,e9d9,f0f1,a8b8,e1f0,b8b1,f0e1,h8f8,e3e4,c9a7,a3a4,c7f7 +232470e7;a0a6,c9a7,c0a2,b7b8,a6a4,h7h3,g3g4,d9e8,a4b6,h3h0,b6b4,h0h5,e3e4,e8d7,b4c6,h5h3,a2c4,b8b4,c6a6,b4b7,a6a9,e9e8,f0e1,i9i8,g0i2,b7b0 +2d6b6941;a3a4,c9a7,c0e2,h7h2,a0a2,b7g7,a2a3,g7b7,a3a2,h2a2,g0i2,a2b2,g3g4,b7b6,e3e4,e9e8,i2g0,g6g5,f0e1,b6b8,e0f0,b2b4,e2c4,b4b7,f0e0,b7b0 +eedc51f6;a0a6,h9i7,e3e4,h7c7,a6a7,g6g5,c0a2,c7e7,g0e2,b7b1,c3c4,e7d7,a7a4,b1e1,a2c0,d7e7,a4b4,e1e4,f0e1,i9h9,b4a4,c6c5,a4a8,e7c7,a8a4,h9h0 +f1f95897;c3c4,b7b6,i3i4,h7h4,a0c1,b9c7,g0e2,i9i7,f0e1,b6b9,e2g4,h4h5,g4e2,i7g7,c0a2,b9b8,c1a1,g7f7,e2g4,f7i7,a1d1,h5a5,d1d4,b8b3,a3a4,a5a2,d4c6,b3b0 +b0f5d85d;g0e2,b7b8,e3e4,b8b4,g3g4,g9e7,g4g5,h7h6,e2c4,c6c5,a0a6,b9c7,a6a5,d9e8,e4e5,b4b5,e0e1,a9a5,e1e0,i9i7,d0e1,b5b6,c0e2,b6a6,e2c0,i6i5,e5f5,h6h0 +997a3582;i3i4,h7c7,g0e2,b7b0,i4i5,c7h7,a0b0,g6g5,i5h5,i9i7,h5i5,h7h5,i5i6,a6a5,b0b2,c6c5,e2c4,a9a8,b2b1,h5h8,c4a2,b9a7,d0e1,a8e8,a2c4,h8h4,i6i7,h4h0 +77060a89;g0e2,e9e8,a0a6,a9a8,e2g0,b7b3,a6a8,e8e7,a8c7,b9c7,g0i2,e6e5,a3a4,c7e8,i3i4,e5e4,g3g4,h7i7,c0e2,b3b5,e2g0,e4e3,f0e1,e7f7,i4i5,f7e7,i5i6,b5b0 +2858490c;g0e2,h7f7,a3a4,c6c5,a0a2,g9i7,a4a5,f7f6,a2a6,b7f7,a6c6,i7g9,c6b8,f7h7,e0e1,h7f7,b8b4,h9g7,b4a4,f6f5,a4a2,f7b7,e1f1,c9a7,d0e1,c5c4,a2d2,b7f7 +681230a2;a3a4,h7h1,g0e2,h1h3,a0a1,i9i8,a1a6,i8h8,a4a5,c9a7,a6a9,b7f7,e0e1,g9e7,c3c4,h3h0,a9b9,a7c9,b9b1,h8h3,b1c3,i6i5,c3b3,f7h7,a5b5,f9e8,b3b4,h3h1 +3a0e9b5f;a0a2,b7b6,a2g2,h7h0,g2i2,h0h5,i2c2,c9a7,c2b2,h5h6,b2b1,h6h8,g0e2,b6b3,g3g4,h8h1,b1c1,i6i5,c1g1,c6c5,e2c4,a6a5,g1a1,i9i6,a1a5,b3e3,a5i5,h1h2,i5i6,h2e2 +a09507aa;g0i2,b7b2,i2g0,h7h6,d0e1,b2a2,a0a1,b9a7,a1a0,g9e7,e1d2,e9e8,a3a4,h6h1,e0e1,h1h2,e1e0,h2g2,d2e1,e8f8,c3c4,a2e2,g0e2,i9i8,e3e4,f8e8,i3i4,a7b9,e2g4,g2g0 +94b33428;a3a4,a9a7,a0b2,h7h4,b2d1,g6g5,e0e1,b7b5,d1b0,h4i4,e1e2,c9e7,b0b2,i4b4,b2d2,b5f5,d2b2,b4e4,e2f2,f5f6,f2f1,a7b7,b2d2,e4h4,f1f2,b7b4,d0e1,g9i7,d2e2,h4f4 +5ccb98a5;e3e4,b7b5,d0e1,h7f7,a0c1,b5c5,c1b3,e6e5,c0a2,c5c4,e4e5,f7i7,b3b4,i7f7,a3a4,c4e4,e1d0,f7b7,b4d3,c9a7,d3f2,g9e7,f2f8,a7c9,f8i8,c9a7,g0i2,b7b2,i8a8,b2e2 +e854fdbb;a0a1,a9a7,a1b1,b7b3,b1i1,h7i7,e0e1,a7d7,g3g4,e9e8,i1i0,d7d6,i0h0,b3b5,e1f1,b5f5,h0h4,f5a5,h4h1,d6d0,h1f2,d0d2,f0e1,d2e2,e1d2,a5f5,f2i2,f5f4,c3c4,i7f7 +fa35359c;a3a4,b9a7,g0i2,h7h3,a4a5,b7b1,f0e1,b1c1,c3c4,d9e8,a0a1,i6i5,a1a6,c9e7,c0e2,i9i7,a6c6,h3e3,c6c8,e9d9,c8a9,d9d8,i3i4,a7c6,i2g4,c6b4,a9g9,e3e4,g4i2,c1c0 +bdd4f73a;a0b0,g9e7,b0b1,h7h0,b1b6,h0h2,f0e1,h9f8,g0i2,h2h8,b6b3,b7a7,b3b7,h8h4,b7b9,h4c4,b9b5,e7g5,e1d2,c6c5,d2e1,i6i5,b5g5,a7b7,g5i6,e9e8,i6g5,i5i4,g5i4,c4c0 +b73a4c8a;a0c1,h7h6,e3e4,b7e7,i3i4,h6h8,c0a2,e7d7,e4e5,h8h6,e5e6,d7d6,c1i1,h9g7,i1b1,a6a5,b1b4,h6e6,b4d3,a9a7,c3c4,g9e7,d3f4,d6d4,f4f7,e9e8,f7f8,e8e9,f8h7,d4e4 +c7ead0b4;c0e2,h7h0,c3c4,c9e7,g3g4,b7b8,e0e1,c6c5,e1d1,b8b5,a0c0,g6g5,d1d2,h0h8,d0e1,b5b3,c0c3,i9i8,e1d0,i8i7,c3c5,e7c9,c5e5,h8e8,e5e8,d9e8,g4g5,i7g7,f0e1,g7d7 +10cd3285;i3i4,b9a7,c3c4,b7e7,a3a4,h7h2,a4a5,h2h5,a0a4,i6i5,a5a6,g6g5,c0e2,i9i7,e3e4,h5h3,g0i2,h3h7,e2g4,e7g7,g4e2,i7i9,a4a7,h7h5,e0e1,i9i6,a6b6,h5h0,a7b9,a9a1 +61f7fc94;a0a1,i6i5,a1a6,e9e8,a6a4,e8e7,a4a5,a9a8,a5d5,e7e8,g0i2,h7h8,d5e5,e8f8,e5e4,b7a7,e4g5,a7e7,g5g4,e7e3,g4i4,a8e8,i4h4,e3e5,h4a4,e5e4,a4a8,e8a8,i2g0,h8h5,i3i4,h5e5 +5e1a0e96;a0c1,b7d7,c1i1,d7f7,i1a1,h7g7,a1a6,f7b7,a6a7,g7c7,f0e1,b9a7,c0e2,b7b6,e1d2,c7g7,d2e1,b6b5,e1d2,b5h5,e0e1,h5c5,c3c4,c5e5,g0i2,a9b9,e1f1,g9i7,e2c0,g7f7,d2e1,e5f5 +3d0f8b49;a0a1,b7b0,a1e1,b0b4,g3g4,b4b0,e1i1,b0b2,i1h1,e6e5,h1f1,h7i7,f1h0,i7e7,a3a4,b9a7,h0h3,c6c5,i3i4,e5e4,h3i1,b2b7,c0e2,e7h7,i1g1,h9i7,f0e1,e4e3,g1i0,e3e2,i0h0,b7b0 +b425417d;a0a1,h7h3,e0e1,b7g7,a3a4,g7h7,e1e0,f9e8,a1a0,h7h6,g0e2,e8f9,e2c4,h3h5,i3i4,h6h7,a0c1,i6i5,c1h1,i9i8,h1h3,h5f5,h3i5,h7i7,i5i8,i7e7,i4i5,h9i7,i8a8,e7e3,a8a9,f5e5 +08492ddd;g0i2,h9i7,d0e1,f9e8,e3e4,h7e7,e0d0,c6c5,e1d2,b7b6,c3c4,e7f7,a0c1,c5c4,c1e2,e6e5,e2e0,b6c6,c0a2,f7b7,f0e1,e5e4,i3i4,a6a5,a3a4,b7b8,d0d1,i7g8,a2c4,g9i7,e0f0,c6d6 +61b267a1;a0b0,d9e8,g3g4,b7c7,b0b7,c7c3,b7b2,b9a7,b2d1,g9i7,d1d8,g6g5,d8d1,a6a5,g0e2,h7g7,g4g5,i7g9,d1d9,e8d9,g5g6,c6c5,c0a2,i9i7,e2g4,g7d7,e3e4,c3h3,g6f6,c9e7,d0e1,h3h0 +907a5c3a;a0b2,b7a7,b2a2,g9e7,a2a0,h9i7,i3i4,h7h2,a0a6,e9e8,c0e2,h2f2,a6a9,c6c5,c3c4,f2f4,c4c5,f4c4,e2g4,i9h9,g4i2,h9i9,c5c6,e8e9,a9c9,e7c9,c6d6,c4c7,f0e1,i7h9,i2g4,c7c0 +654e7a20;a0a6,h9i7,g0i2,b7b4,a6a9,b4b0,g3g4,h7h2,i3i4,f9e8,e3e4,h2b2,e0e1,i9h9,a9a4,e6e5,a4d4,h9h1,e1e2,g9e7,d4d6,h1h2,e2e1,b2b4,d6f6,b4b5,f6c6,e7g5,c6i6,e5e4,c0e2,h2h1 +9482fbe2;a0c1,b7b6,e3e4,b9a7,f0e1,b6b7,a3a4,h7i7,e1d2,a9a8,c1f1,b7d7,f1h2,d7c7,h2h7,f9e8,e0f0,a8d8,f0f1,d8d4,f1f2,e8f7,h7h9,i9h9,e4e5,e9f9,g0e2,h9h4,a4a5,h4h3,g3g4,d4f4 +258e614e;a0a1,h7h5,a1g1,h5h6,g1c1,h6h8,c1e2,h8h0,e2d2,c6c5,d0e1,b7e7,d2b3,h0h7,b3b7,i9i8,g3g4,b9a7,b7d8,e7e8,i3i4,e8e7,d8d6,d9e8,g0e2,a9b9,i4i5,g9i7,d6c4,i8f8,c4f4,e6e5,e2c4,h7h0 +630b0d17;a0a1,f9e8,a1i1,h7c7,g0e2,h9i7,e2c4,c9e7,i1i0,b7b4,e3e4,b4b6,g3g4,c7c9,i3i4,e8f9,i0g0,i6i5,a3a4,b6b4,f0e1,i9i8,e1f2,e7g5,g0g2,i7g8,e4e5,i8i9,g2g1,a6a5,f2e1,c9c4,i4i5,c4c0 +1bb9616f;c0e2,h7h3,e2c4,b7c7,e0e1,c7c8,a0a2,c8i8,c4e2,h3e3,e1f1,g6g5,e2c0,e3i3,a2g2,i8e8,g0i2,h9i7,g2h0,e6e5,a3a4,i3i4,c3c4,c6c5,i2g4,a9a8,h0g2,i7g8,g2i1,a8a7,i1i0,a7e7,a4a5,e7f7 +6d0fb2e2;c0a2,h7h3,g3g4,a9a8,a2c4,b7b3,a3a4,b3b1,a0c1,h9i7,c1d3,d9e8,d3e5,b1b3,e5c5,h3h4,c5g5,e8f7,g5g6,a8b8,g6f4,b8i8,f4h4,i8a8,h4h6,b3b7,h6e6,e9d9,g0i2,i7g8,a4a5,b9c7,f0e1,b7b0 +1ff62929;a0c1,i9i8,c0a2,h9i7,g0i2,h7g7,f0e1,f9e8,e3e4,g6g5,c1c6,b7b2,a2c4,i7g6,c6d8,g9e7,d8d5,b9c7,d5b4,e6e5,i3i4,e5e4,i4i5,g7h7,b4e4,c7e6,e4e6,h7f7,e6d6,b2b4,d6g6,a9a8,g3g4,b4b0 +91be452f;i3i4,h7h8,a0a2,i9i8,g0i2,i6i5,a2c2,h8h5,i2g4,b7a7,c2i2,a7i7,i4i5,a6a5,d0e1,a9a7,i2c2,b9c7,e3e4,e6e5,c2d0,h5h4,d0d6,a7a8,d6c8,a8c8,e4e5,d9e8,c0a2,g9e7,e5d5,h9g7,d5d6,h4h0 +201c9213;e0e1,b7f7,e1f1,f7f8,f0e1,h7c7,a0b0,i9i7,e3e4,f8g8,c0e2,c7c3,e1f2,g8h8,b0b7,a9a8,b7a9,a8c8,d0e1,d9e8,e1d2,g6g5,a3a4,e9d9,a4a5,c3c5,a9c9,d9d8,g3g4,c8a8,e2c4,h8g8,d2e1,c5f5 +3e04949d;a3a4,a9a8,i3i4,h7h2,g0e2,a8g8,e0e1,i9i7,e1f1,h2h7,e2g4,h7h3,c0a2,h3h7,g4e2,h7h4,f1f2,h4e4,g3g4,i7g7,a0b2,g8h8,a4a5,h8h5,a5b5,g7i7,b2a4,i7c7,a4b2,e4e5,e2c0,b9a7,b2c2,h5f5 +505e6dec;e0e1,e9e8,a0a1,h7i7,e1e2,b7b6,a1d1,i9i8,d1b1,i7e7,b1b0,e7e3,d0e1,e8e7,c0a2,i8g8,i3i4,h9g7,b0b9,g7e8,b9a7,a9a7,a2c0,a7a9,e1d2,e3a3,g3g4,a9a7,f0e1,b6b5,g0i2,a7a9,c0a2,b5b2 +4c9a7b1b;e3e4,h7h2,a0c1,b7b2,c1e1,h2h1,e1e2,b2b3,e2i2,b3b1,i2d2,g9i7,i3i4,b9a7,e4e5,b1f1,c3c4,i9i8,e5f5,f1f4,d2a2,i8e8,a2c3,h1h2,a3a4,f4e4,g3g4,h2f2,c3c6,f2b2,g4g5,e8e7,c6b8,b2e2 +96d2d361;c3c4,b7c7,i3i4,h7h2,a3a4,h9i7,g0e2,c7e7,a0a1,e7h7,c0a2,b9c7,a1a0,h2h5,c4c5,a9a8,a0b0,i6i5,b0b7,a8d8,e2c0,d8e8,b7b4,h5e5,f0e1,h7h3,b4f4,e8b8,f4e6,c7e6,e0f0,b8a8,e1d2,i7g8,i4i5,a8f8 +d5bcdac0;a0c1,h7h6,g0i2,b7b1,i2g4,d9e8,c1i1,b1d1,i1e1,b9a7,c0a2,d1d8,e1c2,d8d1,i3i4,a9a8,c2g2,e6e5,i4i5,d1f1,g2i3,f1g1,i3h1,c6c5,d0e1,g1g4,i5i6,g9i7,h1h2,g4h4,h2g0,h4h5,g0h2,a7c6,h2c2,h5h0 +7c794b34;e3e4,g6g5,a0b0,d9e8,b0b2,h7e7,b2g2,b7a7,g2h2,e7e4,h2i0,b9c7,i0g1,g5g4,g1i1,e8f7,i1f1,i9i8,f1f4,a7a8,f4g4,c7d9,i3i4,a8d8,g4g5,d8d4,g5f7,i8f8,g3g4,d4d2,f7d6,f8f3,a3a4,e4e3,d6i6,d2e2 +cc26ad71;a0b2,h7h4,b2h2,b7b2,h2b2,f9e8,i3i4,h4e4,e3e4,i9i7,b2d1,c9e7,d1h1,b9a7,i4i5,e7g5,e0e1,g5e7,i5i6,a7b9,h1f2,i7i8,f2f5,i8i9,e1e2,c6c5,e2d2,i9i6,f5e5,a9a8,d0e1,a8a7,d2d1,i6i1,e5g4,a7d7 +1ebbd9aa;c3c4,h9i7,i3i4,h7d7,g0e2,d7d8,c0a2,g9e7,e2c0,i9i8,a0b2,c6c5,b2b1,d9e8,b1b3,i8i9,b3b7,i6i5,g3g4,e9d9,b7a5,d8c8,d0e1,e8f7,a5c6,a9a7,c6c5,c8h8,c5f5,h8h3,f5f4,a7a9,f4d3,d9e9,i4i5,h3h0 +1492c6eb;a0b0,h7f7,g0e2,b7a7,e0e1,a7b7,e1d1,h9g7,b0b5,b7a7,b5b1,i9h9,e2g0,h9h0,d0e1,i6i5,b1b6,f9e8,i3i4,f7e7,b6c8,e9f9,c8c6,e7f7,g3g4,a9a8,c6g6,a7c7,g6g9,f9f8,a3a4,c7c8,g0i2,f7d7,g4g5,c8d8 +dbc2aa03;a0a1,h7h8,c0a2,h8a8,a1g1,c9e7,g1i2,b7b4,i2c2,b4e4,g0e2,b9a7,c2a1,a9b9,a1f1,h9i7,g3g4,b9b8,f1g3,i7g8,d0e1,g8h6,g3h3,b8b2,h3h1,e4d4,h1h5,b2b4,h5i5,a8b8,i5c5,b8e8,e0d0,e7c9,d0d1,c6c5,e3e4,e8d8 +0e6963bb;e0e1,a6a5,g0i2,e6e5,a0b0,h7f7,e1f1,f7f5,d0e1,b7e7,c3c4,f5f7,b0b6,c6c5,b6b4,h9g7,a3a4,b9c7,i3i4,f9e8,c4c5,e9f9,b4c2,a9b9,c2c3,f9f8,c3b3,b9a9,b3b8,f8f9,i4i5,g7h9,c0e2,h9g7,i2g4,f7f8,b8b1,e7f7 +494da19c;a0c1,h7h0,d0e1,c9a7,c1a0,i6i5,e1d0,e6e5,a0c1,h9i7,e0e1,a6a5,i3i4,a5a4,c3c4,h0h9,e1e2,b9c7,c1e1,h9h1,e2f2,h1h7,e1d1,a7c9,d1e1,h7h2,e1d1,b7b0,d1b2,f9e8,g3g4,i9h9,e3e4,e8f9,d0e1,b0b1,b2h2,h9h2 +f7e0d5e0;e0e1,h7h5,e1e2,h9i7,a0b2,h5d5,b2b0,i9i8,b0b6,g6g5,g0i2,d5d8,f0e1,i8e8,b6b2,a9a7,e1d2,d8d4,d2e1,b7b6,b2a0,d4d3,e2d2,b6b0,d2d1,a7c7,a0b0,d3d5,e1f0,c7d7,g3g4,d5d0,d1e1,d7d1,e1e2,e8h8,b0b7,h8h2 +9e0a49ae;a0b2,e9e8,e0e1,b7b3,b2a0,h7h0,e3e4,b3b1,g3g4,i9i8,e1e2,h0f0,e4e5,f0f6,e5d5,a9a7,d5d6,a7f7,e2f2,b1c1,a0c1,i8i9,a3a4,f7c7,c1i1,c7h7,i1i0,c9e7,c0e2,h7i7,i0i2,i7f7,i2g1,f6f3,g1a1,b9c7,a1a3,f3a3 +46e97fe3;g0e2,h7h8,c0a2,b7d7,a0c1,h8c8,i3i4,d7e7,c1b1,e6e5,b1i1,e7i7,i1a1,c9a7,a1i1,c8c9,i1i6,c9c8,i6h6,i7d7,h6h4,b9c7,h4h6,c8g8,h6h9,g8h8,h9h8,d7h7,h8f7,e9e8,a2c0,i9i6,d0e1,g9i7,f7g9,i6i4,e2c4,h7h0 +bfcc7e33;a0a1,b7c7,a1b1,h7h0,b1b5,c7g7,b5d6,h0h2,g3g4,g7g4,e0e1,h2h8,d6d9,e9d9,e3e4,h8b8,e1e2,f9e8,c3c4,h9g7,e2f2,g4g2,c0a2,b8b4,e4e5,g7f9,a3a4,a6a5,f2f1,i9h9,d0e1,i6i5,c4c5,g2f2,e5e6,h9h2,c5d5,b4f4 +76bb0eb4;d0e1,h7c7,c0a2,b7a7,a3a4,d9e8,e1f2,c7h7,a0b2,a7d7,b2b1,h7h2,b1b9,a9b9,g0i2,e8f7,a2c4,h2h1,c4a2,b9b1,f2e1,d7d4,e1d2,f7e8,a4a5,b1b9,a5b5,e8d9,b5c5,h1h4,c3c4,a6a5,c5b5,h4h7,i2g4,c9e7,d2e1,h7h0 +e712a6a0;a0a1,b7c7,c3c4,c7c8,g0e2,h7e7,a1c1,i9i7,c1h1,g6g5,a3a4,c8c4,h1f2,a6a5,f2g0,e7d7,g0f2,d7d1,e2g4,d1i1,f2h1,i7a7,g4e2,a9a8,h1h5,i6i5,h5h3,i1d1,h3h9,c6c5,h9h3,d1i1,e2g4,c9e7,d0e1,c4c3,h3g5,i1i0 +75af1347;a0a6,h7h3,f0e1,b7b3,c0a2,f9e8,c3c4,i9i8,g3g4,i8i7,a6a9,g9e7,a9a7,b3b0,a2c0,b0b5,c4c5,i7f7,a7c7,f7h7,c7c9,e7c9,e0f0,h3g3,e1f2,h7b7,f0f1,i6i5,c5b5,g6g5,d0e1,i5i4,c0e2,e6e5,g0i2,b7i7,b5a5,g3f3 +a634a85b;f0e1,h7h0,g0i2,h0i0,c3c4,b7b0,a0a1,b9c7,a1a6,i9i7,a6b6,a9a6,b6e6,c7e6,e1d2,b0b9,e0f0,b9b0,i2g4,i7e7,g4e2,c6c5,e3e4,a6a3,g3g4,i6i5,g4g5,b0b3,f0e0,e9e8,i3i4,i0i2,e0e1,i2i1,e1d1,b3c3,e4e5,a3a1 +0a2f3d36;g0i2,h7h6,i2g4,d9e8,a0b0,b7h7,f0e1,h9i7,b0b2,h7g7,e1d2,h6h9,b2b7,g7d7,b7a5,d7e7,a5c4,e7d7,c4c6,d7c7,c6b4,c7h7,b4b9,a9b9,e0e1,a6a5,g4e2,h7h6,e3e4,e8d9,g3g4,h9h7,e2c4,a5a4,e1d1,g9e7,c4e2,b9b1 +60001c46;a0c1,i9i7,c1f1,h7h2,f1h0,h2h5,g0e2,b7b5,h0g2,h5h7,g2h2,i6i5,h2h9,b5b6,h9g9,e9e8,e3e4,i7i6,e2c4,h7b7,g9i8,e8e9,i8i5,a9a7,c0e2,b6b4,i5g5,i6i3,f0e1,d9e8,e2g4,i3i8,g5i5,e6e5,i5i1,i8i3,i1i3,b4b0 +221be0a8;a0a1,h7h4,g0e2,a9a7,a1g1,b7b0,c3c4,a7i7,g1i1,h4f4,e3e4,i7h7,i1b1,h7h4,e2g0,h4h0,b1b6,i9i8,i3i4,i8b8,b6b2,b8b5,e0e1,h0h8,g0e2,f4f6,c4c5,i6i5,c5d5,f6f5,e2c4,i5i4,b2b9,b5b3,e1f1,f5e5,b9a9,b3f3 +ecab9d58;g0i2,a9a7,c0e2,g9e7,e3e4,h7h2,a3a4,h2h7,e4e5,h7h1,e2g4,b7b6,a4a5,d9e8,e0e1,h1h2,e1e2,e7g9,a0a6,b6b8,a6a7,e8f7,i2g0,e9d9,e2f2,h2h4,a7b7,h4h5,b7f7,b8e8,f7g9,e8e9,g9e9,d9d8,e9b9,h5f5,d0e1,i9i8,b9c7,i8f8,c7d7,d8e8,e1d2,f5f7,e5e6,f7g7,f2e2,i6i5,c3c4,f8f5,d7d8,e8e9,i3i4,f5a5,i4i5,h9f8,d8f8,g7c7,f8h8,a5a2,h8a8,f9e8,c4c5,c9a7,a8c9,a7c9,c5b5,c7a7,e6e7,a2b2,b5b6,e8f7,b6a6,b2b6,e2f2,e9f9,i5h5,a7d7,a6b6,c9a7,g0i2,d7b7,h5h6,c6c5,h6g6,c5c4,b6b7,c4d4,f2e2,f9e9,e2f2,d4d3,b7a7,f7e8,e7e8,e9d9,f0e1,d3e3,e1d0,e3f3,f2e2,f3e3,e2f2,e3e2,f2f1,e2d2,f1f2,d2e2,g4e2 +9573e863;g0e2,h7h2,e3e4,b7c7,f0e1,a6a5,a3a4,h2g2,e2g0,f9e8,e1f2,c6c5,g0e2,c7a7,a0a2,g2i2,e0e1,a7c7,a4a5,g6g5,a2b2,i2h2,e4e5,c7b7,b2b3,b7b4,b3b0,a9a5,g3g4,i9i7,b0a2,e8f9,a2b2,i7g7,b2a0,h2h7,a0b0,b4b5,b0b1,h7h4,e2c4,g5g4,b1d1,b5b7,e1e0,c9e7,d1d9,e9e8,d9d6,e8f8,d6i6,a5a9,e5d5,h9i7,i6i7,g4f4,c0a2,a9a2,i7i6,b9c7,i6h4,g7i7,h4i2,b7a7,e0e1,c7a6,i2h2,i7i5,d5d6,a2a3,h2h3,i5d5,e1e0,g9i7,h3h2,a3a1,h2g4,a1a3,g4g3,d5d6,g3i2,d6d3,i2g1,a3c3,g1h1,d3i3,h1d1,f8f7,d1a1,e6e5,a1e1,f4e4,e0f0,e4d4,e1b1,c3c0,b1i1,d4d3,i1a1,i3e3,a1a0,a6c7,a0b2,c5c4,b2b1,e7c5,b1h1,c7e6,h1h3,c0d0,f0f1,a7a8,h3g5,e6g5 +ba4466c1;d0e1,b7e7,a0a1,f9e8,e1d0,e7a7,f0e1,a6a5,a1a2,h7h8,e1f2,a7a6,a2b2,a9a7,b2a0,i9i8,d0e1,a6a3,a0a3,a7d7,e0f0,g9e7,f0f1,d7d5,e1d2,h9f8,g0i2,d5b5,c0a2,e8f9,a3b3,b5d5,i3i4,e9e8,i2g0,d5g5,b3b1,e8d8,b1d1,d8e8,d1d9,c9a7,c3c4,g5c5,d9c7,e8d8,c7d9,d8d9,f2e1,h8h3,i4i5,e7c9,g0i2,d9e9,f1f2,e9e8,e3e4,i8i7,e1d0,h3h0,i5h5,c5g5,d2e1,h0e0,h5i5,g5c5,e1d2,e8e7,f2e2,f8h7,g3g4,c5f5,c4c5,f5i5,i2g0,h7g9,a2c0,i5i1,c5d5,i1i3,g0i2,g6g5,e4e5,i7i8,c0a2,i8g8,a2c4,i3e3,e2f2,e0g0,e5f5,e3e5,f2f1,g8f8,g4g5,g0f0,i2g0,b9c7,f5f6,c7e8,c4e2,e5e2,d5e5,e2g2,g5g6,g2g0,f1e1,f0f5,e1d1,g9i8,f6e6,e7d7,e5f5,i8g9,d1e1,d7d8,e6e7,g0i0,f5f6,i0f0,e7d7,d8d7,e1e2,a7c5,f6f7,c9a7,d0e1,e8c9,f7e7,c5e7,e1d0,e7g5,d0e1,g9h7,g6f6,h7i5,f6e6,f0e0,e6e7,d7d8,e7d7,c9d7 +498808db;a0b2,d9e8,b2f2,h7h5,f2f5,b7c7,e3e4,c7c8,g3g4,e8d7,g0i2,a6a5,f0e1,c8c3,f5d4,c6c5,g4g5,c3c2,d4d2,c2c3,d2a2,a9a6,i2g0,d7e8,g5h5,h9i7,a2a5,a6a8,e1f2,e8f7,c0e2,g9e7,a5b3,i7g8,e0e1,a8a7,b3c3,a7a5,a3a4,a5a9,i3i4,f9e8,h5h6,i9i7,c3f3,g8f6,e2c0,e7g9,h6g6,a9a8,f3h3,i7h7,a4a5,h7h6,g6f6,a8c8,h3f4,g9e7,e1e0,e8f9,i4i5,i6i5,f4g6,b9c7,f2e1,c8b8,e4e5,e9e8,g6g9,h6f6,c0a2,f6g6,e5f5,g6g0,e1f0,b8b1,g9i9,b1b5,a5a6,b5b6,f5e5,c9a7,e5d5,c7a6,i9g8,g0g8,a2c4,b6b8,c4e2,g8g6,d5e5,b8b2,e5d5,b2b0,e2c0,a7c9,e0e1,g6g2,d5d6,g2g5,e1e0,e8e9,d6e6,b0b1,e6f6,b1b2,c0a2,b2a2,f6e6,a2a5,e6d6,f7e8,d6d7,g5g3,d7c7,a5a0,e0e1,e7g9,c7d7,c5c4,e1e2,g3b3,d7c7,a0d0,e2f2,b3i3,f2f1,a6b4,c7c8,d0f0,f1e1,b4a2,c8b8,f0f1,e1e0,i3i2,b8c8,i2h2,e0d0,f1e1,c8d8,a2b4,d8d9,e8d9 +65e644b6;f0e1,b7a7,c0a2,i6i5,c3c4,h7f7,a0a1,a7e7,e1f0,i9i6,a1a0,f9e8,a0c1,i5i4,c1d3,f7f8,d3d9,e8d9,d0e1,i4i3,e1f2,e7b7,g3g4,b9a7,c4c5,a6a5,g0i2,e6e5,e3e4,e9f9,c5c6,b7e7,g4g5,a7b9,c6b6,e7d7,i2g0,c9a7,e0e1,f8f5,e1e2,f9e9,f0e1,f5f9,a3a4,d7d3,b6a6,d3c3,e1d0,c3f3,d0e1,e5e4,a6a7,b9d8,g0i2,i6i7,g5f5,a9a8,a2c4,i7h7,f5f6,i3h3,i2g0,a8a7,g0i2,d8b7,e1d0,h3h2,f6f7,f3g3,c4a2,g9e7,i2g0,f9f2,f7e7,f2f6,a2c4,f6f5,e2d2,f5e5,e7e8,d9e8,g0e2,g3h3,d0e1,h3h5,e1f0,h7f7,f0e1,a7a9,d2d1,g6g5,d1d2,e8f9,c4a2,f7f0,e1d0,e5e7,a4a5,a9a6,a2c0,e7e2,c0e2,f0f6,e2g4,e4f4,a5b5,e9e8,b5b6,f6b6,g4e2,h5i5,e2c4,i5i3,c4e2,b6b2,d2d1,b2e2,d0e1,e2a2,e1f2,a6a4,f2e1,i3a3,e1f2,a4e4,d1d0,f4f3,f2e1,e4e5,e1f0,a3e3,d0d1,e3e0,f0e1,a2b2,e1f2,b2e2,d1d0,e0g0,f2e1,g0h0,d0e0,e2e1,e0f0,e1g1 +a61d5811;a0a2,d9e8,a2c2,h7h2,c0a2,c9a7,c2f2,h2h4,f2f3,h4h2,f3f4,i9i7,f4i4,h2f2,i4b4,f2f4,d0e1,i6i5,e0d0,b7e7,b4b3,g6g5,b3b2,a7c5,d0e0,f4a4,e0d0,a4b4,b2f2,b4b2,f2f7,i7f7,e1d2,e7a7,g3g4,b9d8,g0i2,b2i2,d2e1,f7e7,d0e0,a9c9,e1d2,a7a9,a2c4,i2i1,e3e4,i1h1,f0e1,h1i1,c4e2,g5g4,e2g0,e8f7,e0f0,i5i4,e1f2,i1h1,f2e1,e6e5,e1f2,g4f4,f0f1,g9i7,c3c4,h1h0,d2e1,a6a5,g0i2,e7e8,e1d0,h9f8,i3i4,h0h9,e4e5,h9h1,i4i5,a9a7,a3a4,a7a4,i5h5,a4a0,f2e1,c9b9,h5i5,h1e1,i5h5,b9c9,f1e1,a5a4,h5g5,f4e4,g5h5,f8h7,h5h6,a4b4,h6h7,c9c8,i2g0,e9d9,h7i7,e4e3,e5e6,e8i8,e6e7,b4a4,i7i8,a0g0,e7e8,g0g7,e1e0,e3d3,e0f0,a4a3,d0e1,g7g6,e1d0,d3d2,e8e9,d9e9,c4c5,g6g5,c5c6,g5d5,c6d6,d5d0,i8h8,c8c7,h8g8,e9d9,g8f8,c7e7,f8g8,f7e8,d6c6,e7a7,f0f1,e8f7,f1f2,a3a2,g8f8,a2b2,c6c7,a7c7,f2f1,c7c6,f1f0,d0a0,f8f9,b2b1,f0f1,b1a1,f1f2,c6c1,f9e9,d9e9 +f1d0dac0;e3e4,c9e7,g0i2,b9c7,a0a1,e7c9,a1c1,e6e5,c1b3,c7e8,c0e2,b7a7,b3b0,e8g7,e2g4,a9b9,a3a4,h7i7,b0c2,b9b4,e4e5,i9i8,g4e2,i8a8,c2d2,c6c5,d2d3,b4b5,e0e1,b5b6,d3d5,i7i9,e1e0,f9e8,d5e7,g9e7,i2g4,a8d8,i3i4,d8b8,e5f5,a7a9,i4i5,b6d6,i5i6,d6d3,i6i7,b8b2,f0e1,d3d5,f5e5,a9a4,e5e6,b2b5,c3c4,d5e5,e2c0,a4g4,e6f6,e5e6,i7i8,b5b8,c0a2,b8b4,i8i9,g4g5,f6e6,g5i5,i9h9,g6g5,h9i9,g7f5,e1d2,c5c4,a2c0,e9f9,c0e2,g5g4,e0e1,g4f4,e6e7,i5g5,e1f1,b4b9,d2e1,c9e7,f1f0,g5g7,e1f2,f5h4,f2e1,a6a5,e2c0,h4g6,i9h9,c4d4,c0e2,g7f7,e1f2,b9b4,f0f1,f4g4,f1e1,g6h4,e2c4,b4b7,h9g9,e7g9,e1d1,g4g3,f2e1,b7d7,e1d2,d4c4,d0e1,h4f5,d1d0,f7i7,e1f0,f5h6,d0e0,i7e7,e0d0,e7e2,d0d1,d7b7,f0e1,f9e9,e1f0,g9i7,d1e1,g3f3,e1f1,c4b4,f0e1,b7b5,e1d0,e9f9,d2e1,h6i4,e1f2,f3f2,f1e1,b5i5,e1e0,e8d7,e0e1,e2e8,e1e0,e8e5,e0f0,i4g5,d0e1,i5i6,e1d0,g5i4,f0e0,f2f1 +0b874982;a0c1,c9a7,c1c2,b7f7,d0e1,b9d8,e1f2,d9e8,c3c4,g9e7,c2d0,a6a5,f0e1,e6e5,d0d3,h9f8,g0e2,h7h1,d3d0,i9h9,d0d1,f7h7,d1c3,h1h3,e0d0,e9d9,d0d1,h9g9,d1d0,h7i7,e2g4,h3h4,c3b3,h4h1,b3b6,g9g8,e1f0,h1d1,b6b0,i7h7,e3e4,d1e1,b0b9,a9b9,f0e1,c6c5,a3a4,a5a4,e1d2,b9b2,f2e1,f8e6,e1f0,b2b7,d0e0,g8h8,d2e1,b7b2,i3i4,c5c4,g4i2,h7i7,e1f2,a4b4,e0d0,e8d7,i2g4,d8f7,f0e1,b2b3,g4e2,e7g9,e2c4,f7g5,d0e0,h8h0,e1f0,b3b1,c0e2,h0f0,e0f0,b4c4,e2g0,g5e4,g0e2,e4f2,e2g4,b1b2,i4i5,e6g7,g4i2,i6i5,f0f1,f2d1,f1e1,b2g2,e1f1,g7e8,i2g4,d9d8,f1e1,e8f6,e1e0,g6g5,g4e2,g5g4,e0e1,i7g7,e1f1,f6d5,e2c4,g2g0,c4e2,g7g5,g3g4,i5i4,g4g5,g0g5,e2g4,d1e3,f1f0,e3g2,f0e0,i4h4,e0d0,g9e7,d0d1,g2i1,d1d2,h4i4,d2d1,d8e8,d1e1,d5e3,e1e0,i1g0,e0f0,g5h5,f0e0,e5e4,g4i2,h5e5,i2g4,g0f2,e0d0,e5c5,g4e2,f2g0,e2c0,c5c2,c0e2,c2e2,d0d1,g0f2,d1d0,f2h3,d0d1,e2b2,d1d0,h3f2 +3068122c;a0a6,a9a6,c3c4,e9e8,f0e1,h7g7,g3g4,b7b0,e1f2,a6a7,f2e1,b0b3,e3e4,g9e7,g4g5,g7f7,g5g6,f7f2,c0e2,e8f8,e1f0,f2i2,g0i2,f9e8,e2c0,b3b4,c0a2,i9i7,e4e5,h9g7,e5f5,e8d7,i3i4,a7a3,f5e5,a3a7,g6g7,a7a3,d0e1,d7e8,e5d5,c6c5,i2g4,a3a9,g4i2,a9a8,g7h7,f8f9,e1d2,a8a4,d2e1,i7i9,e1f2,b4b6,i2g0,b6d6,f0e1,e8d7,e0f0,i9g9,h7h8,g9g4,f0e0,b9c7,e1d2,d9e8,e0e1,d6d2,g0e2,e6e5,h8g8,g4g2,d5e5,c7b5,e5f5,e7g5,f5e5,a4a2,g8f8,f9f8,c4c5,a2a0,i4i5,g5e7,e1f1,d2d3,e5f5,i6i5,f1e1,a0a3,e2c0,i5i4,f5e5,g2g1,e1e0,c9a7,c0e2,a3a5,e2g4,e7g5,e0d0,g1b1,c5b5,a5b5,e5d5,d3g3,g4i2,b1h1,i2g4,e8d9,d0e0,b5b3,e0f0,b3e3,d5e5,h1h9,g4e2,e3e2,e5e6,e2f2,f0e0,g3g1,e0d0,g1b1,d0d1,f2f7,d1e1,f8e8,e1d1,f7f0,d1e1,b1d1,e6e7,e8e9,e1d1,g5e7,d1d2,h9h8,d2d1,f0c0,d1d2,e9f9,d2e2,e7c5,e2d2,h8h9,d2e2,d7e8,e2d2,c0c2,d2d1,e8d7,d1e1,h9h7,e1e0,c2d2,e0e1,h7h9,e1e0,d2d1 +dddabe7c;f0e1,b7b3,g0i2,h9g7,a3a4,b3b8,a0a6,b8c8,a6c7,e6e5,c3c4,a9a4,c7a6,a4a0,a6c6,h7h8,c0e2,c8g8,c6d8,i9i7,d8d7,e9e8,c4c5,e5e4,e2g0,a0a5,d7i7,a5a4,i7h9,c9a7,h9h8,e8e7,c5c6,e7d7,h8h3,i6i5,h3h6,f9e8,c6d6,d7e7,d6e6,g7e6,h6h8,e4e3,i2g4,g8g7,h8h1,b9c7,g0e2,e8f7,e1f2,a4a5,h1h9,d9e8,h9i9,e7d7,i9g9,a5e5,g9g6,e8f9,g6g5,d7e7,g5e4,g7i7,e4c3,i5i4,c3c0,e3e2,c0c5,i4h4,c5e5,e7d7,e5g6,a7c9,g6g9,e2f2,g9i8,h4g4,i8f8,f7e8,f8f2,i7g7,f2a2,g4h4,a2a8,e6d8,e0f0,c7d5,d0e1,d5c7,f0f1,c7b5,f1f2,g7f7,f2f1,d7e7,a8a3,b5d4,i3i4,d8e6,f1f0,f7f4,a3a1,f4i4,a1b3,h4g4,e1d0,d4b5,f0f1,b5d4,b3e3,e7d7,e3e5,i4i0,e5g6,g4g3,g6h4,e8d9,f1f0,d4f5,f0e0,i0f0,d0e1,c9e7,h4h8,e6d4,h8g6,d4e2,g6e7,d7e7,e1d2,f0g0,e0e1,g0d0,e1d1,e2f0,d1d0,f0g2,d0d1,e7f7,d2e1,f5h4,e1d0,g3f3,d1d2,g2f4,d2e2,f4d5,d0e1,f7f8,e1f0,h4f5,f0e1,d5f6,e1d0,f6e8,d0e1,f5h4,e2d2,h4g2,e1f0,g2f0 +d401e97b;g3g4,b7b6,a3a4,h7h5,a0a3,h5h3,g0i2,a6a5,a3a5,h3f3,c3c4,e6e5,e3e4,f3f2,i3i4,i9i7,e4e5,i7c7,c4c5,f2h2,c0e2,g9i7,c5c6,c7b7,f0e1,i6i5,a5a9,b6b4,a9a8,b4e4,e5d5,b7b0,c6c7,b0b5,a8a6,e4e3,a6b8,b5b4,i2g0,h2h3,d5e5,i5i4,b8e8,e9e8,e0f0,h3h2,e1f2,b4b3,e5f5,e3c3,f0f1,i7g5,f5g5,b3b1,f2e1,c3g3,c7b7,b9c7,g5h5,e8e9,g0i2,h2h0,b7b8,i4i3,b8a8,h9i7,a4a5,h0h2,f1f0,g3c3,a5a6,b1b4,a8b8,g6g5,e1f2,i7h5,f2e1,c3h3,i2g0,b4b2,b8b9,b2d2,b9a9,c7a6,a9b9,h3f3,e2c4,d2f2,f0e0,g5g4,b9a9,a6b4,a9b9,f3f4,g0e2,f2f1,b9c9,f4f6,c9d9,e9d9,e2g4,f6h6,c4a2,h5f4,e1f0,f1e1,d0e1,b4a2,e1d2,h2e2,g4i2,i3h3,e0d0,h6h8,f0e1,e2h2,e1f2,a2c3,f2e1,c3a2,d0e0,h8f8,e1f2,f8b8,f2e1,b8b6,e1f2,a2c1,e0d0,f9e8,f2e1,h2h0,e1f2,c1a0,d0e0,e8f9,e0e1,b6b5,e1f1,f4e6,d2e1,h3h2,i2g0,e6c7,g0i2,b5e5,e1f0,h2h1,i2g4,c7e8,g4i2,h1g1,f1e1,e8g9,i2g0,h0h9,e1e0,e5e6,e0e1,h9h1,e1e0,g9i8 +8e73856f;g3g4,h7h6,a0c1,b7d7,c1c6,g9i7,c0e2,h6h2,c6a6,a9a6,e3e4,d7e7,e4e5,h2g2,a3a4,g2g3,c3c4,g3e3,f0e1,e3e4,i3i4,e4e3,e5e6,a6b6,g4g5,b9c7,g5h5,c7a8,h5i5,b6b4,a4a5,i7g9,c4c5,i9i8,a5a6,b4b5,i5i6,i8d8,e6d6,e3e1,d6d7,a8b6,c5c6,d8d7,d0e1,e7e1,i4i5,b5e5,e0e1,d7d0,a6a7,d0d5,e1f1,h9g7,g0i2,g7e8,e2g4,e5f5,f1e1,c9e7,e1e0,f5f8,i5h5,d5h5,e0d0,h5h9,a7b7,f8f6,d0d1,f6f4,c6b6,f4c4,b6c6,c4c0,g4e2,c0c3,d1e1,c3d3,i6h6,d3b3,c6b6,h9h6,e2g4,e8c9,e1f1,h6h8,f1f0,h8f8,f0e0,e9e8,e0e1,f8f2,i2g0,b3a3,b6a6,f2g2,a6b6,g2g0,g4e2,g0i0,e2g4,e8f8,b7b8,i0i1,e1e0,a3c3,b6b7,f8e8,e0f0,i1e1,b8c8,e1e4,g4e2,e4f4,f0e0,c3f3,e0e1,f3h3,e2c4,f4c4,e1f1,h3h8,f1f0,h8h9,b7a7,h9i9,f0f1,i9i5,a7b7,c4g4,c8b8,g4g5,f1e1,i5i2,b7c7,g5c5,b8a8,i2b2,a8b8,e8f8,b8c8,b2b6,c8d8,b6b7,c7d7,b7d7,d8e8,f9e8,e1e2,d7d6,e2e1,f8f9,e1e2,e7g5,e2e1,d6d0,e1e2,f9f8,e2e1,d0d8,e1e0,c5c1 +37ba86e7;a0a2,b9a7,g0e2,h7h2,e3e4,b7d7,a2h2,d7d5,a3a4,d5d1,e4e5,i6i5,f0e1,a9b9,h2h1,d1c1,c3c4,b9b3,h1h6,b3b9,i3i4,c9e7,g3g4,h9g7,g4g5,g6g5,h6c6,g7i8,c4c5,c1a1,c6a5,b9c9,e1d2,a7c6,c5b5,c9c7,e5d5,a1i1,d5e5,c6a5,a4a5,i1i2,e0e1,e7c5,c0a2,i5i4,e5f5,a6a5,f5e5,c7c9,e5d5,g5g4,e1e0,e6e5,d5e5,i2i1,e2c4,i1g1,a2c0,e9e8,b5b6,a5a4,c4e2,c9a9,b6a6,g4f4,c0a2,g1d1,e0f0,i8g7,e2g4,i9i8,a2c0,g7h9,d0e1,i8i9,e5d5,e8f8,c0a2,i9i8,e1d0,d1d5,d2e1,a9c9,f0e0,i8h8,e1d2,h8h0,e0e1,h0h4,a2c0,f4g4,a6b6,d5d0,e1e0,a4b4,d2e1,h4h3,e1d2,h3h5,e0e1,h5d5,e1e0,d5d2,c0a2,d2i2,e0d0,f8e8,a2c4,i2b2,b6b7,c9a9,c4a2,e8e7,b7c7,c5a7,c7c8,e7e8,d0d1,g4f4,d1d0,f4e4,d0d1,e8f8,a2c0,f9e8,d1e1,e4e3,e1d1,b2b1,d1d2,b1e1,c8b8,e1b1,b8c8,b4c4,c0e2,b1b7,e2c4,b7b2,d2d1,b2b6,c8b8,b6b8,d1e1,b8b1,e1e0,h9g7,e0d0,b1f1,c4e2,e3e2,d0e0,f1f7,e0d0,g7h5,d0e0,h5i3,e0d0,a9a8,d0e0,i3g2,e0d0,e2e1 +bd4e2550;c0a2,c9a7,a0b2,c6c5,b2b9,a9b9,d0e1,h9g7,i3i4,f9e8,e1d2,h7h3,e3e4,b7b2,e0d0,h3h7,g0i2,b9a9,d0e0,a9c9,i4i5,c9c8,d2e1,e9f9,e0d0,b2b9,a2c0,h7h3,c0a2,f9f8,e1f2,h3h7,i5h5,c8c9,h5h6,b9b5,d0d1,i9i7,i2g4,e8f7,h6h7,g7h9,d1d2,c9c7,a3a4,c7b7,h7i7,b5b6,a4a5,g6g5,i7h7,f8f9,e4e5,f7e8,c3c4,a6a5,e5e6,b7c7,g4e2,e8d7,e2c0,b6a6,c4c5,a6a2,c5b5,c7c3,e6e7,c3f3,b5b6,f3a3,h7i7,g9i7,c0a2,g5g4,g3g4,i6i5,g4g5,a3h3,b6a6,d7e8,a2c0,h3h0,f2e1,h0h3,g5h5,i7g9,e1f2,h3h4,a6b6,f9e9,e7d7,a5a4,f2e1,e8f7,c0e2,h4h3,e2g4,a4b4,e1f2,h3h4,h5i5,h4h1,i5h5,h1g1,b6b7,e9f9,h5i5,g1h1,d7d8,h1h8,i5i6,h8e8,g4i2,e8e6,f0e1,e6i6,e1d0,i6i5,i2g4,d9e8,g4e2,i5i9,b7a7,i9i6,e2c4,i6i8,d2e2,i8i2,d8e8,g9e7,a7a8,i2i1,a8a9,i1i6,e8e9,f9e9,e2d2,i6d6,d2e2,d6d3,a9b9,b4b3,b9a9,d3d8,d0e1,b3a3,e1f0,d8d0,e2e1,d0d5,a9b9,h9f8,c4a2,e9f9,b9c9,a3a2,c9d9,d5d2,d9e9,f9e9,e1e0,e9e8,f2e1,e7g5 +ad22a1bd;a0c1,c6c5,c3c4,b7c7,c1e1,c5c4,a3a4,c7c0,e1c0,h7a7,c0c9,e9e8,a4a5,b9c7,g0i2,a7a5,i2g4,a5a1,c9a9,a1g1,d0e1,g9e7,e3e4,c7d5,a9b9,g1g4,e4e5,g4f4,b9b5,d5c7,e0d0,f4f1,g3g4,f1h1,b5d5,e6e5,d5d8,e8e9,d8f9,e9f9,e1d2,h1d1,g4g5,i9i7,d0e0,a6a5,g5h5,c4b4,i3i4,g6g5,i4i5,i7f7,e0d0,c7a6,d0d1,e7c9,h5h6,f7f2,h6g6,f2f8,g6f6,h9g7,d2e1,f8h8,d1d2,h8g8,f6f7,a5a4,i5i6,g8a8,f7f8,f9f8,d2d1,a8a9,e1f2,f8f7,i6i7,g7i6,d1d0,a6b8,f2e1,i6h4,e1f2,b4c4,i7i8,a9a8,i8h8,h4g6,f0e1,g6i7,e1d2,a8a9,f2e1,f7e7,h8i8,a9a6,d0e0,a6a5,e0f0,c4c3,f0f1,b8d7,e1f2,c3c2,d2e1,a4a3,i8i9,c2c1,e1d2,a5a8,d2e1,c1b1,e1d2,a8a6,f1e1,a6c6,e1f1,a3a2,i9h9,c6c7,f1e1,e7f7,e1e2,c7b7,h9g9,c9e7,g9h9,b1c1,f2e1,b7b0,e1d0,i7h9,e2e1,b0c0,e1e0,h9i7,e0e1,e7c9,e1e2,g5g4,d0e1,g4g3,e1f0,c0b0,d2e1,b0b4,e1f2,b4d4,f0e1,d9e8,e1d0,d7f6,e2e1,e5e4,e1f1,d4d0,f2e1,g3f3,e1f0,e8f9,f1e1,i7g6,e1e2,f3f2,e2e1,a2b2 +659c772b;g3g4,c6c5,c0e2,i6i5,e2c4,f9e8,c4e2,b7b1,e3e4,b1c1,a0c1,i9i6,g0i2,i6i8,c1c5,h7e7,c5a6,e7c7,i3i4,c7d7,a6b8,e6e5,b8e8,d9e8,e2c4,d7c7,e0e1,i8i7,g4g5,h9g7,e1e2,e8d9,e2d2,g7e8,c4a2,a9a3,g5f5,e5e4,f5f6,i7e7,c3c4,a3a6,f0e1,e4e3,c4c5,a6f6,a2c0,e7e6,c0e2,c7c8,c5b5,f6f2,b5b6,e6e7,d2d1,c8c1,e1f2,c1a1,d1d2,e3e2,d2d1,a1c1,d0e1,i5i4,e1d2,e8g7,b6a6,g6g5,d2e1,i4i3,e1f0,b9a7,i2g0,d9e8,a6b6,e7c7,g0i2,g7f5,i2g4,c9e7,g4i2,c1b1,b6b7,e2f2,b7b8,c7c6,b8a8,f5d4,i2g0,b1b9,f0e1,c6f6,d1d2,f6d6,e1f0,d6b6,a8b8,b6f6,g0e2,f6f3,e2c0,g5g4,c0e2,f3b3,f0e1,b3b7,e1f0,f2g2,e2c0,b7d7,b8b9,g9i7,c0e2,g4g3,f0e1,g2h2,d2d1,e8f7,e1f2,g3f3,d1e1,h2g2,e1e0,e7c9,e0e1,i7g5,e1f1,d4b3,e2c0,b3d2,c0e2,i3h3,b9c9,a7b9,f1e1,g2f2,c9d9,d7d9,e1d1,h3g3,d1e1,d9c9,e1d1,f2f1,d1d0,g5i7,e2c4,d2e4,c4e2,c9c7,d0d1,i7g9,d1d0,f3f2,d0d1,c7c6,d1d0,c6e6,e2g4,e9e8,g4i2,f1g1,i2g4,e6f6,d0d1,e4c3,d1d2,f2g2,g4e2,f6f2 +a718640d;a0b2,h7h4,b2b5,b7b6,b5e5,c9e7,e5d5,b6b8,d5d2,b8f8,d2d8,h4h7,a3a4,b9a7,g3g4,h7h4,c0a2,i6i5,d8d7,e9e8,a4a5,a7b9,a2c4,f8g8,d7d9,h4c4,d0e1,i9i8,e0d0,i8i9,d9d5,b9d8,d5f6,e8e9,f6f9,e9f9,e1f2,d8f7,c3c4,a9c9,d0e0,g6g5,e0d0,g8d8,e3e4,h9i7,e4e5,e7c5,g0i2,i9i8,d0d1,c9e9,e5f5,a6a5,f5g5,i5i4,d1d2,e9e8,f0e1,d8d5,e1f0,i7g6,d2d1,i4h4,f2e1,d5f5,i2g0,i8i6,c4c5,f5d5,c5b5,h4h3,d1d0,g6f8,g5g6,d5i5,g6g7,f7e5,g7f7,e5f3,f7f8,f9e9,f8f9,e9f9,d0d1,e8c8,e1d0,f3d2,d1d2,c8c7,b5a5,i6f6,d0e1,c7c9,g0i2,f6f4,i2g0,i5g5,a5b5,c9c8,d2e2,c8i8,b5b6,g5g0,b6c6,h3i3,c6d6,i8i7,d6d7,i7i5,e2d2,f9e9,e1d0,f4g4,d2e2,g0g1,e2f2,e9d9,f2f1,g4g5,f0e1,i3h3,d7d8,d9e9,e1d2,g5b5,d8d9,e9d9,f1f0,g1d1,d0e1,i5f5,e1f2,b5b6,d2e1,d1d7,f0f1,f5b5,e1d2,b5b1,f1f0,d7d6,f2e1,e6e5,e1f2,d9d8,f0e0,b1b3,d2e1,b6b4,e0f0,b4i4,f0f1,b3b0,e1d2,h3g3,f2e1,i4i6,f1f2,d6e6,f2f1,d8e8,e1f0,b0b1,d2e1,b1a1,f1f2,i6i2,f2f1,i2g2 +8ca924f4;g3g4,b7c7,c3c4,c7a7,a0c1,b9c7,c1b1,h7h5,b1d2,h9i7,d2d8,a7a3,e3e4,h5h9,d8h8,c9e7,h8a8,h9h4,a8c7,a3a4,c7e7,g9e7,i3i4,h4h3,f0e1,h3h8,c4c5,i9h9,e1f2,h8h0,g0i2,a9a7,i4i5,f9e8,c0a2,h9h1,c5b5,h1h6,e0f0,h0g0,a2c4,i7g8,b5c5,a4b4,f0f1,a7c7,c4e2,e7c9,c5b5,a6a5,i5i6,b4g4,d0e1,h6i6,b5c5,i6i2,e1d0,i2i9,e2c4,e8f9,e4e5,g0f0,e5f5,g8i7,c4a2,g4g2,d0e1,g2i2,f1f0,c7a7,f5g5,a7d7,c5c6,d7d4,g5f5,i9g9,f0e0,d4d0,e1d0,i2i5,a2c4,c9a7,c6d6,a5a4,d0e1,i5i6,c4e2,a7c9,f5f6,i6i5,d6d7,i5i1,d7d8,e6e5,e1f0,a4b4,e0d0,i7g8,e2c0,i1b1,d8e8,f9e8,d0e0,b1e1,f2e1,g9h9,f6e6,g6g5,e1f2,b4c4,f0e1,c9a7,c0a2,a7c5,e6f6,c4c3,e1d2,e5e4,f6g6,h9h4,f2e1,g8i9,g6h6,h4i4,h6h7,e8f9,h7g7,g5g4,e1f2,i4h4,g7f7,e4e3,f7f8,h4h1,f8e8,d9e8,d2e1,h1h2,e0d0,e8f7,d0d1,h2i2,d1d0,i2h2,a2c0,g4f4,d0e0,e9e8,e1d2,i9h7,d2e1,f4f3,e1d0,h2h3,e0e1,e8f8,e1f1,f7e8,d0e1,c3d3,e1d2,h3h5,c0e2,e3e2,f1f0,h5h1,f0e0,e2f2,d2e1,d3e3,e0f0,h1e1 +827ff72c;a3a4,b7e7,a0a1,h7h3,d0e1,g9i7,e1f2,b9a7,a1a3,h9f8,c3c4,e7d7,c0a2,h3e3,a4a5,i7g5,a3e3,g5e7,g0e2,d7d2,g3g4,g6g5,e3g2,d2d6,g2g0,a6a5,a2c0,i9h9,g0g3,h9h4,c0a2,f9e8,a2c0,h4h6,g3a3,e6e5,a3a2,d6d1,e0d0,a9b9,f2e1,f8e6,d0d1,e7c5,a2a7,e6d8,e2g0,g5g4,a7c6,d8c6,e1d2,c9e7,c4c5,h6g6,g0i2,e5e4,i2g0,b9b1,d1d0,g6h6,d0e0,e8d7,d2e1,d9e8,c5c6,e4d4,c6b6,h6b6,e1d0,b1g1,g0i2,d4d3,f0e1,g4h4,i3i4,e7g9,i2g0,b6e6,c0a2,g1g8,e0f0,e6c6,a2c0,c6c4,g0i2,g9e7,c0e2,g8g0,e2g0,c4c1,g0e2,c1c0,i2g4,e8f9,f0f1,c0a0,e1f0,h4h3,f0e1,e7c5,e2c0,e9d9,e1f2,a0c0,g4e2,h3i3,f1f0,i6i5,e2g0,c0c2,i4i5,f9e8,g0e2,c2c3,d0e1,c3c4,f0f1,d9d8,i5i6,c4d4,e2g4,e8f9,g4i2,d4a4,i6i7,a4d4,i7h7,f9e8,f1f0,d8d9,i2g4,i3h3,e1d0,h3i3,d0e1,d4c4,g4e2,i3h3,e2c0,c4c0,f0f1,h3i3,e1d0,e8f7,h7i7,c0b0,f1e1,b0a0,i7h7,i3h3,h7h8,d3d2,e1e0,c5a7,e0e1,a0d0,e1f1,d0d1,f2e1,a5a4,h8g8,h3h2,g8f8,d7e8,f8f9,d2e2,f9g9,d1e1,f1f0,d9e9,g9f9,e9d9,f9e9,d9e9 +0bf1875a;a0a6,h7h4,a6c5,h4e4,c0e2,i6i5,c3c4,b7b0,d0e1,b0b2,c5h5,g9e7,h5g7,d9e8,g7g8,e9d9,g8e8,c6c5,e2c0,a9a5,g3g4,e4e1,g0e2,i9i6,e2g0,i6i9,e8c7,d9d8,c4c5,b2b6,c7b7,b9c7,b7e7,b6c6,e0e1,c7a8,e7e9,a5a3,e9h9,c6c0,h9h4,c0g0,g4g5,g0g4,h4h0,i9h9,h0i0,g4g0,c5c6,h9i9,i0g1,g0g5,g1g2,g5a5,g2f2,a5e5,e1f1,a3a5,f2e4,a5a2,e4c5,e5d5,c5i5,i9i7,c6c7,d5d4,i5f5,a2i2,f5f4,i2i3,f4f7,d8e8,f7g9,d4f4,g9i8,e8e7,i8g8,e7f7,g8d8,f7e7,d8c8,e7f7,f0e1,i3i5,f1f2,i5f5,c7d7,i7i9,c8c1,f4b4,f2e2,b4b1,d7c7,f5i5,e1d2,b1b0,c1i1,b0b8,d2e1,i9i8,e3e4,b8b5,i1g0,i5h5,e1f0,e6e5,g0i1,i8h8,i1i7,f7f8,i7g7,b5b2,g7g8,f8f7,g8e9,f7f8,e9f7,f8f7,c7c8,h5g5,e2e1,h8c8,e1e0,c8h8,e4e5,g5g0,e5f5,b2b8,e0e1,h8h9,f5e5,b8d8,e5d5,h9h4,d5e5,d8b8,e5e6,g0f0,e6f6,f0f6,e1e2,f6f4,e2d2,b8g8,d2e2,h4h3,e2d2,g8d8,d2d1,h3h2,d1d0,f4i4,d0d1,h2b2,d1e1,b2h2,e1e0,i4d4,e0e1,d4c4,e1e0,d8h8,e0d0,h8h4,d0e0,c4d4,e0e1,h2b2,e1e0,b2c2,e0e1,h4e4,e1e0,d4d1 +42c659d8;a3a4,b7b2,a0a6,b2c2,a6c6,h7h0,i3i4,c2f2,c6e5,g9e7,e5i5,h0h3,c0a2,h3h4,i5g4,a9a6,g4g6,f2b2,e0e1,i6i5,g6i7,e7g5,e1e2,h4h0,i7g7,a6a7,c3c4,i9i6,i4i5,i6g6,g7a7,h0h7,e2e1,b2b6,a7f7,e9e8,f7g7,e8d8,g7h7,b6b2,c4c5,g6g9,h7h4,g9g8,g3g4,b2h2,c5d5,d9e8,a2c0,h9i7,e1d1,h2g2,h4h7,b9c7,h7f6,c7d5,i5h5,g8g7,d1e1,g2c2,e1e2,d5f4,f6f4,g7g9,h5g5,c2c4,f4f2,c4c5,f2f6,c5c8,e3e4,c8c4,f0e1,i7g6,e1d2,g6i7,g5h5,i7h9,g4g5,c4d4,f6h6,d8d7,h6i8,d7e7,e2f2,d4b4,i8f8,e8f7,f8d8,e6e5,d8g8,e7d7,g8i7,g9g7,h5i5,c9e7,e4e5,b4h4,g5h5,g7g2,f2f1,h4h0,d0e1,h0h4,e1d0,h4h1,i7e7,d7d8,h5h6,g2g7,d2e1,g7g0,h6g6,g0h0,e7c7,h1h3,e1f0,h0g0,i5i6,h3h6,c7f7,d8d9,f0e1,h6h3,g6f6,h3d3,i6i7,d3d5,e5f5,d5d4,i7h7,f9e8,f7g7,d4d3,h7h8,d3d7,g7g5,d9d8,g5g7,d7d1,e1f0,d1d5,g7e6,d8d7,e6a6,e8d9,f1e1,d5d1,a6e6,d1d5,e6e4,g0g6,e1d1,d5d6,h8g8,d6d4,f6g6,h9g7,e4d6,d7e7,d6f7,e7e8,g6f6,d4d7,f7d7,g7e6,g8g9,e6d8,d0e1,d8b9,d1d2,b9d8,d7f7,d8e6,f6e6 +bfe73b5b;c0a2,h9g7,f0e1,b7e7,a2c4,h7h2,a0b2,e7e8,b2f2,h2h8,f2f7,h8f8,f7f3,a9a7,g3g4,a6a5,e1f0,e8a8,g4g5,f8f4,e3e4,g6g5,f3g1,i9i8,g1a1,a8h8,a1a2,h8e8,a2i2,a5a4,i2e2,a4a3,e2e1,e8h8,e1g1,i6i5,g1g2,f4c4,g2h2,g7h9,h2f1,h8h6,g0i2,a7c7,f1c1,i8f8,i3i4,f8e8,c1c4,h6h7,c4d4,h7h5,c3c4,e8e7,d4d6,g5g4,i2g4,a3b3,c4c5,i5i4,d6e6,d9e8,g4i2,c9a7,i2g0,h5h2,e6e7,c7e7,e4e5,g9i7,e0e1,a7c5,e1d1,e7d7,e5d5,c5a7,g0i2,d7f7,d5d6,b9d8,d6d7,h2b2,d1d2,f7f0,d7d8,f0h0,i2g4,a7c9,d0e1,b3a3,g4i2,h0h8,e1f0,h8h7,d8d9,e8d9,d2d1,h9g7,d1d0,b2e2,i2g4,e2a2,g4e2,g7h5,e2g0,h7b7,d0d1,i4h4,g0e2,i7g9,e2g0,g9e7,d1e1,b7b5,e1f1,h5f6,f1e1,b5e5,e1d1,a2f2,f0e1,e5d5,e1d2,e7c5,g0i2,d5d7,d1d0,f2e2,d0e0,e2e7,e0f0,e7e3,f0e0,d7d2,e0e1,e3e2,e1e0,d9e8,e0f0,d2c2,i2g4,e2e3,f0e0,e3f3,e0e1,h4i4,e1f1,c2f2,f1e1,e9d9,e1e0,f6g4,e0e1,f2f0,e1e2,f3f6,e2e1,f6f5,e1e2,f0f4,e2e1,f5f7,e1e2,e8d7,e2d2,f4e4,d2d1,e4e6,d1d2,f7i7,d2d1,i7i9,d1d2,i4i3,d2d1,c5e7,d1d2,g4f2 +e89bd94d;g0e2,b7g7,d0e1,g7d7,a0a1,d7d0,e1f2,c9a7,e2g0,b9d8,a1a6,a7c9,g3g4,d0f0,e0f0,e9e8,a6a4,h7c7,a4a8,e8e7,a8a6,h9g7,a6b8,a9a3,b8b2,a3a6,b2e2,a6a7,f0f1,i9i8,g0i2,i8h8,i3i4,h8h9,e2d0,c7c8,c0a2,h9h5,c3c4,a7a6,d0g0,a6b6,g0g2,g7i8,c4c5,e7e8,g2i1,h5h7,c5b5,e8e7,g4g5,c6c5,a2c4,h7i7,c4e2,i7g7,e2c0,g6g5,i1g2,g7h7,c0a2,c8a8,e3e4,c5c4,g2h0,e7d7,a2c0,d8b9,b5c5,a8a0,h0d0,a0d0,f1e1,b6b5,c5c6,c4d4,c6c7,b9c7,e1f1,f9e8,c0a2,h7h8,a2c0,d0f0,i4i5,i8h6,i2g4,d4c4,f1e1,b5d5,c0e2,h8g8,e2g0,c7a8,g0i2,d5d4,e1f1,h6g4,i2g4,f0h0,i5i6,g8h8,f2e1,h0h4,e1f2,e8f7,e4e5,h8h5,f1f0,d4d2,f0e0,d2d3,g4e2,a8b6,e5d5,d3d5,i6i7,d9e8,f2e1,b6c8,e0f0,d7d8,e2c4,g9i7,e1d0,d5d0,f0f1,d0a0,c4e2,h4g4,f1f2,a0i0,f2f1,e8f9,e2g4,h5h7,g4i2,i0h0,f1f2,h7h4,i2g4,h0a0,g4i2,a0a7,i2g0,a7a4,f2f1,a4a8,f1f2,h4f4,f2e2,c8a9,g0i2,f4b4,e2e1,f7e8,i2g4,g5g4,e1e0,i7g5,e0e1,e8d7,e1f1,d8d9,f1e1,f9e8,e1f1,b4b1,f1f0,a8a1,f0e0,a1a4,e0f0,e6e5,f0e0,a4b4,e0d0,b4e4 +44e8504d;a0c1,i9i7,i3i4,c6c5,c1g1,b7b8,g1g6,b8h8,g6g5,h7a7,i4i5,h8h1,g5f3,d9e8,f0e1,h1h3,i5i6,e8d7,c0e2,h3h8,f3g5,i7h7,g5f3,h8b8,e1f0,h7e7,f3f5,e6e5,f5f4,e9e8,f4h4,e7e6,h4g6,b8b3,g6h8,e8e7,h8h5,a7b7,e2c4,a9a7,h5h4,b3e3,a3a4,e6f6,h4d4,f6f4,d4e4,f4e4,i6h6,d7e8,h6g6,b7c7,a4a5,c7c8,g6f6,e8d7,c4a2,e3e2,a2c0,e4e3,g0e2,c5c4,a5a6,a7a9,c0a2,e3g3,a2c4,c8f8,e2c0,g3g1,c4a2,h9g7,c0e2,e5e4,a2c0,b9c7,f0e1,g1g0,e2g0,g7f5,c0a2,c7a6,f6e6,e7f7,e0f0,a9a7,g0i2,g9i7,a2c4,a7a8,e1d2,a8a7,i2g4,a7c7,d0e1,c7c6,c4a2,f8c8,f0f1,f7f8,e6e7,c6h6,e7e8,f9e8,g4e2,c9a7,e1d0,e8d9,c3c4,a7c5,f1f2,d7e8,a2c0,h6h0,d2e1,a6b8,e2g4,h0d0,f2e2,c5a7,c4c5,f5g7,e1f2,g7h5,c0a2,h5f6,a2c4,d0d3,e2e1,d3f3,c4a2,f6h7,e1e2,f3f5,c5b5,f5f7,a2c4,f8f9,b5c5,e4d4,f2e1,f7e7,e2d2,c8c7,g4e2,b8a6,d2d1,c7c8,d1d2,e7e2,c4e2,a6c5,e1d0,c5a6,d0e1,i7g9,e2g4,h7f6,e1f2,a7c5,g4i2,a6b4,f2e1,f6d5,i2g0,e8d7,e1f2,b4c2,g0e2,c2b0,e2g0,c8e8,g0e2,e8h8,e2c0,d7e8,c0e2,h8h2 +c8535d91;f0e1,g6g5,g0i2,h7d7,i3i4,c9a7,i2g0,d7i7,e1f2,b9c7,a0a2,e6e5,a2a6,h9g7,a6c5,f9e8,g0i2,b7b2,c5a4,e8f7,i2g4,d9e8,a4a7,b2d2,a7f7,e9d9,c3c4,a9a3,f7c7,d9e9,i4i5,a3a8,c7c8,a8c8,c0a2,e8f9,e0f0,g7h5,i5h5,d2d6,c4c5,d6d8,c5d5,i7g7,h5g5,d8d9,d0e1,g7h7,g5h5,f9e8,d5c5,g9e7,a2c0,e8d7,f0f1,d9c9,e3e4,c8d8,f1f0,c9c5,c0a2,i6i5,e4e5,i9g9,a2c4,g9g5,e5f5,g5g6,c4e2,h7g7,h5h6,c5a5,f0e0,a5c5,f5e5,g7g4,e1f0,g4d4,f0e1,g6g3,e0d0,d4d6,d0d1,g3f3,h6h7,f3e3,e5f5,d8e8,e1d0,e3e4,f5f6,e8i8,f6g6,d7e8,g6f6,e9f9,f6f7,e7g9,f7f8,i8f8,f2e1,e8d9,e2g0,e4b4,g0i2,b4b8,i2g0,d6d8,h7i7,f8g8,g0e2,b8b0,i7h7,b0a0,e2c4,c5b5,e1f2,a0a3,c4a2,b5b6,f2e1,f9f8,d1d2,b6b1,h7g7,a3a5,a2c4,b1a1,e1f0,a1a0,d2e2,d8d1,f0e1,a0a1,e1f0,a5b5,e2d2,g8g7,d0e1,g7e7,d2d1,e7e3,c4a2,f8f7,e1d2,c6c5,f0e1,b5b6,e1f0,e3e4,d1d0,e4f4,d2e1,b6c6,e1f2,c6g6,d0e0,f4f2,a2c0,g6g4,c0e2,a1a2,e2c4,c5c4,e0d0,f2f0,d0d1,f0i0,d1e1,g4g1,e1e2,g1i1,e2d2,a2a7,d2e2,i1f1,e2d2,i0d0,d2e2,a7a2 +103a6bb3;a0a1,b7b6,i3i4,b6b1,a1a2,h7b7,a2d2,b7e7,d2b1,e7g7,d0e1,g7b7,b1b0,a9a7,b0b9,f9e8,c3c4,h9g7,b9d9,e8d9,g0e2,i9i7,e0d0,b7b3,e1f2,c6c5,e3e4,b3b6,d0d1,a7d7,d1e1,d7e7,g3g4,g7h9,e4e5,b6c6,e5e6,c6c7,c4c5,h9g7,i4i5,e7d7,e1f1,d7d3,e6f6,d3c3,i5i6,i7i9,f6e6,g7f9,c0a2,c3c2,f1e1,i9i6,e1f1,g9i7,c5d5,c7c5,a3a4,f9h8,d5d6,i6h6,f1e1,c2a2,a4a5,c5c2,e6f6,c2c6,f6e6,h8f7,e2c4,f7h8,e1e0,h6h1,e0d0,c6c7,c4e2,a2e2,f2e1,e9e8,d6c6,e2e1,e6d6,e1a1,d6e6,h8i6,a5a6,i6g5,c6c7,h1g1,c7d7,g1g3,a6a7,a1a7,e6e7,c9e7,d0d1,g3c3,d7d8,e8f8,d1e1,c3c2,d8d9,a7b7,g4g5,f8f9,g5g6,b7b6,e1e0,c2g2,g6f6,b6b5,f6g6,f9f8,g6f6,g2h2,f0e1,b5b3,e1d2,b3b5,e0d0,b5g5,f6e6,h2i2,e6f6,g5g7,d9e9,i7g5,e9d9,g7f7,f6e6,i2g2,d9c9,g2h2,d0e0,f7f2,e6f6,h2h6,c9d9,h6h9,f6f7,f8f7,d9e9,f2f4,e9f9,f4d4,f9e9,d4d5,e9d9,h9g9,e0e1,d5d3,d9e9,e7c5,e9f9,d3g3,e1d1,f7f8,f9e9,g3d3,e9d9,g9d9,d1d0,d9d8,d0d1,c5e7,d1d0,d3f3,d0d1,f3f4,d1d0,f4f2,d0d1,e7c5,d1d0,d8e8,d2e1,f2d2,d0e0,e8e4 +010bafbd;a0b2,e9e8,b2i2,b7b3,i2i1,h7g7,i3i4,b9a7,e3e4,i6i5,i1d1,i9i6,d1d4,g7c7,d4e2,i6i9,i4i5,e8f8,e0e1,b3b4,c0a2,a6a5,a2c0,b4b2,g3g4,b2b1,e2h2,c7c8,h2f1,b1f1,e1e0,a5a4,c0e2,g6g5,e2c0,f9e8,a3a4,i9i6,c3c4,i6i7,g0i2,g5g4,e4e5,i7d7,i2g4,c8c4,e5f5,c4f4,e0e1,f1f5,c0a2,f5f7,e1e2,a7c8,d0e1,c9e7,g4i2,f4f2,a2c0,g9i7,i2g4,a9b9,i5i6,c8a9,i6i7,d7d4,i7i8,b9b0,i8h8,e7c9,g4i2,b0c0,a4a5,f7f6,a5a6,d4d7,e1d0,d7d5,h8g8,f8f7,a6a7,c9a7,d0e1,d5d2,e2d2,f2e2,g8f8,c0a0,d2e2,c6c5,i2g4,a0a2,e1d2,f6f2,f0e1,a2a6,e1f0,h9i7,e2e1,f2f8,g4e2,a6a4,e1d1,i7g8,e2c4,c5c4,d1d0,e8f9,d0d1,f8e8,f0e1,a4a5,e1f0,f7f8,f0e1,a5a2,e1d0,c4c3,d2e1,a2i2,e1d2,e8b8,d2e1,i2i0,e1f0,d9e8,f0e1,i0d0,d1d0,g8i7,d0d1,b8b1,e1f0,a9b7,f0e1,i7g6,e1d2,g6h4,d1d0,e6e5,d2e1,b7a9,e1d2,b1b7,d0e0,b7d7,e0e1,h4f3,e1f1,d7b7,f1f2,b7f7,f2e2,f3d2,e2f2,e5e4,f2e2,e8d7,e2e1,f7f4,e1d1,c3d3,d1d0,f4h4,d0d1,a7c9,d1e1,c9a7,e1d1,d2b3,d1e1,f9e8,e1e2,e8f7,e2e1,d3d2,e1e0,b3c1,e0d0,f8e8 +98b062be;e3e4,a9a7,g0e2,b7b5,e2g4,b5b1,c0e2,b1b4,a0b2,b4c4,b2c2,h7i7,e2c4,a7g7,d0e1,g9e7,c2b2,d9e8,b2c0,g6g5,i3i4,g5g4,c0a1,b9a7,a1c1,g4g3,c1d3,a7b9,i4i5,g7h7,d3d9,e8d9,c4a2,e9e8,e1d0,h9g7,f0e1,h7h5,e0f0,i9i8,i5i6,h5e5,a2c4,c6c5,c4a2,g7f5,e1f2,a6a5,a2c0,i7h7,a3a4,e8f8,d0e1,f9e8,c0e2,g3f3,f0e0,e5d5,a4a5,h7h2,c3c4,f8f9,i6h6,f3f2,e4e5,d5d4,e0f0,i8i7,e2g0,h2g2,e1f2,e8f7,h6h7,d4c4,h7i7,g2g3,g0i2,g3c3,a5a6,c3e3,i7i8,c4e4,e5d5,e3h3,d5e5,e7g5,a6a7,h3h5,i8i9,e4e5,i2g4,h5h8,i9h9,e5e0,f0f1,h8c8,h9g9,f9e9,g4e2,b9c7,g9h9,c8d8,h9g9,d8e8,g9h9,e8g8,e2c0,c7b9,a7a8,e0g0,c0e2,g0i0,e2c0,i0f0,f1e1,f0e0,e1d1,e0c0,d1d2,c0a0,d2e2,f5g7,h9g9,g8d8,e2d2,a0b0,a8a9,d8c8,a9b9,c8c7,g9h9,b0e0,b9c9,e6e5,f2e1,e0i0,h9i9,i0i9,e1d0,g5i7,d2d1,i9h9,d1d2,c7a7,d2d1,h9h0,c9b9,h0h3,d1d2,g7f9,d2e2,f7e8,d0e1,a7a1,e2f2,h3h8,b9a9,h8g8,e1d0,i7g9,d0e1,g8g1,a9b9,a1d1,e1f0,g9e7,f0e1,g1e1,b9a9,d1d0,a9b9,e5e4,b9a9,f9h8,a9b9,d0c0,b9c9,e8f7,c9d9,e9d9 +3c20ddb2;a3a4,i6i5,a0b2,a6a5,b2b9,g9i7,g0e2,a5a4,e3e4,h7h1,f0e1,e9e8,e1f2,h1h8,e0e1,e8f8,b9b8,h8b8,e1d1,b8b9,e2g4,c6c5,e4e5,a4b4,e5d5,i5i4,d5c5,b7g7,g4e2,a9a4,c5c6,f8e8,c6b6,g7e7,d1e1,h9f8,i3i4,e7g7,e2g0,e8d8,e1e0,g7d7,c0a2,d7a7,f2e1,a7a5,e1f2,a5a7,d0e1,a7a2,g0i2,a2i2,e0f0,i9i8,f0e0,b9b8,e1d0,e6e5,c3c4,b8a8,e0e1,a8a9,e1f1,d8e8,d0e1,a4a5,g3g4,a9a7,e1d0,a7h7,g4g5,i2h2,b6a6,h7f7,g5f5,f7f6,a6a7,f6f2,c4c5,f2c2,d0e1,e5e4,e1f2,a5a2,f1e1,c9a7,e1d1,i7g9,c5c6,e8e9,d1e1,i8i5,f5e5,i5i8,c6c7,a2a5,e5d5,a7c5,e1e0,a5a0,e0e1,a0a4,d5c5,i8g8,c7b7,a4a5,i4i5,a5c5,i5i6,g8i8,e1e0,h2h1,b7c7,c5i5,f2e1,h1i1,c7d7,f9e8,i6i7,e8f9,e1f2,i1h1,i7h7,h1e1,e0e1,i5i1,e1e2,i8i6,h7i7,i1d1,d7c7,c2f2,i7h7,b4b3,c7d7,d1g1,d7d8,g1d1,d8e8,f9e8,h7h8,d1c1,e2d2,c1c9,h8h9,f2f7,d2d1,f7f5,d1e1,i6i7,h9i9,i7i2,e1f1,c9c3,f1f0,c3g3,f0e0,e9f9,e0e1,i2e2,e1f1,g3h3,i9h9,h3h9,f1f0,b3a3,f0f1,f5i5,f1f0,i5c5,f0f1,c5c9,f1f0,h9h8,f0f1,h8i8,f1f0,i8h8,f0f1,c9c0,f1f0,e2e1 +1cdb1296;i3i4,b7b2,c0a2,i9i8,g0i2,b2b8,a0c1,h7a7,i4i5,b8b2,c1i1,b2b3,i5h5,i8h8,i1d1,f9e8,d0e1,b3b0,d1d9,e8d9,e1f2,a7e7,h5g5,h8c8,a3a4,e9f9,a4a5,b0a0,g5f5,f9e9,e0d0,a6a5,f2e1,e7b7,i2g4,b7b3,f5e5,a9a7,g4e2,b3b8,e1d2,a7h7,e2g4,c8h8,g4e2,i6i5,g3g4,c9e7,f0e1,b8b1,e3e4,g6g5,e1f0,e9f9,d0d1,b9d8,e5d5,h7f7,d5c5,i5i4,c3c4,f7f8,d1d0,f8f4,e2g0,a0f0,c5c6,f4f6,g0e2,f6f8,d2e1,f8g8,e1f2,g5g4,d0d1,d8b9,f2e1,h8h3,c6c7,g8d8,e1d2,b9c7,d1e1,f0g0,e2c0,d8d2,c4c5,d2c2,c5d5,c2i2,a2c4,i2d2,d5e5,d2d7,c0e2,b1c1,e2g0,h3i3,e5e6,h9g7,e6f6,c1d1,e1f1,g7h9,f6f7,d7d5,f1e1,d5d6,e1e0,d6f6,g0i2,f6f1,f7e7,d1c1,i2g4,f9f8,e7e8,d9e8,e4e5,f1i1,e5e6,i3h3,e6f6,h3h1,f6g6,g9i7,c4e2,h1h3,e2c0,h9g7,g4e2,e8d7,g6f6,c7b9,f6g6,b9d8,e0d0,a5a4,g6g7,h3h5,g7f7,d8f7,e2g4,i1f1,g4e2,h5h9,d0e0,h9h6,c0a2,h6d6,e2c0,c1c4,a2c4,d6h6,c0e2,f1d1,c4a2,d1h1,a2c0,h1a1,c0a2,a1a0,a2c0,f7e5,e2g4,h6b6,g4i2,e5d3,i2g0,b6b2,g0e2,d3c1,e0d0,c1d3,d0d1,a4b4,c0a2,b2c2,e2g4,c2a2,g4i2,a2i2 +73c67082;f0e1,h7h3,e1f2,c9a7,a0a2,g9e7,a2a1,d9e8,g0e2,h9i7,a1b3,a9a8,b3b4,a6a5,b4d5,e8d7,d5e5,b9d8,e5d3,a5a4,g3g4,e9d9,d3h3,b7b2,h3h0,d8b9,h0h1,b9c7,g4g5,b2a2,h1h8,i7g8,h8h3,e7g5,h3h1,a8f8,e2c4,g8i7,h1i1,c6c5,i1h3,f9e8,e3e4,i6i5,h3h0,i7h5,h0f0,f8g8,c0e2,h5i3,f0f1,c7d5,e0f0,i3g2,f1g1,e6e5,f0f1,g8g7,g1g2,g5i7,g2f0,e8f7,f0e0,g6g5,e0e1,i9g9,f1f0,d9e9,e1c0,a4a3,c0c2,e5e4,e2c0,a2a1,c4e2,g9g8,d0e1,e4d4,e2g4,g8e8,c2a2,e8e1,a2d2,d5f4,g4i2,g7g6,c3c4,g6c6,d2d3,i5i4,i2g0,c6b6,c4c5,e1f1,f0f1,f4d3,c0e2,d3f4,c5b5,e9e8,b5b6,i4h4,e2c0,d4e4,f1f0,a1a2,g0i2,f4g2,f0e0,g5g4,i2g0,e4e3,f2e1,g2h0,b6b7,a2d2,e0d0,e3d3,e1d2,a7c5,d2e1,h0g2,b7c7,g2f0,g0i2,g4f4,e1f2,f0h1,i2g4,f4g4,f2e1,d3c3,d0e0,a3b3,c0e2,i7g9,e1d0,h1g3,d0e1,g9i7,e0d0,g3h1,e1d2,h1f2,d0d1,f2e4,e2g0,c3d3,c7c8,d3d2,d1d0,e8e9,g0e2,e4c3,d0e0,h4h3,c8d8,c5a7,e0e1,d2d1,e1f1,b3a3,e2g4,h3i3,d8e8,f7e8,g4e2,c3e2,f1f2,i3i2,f2f1,e8f7,f1f2,e2c1,f2f1,f7e8,f1f2,i7g9,f2e2,e9f9,e2d2,c1b3,d2e2,a7c5 +8f9d7abe;g0e2,h7g7,a0a1,b9c7,a1h1,b7a7,h1a1,i9i8,a1g1,f9e8,c0a2,a7b7,a3a4,c6c5,a4a5,g7g3,e2g4,g3c3,g1g2,c9a7,f0e1,g9i7,g4e2,i7g9,g2g5,i6i5,g5e5,a6a5,a2c4,i5i4,e5g4,e8d7,g4f6,c5c4,e2c0,i4h4,e1f0,a9a8,f6e6,e9f9,e6c7,c3c2,c7a7,i8d8,a7b7,h9g7,b7b8,c4c3,b8d8,g7e8,d8e8,c2h2,e8e5,a8c8,e5e8,h2g2,e8g9,f9f8,g9g7,c8c6,e0e1,a5a4,g7e6,f8f7,e6e5,c6e6,e5d5,g2d2,d5c5,e6e7,e1e0,g6g5,c0a2,d2g2,c5c3,e7e8,c3e4,g2c2,a2c0,c2c6,e4c3,h4g4,c3d5,e8d8,d5b4,d8i8,b4a4,c6i6,a4a3,i8f8,e0e1,f8b8,a3a5,b8b4,a5a2,i6g6,a2a1,g6h6,a1b1,h6e6,c0e2,g4g3,b1b4,f7e7,b4c6,e6d6,c6d6,d9e8,e1f1,g3f3,d6e4,e7f7,i3i4,e8d9,e4h4,f3f2,f1e1,f2e2,e1e2,d7e8,h4g6,e8f9,g6b6,g5g4,e2d2,f9e8,b6b2,g4f4,f0e1,e8f9,b2b5,f4f3,b5c7,f7f8,c7d9,f8e8,e3e4,f3e3,d9d8,e8e9,d8h8,f9e8,h8h3,e8f9,h3f4,e3e2,d2d1,e9e8,f4f8,e8e7,f8f0,e2f2,f0h1,e7e8,e1f2,e8f8,h1h6,f8e8,d1e1,e8e9,h6h0,f9e8,h0i2,e8d9,e1e0,e9f9,i2i1,f9e9,i1e1,e9f9,i4i5,f9f8,e1f3,f8e8,f3f6,e8d8,f6h5,d8d7,h5f6,d7d8,e4e5,d9e8,f6e8,d8d9,i5h5 +84ad7a5b;a0a2,g9i7,g3g4,h7h8,a2g2,h8b8,f0e1,c9e7,g2e2,f9e8,e2c2,e8f7,c0a2,i7g5,c2g2,b8d8,e1f2,g5i7,a2c0,e9f9,a3a4,e6e5,g2g1,d8i8,c3c4,i8f8,g4g5,f8g8,g1c1,b7b4,c1e2,b4b1,e2c2,b1b7,c2a3,g8g9,e0f0,b7a7,a3c3,g9g7,g0i2,i7g9,g5f5,g7g8,c3b3,a7a4,f5g5,g8c8,g5g6,c8c4,i2g0,i9i8,b3b1,i8a8,b1g1,a8d8,g1g9,e7g9,g6h6,f9f8,e3e4,a4a0,f2e1,d8e8,e1d2,e8d8,h6g6,a0a3,c0a2,a3e3,i3i4,e3c3,g6f6,c3a3,i4i5,a9a8,f6f7,f8f9,f0f1,c4c2,f1f0,d8d6,g0e2,a8a9,a2c0,d6g6,e4e5,a3h3,e5d5,h9g7,d2e1,h3i3,f0e0,a6a5,e2g0,c2b2,e1f0,i3i2,f7g7,b2b4,g0i2,g6g7,d5c5,g7a7,c0a2,c6c5,a2c4,a7d7,i5h5,b4b7,c4e2,d7e7,f0e1,e7e3,e1d2,a9a6,h5h6,a5a4,h6i6,e3f3,e2c0,a6i6,c0a2,g9e7,d2e1,f3f4,e1d2,i6i9,d2e1,f4i4,i2g4,e7g9,g4i2,b7b1,e1f0,b9d8,d0e1,i4i3,e1f2,i9i4,f0e1,b1b8,i2g0,i3a3,e1d2,d8b7,g0i2,i4e4,e0f0,a3i3,i2g4,a4a3,a2c4,i3b3,f0f1,e4e0,g4e2,b3b1,d2e1,g9e7,c4a2,e0b0,e2c4,b8i8,c4e2,i8a8,a2c4,a8e8,c4a2,a3b3,e2c0,b7d8,f1f0,e8g8,e1d2,g8g6,f0e0,g6g0,e0d0,d8b9,d0e0,g0g5,e0f0,g5g3,d2e1,b1e1 +2c0891f4;e3e4,g6g5,d0e1,c9a7,a0a1,b7b3,g0i2,h7h5,c0e2,f9e8,i3i4,h5h1,g3g4,g5g4,e2g0,h1h0,e1f2,b3b2,a1c1,b2b3,c1h1,b3b6,h1a1,g9e7,a1c2,h0h1,c2e1,e9f9,e1h1,e8f7,h1d1,a6a5,d1d6,b6a6,d6d8,d9e8,e0e1,e7c5,d8b7,i6i5,b7b0,i5i4,b0a2,h9f8,g0e2,i9i7,i2g4,i7g7,a2c1,a5a4,c1b1,c5e7,b1b9,a9b9,e2c0,a4a3,e1e2,b9d9,f0e1,d9d6,e1d2,f8h9,g4i2,d6d9,e2e1,a6a5,c0a2,a5f5,e1e0,d9d5,i2g0,f5i5,e4e5,g7g0,e0e1,g0h0,e5f5,h0h7,f5f6,a7c5,a2c0,c5a7,f6e6,e8d9,e1e0,h7h6,e6e7,d5a5,e7e8,f7e8,e0d0,a5a4,c0a2,a7c5,d2e1,a3a2,c3c4,a4a7,c4c5,i5h5,d0e0,a7d7,e1d0,h6f6,d0e1,f6e6,e0f0,d7d2,f0f1,h5d5,c5d5,d2c2,f1f0,e6i6,f0e0,i6i9,e1d2,c2c3,f2e1,i4h4,d5e5,c3d3,e5f5,d3d4,f5f6,d4d5,e1f0,h9i7,f6g6,a2b2,d2e1,b2b1,e1d2,i7g6,f0e1,h4h3,e1f0,b1c1,f0e1,g6e7,e1f2,c6c5,d2e1,e7c8,e1f0,d5d3,f0e1,c1b1,e0f0,d3b3,f0e0,b3d3,e1f0,d3g3,e0d0,g3g6,d0d1,e8d7,d1d0,d7e8,d0d1,i9i2,d1d0,c8e7,d0e0,g6g7,f2e1,e7d5,e1f2,g7g0,e0d0,h3i3,d0d1,i2f2,d1e1,f2c2,e1d1,g0i0,f0e1,c2e2,e1f0,e8d7,f0e1,e2e1,d1d2,d5c7 +4f726a57;d0e1,h7h1,a0b2,h9g7,b2b4,e6e5,b4b2,g7e8,b2f2,c6c5,f2f9,e9f9,g3g4,b7h7,e3e4,h1h6,e4e5,b9a7,e1d0,a9a8,e5f5,h7e7,e0e1,h6h2,e1e2,h2h3,f0e1,e7e1,e2e1,a8a9,f5f6,h3h7,e1e2,e8f6,d0e1,h7h6,g0i2,a7c8,e1d0,d9e8,e2e1,c8e9,c0e2,h6h1,e1d1,f6e4,e2c4,h1g1,d1e1,g1h1,e1e2,h1h4,i2g0,e8d9,g4g5,h4c4,d0e1,c9e7,c3c4,e4c3,e2d2,c3e2,g5g6,i9i8,g6f6,d9e8,d2e2,a9d9,g0i2,i8i7,i2g0,d9d4,f6f7,d4d3,f7f8,f9f8,e1f0,e9c8,i3i4,f8f7,i4i5,e8d9,f0e1,c5c4,e1f2,c8b6,g0i2,d3i3,a3a4,i3e3,e2d2,e3c3,d2d1,c3b3,f2e1,e7c5,d1d0,b3d3,d0e0,i7i9,i5h5,d3d7,a4a5,f7f8,i2g0,c4c3,e1d0,f8f9,e0e1,d7d0,e1e2,d0f0,h5h6,c3d3,h6g6,g9i7,g6h6,i9g9,a5a6,c5e7,a6a7,f0f8,e2e1,f8f7,g0i2,g9i9,a7b7,f7h7,i2g4,d3c3,b7c7,b6a8,c7d7,i9i8,e1e0,e7c9,d7d8,h7c7,d8d9,i8d8,h6h7,c3c2,h7i7,c2d2,i7h7,c7c1,g4e2,c1c0,e0e1,d8b8,h7g7,b8b9,e2g4,b9b3,g4e2,a8c7,g7f7,b3b9,d9e9,f9e9,e1f1,d2e2,f7g7,c0c4,f1f0,c4c0,f0f1,b9b8,g7f7,c7e8,f7e7,c0c6,e7e8,e9e8,f1f0,e8d8,f0e0,i6i5,e0f0,c6c3,f0e0,d8d9,e0f0,b8f8,f0e0,c9e7 +d0fe4639;a0b0,g9i7,b0b7,h7h8,b7b3,g6g5,g0i2,h8a8,c0a2,a8c8,a2c0,g5g4,e3e4,c8b8,c3c4,b8i8,f0e1,g4f4,b3b8,i7g9,e0f0,f4e4,b8b1,f9e8,b1b7,a9a7,b7f7,a7f7,e1f2,i8h8,c0e2,e9f9,c4c5,f7b7,c5d5,g9e7,e2g0,b7d7,i3i4,d7a7,g0e2,h8h2,i4i5,e7c5,g3g4,a7a9,e2g0,c5a7,g4g5,a6a5,g5f5,e4f4,f2e1,h2h4,e1d2,h9i7,f0f1,h4g4,i5h5,g4g1,d5e5,e8f7,d0e1,d9e8,f1f0,g1g4,e1d0,g4g3,h5h6,f9e9,d2e1,i6i5,e5d5,i9g9,f5g5,g3g4,h6g6,e8d9,e1d2,g4i4,g0e2,i4h4,i2g4,c6c5,e2c0,h4h8,a3a4,h8h9,g6h6,e9f9,h6g6,h9h0,f0e0,g9g8,g4e2,f4g4,e2c4,g8g7,d2e1,e6e5,c0e2,g4f4,e2g4,i5i4,g4i2,h0d0,e0d0,a5a4,g6f6,c9e7,i2g4,a4a3,g5g6,e7g9,d5d6,g7g6,d6d7,a3b3,e1d2,f9f8,d7d8,b3c3,d8d9,b9d8,c4a2,d8b9,a2c0,b9c7,d9c9,g6f6,c9b9,c7d9,c0e2,d9c7,d2e1,i7h5,g4i2,c7a8,e2g4,f6a6,b9c9,a6a2,g4e2,f4e4,e1d2,c3c2,d0e0,c2b2,e2g0,h5g3,d2e1,a2a1,i2g4,a1c1,e1d0,c5c4,d0e1,c1c0,e1d0,a9c9,e0f0,c9e9,g4e2,e9a9,g0i2,c0c2,e2c0,g3h1,f0f1,c2i2,c0a2,e4e3,f1e1,a9d9,a2c0,d9d3,c0e2,a7c5,e1e0,c4d4,e2g0,d3d2,g0e2,i2e2,d0e1,a8c7 +090e24f6;a0b2,f9e8,b2b4,g9i7,b4b6,e8f7,b6a4,b7b1,f0e1,h7h3,e1f2,i9i8,g0i2,h3h0,c3c4,b1b6,i2g4,g6g5,a4b6,b9a7,b6e6,e9f9,e6g7,f9e9,a3a4,h0h4,g4e2,h4h1,g7f5,i8c8,i3i4,c8g8,f5f6,h1g1,f6h6,g8b8,e0e1,b8c8,h6i8,c8a8,i8e8,f7e8,a4a5,c6c5,e1f1,a7c8,e2g4,c9e7,f1e1,e7c9,e1d1,a8b8,d1d2,b8b2,d2d1,e8d7,e3e4,c9e7,c0a2,d9e8,a5b5,e9d9,b5b6,b2f2,e4e5,f2d2,d1e1,c8b6,e5e6,b6c8,e6d6,g5g4,d6e6,i6i5,g3g4,g1g2,e6e7,d2d1,e1d1,c8e7,d0e1,a9b9,d1d2,e7f5,d2d1,g2b2,d1d0,b2b4,c4c5,b4e4,a2c4,f5h6,e1f2,h6g4,d0e0,g4i3,c5b5,b9c9,e0d0,i5i4,d0d1,c9a9,d1d0,e4e5,b5c5,e5d5,c4e2,i7g9,e2g0,h9i7,d0e0,d5d2,c5d5,d9d8,d5d6,e8f7,g0i2,d2i2,e0e1,a9b9,e1f1,b9c9,f1f0,c9c5,d6e6,i7g6,f0f1,i2i0,f2e1,i3g2,e1f2,c5f5,e6d6,g2i1,f1e1,g9e7,e1e2,d7e8,f2e1,i0f0,d6d7,d8d9,e1d2,f5g5,d7e7,g5e5,e2f2,e5d5,f2e2,d5d2,e2e1,f0f3,e1e0,d2d3,e7f7,g6f4,f7e7,f3h3,e7d7,h3e3,e0f0,e8d7,f0f1,d3a3,f1f0,e3e2,f0f1,e2e8,f1f2,e8e5,f2f1,e5g5,f1f2,g5a5,f2f1,i1g0,f1f0,a3b3,f0f1,i4i3,f1f0,d9d8,f0e0,g0f2,e0d0,i3h3 +cdfb204a;a3a4,b7d7,g3g4,d7d2,a0a2,h7h4,a2a3,c9e7,a3a6,h4h7,e3e4,d2g2,e0e1,a9a8,e1d1,a8f8,a6b6,b9c7,b6c6,f8d8,c6d4,h7h5,d4d5,d8h8,d5f4,h5h6,c0a2,c7b5,f4f9,e9f9,d1e1,g2g1,a4a5,g1g2,i3i4,h8c8,e1e0,c8c3,i4i5,c3c8,g0e2,c8h8,e2g0,i6i5,a5a6,h6h7,f0e1,h7h0,g0i2,h0f0,a6a7,h8h3,e4e5,b5a7,a2c0,h3h8,e5e6,g2e2,e1f0,i9i8,e6f6,a7b9,c0e2,h8e8,g4g5,i8g8,g5h5,b9d8,e2c0,g6g5,f6g6,d8e6,g6f6,h9i7,f6g6,i7g6,d0e1,e8b8,i2g0,e6d4,g0i2,d9e8,e1d0,g8h8,h5g5,b8b3,g5h5,g6e5,f0e1,h8h6,i2g0,d4c2,g0e2,e5f7,e1d2,h6h9,d0e1,h9h7,e2c4,b3d3,e1f0,d3d9,c0a2,f7e5,a2c0,c2b0,e0e1,d9d7,e1e0,d7b7,h5g5,b7b9,g5h5,b9b5,f0e1,e8f7,h5g5,e5c6,c0a2,b5b1,e1f0,c6b4,d2e1,b1e1,e0d0,e1g1,g5h5,g1g3,h5h6,g3e3,c4e2,b0d1,d0d1,b4d3,a2c0,e3f3,e2g0,f3f2,h6g6,f2e2,f0e1,e2b2,d1d0,d3b4,g6f6,h7h1,f6e6,b2b1,c0e2,b4c2,d0e0,f9e9,e0f0,h1h9,e2c4,b1b2,c4e2,b2b4,e2c4,c2d0,f0e0,b4c4,e0d0,h9h5,e1f0,c4c0,d0d1,h5c5,g0e2,c5c1,d1d2,c1f1,e6e7,f1f0,e2g4,c0c5,d2e2,c5c6,e2d2,f0f1,g4e2,f1f6,e7e8,e9e8,e2c0,f6f2,d2d1,c6c0 +67b0dea1;a0c1,h7h1,e0e1,b9c7,e1d1,e6e5,g0e2,c6c5,e3e4,c7d5,c1b1,g6g5,b1c1,e5e4,e2g4,b7b6,c1b1,h1g1,i3i4,g1e1,b1e1,c9e7,e1g1,d5f6,d1e1,f9e8,g1f3,b6c6,f3g5,c6c7,a3a4,c7d7,e1e2,e4d4,f0e1,d7d8,g5f7,e8f7,g4i2,d4e4,c3c4,d8d4,i2g0,i6i5,i4i5,d4d2,c4c5,f7e8,e1d2,e8d7,c5b5,a6a5,i5i6,f6e8,g3g4,i9i7,e2f2,a9a6,g0i2,e4e3,c0e2,e7c5,i6i7,e8d6,i7i8,d6e4,e2c4,e4d6,f2f1,g9i7,c4e2,d6b7,i2g0,a5a4,f1f0,a6b6,e2c0,i7g5,f0f1,b7c9,f1f0,e9e8,f0e0,b6b9,d2e1,c5e7,c0a2,b9b5,g0i2,e8e9,g4g5,b5a5,e1f0,e7g9,i2g4,h9i7,d0e1,i7h5,g5f5,h5i3,f5f6,a5a8,e1d2,e9e8,i8h8,c9b7,f6f7,a8a9,f7f8,e8e9,e0d0,e3f3,a2c4,f3g3,g4e2,a4a3,f8g8,g3g2,d0d1,b7c9,e2g4,c9b7,h8h9,g2f2,f0e1,a9a5,g8f8,b7d6,e1f0,a5i5,h9i9,i3g2,i9h9,i5i6,g4e2,i6i0,f0e1,i0a0,e1d0,a0b0,e2g4,g2h0,c4a2,b0d0,d1d0,a3a2,h9i9,h0f1,d0d1,f1d2,f8f9,e9f9,d1e1,g9e7,g4i2,d2e0,i2g0,e0c1,i9h9,c1a0,e1d1,d6b7,h9g9,e7g9,d1d2,d7e8,g0e2,f2e2,d2d1,a0b2,d1d0,b2d3,d0e0,d3f4,e0f0,g9e7,f0f1,b7d8,f1f0,e7g9,f0e0,f9f8,e0d0,d8e6,d0e0,e6d4,e0f0,e2e1 +7d77869d;c3c4,c9e7,a0a2,e7g5,f0e1,g9e7,a2a1,h7h5,e0f0,h5h8,a1c1,h8h7,c0a2,h9f8,c1b1,f9e8,b1c3,h7i7,g0e2,i7i3,g3g4,i9i8,c3i3,i8i7,e1d2,e7c9,i3f3,b7f7,f3h4,e8f9,a2c0,g5e7,h4i4,f7f3,c4c5,f3f1,g4g5,f1f3,f0e0,e7g5,i4i7,b9c7,i7f7,f3g3,c5b5,i6i5,b5a5,c9e7,f7h7,f8d7,d2e1,g3a3,e1d2,a3b3,h7i5,b3b7,i5g5,e7c9,g5h3,b7b6,h3h7,f9e8,h7f8,e6e5,e0f0,d7c5,d0e1,c7b9,e3e4,a9a7,f8f3,e8f7,e2g0,a7e7,f3g5,b6b1,g0e2,b9a7,g5f3,a7c8,f3h2,e9f9,a5a6,b1a1,h2g2,a1a0,f0f1,c5a6,g2e3,e7c7,e3c3,g6g5,c3d5,c6c5,d5d7,a0a2,d7d8,d9e8,d8e6,c8a9,e6a6,a2a0,a6f6,f9e9,f6f2,a9b7,e1f0,e9d9,f2i2,a0a4,i2i1,e8f9,i1g0,e5e4,g0h0,b7a9,d2e1,f7e8,h0h7,a4a7,e1f2,a9b7,c0a2,c7g7,f0e1,b7a9,h7g9,g7g9,e1d0,g9g8,e2g4,g5g4,f1e1,a7h7,e1f1,c9e7,d0e1,g8g5,e1d0,d9d8,a2c0,h7h2,c0e2,c5c4,d0e1,e7g9,e1d0,h2h1,f2e1,g5b5,f1f2,h1h7,e2c0,h7h0,f2e2,b5b0,e1f2,g9i7,d0e1,g4f4,e1f0,f4f3,c0a2,e8d7,e2d2,c4d4,a2c0,b0a0,d2d1,h0h9,d1d0,d8d9,f2e1,h9h3,d0e0,h3h1,e0d0,h1h2,e1d2,i7g9,d0e0,a0c0,e0e1,c0c7,e1f1,h2i2,d2e1,c7c1 +49b12231;a0b2,i6i5,i3i4,b7b3,b2a0,b3b8,g0e2,c6c5,i4i5,b8e8,d0e1,e8d8,i5h5,d8d4,a0b2,h7h8,h5g5,h8f8,b2b0,f8f7,g5h5,e9e8,h5h6,d4d6,h6g6,i9i2,e1d0,d6d1,f0e1,g9e7,g6f6,h9i7,b0b6,d1d3,b6c4,i2i5,c4b4,f7h7,e1f2,i7h5,b4c2,d3d2,a3a4,h7h9,e3e4,i5i3,f6f7,d2d5,c2e3,d5f5,e2c4,h9h7,f2e1,i3g3,f7f8,e8f8,e3g3,b9a7,c4e2,f5f2,g3h1,d9e8,e4e5,f2f5,h1f2,h5f6,e2g4,f5f3,a4a5,h7f7,c0e2,a6a5,f2h3,f3e3,h3e3,a7b9,e1f0,f6g4,e3g2,g4i5,g2h2,i5g4,h2i4,g4h6,c3c4,f7f2,e2g0,f2f7,e5d5,h6i4,d0e1,e8d9,d5e5,i4g5,c4c5,a5a4,e5d5,a4b4,e1d0,b9a7,g0i2,f7f2,c5c6,d9e8,f0e1,f2f5,e1f0,f5f4,f0e1,b4c4,c6c7,f4i4,i2g0,g5h7,g0e2,i4i0,d5d6,h7f6,d6e6,f8f7,e0f0,i0i7,e2g0,i7i0,f0f1,c4b4,e6e7,c9e7,f1f2,i0i3,f2e2,a9e9,c7d7,b4c4,e1f0,a7b5,f0e1,i3e3,e2f2,e3g3,e1d2,b5c3,g0i2,g3i3,i2g4,i3i2,g4i2,c3e2,f2f1,e9d9,i2g0,d9a9,f1f2,a9a4,d2e1,e7c5,f2e2,e8d9,e2d2,a4a3,g0i2,a3a9,d7e7,f7e7,e1f2,f6g4,i2g0,g4f2,g0e2,a9a4,d0e1,a4a6,e2c0,f2g0,c0a2,g0e1,a2c0,e7f7,d2e2,a6a2,e2e1,a2a5,c0a2,a5a2,e1e0,a2d2,e0e1,c4c3,e1e0,d2d1 +14c08b46;g0i2,g9i7,c0e2,a9a8,e0e1,h7e7,e1d1,a8c8,a0c1,b7b2,c1b1,b2b6,d1d2,c8e8,b1b9,e8h8,i2g4,g6g5,b9c9,e9e8,c9a8,e8e9,a8e8,d9e8,e2g0,e8d9,g0i2,h8a8,d0e1,i6i5,d2e2,i9i8,e3e4,a8h8,c3c4,f9e8,e2d2,b6b8,d2d1,i8i9,e1d2,b8b2,d1e1,h8h4,a3a4,h4h5,i3i4,b2b9,c4c5,b9a9,c5b5,e9f9,b5b6,h9f8,i2g0,f9e9,e4e5,e7b7,e1e2,i5i4,a4a5,i7g9,e2f2,f8h9,e5e6,a9a5,g4e2,c6c5,e6e7,i4i3,e2g4,a5b5,e7f7,h5h2,f2f1,i3i2,g4e2,e8d7,b6c6,b7b9,e2c0,b5b8,c6b6,b8h8,c0a2,a6a5,d2e1,h2h7,a2c0,i2i1,g3g4,g5g4,e1f2,i1i0,c0a2,h7h0,f7g7,h0h7,a2c4,i9i2,b6b7,g4f4,g7g8,i0h0,c4a2,i2i5,g8g9,i5g5,g0i2,g5g9,i2g0,h8c8,a2c0,h7e7,c0a2,g9g7,b7a7,b9b0,a2c4,c8c9,a7b7,e7f7,g0i2,g7g8,c4a2,g8g7,i2g0,c9c6,b7c7,f7f6,f2e1,g7f7,c7d7,h9i7,d7d8,c6d6,e1d0,e9f9,g0i2,b0f0,f1f0,f7e7,d8c8,i7h5,f0f1,e7e1,f1e1,d6c6,c8d8,f6i6,e1d1,i6f6,a2c4,f6f5,c4e2,c6a6,d1d2,f9e9,i2g4,a6i6,d2d1,h5g3,d1e1,i6i9,d8d9,e9d9,g4i2,f4e4,e1e0,e4e3,e2g4,f5f6,i2g0,f6b6,e0f0,b6h6,g4e2,h6h9,f0e0,g3h1,e2g4,h9h4,e0e1,i9e9,e1f1,h0g0,g4e2,h4f4,f1e1,g0f0 +896cc1ba;c3c4,h9i7,e0e1,b9c7,a0c1,i9i8,g0e2,b7b9,c1a2,b9b8,a2d2,g9e7,e1f1,e7g9,f1e1,h7f7,d2d6,b8f8,c4c5,f8g8,d6d7,g8d8,d7c7,f7f6,c7a7,d8f8,a3a4,g6g5,e3e4,f8g8,a7f7,e9e8,e1d1,a9b9,f7f6,e8e9,c5b5,c6c5,f6e8,e9e8,i3i4,g8g6,a4a5,g6g3,e4e5,e6e5,c0a2,e8e9,i4i5,g3f3,d1e1,f3f5,b5b6,i7g6,e1f1,i8d8,i5h5,e5e4,d0e1,e9e8,h5h6,c5c4,h6i6,d8d6,i6h6,f5f7,f1f2,d6d7,e1d0,d7c7,e2c4,e8e9,f2f1,f7i7,a2c0,i7e7,b6a6,b9b3,a6b6,c7a7,h6i6,a7c7,i6i7,e9e8,f1f2,c7c4,a5b5,c4b4,i7i8,e7e6,i8i9,g6h8,f0e1,b3a3,c0e2,e6e5,i9h9,a3a1,e1d2,a1a9,e2c0,a9a4,b6a6,e5e7,c0e2,e8d8,d0e1,d8d7,e1f0,e7e2,b5c5,e4e3,a6b6,a4a5,f2f1,b4h4,b6c6,h4h2,c6b6,a5c5,b6c6,c5c2,f0e1,c2d2,h9i9,e3f3,e1d0,h2i2,i9h9,i2i0,d0e1,i0i9,c6c7,d7d8,f1f0,i9i8,e1f2,d2d7,f0e0,i8i7,h9i9,d7d1,c7c8,d8e8,c8d8,e8e9,d8c8,h8g6,i9h9,d9e8,h9g9,f3f2,c8c9,i7a7,c9d9,e9d9,g9f9,g6e7,e0f0,g5g4,f9e9,d9e9,f0e0,e7c8,e0f0,d1d7,f0e0,a7a1,e0f0,e9d9,f0e0,d7d5,e0f0,d5b5,f0e0,c8d6,e0d0,b5b1,d0e0,a1a6,e0d0,e2e4,d0e0,d6b7,e0f0,a6a2,f0e0,f2e2,e0f0,e4e6 +a74194ab;a0a1,e6e5,i3i4,b7b1,a1a6,d9e8,g3g4,b1b6,a6b6,h7b7,i4i5,g6g5,b6b1,b7b3,b1b0,b9c7,i5i6,i9i7,b0b1,e8d9,d0e1,c7a6,b1d2,a6b4,d2d3,i7d7,d3c1,d7d8,c1c6,b3b0,e1d0,d8d7,g0e2,b4c6,g4g5,d7d4,e2g0,f9e8,g0i2,e8d7,i2g4,c6a5,i6h6,e9e8,a3a4,a5b7,g5f5,a9a5,e3e4,d4c4,f5f6,b0a0,h6g6,b7c5,e4e5,g9e7,f0e1,c5b7,e5e6,e8f8,g6h6,c4a4,f6f7,f8f9,e6f6,a5a9,e1d2,a9a7,c3c4,a0a1,e0f0,a1a2,c4c5,e7c5,f6g6,c5e7,f0f1,a2a3,f7f8,h9f8,f1f2,a7a6,c0e2,a6e6,g4i2,a4e4,h6i6,e6b6,g6h6,a3a4,e2c0,e4g4,c0e2,b6g6,i6i7,d7e8,h6i6,c9a7,e2g0,a4a0,d2e1,g4g1,f2e2,g1g5,i2g4,g6g8,g4i2,a0a2,i7i8,a2c2,e2f2,a7c5,i6h6,c2d2,h6i6,g5g1,f2e2,g8g9,i6i7,g9g4,e1d2,g4b4,d0e1,b4b3,i2g4,b3b5,e1d0,f8g6,d2e1,g1g2,e1f2,g6e5,e2d2,e7g5,i7h7,e8d7,h7h8,g5i7,d2e2,b7a9,h8g8,b5b9,g8g9,i7g9,d0e1,e5g4,i8i9,d9e8,e1d0,g2g0,e2e1,g0g2,e1e2,b9b8,e2d2,g2g0,i9h9,b8b0,d2d1,b0a0,d1d2,a0a2,d2d1,g4f2,d1e1,g0g6,h9g9,g6g9,e1f1,a2a4,d0e1,a9b7,e1d0,a4c4,d0e1,g9g5,e1d0,g5e5,d0e1,e5e6,e1d2,e6b6,f1e1,b6b0,e1e2,b7a5,e2e1,f2h3,e1e2,b0e0,d2e1,c4d4 +9f0c968e;a0c1,c9a7,e0e1,b7b3,c1a0,h7h2,c0a2,a6a5,e3e4,h2c2,a0b2,c2e2,b2d3,e2f2,d3d5,f2f6,d5f4,c6c5,f4f5,c5c4,f5f4,c4b4,g0i2,b3b2,i2g4,a9a8,e1d1,a8c8,i3i4,f9e8,d0e1,h9i7,e1d2,c8d8,f4g2,d8d3,c3c4,e9f9,g2e3,d3d2,d1e1,d2d4,g4i2,b2b1,e1e0,d4c4,e3c4,e8f7,c4c1,a7c9,c1c6,b1b2,c6b8,f6f2,b8a8,b4a4,e0e1,g9e7,e1e0,b2b6,a8g8,a4b4,g8i9,f9f8,i2g4,f2f4,i9h9,i7h9,f0e1,b9a7,e1f2,f8e8,e0e1,b4c4,i4i5,b6b1,e1e2,c4b4,e4e5,b1b0,g4i2,b0i0,i2g0,f4d4,g3g4,d4d2,e2d2,i0i2,d2d1,a7b5,a2c4,e7g9,g4g5,i6i5,c4e2,i2e2,g0e2,b5d4,g5f5,d4e2,d1e1,g9e7,e1e2,b4c4,e2e1,e6e5,a3a4,e5e4,e1e0,c4d4,e0e1,e8f8,e1e0,e7g5,e0f0,h9i7,f5e5,d4c4,e5e6,a5a4,f0f1,c4d4,f1e1,d4d3,e6e7,e4d4,e7e8,d9e8,e1d1,e8d9,f2e1,d3e3,d1d0,e3f3,e1f2,f8f9,f2e1,a4b4,e1f0,b4b3,f0e1,d4d3,e1f2,f3f2,d0d1,d3c3,d1e1,c9e7,e1d1,i7h9,d1e1,f7e8,e1e0,h9g7,e0d0,e7c9,d0e0,f2f1,e0d0,g7h9,d0d1,b3a3,d1d2,h9g7,d2e2,f1f0,e2e1,c3d3,e1d1,g7i6,d1e1,f0g0,e1e0,d3d2,e0e1,d2e2,e1e0,c9a7,e0d0,f9f8,d0d1,f8f7,d1d0,a3b3,d0d1,a7c5,d1d0,g0f0,d0d1,e2d2,d1e1,i5i4 +c65ca897;e0e1,b7b1,i3i4,h7h3,a0a1,h3h1,c3c4,g6g5,g3g4,f9e8,e1e0,b1b6,a1a0,b9c7,g0e2,c6c5,a0b0,i9i8,e0e1,g9i7,e1f1,h1i1,f0e1,i1i0,f1f2,i0g0,a3a4,i8h8,f2f1,h8h1,f1f2,i7g9,e2g0,h1h0,c4c5,h0h3,c5d5,c9a7,b0b6,g5g4,b6a8,h3h4,c0e2,e6e5,d5e5,h4i4,a8c7,h9i7,e5e6,e8f9,e6d6,i7h9,f2f1,a9a8,e3e4,i4i2,c7c9,a7c9,d6c6,c9e7,e2g4,i2g2,g4i2,g2a2,e1f2,e7c5,i2g4,h9i7,e4e5,a2a4,c6d6,a4d4,f1f0,d4f4,f0f1,a8e8,g4i2,e8g8,i2g4,i7g6,d0e1,g6h4,e5d5,f4b4,d6c6,b4a4,d5d6,g9i7,c6b6,a4e4,e1f0,h4i2,f2e1,i6i5,e1f2,g8e8,d6e6,e8a8,f2e1,e4g4,f1f2,a8a9,b6b7,g4g5,b7b8,i2h0,e1d0,g5g0,b8b9,i7g5,e6e7,g0f0,f2e2,f0e0,e2d2,c5a7,e7d7,e9e8,d0e1,e0b0,e1f2,b0f0,b9c9,e8e9,d7d8,f0c0,c9d9,a9d9,d8d9,e9e8,d9e9,c0c7,d2d1,a6a5,e9d9,c7c4,d1d2,c4f4,d9e9,h0i2,d2d1,a5a4,d1d2,i5i4,e9d9,i2h0,d9e9,f4f2,d2d1,e8e9,d1d0,f9e8,d0d1,f2f6,d1e1,f6i6,e1d1,e8f7,d1d2,i6i7,d2d1,i7h7,d1d2,e9e8,d2d1,h7h5,d1d2,h5i5,d2d1,h0g2,d1d2,g2i1,d2d1,g5i7,d1d2,e8e7,d2d1,i5h5,d1d2,i4i3,d2d1,a7c5,d1d0,h5h2,d0d1,h2d2,d1d2,i1h3,d2d1,h3f2,d1d2,a4a3 +afbb563c;a0a1,d9e8,a1a0,h7c7,a3a4,c9e7,c0e2,i6i5,e2c0,b7b0,a0a3,i5i4,a3c2,i9i5,c2c6,i5g5,c6e6,b0b6,c3c4,g5i5,e6d4,i4i3,c0e2,a9a7,d4e4,i5c5,e4c5,b6b0,d0e1,b0b6,c5d7,e8d7,g0i2,c7c8,a4a5,b9c7,e1f2,g6g5,e2g0,f9e8,f0e1,b6c6,i2g4,c6d6,c4c5,a7a9,e0d0,e7c9,e1f0,h9g7,g0i2,e9f9,e3e4,c9a7,f0e1,c7b5,c5b5,d6g6,a5a6,g7i8,e1f0,c8c4,d0e0,c4g4,a6b6,a9b9,f2e1,g6f6,b5a5,f6f2,a5b5,f2e2,e1d0,a7c9,g3g4,e2b2,e0e1,c9a7,i2g0,b2i2,g4g5,i3h3,g0e2,g9e7,b6a6,i2i7,b5a5,b9b8,g5h5,b8b5,e2c0,e8d9,e4e5,b5b0,a6a7,f9f8,e5f5,e7g5,e1e2,b0b3,e2d2,d9e8,d2e2,e8d9,c0a2,b3b5,e2f2,b5e5,a7a8,e5f5,f2e2,f5f6,a2c0,i8g7,e2e1,g7e8,e1e0,i7g7,e0e1,f6i6,e1e2,i6b6,h5h6,b6d6,a5b5,g7g8,b5b6,g8i8,b6a6,d6d1,h6i6,d1d0,i6h6,h3h2,e2e1,i8i2,c0e2,d0d6,a8a9,i2i4,a9b9,d6f6,b9a9,h2h1,e2c0,i4g4,e1e0,f6c6,a6b6,g4e4,h6g6,f8f9,a9b9,c6c9,b6c6,e8g7,e0e1,e4e7,e1e2,h1g1,g6f6,c9b9,f0e1,b9c9,e1f2,g7i8,c6d6,c9c0,e2e1,c0g0,f6f7,e7e8,e1e2,e8c8,d6c6,g0e0,f2e1,e0e1,e2d2,e1a1,f7f8,f9e9,f8e8,d9e8,c6d6,a1e1,d6d7,e8d9,d7d8,i8g7,d8e8,e9f9,e8f8,f9f8 +d748aecd;d0e1,b7f7,g0i2,f7f6,a0c1,f6f7,g3g4,h7h2,c0a2,h2h3,i2g0,f7c7,c1d3,c7h7,d3d1,g9i7,e1d0,i6i5,d1d7,e9e8,d7e9,e8e9,g0i2,h3h2,e0e1,i9i8,e1d1,i8f8,e3e4,h7h4,g4g5,f8e8,e4e5,h2b2,a2c4,b2g2,i3i4,h4h0,f0e1,h0f0,g5g6,g2e2,g6h6,i5i4,c4e2,f0h0,e5f5,e8h8,i2g0,b9c7,h6h7,h8c8,f5e5,f9e8,d1d2,a6a5,g0i2,a9b9,e1f2,h0e0,e2c4,h9g7,a3a4,c8b8,c4e2,e0i0,d2d1,b8b3,d1d2,e8f7,i2g4,i0g0,h7h8,c9a7,f2e1,c7a8,e5e6,b9a9,e2c4,f7e8,d2d1,a7c5,d1d2,g7i8,a4a5,i4i3,e1f2,b3b0,d2d1,b0c0,h8h9,e8f7,h9g9,c0c3,e6e7,c5a7,a5b5,i8g7,g4e2,g7i8,b5c5,c3e3,c4a2,e3f3,e7e8,e9e8,f2e1,c6c5,g9f9,a8c7,a2c0,c7b9,e2c4,a9a8,c4e2,g0i0,e1d2,i0h0,d1e1,b9c7,f9g9,f3a3,g9h9,c7b5,e1d1,b5c7,e2g0,h0h2,c0a2,a3a5,h9g9,a5a2,g0e2,i8g9,d1e1,c5c4,e1d1,h2h3,d1e1,c7e6,e2g0,a7c9,g0i2,a2d2,i2g0,a8a0,e1f1,g9h7,f1e1,d2d3,g0e2,h3h0,e2g4,i7g5,e1e2,d3d7,g4i2,h0h4,i2g0,h7f8,e2f2,e6c5,d0e1,c4c3,f2f1,e8e7,e1f2,f7e8,f2e1,d7d2,e1f2,c5e6,g0i2,d2d7,f2e1,a0a6,f1f0,e8f9,f0f1,a6c6,f1f2,d7d6,e1d0,h4b4,d0e1,i3i2,e1f0,b4e4,f2f1,d6d8,f1e1,c6c8,e1e0,d8d1 +a5c5016c;c0a2,i9i8,e0e1,e6e5,a0a1,i8e8,c3c4,e8e6,a3a4,a9a8,g0i2,f9e8,e1d1,a8c8,a1c0,b7e7,g3g4,e7b7,c4c5,g6g5,c0c1,h7c7,e3e4,h9i7,c1d3,g5g4,d3b2,c7h7,b2e2,e6e7,e2g2,h7h1,a2c0,g4g3,g2g3,h1i1,g3e3,e7f7,e3a3,f7f4,a3c4,f4f9,e4e5,b7b8,c0a2,b8b5,i3i4,b5b8,e5d5,g9e7,i2g4,c6c5,c4d2,b8b6,d2c4,b6b1,d1d2,c8c7,c4e4,b1b0,e4e6,i1e1,e6c6,f9f0,c6f6,e1e2,i4i5,b0b3,i5h5,c7c8,f6e4,e2a2,e4c3,f0f2,c3e2,a6a5,h5g5,b9a7,d2d1,b3h3,e2e0,a7b9,g5h5,h3d3,d1e1,f2d2,a4a5,d3h3,e0h0,d2d1,e1d1,b9a7,d5c5,h3f3,d1d2,f3f8,h0f0,c8c5,f0d1,c5e5,h5h6,f8f1,d1e1,f1f9,e1d1,e5e3,h6g6,f9i9,d1f0,i6i5,f0h1,i9h9,h1h6,a7b9,h6i4,e3b3,g6h6,i7h5,a5b5,h9h7,i4i2,h5f6,i2g1,b3b2,d2d1,b2b5,g4e2,b5b0,e2g4,a2a1,g1f3,b0c0,f3d4,h7g7,d4f5,e7c5,f5f3,a1c1,g4i2,c0b0,d0e1,f6h5,f3f7,e8f7,i2g0,g7g6,h6g6,b0b5,g6f6,f7e8,d1d0,c1d1,g0e2,i5i4,e1f2,h5g3,f6g6,c9e7,g6g7,b5b1,e2c4,b1b4,g7f7,e9f9,d0e0,d1d6,e0f0,c5a7,f7e7,d6d0,f0e0,b4c4,f2e1,i4h4,e1d2,a7c5,e0d0,g3e2,d0d1,f9e9,d2e1,c4a4,d1d2,a4e4,e1f0,e4e6,e7e8,e9f9,d2d1,e2f0,d1d0,e6e7,e8e9,f9e9 +a2a4b36b;e3e4,b7b3,f0e1,h7i7,g0i2,i7g7,a0a2,g9i7,e1d2,b3b0,g3g4,b0d0,c0e2,g7g9,e0f0,b9a7,i3i4,c9e7,a2c2,g9g4,c3c4,i7g9,c2c6,g4g5,c6c5,i6i5,c5e5,g5g3,e5e7,g9e7,e2g4,h9i7,a3a4,g3h3,d2e1,h3h7,f0f1,h7g7,c4c5,g7g4,i2g4,d0d1,e1f0,e7g9,c5c6,d1d3,g4e2,d3d8,e4e5,a9c9,c6d6,d8i8,d6e6,c9c1,f0e1,c1c8,e2g4,i8f8,e6e7,i9h9,e1d2,a7b9,f1f2,i7h5,f2e2,f8f5,e2e1,c8g8,e1e2,g8f8,e5d5,g6g5,a4a5,i5i4,d2e1,f8a8,e1f0,h9i9,a5b5,i4i3,d5e5,a8e8,e2d2,i3h3,b5b6,g9e7,e5f5,g5g4,b6c6,e7c9,f5e5,e8e6,c6c7,e6i6,c7c8,g4h4,e5e6,h3h2,e6d6,c9a7,c8b8,i9g9,d2d1,h5f4,d1d2,f4e6,f0e1,a7c5,d6d7,h4g4,d7e7,e6d4,d2d1,g4g3,b8a8,d4f3,e7f7,i6i9,e1f2,i9i0,f7e7,i0g0,d1e1,g9g5,e1d1,g0a0,a8a9,c5a7,d1e1,g5g6,e7e8,d9e8,e1d1,a0g0,f2e1,h2i2,e1f0,g0h0,d1d0,h0h3,a9b9,g6d6,d0e0,e9d9,f0e1,d6d8,e0f0,h3h7,b9a9,d8b8,e1d0,b8b4,a9b9,b4b7,b9a9,h7h9,f0f1,f3d2,f1f2,b7b2,d0e1,d2c0,e1d2,e8f7,f2e2,b2b1,e2f2,g3h3,d2e1,b1b9,e1d2,b9c9,f2e2,h9h8,d2e1,f7e8,e1f2,c0d2,e2e1,h8g8,e1d1,c9c4,f2e1,g8g0,e1d2,c4c9,a9b9,h3h2,d1e1,c9c6,b9c9,a7c9,e1d1,c6e6 +9d6dda1a;a0a1,e9e8,d0e1,b9a7,c0e2,b7b5,e2g4,a6a5,a1c2,b5b7,c2c6,e6e5,c6i6,a9b9,e3e4,a7b5,e4e5,a5a4,e5d5,h9g7,i6i7,c9e7,i7i4,b7d7,d5d6,d7c7,c3c4,c7a7,i4i9,e7g5,c4c5,h7h2,g4e2,a7a5,g0i2,a4b4,e2g4,e8e9,i9i6,b9c9,d6c6,h2h0,i2g0,c9c6,i6i7,e9e8,g4i2,h0h7,g3g4,a5a4,i7h7,c6c9,h7h1,c9c6,h1g1,b4c4,g4g5,g9i7,g1f1,b5c3,i2g4,c6f6,f1f5,a4b4,g4i2,e8e7,c5d5,b4a4,f5g3,a4a5,g3i4,a5c5,i4g3,f6a6,a3a4,c5g5,g3f3,c3b1,f3f4,a6a8,g0e2,a8a9,f4h5,g5e5,d5c5,b1a3,i2g4,a9a8,h5h8,g7i6,g4i2,c4d4,h8e8,e7f7,e8f6,f7e7,f6f8,e5h5,e2c4,h5f5,f8f7,e7f7,i2g0,g6g5,e1f2,f5d5,c5d5,g5g4,e0d0,a8f8,d5c5,a3c2,d0d1,g4g3,f0e1,c2e3,c4e2,d4c4,c5b5,d9e8,b5a5,i6g5,i3i4,f8g8,a5b5,g8g6,a4a5,g3h3,b5b6,g6g8,a5a6,g5i6,b6c6,g8g3,d1d2,g3g0,e2c4,g0e0,e1d0,i7g5,a6a7,g5e7,c4a2,e3g2,d0e1,i6g5,d2d1,g5i4,d1d2,e7c5,d2d1,h3h2,d1d2,e0e1,a2c0,c5a7,c0a2,e1c1,a2c4,a7c9,f2e1,i4h6,e1d0,h2i2,d2e2,c9a7,c6c7,e8d7,c7d7,c1a1,d7c7,h6i4,d0e1,f9e8,c4a2,g2f4,e2f2,a1a2,e1d2,f7e7,c7d7,e7d7,f2f1,e8f9,f1f2,a2d2,f2f1,d7e7,f1f0,d2a2,f0f1,a2f2,f1f2,i4h2 +8e647e12;a0c1,h7h1,c1b1,g9i7,a3a4,b7h7,b1h1,b9c7,h1f2,h7h1,f0e1,h9f8,e3e4,c9a7,f2d2,a7c5,e4e5,h1f1,g3g4,i7g5,e0f0,a6a5,d2d1,g5e7,f0e0,c7b9,g0i2,e9e8,d1b2,e6e5,b2c2,i9i8,c2c5,a9a7,c5a5,b9d8,e1f0,e8e9,d0e1,a7c7,a5a8,d9e8,a4a5,e8d9,a8a7,c7b7,a5a6,f1h1,e1d0,f8h7,f0e1,h7i5,c0a2,f9e8,a7b7,d8b7,g4g5,i8g8,c3c4,h1h3,e0f0,e7g5,a2c0,b7a5,e1f2,h3h2,d0e1,g8g7,f0f1,g7h7,i2g0,h7h4,g0i2,h4d4,i2g4,d4d2,c4c5,h2h9,c0a2,c6c5,g4i2,d2f2,f1f2,h9h8,a2c0,h8i8,i2g4,i5g4,f2e2,g5i7,c0a2,a5c6,i3i4,i6i5,e1f0,e8f7,a6a7,i7g9,e2d2,i8f8,d2e2,f8f0,a7b7,f0a0,e2e1,e9f9,b7a7,g6g5,a7b7,c6a5,b7c7,g4e3,a2c0,a5b7,c7d7,b7c9,d7e7,e3f5,e7e8,f5e3,e8d8,a0a3,e1e0,c9b7,d8c8,b7a5,c0e2,i5i4,c8b8,a5b3,b8b9,a3a5,e2g4,b3c1,e0f0,i4i3,g4e2,a5a6,e2g4,e3g4,b9c9,a6b6,c9d9,b6b8,d9e9,f9e9,f0f1,e5e4,f1f0,g4e5,f0f1,e4d4,f1f0,e9e8,f0f1,b8c8,f1f0,e5d3,f0f1,d4e4,f1f0,c8c9,f0f1,e4e3,f1f0,g5g4,f0f1,e8e7,f1f0,e7e8,f0f1,g4h4,f1f2,c9a9,f2f1,h4h3,f1f2,e3e2,f2f1,c1a0,f1f0,e8e7,f0f1,d3c1,f1f0,h3h2,f0f1,e7e8,f1f0,e8e7,f0f1,a0b2,f1f0,e2f2 +158256ff;a0a1,h7h1,a1h1,b7b6,a3a4,b6b1,h1h4,a6a5,c3c4,b1b8,c0a2,b8e8,h4f5,e8a8,f5i5,a8a6,a2c0,i6i5,d0e1,i9i6,e3e4,b9a7,e4e5,e6e5,c4c5,d9e8,i3i4,a6b6,e1d2,h9i7,e0d0,b6b8,d0d1,b8b2,a4a5,g6g5,a5a6,i6g6,c5c6,i5i4,c6c7,a7c8,d1d0,a9b9,c7d7,c9a7,f0e1,i7g8,e1f2,c8e7,d7d8,g9i7,d8d9,e9d9,c0a2,b9b7,g3g4,b2b3,a2c0,g6g7,g0e2,e7c8,a6b6,i4h4,e2g0,c8b6,d0e0,e5e4,e0d0,b3g3,c0e2,g7f7,g4g5,g8e7,e2c4,d9e9,g5h5,g3g4,f2e1,f7f4,h5g5,e4e3,g0e2,f4f1,e1f0,e3e2,d0e0,e2d2,g5f5,d2d1,f5e5,e7c6,e5d5,g4g5,d5e5,g5f5,e5f5,b7e7,c4e2,e7e2,f0e1,e2a2,e1d0,i7g9,f5f6,d1e1,d0e1,b6c4,f6e6,e8d9,e6e7,f1f3,e7d7,a2a4,e0d0,f3f6,e1d2,f6f0,d0d1,h4i4,d2e1,f0f4,d1d0,f4f7,e1d2,f7f5,d7e7,c4e3,e7e8,f9e8,d0e0,e8f7,e0d0,a4f4,d0e0,f5c5,e0e1,f7e8,e1e0,e8f9,e0e1,f4g4,e1e2,e9e8,e2e1,g4g7,e1e2,g7g2,e2e1,c5g5,e1e0,i4h4,d2e1,g2a2,e1f2,g5e5,e0e1,e3g2,e1d1,c6d4,f2e1,g2e1,d1d0,e5e4,d0e0,e8e7,e0f0,d9e8,f0e0,a2a5,e0d0,e1c0,d0d1,a5e5,d1d0,a7c5,d0d1,e4e3,d1d0,e8d9,d0d1,e3i3,d1d0,h4h3,d0d1,f9e8,d1d0,e5f5,d0d1,f5e5,d1d0,e5g5,d0d1,i3i1,d1d0,e8f9 +2349a434;e0e1,h7h0,i3i4,h0h7,e1d1,h7h6,a0a1,b7f7,g0e2,e6e5,a1b1,h6h1,b1c1,h1c1,e2g0,e5e4,e3e4,i9i7,g0i2,f7h7,c3c4,i7i8,a3a4,d9e8,i4i5,h7h3,d0e1,h3i3,g3g4,c9a7,c0a2,c1b1,a4a5,i6i5,c4c5,a9a8,a5b5,a8d8,e1d2,a7c5,d1d0,h9g7,b5b6,b1b4,a2c4,d8d6,c4e2,e8f7,b6a6,i5i4,a6b6,i8c8,f0e1,b4d4,d0e0,d4d5,e2g0,c8d8,e0f0,f7e8,e1f2,d5h5,g0e2,g6g5,e2g0,d6d4,e4e5,h5e5,f0e0,d8a8,b6c6,g7i8,e0e1,d4a4,e1e0,i3d3,c6d6,b9a7,d6d7,e8d7,e0e1,f9e8,e1e2,g9e7,e2e1,e9f9,e1e2,g5g4,i2g4,a8c8,d2e1,a4a5,g4i2,e7g9,e1f0,d3a3,e2e1,c8b8,e1e0,a7b9,e0d0,b8b2,d0e0,b9a7,i2g4,b2a2,e0d0,i8h6,f2e1,g9e7,g4e2,h6g4,e2c4,i4h4,c4e2,e5e1,e2c4,a2e2,g0i2,a3d3,c4a2,g4f2,i2g4,e2a2,g4e2,e1f1,d0e0,f1c1,e0d0,f2e0,d0d1,e7c9,d1d0,d3d2,e2g0,c1c3,d0d1,a2a0,f0e1,a0a4,d1d0,c3i3,d0d1,d2a2,g0i2,a2a3,i2g4,i3i5,d1d2,i5i7,d2e2,e0g1,e2d2,a4g4,e1d0,g4g2,d2d1,a3b3,d1e1,g2f2,e1d1,f2f1,d1d2,g1f3,d2e2,i7i2,d0e1,a7b9,e1d0,i2g2,d0e1,f3h2,e2d2,h4g4,d2e2,a5a0,e1d0,g4g3,d0e1,f1f7,e2d2,f9e9,d2d1,a0a6,e1f2,b3b8,d1d0,a6a2,d0e0,b8d8,e0f0,g2g0,f0e0,b9c7,e0f0,e8d9 +24c1a129;a0b0,b7d7,a3a4,e9e8,a4a5,h7e7,c0e2,i6i5,a5a6,b9a7,b0b1,i9i7,a6a7,e7a7,b1b5,d7d1,b5b7,g9e7,b7e7,c9e7,g0i2,d1d6,i2g4,a7a8,g4i2,g6g5,i2g4,d6d7,i3i4,g5g4,e0e1,a8a6,e2c4,a6a0,g3g4,d7d8,e1e2,i7i6,e2e1,a0f0,c4a2,f0f5,e1e2,i6i7,e2e1,d8c8,c3c4,i7h7,g4g5,c6c5,g5h5,h7i7,e1e2,c8a8,e3e4,f5e5,e4e5,e6e5,c4c5,e8e9,i4i5,e9e8,c5d5,i7i9,d5d6,a8a7,a2c0,a7a8,d0e1,a9b9,c0a2,b9c9,e1f2,a8a4,a2c4,c9c5,e2d2,h9g7,d2d1,e7g9,c4e2,c5c9,d6c6,i9i8,e2c4,a4a8,h5h6,i8h8,c6d6,e8e7,h6g6,a8e8,i5i6,c9c4,d1d0,c4c8,f2e1,g7i6,g6g7,e5e4,d0e0,c8c5,e1d0,h8h7,d6e6,e7d7,g7g8,h7h5,e6e7,d7e7,g8h8,c5b5,d0e1,b5g5,e1d0,g5c5,e0f0,h5h7,h8g8,c5d5,g8h8,e8g8,h8g8,e4e3,g8g9,h7h5,g9f9,d5a5,f0e0,a5d5,e0e1,d5d2,f9g9,d2d3,g9f9,e7f7,e1e0,h5h1,f9g9,h1h3,d0e1,d3d5,e1f0,d5b5,g9f9,h3f3,e0d0,f3f1,f0e1,b5c5,d0e0,i6g5,f9e9,c5f5,e1d2,f1g1,e9d9,e3e2,d9e9,f7e7,e9f9,f5f7,d2e1,e2d2,f9e9,g5h7,e9f9,e7e8,e0d0,d2e2,f9g9,g1g0,d0d1,f7a7,e1d0,g0g8,d0e1,a7a5,e1f0,a5a7,f0e1,g8g9,e1f2,a7a6,f2e1,e8e9,e1f2,e2d2,d1d2,a6a0,f2e1,a0a1,e1f0,g9g3,f0e1,a1e1 +917b3c10;f0e1,b7b6,c3c4,g9i7,g0e2,h7b7,a0c1,b7d7,e1f0,d7h7,c1a2,h7h2,g3g4,b6b0,a2b0,c9e7,d0e1,h2g2,c0a2,g2g0,e2g0,e7g9,a2c0,a9a7,a3a4,g9e7,g4g5,e7g5,b0c2,a7a8,e1d0,b9d8,c2g2,e6e5,g2f2,f9e8,f2f9,e8f9,a4a5,a6a5,g0i2,i6i5,e3e4,i9i8,f0e1,d8f7,i3i4,a5a4,i4i5,a8b8,c0a2,b8b1,e0f0,e5e4,e1d2,i8g8,c4c5,b1a1,c5d5,g8i8,i2g0,f7g9,i5h5,a1e1,h5h6,e1c1,d5e5,i8i9,h6g6,c1f1,f0f1,g9f7,d0e1,e4e3,e5f5,c6c5,f5g5,e3d3,g6g7,f7d8,g5f5,e9e8,e1f0,e8f8,g7f7,d8f7,f1e1,f7h6,g0e2,c5c4,e1e0,h6g8,f0e1,a4b4,e2g0,d9e8,e1f0,b4a4,d2e1,a4a3,e1d0,c4b4,e0e1,b4c4,e1e0,d3d2,a2c4,f8f7,e0e1,h9f8,e1e0,d2e2,f5f6,f7e7,g0e2,g8h6,f6f7,e7f7,e0e1,h6i8,e2c0,e8d7,c0a2,i7g9,c4e2,g9i7,e2c4,f8h7,e1d1,a3a2,c4e2,i7g5,d1d2,g5i7,d2d1,f9e8,f0e1,e8f9,e2g4,h7g5,e1f2,i7g9,f2e1,i8h6,e1d2,g5e4,d1e1,h6i4,e1e2,e4d2,g4i2,i9i5,i2g0,d2b1,d0e1,i5i7,e1f0,i7g7,g0i2,g9e7,e2e1,g7g1,e1e2,g1g8,i2g0,i4g3,e2e1,e7c5,e1e0,g3e2,e0e1,e2g1,e1d1,f7e7,g0e2,a2b2,e2g0,g8g6,g0e2,g1e2,f0e1,g6i6,e1f0,e2d0,f0e1,i6i9,e1f2,i9i3,f2e1,i3i6,e1d2,i6i5,d2e1,i5d5,e1d2,e7e8 +172b0843;a0b2,a9a7,b2b9,h7h3,c0a2,h3h5,b9a9,b7c7,c3c4,h5d5,g0i2,g9e7,i2g0,d5d1,a9d9,e9d9,d0e1,c7d7,a2c0,h9g7,e1d0,a6a5,i3i4,a7a9,g0i2,d7d0,c0a2,i9i7,c4c5,d1f1,e0e1,f1h1,c5b5,f9e8,a2c0,d0d5,a3a4,i7i8,e1e0,a5a4,f0e1,h1h6,g3g4,d5d0,e1d0,e8f7,d0e1,e7g9,e0f0,i8d8,b5b6,d8a8,c0e2,g7i8,f0f1,c9a7,b6c6,a7c9,c6d6,i8g7,e1f2,g9e7,d6e6,h6h4,e3e4,a8a5,i4i5,h4h5,f1e1,g7i8,e6e7,h5h4,e7f7,h4h1,f7e7,a4b4,i5h5,a5g5,h5g5,a9a2,e7f7,h1h7,e2c0,a2f2,g5g6,h7h3,c0a2,h3h2,g4g5,i8g9,g6h6,i6i5,e1e0,b4c4,g5h5,h2h0,i2g4,f2c2,e0f0,h0g0,a2c4,c2e2,f0f1,g0g2,f7e7,g9f7,h6g6,g2g1,c4e2,g1g0,h5i5,f7e5,f1f0,c9a7,f0f1,g0a0,g6h6,a0b0,h6i6,b0d0,e4e5,d0g0,i5h5,g0g3,e7d7,g3a3,i6h6,a3a1,g4i2,a1c1,e2g4,c1a1,h6g6,a7c9,f1f2,a1b1,h5g5,b1g1,e5d5,g1d1,d7c7,d1i1,c7d7,i1c1,d7e7,c9a7,g6h6,c1c9,g5h5,d9e9,h6h7,c9c0,f2f1,c0c5,g4e2,c5c1,e7e8,e9d9,e2g4,c1c5,e8d8,d9e9,h5h6,c5c0,f1f0,a7c9,i2g0,c0c5,g4e2,c5c1,d5d6,c1b1,h7g7,b1i1,d6e6,i1i2,g0i2,c9a7,d8c8,e9e8,e2c0,a7c5,i2g4,c5a7,c8c9,a7c9,h6g6,e8e9,g7h7,c9e7,e6e7,e9d9,g4e2,d9d8,g6g7,d8d9,e7e8 +656ab052;g0e2,h7f7,e0e1,g9i7,i3i4,b7b2,e1e0,f7d7,d0e1,a9a7,e2g0,b2c2,c0a2,c2d2,e1f2,e9e8,a0c0,g6g5,c0c6,d7d4,e0e1,d4c4,c6c7,a7c7,e1e0,d2d8,a2c4,d8d3,g0e2,b9a7,e0e1,c7c4,e2g4,d3d4,e1d1,d4i4,d1d2,c4c3,d2d1,c3c1,d1d0,c1d1,d0d1,i4i1,e3e4,a7b9,e4e5,e6e5,d1d2,i1i2,d2d1,a6a5,f2e1,b9c7,e1d0,i2f2,a3a4,c7b9,f0e1,h9f8,e1d2,f2f3,d0e1,e8e9,a4a5,f8d7,a5a6,f3f5,a6a7,i6i5,a7a8,d7f6,e1f2,f6d7,g4e2,c9a7,e2g0,f9e8,a8b8,i9f9,b8c8,e5e4,d2e1,e8f7,g3g4,f5f6,e1f0,f9g9,c8c9,f7e8,c9b9,f6g6,g4g5,g6g0,f2e1,d7e5,g5f5,e5c6,d1d2,c6b8,f5g5,e8f9,d2d1,g0g2,b9c9,i5i4,d1d0,i4h4,d0d1,g2d2,d1d2,e4d4,c9d9,e9e8,d9c9,b8a6,e1d0,a7c5,g5h5,h4h3,d0e1,h3g3,d2d1,a6b8,e1f2,g9g4,c9b9,e8e7,b9c9,g4h4,f2e1,b8d7,e1d2,d7c9,f0e1,c9a8,h5h6,h4g4,d1d0,g4h4,h6g6,h4h9,d0d1,d4d3,e1f2,h9h5,g6g7,a8b6,d1d0,e7e8,g7g8,h5h2,d0d1,h2h5,g8h8,b6c4,d1d0,e8e9,h8h9,h5h1,f2e1,h1i1,d0d1,d3e3,h9g9,f9e8,g9f9,e9d9,d1d0,c4a3,d0d1,e8d7,f9e9,d9e9,d1d0,i1i2,d0d1,i2g2,e1d0,a3b5,d1e1,g2d2,e1e0,i7g5,e0e1,d2d0,e1f1,d0a0,f1e1,b5c7,e1d1,c5e7,d1e1,c7d5,e1f1,a0a1,f1f2,d5b6 +f6048368;a0a1,g9e7,a1g1,h7h2,g0i2,h9g7,g1g6,b7c7,c0a2,e6e5,d0e1,a9a7,e1d2,a6a5,g6g4,f9e8,g4e4,e7g9,a2c0,c7e7,e4a4,g7h5,e0e1,h5i7,a4a7,h2h0,i3i4,h0h2,a7a6,i7g6,c0a2,h2g2,i4i5,g2a2,a6a7,a2b2,a7a9,b2b1,i2g4,g6i7,a9a8,e5e4,g4e2,b1b2,a8b8,e8d7,e1e0,e7g7,a3a4,b2b6,e2c4,c9e7,c4e2,b6b1,b8a8,b1b4,a8c8,b9d8,e0d0,g7g4,c8b6,b4b2,a4a5,d8b9,d0e0,g4g5,g3g4,b2a2,b6b2,e4e3,b2b9,e9e8,b9b7,g5g6,b7c9,e3e2,a5a6,g6g7,c9g9,i9g9,f0e1,g9h9,a6a7,e7g5,a7b7,a2a5,e1f2,e8e7,b7c7,g7g4,c7d7,e7d7,d2e1,a5a1,e1d2,a1c1,e0f0,g4d4,f0f1,c6c5,i5h5,d7d8,f1f0,c1e1,h5i5,e1f1,i5h5,e2d2,f0f1,d4f4,f1e1,f4h4,h5h6,h4h0,e1e0,h0h1,h6i6,h1d1,e0e1,h9i9,e1e0,d9e8,e0f0,d2e2,c3c4,d1d4,f0e0,i7g6,c4c5,e8d9,i6h6,d4b4,e0f0,i9i3,h6i6,i3c3,f0e0,c3a3,c5d5,b4c4,i6h6,c4h4,h6g6,a3g3,g6h6,g3i3,h6h7,h4d4,d5e5,i3a3,e5f5,d4g4,f5g5,g4b4,h7h8,a3a8,g5f5,e2f2,h8i8,b4b6,i8i9,a8a2,f5g5,a2a6,g5g6,b6b1,g6g7,b1d1,g7h7,a6a7,i9h9,d1b1,h7i7,a7h7,h9i9,b1b9,e0e1,b9i9,e1e0,h7c7,e0f0,i9f9,f0e0,f9e9,i7h7,e9f9,h7i7,c7i7,e0e1,i7b7,e1e0,b7b4,e0e1,b4h4,e1e0,f9f6,e0e1,h4h0 +e6c2f39b;e3e4,b7d7,a0b0,h7h6,e0e1,g9i7,b0b3,d7f7,a3a4,f7e7,e4e5,g6g5,b3b5,a9a7,c0e2,e7b7,b5b4,b7h7,b4d5,h6h2,e1f1,a7e7,d0e1,h2h0,g0i2,h7g7,d5b4,h0h7,b4b7,h7h1,b7a9,h1i1,i3i4,e7d7,f1f2,c6c5,a9b9,g7h7,b9c9,e9e8,e2c4,d7c7,e5e6,c7d7,c9b7,i1f1,b7b2,d7d8,c4e2,d8d0,e1d2,f1c1,b2b4,i9i8,i2g4,e8d8,b4b3,c1e1,b3a5,h7a7,e6f6,e1f1,e2c0,d0e0,a5g5,a7a9,d2e1,a9a4,f6f7,h9g7,g5e6,d8d7,e6g6,e0c0,g4e2,c0b0,g6c6,b0a0,e2g4,d9e8,c6a6,a4e4,a6a8,e4f4,a8a9,g7f5,c3c4,a0a2,g4e2,f5d6,a9d9,e8d9,c4c5,i7g9,f7g7,d6b7,e1d0,a2d2,g7h7,f1e1,h7i7,i8i9,i7i8,b7c5,i4i5,f4f7,i5h5,d7d8,h5g5,i6i5,g3g4,c5e4,f2f1,f7f2,i8h8,i9i8,g5f5,d2e2,h8g8,e1c1,g8f8,c1c7,f8e8,d8e8,f5e5,c7e7,g4g5,e7e6,g5f5,f2g2,f0e1,g2g6,f1f0,g6g1,f5g5,e4f2,g5f5,f2e0,f5f6,g1g7,e1d2,e8d8,f6f7,i8i9,e5d5,g7i7,f0f1,e2d2,f7g7,e6e2,d0e1,i9h9,g7f7,h9h5,e1d0,h5h4,d5d6,h4c4,d6e6,c4c5,f1e1,d2d7,f7f8,c5c9,e1e0,d7d1,e6e7,c9a9,f8e8,d9e8,e7d7,d1d7,e0f0,e2h2,f0f1,a9d9,f1f2,i7i8,d0e1,h2g2,e1d0,d7d0,f2e2,d0c0,e2e1,c0c1,e1e0,g2g3,e0f0,c1a1,f0e0,g3b3,e0f0,g9i7,f0e0,a1e1,e0f0,e8d7 +86f8d8fa;a0b0,b7a7,b0b5,a7e7,b5g5,h7f7,c0e2,g9i7,g5e6,d9e8,e6e7,e9d9,e7f5,c9a7,f5d6,d9e9,d6e4,f7e7,f0e1,a9a8,e4d4,a8d8,d4d1,d8d7,d1b0,i7g9,b0b7,e7f7,b7b5,d7d9,b5b8,e8d7,b8c6,f7f2,c6g6,h9g7,g6g5,f2f5,g5h7,f5f0,c3c4,g9i7,h7g5,f0f2,g0i2,a7c5,g3g4,i9i8,g5f5,b9a7,f5f6,i8g8,f6h6,g8f8,h6h5,i7g5,h5h1,f8f5,g4g5,f5f8,h1f2,f8f3,e2g0,f3g3,e0f0,g7i8,f2f6,i8g9,i2g4,a7c6,f6f1,d9d8,f1f9,e9e8,f9f6,e8e7,f6f1,g3g2,e1d2,g2f2,g4i2,g9i8,g5f5,d7e8,i2g4,c6b8,f5f6,d8d2,g4e2,f2g2,f1h1,e8d9,f6g6,g2h2,h1a1,b8c6,a1a0,c6a7,a0a2,a7b9,a3a4,h2e2,a2b2,e7e8,b2b5,b9d8,b5c5,d2c2,f0f1,c2d2,g0i2,e2g2,c5d7,d2d1,d7d1,e8e7,d1f2,i8g7,f2f7,e7e8,f7e9,e8e9,a4a5,d8f9,i2g4,g2i2,f1f0,i2e2,f0f1,e2b2,g6g7,b2b9,g4e2,b9b2,g7g8,b2b4,e3e4,b4b2,e2c0,b2b7,d0e1,b7b0,e1f2,b0b8,a5a6,b8b2,g8f8,b2b6,i3i4,b6b4,f2e1,b4b6,f1f2,b6f6,f2e2,f6f1,f8e8,e9e8,c0a2,f1e1,e2e1,f9g7,e1f1,e8d8,e4e5,d9e8,a6b6,e8f7,i4i5,d8e8,c4c5,g7h5,f1f2,e8f8,a2c0,h5f6,i5i6,f6g4,f2e2,f8f9,b6b7,g4i5,e2f2,i5h3,f2f1,h3f2,f1f2,f9e9,e5d5,e9e8,c0a2,e8e7,b7b8,e7e8,b8c8,e8f8,c8d8,f8f9,d8e8 +ee905fbb;c0e2,b7b0,d0e1,h7f7,e2c4,f7c7,a0b0,c6c5,g0e2,d9e8,b0a2,c7g7,e1d2,g7e7,a2b0,e7h7,b0b6,e6e5,a3a4,h7d7,e2c0,d7b7,c4e2,e9d9,b6b3,b7b4,e2c4,d9d8,b3a3,g9i7,a4a5,a6a5,i3i4,a5a4,d2e1,e5e4,c4e2,b4b5,e1d0,b9a7,d0e1,a4b4,a3a7,g6g5,a7a3,i7g9,a3a7,c9a7,c3c4,b4b3,e1d0,a9c9,g3g4,c9a9,g4g5,e4f4,d0e1,b5b7,i4i5,a9d9,e1d0,f4f3,g5f5,b7b4,d0e1,e8f7,f5g5,b4b7,e2g0,d9e9,c4c5,e9e8,g5g6,f3f2,g0i2,e8f8,e3e4,i9i7,c5b5,f8i8,c0e2,b7e7,e2g0,b3b2,g0e2,i8e8,e1d2,b2b1,i5i6,i7i8,i6i7,b1b0,b5c5,f2e2,f0e1,e7b7,e0d0,b7i7,g6h6,d8d9,h6i6,a7c5,d0e0,b0c0,i6h6,e8b8,e4e5,e2e1,e0e1,b8b9,e5e6,i8b8,e1e0,c0d0,e0f0,c5e7,e6e7,f7e8,i2g0,i7i3,d2e1,b8a8,e7e8,b9b7,e8d8,d9e9,d8c8,a8a6,c8b8,a6h6,e1f2,h9g7,b8c8,g7e6,f2e1,i3i1,f0f1,e6d4,c8b8,i1i2,f1f0,h6h5,b8c8,h5h7,c8b8,i2i6,b8b9,i6i1,g0i2,g9e7,b9a9,h7h2,i2g0,d4f3,a9b9,h2h8,g0e2,h8f8,b9c9,d0c0,e2c4,c0d0,f0f1,d0e0,e1d2,f3e5,f1e1,e5c4,e1d1,f8g8,c9d9,e9d9,d1e1,f9e8,e1e2,i1i7,d2e1,d9e9,e1d0,e7g9,e2e1,i7i5,e1d1,e8d9,d0e1,b7e7,e1d0,e0f0,d0e1,c4d2,e1f2,d2f1,f2e1,g8g5,e1f2,g5g3,f2e1,g3g7,e1d2,e7d7 +ae1af7d2;d0e1,b9c7,e1d2,e9e8,g0i2,h7g7,d2e1,i6i5,c3c4,a9a7,c4c5,b7b8,c5c6,b8b1,e1d0,i9i8,c6d6,g6g5,a0a2,i8g8,a2d2,a6a5,e0e1,g7g3,d2b1,g3f3,e1d1,e8d8,b1c3,f3f1,d1e1,e6e5,e3e4,a5a4,e1e2,a4a3,c3d5,d8e8,d5c5,e8e9,c0a2,g8e8,c5c1,f1f6,d6e6,e8i8,e6f6,c7b5,c1i1,a7a5,i1b1,a5a6,e2f2,g9i7,f6g6,a6f6,g6f6,i8c8,i2g4,i5i4,b1b0,c9e7,b0b2,c8e8,f2f1,h9g7,a2c4,g5g4,f0e1,e8f8,c4e2,g7h9,e1f0,b5d4,b2b6,f8i8,e2c0,i8g8,c0a2,h9f8,b6b7,a3a2,b7d8,f9e8,f0e1,e5e4,d8b9,e7c9,b9b2,g8i8,b2b7,e8d7,f6g6,d9e8,f1f2,i8i9,b7b9,e8d9,b9b8,f8e6,b8b0,d9e8,b0c2,g4g3,c2a1,a2a1,g6h6,g3g2,f2f1,a1b1,e1f0,b1a1,h6i6,a1b1,i6h6,b1c1,f1e1,e4e3,h6g6,e6c5,g6f6,c5b3,f6e6,e8f7,e1f1,d4e6,i3i4,c1d1,d0e1,i9f9,e1d2,b3a5,f0e1,d1e1,f1f0,e6f8,i4i5,i7g5,i5i6,a5b3,d2e1,f8h9,e1f2,e3f3,f2e1,h9i7,i6i7,f7e8,f0e0,b3d2,e0d0,f3f2,i7i8,g2g1,e1f2,d2b1,d0e0,g1h1,f2e1,h1i1,i8i9,b1d0,i9h9,f9f3,h9g9,c9e7,g9f9,e9d9,e1f2,f3f5,f9g9,f5e5,e0f0,e5f5,f0e0,d0e2,g9f9,e2c3,f9e9,d9d8,e9d9,c3e4,e0e1,f5b5,d9e9,e8d9,e9f9,b5f5,e1d1,e4f2,d1d0,i1h1,f9e9,f2e4,e9d9,e4c3,d0e0,f5f0,e0e1,d8d9 +92251ecb;a0a6,h7c7,a6c5,b7b0,c5g5,g9e7,g5e4,b0b6,c3c4,i6i5,d0e1,e6e5,e1d2,b6b1,e4e7,c9e7,g0i2,f9e8,g3g4,c7a7,i3i4,e8f9,e0d0,b1b5,d0d1,b5a5,d1e1,e7c5,i4i5,a7a8,e1e0,a8g8,c4c5,g8e8,e0d0,h9g7,c0a2,b9d8,c5d5,c6c5,d5c5,e8i8,i2g0,i8h8,i5i6,a5a8,g4g5,e9e8,g5g6,h8h9,i6i7,e8e7,g6g7,h9h1,d0e0,h1h3,f0e1,i9i8,e0f0,e7e8,e3e4,h3f3,f0f1,a8a5,c5b5,i8h8,f1f2,d8c6,b5b6,a9a8,b6a6,h8f8,g7g8,a8d8,i7i8,c6d4,a2c0,f3e3,g8f8,e8e7,a3a4,d4e2,g0i2,d8a8,f8g8,e7d7,e4e5,e3e4,i2g0,e4d4,c0e2,d4f4,g8h8,f4e4,e5f5,e4e6,a4a5,a8b8,e2g4,e6g6,a6a7,b8b3,a5a6,g6i6,a6b6,f9e8,b6b7,e8f7,h8h9,i6i7,g4i2,b3b1,a7a8,b1b5,f5f6,i7i6,f6g6,i6i3,h9g9,b5f5,f2e2,i3g3,i2g4,g3i3,g6g7,f5f2,e1f2,i3i0,a8a9,d9e8,d2e1,i0i2,g0i2,d7d8,g7g8,e8f9,b7b8,f7e8,g9f9,e8f9,e1f0,d8d7,i8i9,f9e8,f0e1,e8d9,i2g0,d9e8,b8c8,e8f7,g8f8,f7e8,c8d8,e8f9,a9b9,d7d8,e1f0,d8d9,f0e1,f9e8,g4i2,d9e9,f8g8,e9d9,g8h8,d9d8,b9a9,d8d9,e1d0,d9d8,i9h9,e8f7,h9g9,d8d7,f2e1,d7d8,e1f2,d8d9,f2e1,f7e8,e1f0,d9d8,g9f9,e8d9,h8g8,d9e8,g8f8,e8d9,i2g4,d8d7,g4i2,d7d8,f0e1,d9e8,f9e9,e8d7,e1f0,d7e8,f8e8,d8d7,a9b9 +1f849fb3;e0e1,h7h8,a3a4,h8a8,a0b2,a8f8,b2b1,b7b6,b1d2,b6b7,e1e0,d9e8,d2d4,f8f2,c0e2,f2g2,d4d6,g9i7,d6e8,f9e8,d0e1,b7b3,g3g4,e8d7,e2c0,d7e8,e1d0,i7g5,e0e1,g2a2,g0i2,g5e7,c0a2,e9d9,g4g5,b9c7,g5h5,a6a5,e1f1,a9a6,i3i4,c7a8,e3e4,b3b8,f0e1,b8b1,f1f0,i9i8,i4i5,e7g9,e1f2,b1b9,h5h6,i8g8,i5i6,c9a7,a2c4,h9i7,c4a2,g9e7,d0e1,a5a4,i2g0,b9b5,e1d2,a4a3,a2c0,e7g9,f0f1,b5b1,i6i7,a6a5,i7i8,e8f9,h6i6,b1c1,i6h6,c6c5,i8i9,a3a2,c0a2,d9d8,e4e5,g9i7,h6i6,c5c4,e5d5,c1b1,g0i2,b1b7,f1e1,b7g7,a2c0,i7g9,c0a2,a5a2,e1f1,g8h8,i6i7,h8h6,f2e1,c4b4,i7h7,d8d9,e1f0,h6i6,f1e1,a2a6,h7g7,i6i7,c3c4,f9e8,d5e5,a7c5,e1e0,i7g7,c4c5,g7i7,e5d5,a8c7,e0d0,i7i9,c5b5,e8f7,i2g0,d9d8,d0d1,a6a0,d1e1,c7d9,e1d1,g6g5,g0i2,g9e7,d5c5,d9f8,c5d5,f8h9,d5d6,i9i2,d1e1,i2i8,e1d1,i8e8,d2e1,a0b0,e1d0,b0b1,d1d2,b1h1,d6d7,d8d9,f0e1,g5g4,e1f0,e8i8,d7d8,i8d8,d2e2,d8d0,b5a5,g4f4,a5b5,e6e5,b5b6,b4b3,b6a6,h9i7,a6b6,h1h5,b6b7,f4g4,b7c7,e7c5,e2e1,i7g6,c7d7,d0f0,d7e7,d9e9,e7e8,e9e8,e1e2,b3a3,e2e1,e5e4,e1d1,f0b0,d1e1,b0b1,e1e0,g6i7,e0d0,b1f1,d0e0,g4g3,e0d0,h5h7,d0e0,f1e1,e0f0,i7h9 +43540ba4;g3g4,d9e8,c3c4,h7e7,a0c1,i6i5,c1c3,h9i7,i3i4,e7g7,g0i2,g7c7,c3d1,g9e7,i2g0,b7a7,d1f1,e9d9,f1c1,c7c4,c1c2,a7a3,c2d4,d9e9,d4d5,a3a1,d5d8,e6e5,c0e2,c4d4,g4g5,c9a7,d8d5,b9d8,d5d8,a1c1,e2c0,c1i1,d8e8,e9d9,f0e1,d4d5,e8c8,i9i8,c8a9,d9d8,e1f0,d5d7,d0e1,i8g8,a9c8,d8d9,g5f5,a7c9,c8d6,d9e9,d6e6,f9e8,e6g7,i5i4,f5g5,e9d9,g7g6,d7d6,g6e5,d6d5,e5d5,d9e9,d5d8,i7h5,g5h5,g8g0,d8d7,e9f9,c0a2,i4i3,d7d5,e8d9,d5b5,g0i0,b5b7,e7g5,b7d8,f9e9,d8h8,i3i2,h5i5,c6c5,h8f7,e9e8,e0d0,i0g0,a2c4,g0g4,e1d2,g4e4,f7h7,e4e7,h7i7,i1g1,i7e7,e8e7,d0d1,g1g3,c4e2,e7e8,e3e4,g3g1,d1d0,g1g4,d0e0,g4i4,f0e1,a6a5,e1d0,e8d8,e0e1,g5e7,i5h5,i4i5,e4e5,i5e5,e1f1,e5e3,f1f0,a5a4,e2g0,e3e1,d2e1,i2i1,g0i2,e7g5,e1f2,c5c4,i2g0,c9a7,f0e0,c4c3,g0e2,d9e8,f2e1,a7c5,e1f2,c3d3,e2g0,d3c3,g0e2,e8d9,h5h6,d8d7,h6g6,d7d8,g6f6,c3c2,f6e6,d8d7,e2g4,c2d2,g4e2,d9e8,e2g4,a4a3,e6e7,g5e7,f2e1,c5a7,g4e2,d2e2,e1f0,e8d9,d0e1,e7c9,e1d2,a3b3,d2e1,b3c3,e1f2,c3d3,f2e1,i1h1,e1d0,h1g1,d0e1,e2d2,e0d0,d3e3,e1d2,d7d8,d0d1,g1f1,f0e1,e3e2,e1f2,e2f2,d1d0,f1f0,d0d1,f0e0,d1e1,d9e8,e1d1,f2e2 +553a4a36;c0a2,b7b4,f0e1,b4b5,c3c4,b5b3,e3e4,i6i5,a3a4,b3c3,a0a1,c3e3,e0f0,i9i8,a4a5,h7h4,a1c0,h4h3,c0c1,i8i7,c4c5,e6e5,c1d3,e5e4,d3b2,i7h7,e1f2,h3h1,b2d2,a6a5,d2e0,h7b7,c5d5,c9a7,a2c0,h1i1,g0i2,i1i3,d5e5,a7c9,e0e2,e3f3,f2e1,f3a3,e2g1,a5a4,e5d5,a4b4,c0e2,a9a8,d5e5,b9a7,g1g0,a8c8,g0f2,c8c7,e1d2,c9e7,g3g4,g6g5,f2h2,i3e3,h2i4,e4f4,i4h4,h9g7,e2g0,e3e2,g0e2,e7c5,h4h0,a3h3,e2g0,h3f3,g0e2,a7b5,e2g0,f3b3,f0e0,c5a7,h0h8,f4f3,h8h4,g9e7,h4h1,f9e8,e5d5,b3c3,h1e1,g7f5,e1c2,b7b6,c2c0,f3e3,e0f0,b5d4,f0e0,c3c1,d0e1,b4b3,d5e5,c6c5,c0c1,f5g3,g4g5,e8f9,e1f2,g3e2,e0e1,b3b2,c1d1,e7c9,d1c1,b6f6,c1a2,b2b1,a2a5,e3f3,e5e6,c7e7,a5a7,d4c6,i2g4,e9e8,a7c7,e7c7,g0i2,c7h7,e1f1,c5c4,i2g0,f6i6,e6d6,c4b4,g5h5,h7i7,d2e1,f3g3,h5h6,g3g2,h6g6,e2f4,e1f0,b1b0,d6e6,f4e2,f1e1,i6h6,g4e2,i7b7,e1f1,g2f2,f1f2,h6h5,e6d6,b7a7,f2f1,a7e7,e2g4,i5i4,f1f2,c6d4,d6e6,h5h8,g4i2,e7c7,g0e2,d4e6,g6f6,e8e9,f6f7,c7c4,e2g4,e6d8,i2g0,h8h3,f7e7,h3h6,g0e2,c4g4,f2f1,g4d4,f1e1,d4d0,e7e8,f9e8,e2c4,d0f0,e1e2,f0f5,c4a2,h6h3,e2e1,h3g3,e1e0,g3g8,a2c4,f5f6,c4e2,g8g1,e2c0,b0c0 +4a0bc6b4;a0a2,i9i8,f0e1,b7b3,e1f0,h7g7,a2i2,b9c7,d0e1,a9a7,i2h0,c6c5,h0h9,i8a8,a3a4,c5c4,h9i9,b3b1,e3e4,c4c3,i9g9,e9e8,g9d9,b1d1,d9d5,g7e7,e1d2,c7b9,d5b6,a8d8,b6g6,i6i5,g6f6,e7f7,f6f9,f7i7,f9d8,e8e7,d8d4,a7a9,g0i2,i5i4,d4d8,i7i8,e0d0,d1a1,f0e1,c3c2,d8c8,e7d7,c0a2,i8h8,c8h8,a1b1,h8f9,b1b4,i2g4,b4b3,f9c9,d7d8,e1f2,b3b7,g4e2,a9a7,c9e9,a7a9,e9g9,a9a8,e2g4,b7f7,a2c4,c2c1,g9c9,f7c7,a4a5,c7h7,g4i2,h7h2,c9b7,d8d9,b7b3,a8f8,b3a3,f8f3,a3c2,f3b3,i2g4,h2h3,c2b2,h3h9,d0e0,h9i9,b2b9,i9b9,g4e2,b9b4,a5b5,b3g3,b5b6,g3i3,b6c6,d9d8,f2e1,i3d3,e0d0,b4b7,c6d6,d3h3,d6e6,a6a5,e4e5,h3e3,c4a2,i4i3,e5f5,b7f7,f5g5,f7f2,e2g0,d8d9,e1f0,e3e2,e6e7,e2e4,e7d7,d9e9,g5h5,e4i4,d2e1,i4g4,e1f2,g4c4,g0i2,e9f9,a2c4,a5a4,c4a2,f9f8,a2c4,f8f7,f2e1,a4a3,e1d2,f7f8,c4a2,c1b1,d7c7,i3h3,c7b7,f8f7,b7b8,b1b0,h5h6,h3h2,b8b9,f7e7,b9c9,e7e8,h6g6,b0c0,a2c0,e8e9,g6g7,a3b3,c0a2,e9f9,a2c0,b3b2,g7f7,b2a2,c9d9,h2h1,f7e7,a2a1,d0d1,a1a0,d2e1,a0b0,d1d0,h1h0,d9c9,b0c0,d0d1,c0d0,d1d0,f9e9,d0d1,h0g0,c9b9,g0f0,e1d0,f0g0,i2g0,e9f9,d0e1,f9f8,b9c9,f8f9,g0e2,f9e9,c9b9,e9f9,e7e8 +fc365fc6;e3e4,b7d7,g0i2,b9a7,a0a1,h7f7,a1f1,e6e5,i2g4,f7f0,f1f7,e9e8,e4e5,c6c5,f7f6,e8e9,a3a4,a7b9,f6e8,f9e8,e5f5,d7d5,f5g5,c9e7,g5g6,d5d8,g6f6,a9a8,g4i2,d8d2,f6e6,d2f2,e0e1,f2c2,e1d1,f0f1,d1e1,f1f5,e6d6,c2f2,e1e0,b9a7,a4a5,e7g5,i2g0,a8b8,a5a6,b8b4,a6b6,f2d2,g0e2,d2d4,d6d7,g9i7,d0e1,b4c4,d7d8,f5f0,i3i4,h9g7,i4i5,g7f9,i5i6,c4c3,d8d9,e9d9,i6i7,d4c4,e1f2,c4b4,b6c6,i9i8,c6d6,b4d4,i7h7,i8g8,g3g4,f9h8,d6c6,c3c0,e2c0,g8g7,c6d6,d4c4,c0e2,g7h7,d6c6,f0i0,e0e1,c4c6,e1f1,c6c9,e2c0,c9b9,f1e1,d9d8,e1e2,h8f7,e2e1,i0f0,g4g5,b9e9,c0e2,a7c8,e1f1,h7h3,f1f0,h3h0,e2g0,h0h7,f2e1,e9e1,g0e2,e8d7,e2g4,c8a7,g4e2,a7b9,f0e0,d7e8,g5f5,e1h1,e2g0,h7h3,f5e5,h3h8,e0f0,h8f8,e5d5,f8h8,f0f1,h1h2,d5c5,d8d7,f1f0,h2b2,c5d5,b2b1,f0e0,b1c1,d5c5,f7e5,c5b5,h8h4,g0i2,c1c7,i2g4,h4h9,g4e2,h9h3,b5a5,h3h0,e2g0,c7c2,e0f0,c2c1,a5a6,h0h9,f0e0,c1c3,g0e2,c3a3,e2c4,a3e3,a6b6,e5c6,b6a6,c6e5,e0f0,h9d9,a6b6,e3e2,b6b7,b9d8,f0e0,e5c4,e0f0,e2a2,f0f1,d9c9,f1e1,e8f9,e1f1,c4e3,f1e1,e3d1,e1e2,a2d2,e2e1,c9b9,e1d1,b9e9,b7c7,d7e7,d1d0,d2d6,d0d1,e9e8,d1d0,d6d2,c7d7,e7d7,d0d1,d8b9,d1d0,e8e1 +45146a05;g0e2,h7h5,a0b2,f9e8,b2b0,h5a5,b0a2,a5e5,a2c1,b7d7,c1d1,d7i7,e3e4,i7i3,d1d3,i9i7,f0e1,e5c5,c3c4,i3i1,c0a2,e8f9,d3d6,i7b7,e2c0,c5e5,a3a4,i1i4,a4a5,i4i2,a5a6,h9g7,c4c5,g7e8,d6d9,e9d9,e0f0,e5d5,a2c4,i2i4,a6a7,e8d6,c5d5,b7c7,d5d6,g9e7,e1f2,e7g5,c4e2,i4i3,c0a2,i3i4,e2c0,c7e7,a2c4,i4h4,g3g4,e7b7,c4a2,b7i7,d6d7,i7g7,a2c4,h4h9,d7e7,c9a7,f0e0,g7h7,e7d7,h7f7,f2e1,f9e8,d7e7,f7f3,e1d2,f3f5,e7e8,b9d8,e4e5,h9h6,d0e1,a9a8,e1d0,f5f1,e5f5,a8b8,e8f8,d8f7,c0a2,f1f3,f5e5,b8b4,f8e8,b4b0,g4g5,f7g5,e5f5,h6h1,f5e5,f3f9,e5e6,h1c1,d2e1,b0b8,e1d2,c1c0,a2c0,g5i4,c4a2,b8b2,d0e1,f9f8,e1f2,f8i8,d2e1,i8i9,e6f6,b2c2,e8e9,d9d8,a2c4,c2c1,c0e2,i9i7,e9d9,c1b1,f6g6,d8d9,g6h6,d9e9,c4a2,b1b3,e2c0,b3b4,a2c4,i7g7,c4e2,g7i7,e2g4,i7i9,g4i2,b4b8,e0f0,b8a8,h6h7,e9d9,h7g7,a8h8,c0e2,h8h0,f0f1,h0b0,e1f0,b0b5,g7g8,b5f5,f1e1,f5f2,g8f8,i9f9,e1e0,c6c5,e0e1,f2g2,f8g8,g2f2,i2g4,a7c9,g8g9,f2h2,e2g0,f9g9,g4i2,h2e2,e1f1,g9e9,i2g4,e2e0,g4i2,e0e5,i2g4,e5g5,f1f2,g5g4,f2f1,g4g5,f0e1,g5g9,e1d2,g9g7,d2e1,g7a7,e1d2,a7d7,f1f2,d7d5,g0i2,i4g3,i2g4,d5d4,g4e2,i6i5,e2g0,e9e1,g0e2,d4d2 +6c845287;a0b0,b7b4,b0b2,b4c4,i3i4,h7f7,b2b1,c4h4,g0e2,f7f6,b1b5,a9a7,b5f5,a7d7,e2g4,b9c7,f5d6,e9e8,d6c6,c7b9,c6d6,h9i7,d6d2,i9h9,c0a2,h4h3,a2c4,h3e3,d2h2,i6i5,h2c2,h9h5,c2b0,d7a7,b0c2,g9e7,c2e3,f6f7,c4a2,f7f8,e3f1,a6a5,f1f8,e8f8,e0e1,g6g5,g4i2,h5h4,a2c4,h4f4,e1d1,f4c4,d1d2,c4i4,c3c4,i4c4,g3g4,a7a6,d2e2,c4c0,i2g0,a6a9,g4g5,c0a0,g5f5,f8f7,e2e1,e7g9,g0e2,f7f8,e2g4,a0a1,e1e2,b9c7,e2f2,c7d5,f0e1,a1c1,g4e2,d5f4,f5g5,c1c4,e2g0,i7g6,g0i2,c4c2,f2f1,c2c5,e1f0,c5c0,g5f5,a5a4,d0e1,f8e8,f5f6,g6h8,f6g6,f4g2,e1d0,c0c3,d0e1,c3b3,g6g7,b3b2,e1d2,g2e3,f1e1,h8f7,e1e2,b2d2,e2e1,g9i7,i2g4,c9a7,g4e2,d2d4,e1e0,a7c5,e2c0,e3f1,c0e2,f1d0,e0e1,e8d8,a3a4,d4f4,e2g0,f9e8,a4a5,e8d7,g7g8,f7e5,g0i2,e5g4,i2g0,g4f2,g0i2,i5i4,g8g9,d0b1,a5b5,f2h1,b5a5,d7e8,i2g4,f4f1,e1e0,f1e1,f0e1,h1i3,g4e2,a9a5,e1f0,c5e7,e2g0,a5a7,f0e1,e6e5,e1f0,e8d7,g0e2,e5e4,e2g4,a7a0,e0e1,i7g9,e1e2,d8e8,f0e1,a0c0,e1d0,i3g4,e2e1,e7c5,e1f1,g4i5,d0e1,c0d0,e1f2,c5a7,f2e1,e4f4,e1d2,f4g4,f1f2,i5g6,d2e1,d0a0,f2f1,i4h4,e1d0,g9i7,f1f2,g6e5,f2f1,a0d0,f1f2,e5c4,f2f1,d0d6,f1f2,a7c9,f2f1,e8e7,f1f2,b1d0 +8607cd6a;a0b2,g9e7,e3e4,e7g9,i3i4,b7g7,b2a2,i9i7,a2b2,c9e7,b2i2,g6g5,e0e1,h7h0,i2i6,g5g4,i6c6,h0h7,c6a7,i7i8,c0a2,h7h6,g3g4,h6i6,a7d7,e9e8,d7c7,e8d8,c7c9,g7i7,c9a9,i6g6,a2c0,b9a7,a9f9,d8d7,f9e7,d7e7,c3c4,i8a8,c4c5,e7e8,c5b5,a7c8,e1e0,e8f8,a3a4,i7c7,e4e5,c8e9,b5b6,g6h6,g0e2,c7c9,e0e1,a8d8,a4a5,h9i7,c0a2,h6b6,i4i5,c9a9,e5f5,d8d2,e1e0,b6b9,e0e1,d2d5,f5e5,a6a5,e5d5,b9b8,g4g5,e9f7,e2c0,i7h9,g5f5,e6e5,f5f6,e5e4,f6e6,a9a7,e6d6,f7h8,d6d7,h8f7,d7c7,a7b7,c7b7,f7e5,c0e2,b8c8,d5e5,f8f7,a2c4,c8e8,c4a2,e8e9,e2c0,e9e6,c0e2,h9g7,a2c0,g7h5,i5h5,e4e3,b7c7,e6h6,e5e6,g9e7,e6d6,h6g6,e1d1,g6g4,d1d2,g4g1,c7b7,f7f8,h5g5,g1g3,e2c4,g3g2,f0e1,a5a4,c4a2,e3e2,d2e2,e7c9,g5f5,g2a2,d6d7,d9e8,e1f2,a2b2,d7c7,b2a2,d0e1,f8f7,b7b8,a4b4,e1d0,c9e7,b8c8,e8d7,c7d7,a2d2,f5g5,d2c2,g5f5,e7c9,c8c9,b4c4,f5g5,c4b4,d7c7,c2c3,c7d7,c3c8,d7c7,c8i8,g5g6,i8d8,e2e1,d8f8,c7c8,f8e8,g6g7,f7f8,c9b9,f8f9,c8c9,e8f8,e1e0,f8e8,c0a2,e8e1,e0e1,b4c4,e1d1,c4c3,d0e1,c3b3,e1f0,f9e9,a2c0,b3c3,g7f7,c3c2,b9a9,e9f9,f7e7,c2d2,d1d0,f9e9,c0a2,d2d1,d0d1,e9f9,c9d9,f9f8,d1d2,f8f9,d9c9,f9e9,a2c4,e9f9,e7e8 +3c3f25ad;a0c1,c9e7,f0e1,e7g5,a3a4,b9a7,g3g4,h7h3,g0i2,a6a5,i2g0,h3g3,c1c2,e6e5,c2f2,a9b9,f2f1,b7b5,g0i2,b9a9,i3i4,b5b1,f1g1,a9a8,e1f2,b1b6,g1f3,b6a6,c0e2,a8e8,f3f6,a5a4,e2c4,e8f8,e0f0,c6c5,f6e8,f9e8,e3e4,a6a5,f0f1,a7c8,c4e2,e5e4,i4i5,i9i7,i2g0,g3i3,e2c0,i3i2,c0a2,f8h8,i5h5,h8h5,g0i2,i7a7,a2c0,h5h0,c3c4,c8b6,c4c5,h0i0,f2e1,e8f9,c5c6,a7g7,e1f2,i6i5,c0e2,g7c7,c6d6,d9e8,f1e1,a4b4,e2c0,c7i7,c0a2,i0h0,e1d1,h0h3,d1e1,i7a7,g4g5,a7h7,d6e6,a5a8,a2c0,h3h1,e1e0,h7a7,g5f5,h1a1,f5e5,a7i7,i2g4,a1h1,c0a2,b6d5,e0f0,e4e3,e6e7,e8d7,e7e8,e9e8,f0e0,h1h2,f2e1,a8a3,g4i2,a3b3,e1d2,h2e2,e0f0,i7g7,d0e1,d5e7,e1d0,e2e0,f0f1,e8d8,e5d5,g7i7,d5e5,e0e1,f1f0,d7e8,e5e6,i7g7,a2c0,e1c1,d0e1,c1e1,i2g4,b3b0,c0a2,e1d1,a2c4,d1a1,g4e2,h9i7,e6e7,a1a7,e7d7,g7d7,d2e1,g6g5,e2g4,d7h7,g4i2,b0d0,i2g0,e8f7,e1d0,h7h8,g0i2,b4b3,f0e0,f9e8,i2g0,h8h7,c4e2,h7h2,d0e1,e3e2,e1f0,h2h1,f0e1,a7a1,e1f2,i7h9,e0f0,h1h0,f2e1,a1a3,e1d2,h0g0,f0f1,a3a9,d2e1,g0b0,e1f0,b3b2,f0e1,a9f9,e1d0,f9b9,f1f0,b9b4,f0f1,b4g4,d0e1,g9i7,e1d0,e8d7,d0e1,g4g0,e1d2,g5g4,d2e1,i5i4,e1f2,b0d0,f2e1,d7e8,e1d0,e2f2,f1e1,i4i3 +35a10a6e;f0e1,g9e7,e3e4,h7h5,e4e5,h5h1,e0f0,h9g7,g0e2,e9e8,a0a1,b7d7,g3g4,h1h5,a1c2,h5i5,c2d2,i5h5,d2d9,g6g5,d9e9,e8d8,e9c8,d8d9,c8c6,e7g9,c0a2,d7b7,e5f5,b9c7,f0e0,b7b5,f5g5,h5h7,c6c5,b5b6,c5c4,g7e8,e1f2,b6b5,g5f5,b5b8,c4a4,i9h9,f5f6,h7h0,a4e4,h9i9,e4e3,c7d5,f2e1,b8b3,e3g3,h0f0,a3a4,e8g7,f6f7,e6e5,f7g7,f0f5,g7f7,i9i7,a4a5,d5c3,g3f3,c9a7,f3f1,b3b1,e1d2,c3d1,f7e7,i7i9,d0e1,f5f8,a5b5,d1b2,e1d0,f8f7,f1f0,b2d1,b5b6,e5e4,e7f7,d9e9,f0h0,e9d9,h0h7,i9h9,h7f8,d9e9,f8e8,f9e8,d2e1,a7c9,b6b7,e9d9,g4g5,g9e7,b7b8,h9h5,a2c4,b1b2,e0f0,d1c3,f7e7,e4e3,e1d2,c3a4,g5g6,b2b3,f0e0,h5h1,g6h6,h1h0,e0e1,h0h2,e1d1,h2h6,b8c8,a9b9,c8b8,h6h1,d0e1,a6a5,i3i4,e8f9,e7f7,i6i5,f7e7,b3c3,e2g0,h1h3,g0e2,f9e8,e7f7,h3f3,b8b9,c3c2,e1f2,c2e2,f7g7,e3d3,c4a2,e2e3,i4i5,f3f8,d1e1,e3f3,i5h5,c9a7,e1e0,d3d2,a2c4,a7c5,h5g5,f3f5,g7f7,c5a7,c4e2,f5c5,f2e1,f8h8,e2c0,h8h9,f7e7,c5f5,c0e2,d9d8,e0f0,d2e2,g5f5,h9c9,f0e0,e8f9,e1f2,c9d9,f5e5,d9b9,e7d7,d8e8,e0f0,a7c9,d7c7,e8e7,f2e1,a4b2,c7d7,e7d7,e1d0,f9e8,e5e6,c9a7,e6e7,d7d8,f0e0,b2c0,e7e8,d8e8,d0e1,b9g9,e1d2,e8d8,e0f0,d8d9,f0e0,g9e9,e0d0,e2d2 +06c769b2;a3a4,a9a8,g3g4,h7h4,e3e4,h4h5,f0e1,i6i5,a0a2,b7d7,g0i2,d7d1,a2a0,d1d2,a4a5,h5g5,a0a4,a8a9,a4a6,g9e7,c0e2,d9e8,a6c6,c9a7,c6b8,d2d3,b8b7,g5e5,b7c5,e5e2,e0f0,e7c5,g4g5,g6g5,f0f1,i5i4,e1d2,d3d8,i2g4,e9d9,d2e1,e2e3,f1f2,i9i6,i3i4,e8d7,e1d2,d8a8,c3c4,i6i9,d0e1,d9d8,f2e2,e3f3,e1d0,f3f5,e2e1,f5e5,e4e5,e6e5,c4c5,a7c5,g4e2,b9c7,a5b5,a9d9,e1e0,c7a6,d0e1,a8a9,e2g4,h9i7,g4e2,d7e8,i4i5,i7h5,e1f2,a9a8,b5a5,d9e9,f2e1,e9c9,e2g4,e5e4,i5i6,e4d4,a5b5,c9c8,e1f0,c8c6,i6i7,d8d9,e0e1,d9e9,e1f1,d4e4,f0e1,c6c9,g4e2,i9i8,b5a5,e4e3,f1f2,i8g8,e1f0,c9c8,e2c0,a8a7,a5a6,a7h7,c0a2,g8i8,a2c4,h7b7,c4e2,h5g3,e2g0,g5g4,d2e1,b7g7,a6b6,i8i7,b6b7,g3i4,b7a7,c8c6,g0e2,g7g8,e1d2,i7b7,a7a8,g8g5,f2f1,e3e2,a8b8,g5g9,b8c8,c6c7,c8c9,c7i7,c9d9,e9d9,f0e1,i7h7,e1f2,c5e7,f1f0,b7b4,f2e1,b4b8,f0f1,h7h0,e1d0,g9i9,d2e1,g4f4,e1f0,b8b6,d0e1,h0f0,e1f0,f4g4,f0e1,e8d7,e1d2,b6a6,d2e1,a6a2,e1d0,g4f4,f1f0,a2a4,f0e0,i4g5,e0f0,a4a5,f0f1,e7c9,f1f0,g5h7,f0f1,f4g4,f1f0,i9i8,f0f1,a5a2,f1f0,i8e8,f0f1,h7f6,f1f0,e8e9,f0f1,c9a7,f1f0,a2b2,d0e1,f9e8,e1d2,f6g8,f0f1,b2b1,d2e1,g4h4,f1f0,a7c5,e1d2,e2d2 +7860cedc;i3i4,c9e7,a3a4,h7h1,i4i5,e7c9,a0a6,h1e1,a6a8,g9i7,a8h8,b7e7,h8h1,e7d7,g0e2,e9e8,f0e1,d7d2,e0f0,e6e5,c3c4,c9e7,i5i6,a9a7,e3e4,b9c7,f0e0,e5e4,h1h4,c6c5,e1d2,c7e6,h4i2,e4f4,i2i4,a7a5,d2e1,e7g9,i4i0,a5a8,i0f0,a8a4,e1d2,a4a5,d2e1,a5a3,f0f3,a3a7,f3c3,a7c7,e1d2,h9g7,e2g0,c5c4,i6h6,g9e7,e0f0,c7c8,c3a2,c8c5,g0e2,e6d4,g3g4,f4g4,h6i6,c5b5,d0e1,c4c3,a2a3,g4g3,c0a2,g6g5,a3g3,d4e2,a2c0,e7g9,g3e3,e8d8,c0e2,b5d5,e2g0,d5e5,e1f2,c3d3,e3d5,d8e8,d5d9,g7i6,f2e1,e5e4,g0i2,e4g4,e1d0,g4g1,i2g0,g1e1,d9d7,e1e4,d7b7,e4c4,b7d7,c4c9,g0i2,c9d9,d7h7,d9d8,h7f8,e8e9,f8e8,d8e8,i2g0,e8e0,f0f1,i9i8,f1f2,e0g0,d2e1,d3c3,e1f0,i8g8,f0e1,g0i0,e1d2,g8i8,f2f1,i8i9,d2e1,i0e0,f1f2,e0f0,e1f0,i6g7,f0e1,g5g4,f2f1,c3b3,e1d2,g9e7,f1f0,e7g9,f0f1,b3a3,d0e1,g7f5,f1f0,f5e7,e1d0,e9d9,d2e1,e7g8,f0e0,g9e7,e1d2,d9d8,d0e1,e7g5,e0f0,i9g9,e1f2,a3b3,f2e1,g8f6,e1f2,b3c3,f2e1,g9g6,f0f1,d8d9,e1f2,g4f4,f1e1,f9e8,e1e0,f6e4,d2e1,d9e9,e0f0,e4d6,f0f1,e8f9,e1d0,d6e8,f2e1,g6e6,e1f0,i7g9,f0e1,e8c7,e1d2,c7a8,f1f0,f4f3,d2e1,c3c2,f0e0,e6e7,e0f0,e7f7,e1d2,c2b2,f0f1,a8c9,f1f0,f3f2,d2e1,f7f6,f0e0,f2g2 +d165fbcd;a0c1,b7f7,c1d1,f7b7,i3i4,b7e7,d1i1,h9i7,f0e1,e7b7,e3e4,b7g7,i1h1,g7d7,h1f0,a9a7,f0f4,a6a5,e1f0,e6e5,f4f2,h7f7,f2g4,i9h9,g4g5,b9c7,g3g4,h9h5,g5g6,i7g6,e0e1,g6e7,e1f1,a7a8,c0a2,a8e8,a2c0,h5h9,f1f2,c7a8,g0e2,f7f3,e2c4,i6i5,i4i5,e7g6,c4a2,g6i5,f2f1,a5a4,g4g5,d7d4,f1e1,c9e7,a2c4,f3f5,a3a4,h9h8,e1e0,h8h9,c4e2,e7c5,d0e1,i5h3,g5f5,d4d5,e1d2,e8g8,f5g5,h9h5,e2c4,g8g5,e0e1,h3g1,c0e2,h5h9,e2c0,h9h7,e4e5,g1e2,c4e2,d5d8,e2g4,c5e7,c0e2,g5e5,e1f1,h7h0,c3c4,a8c9,f0e1,d8a8,e1f0,a8a9,d2e1,e5d5,a4a5,d5d0,c4c5,a9a8,a5b5,a8g8,c5d5,g8a8,b5b6,d0d1,d5c5,a8a6,b6b7,d1d6,e2c4,a6a2,g4i2,a2a7,i2g4,a7a9,c4e2,d6e6,c5d5,a9a4,e2c0,a4d4,e1d2,h0h8,c0a2,e7g5,d5d6,e6e7,b7b8,h8b8,f0e1,b8b2,d6d7,b2b8,f1f2,b8d8,d7c7,d4b4,e1f0,e7e4,f2f1,d8d7,a2c0,c9b7,d2e1,b4g4,c7c8,d7h7,c8d8,h7i7,d8e8,e9e8,c0a2,b7d6,f1f2,g4f4,e1d2,e4e6,a2c4,e8d8,f0e1,f4g4,e1f0,i7i1,d2e1,f9e8,e1d0,e6h6,f2e2,i1c1,e2f2,g4g1,d0e1,g1f1,c4a2,h6h7,e1d2,h7h3,a2c4,h3e3,d2e1,e3c3,f2e2,c1c2,e1d2,c2a2,f0e1,f1f2,e1f0,f2f6,e2f2,f6f0,f2f1,f0e0,d2e1,e0f0,c4a2,c3c2,e1f2,c2a2,f1f0,d6c8,f0f1,c8d6,f1f0,a2e2,f2e1,e2e1 +40274a56;d0e1,b7e7,e1d0,h9g7,e0e1,e7c7,i3i4,c7b7,g3g4,b7b1,a0b2,g7h9,b2b9,a9a8,c3c4,a8c8,b9b4,c8e8,g0i2,h7g7,b4b3,i9i7,b3b4,g7e7,e1e2,c9a7,b4b2,g6g5,e2d2,e8h8,a3a4,i7i8,b2b7,h8h3,e3e4,a6a5,b7d6,i8c8,e4e5,b1h1,e5e6,h3b3,f0e1,b3b0,d6d7,e9e8,g4g5,b0b1,d7c7,e8f8,i4i5,h1i1,c7e7,h9i7,e7f7,f8f7,i2g0,d9e8,i5h5,e8d7,g5f5,b1b7,f5f6,f7f8,c0a2,i1h1,f6g6,b7c7,g6g7,a7c9,g0e2,h1g1,g7f7,f8e8,e2c0,g1g8,a4a5,g8g2,f7e7,c9e7,d2d1,i7g8,a5b5,g2g4,h5i5,g4d4,i5h5,d4h4,e6e7,e8f8,h5i5,c8a8,b5b6,a8a6,e1d2,a6a9,e7d7,g8h6,i5i6,h4h0,i6h6,h0g0,h6g6,a9d9,g6h6,d9e9,d0e1,f8f7,d7c7,g9e7,d1d0,e9e8,d0e0,g0c0,e1f0,e7c5,f0e1,e8e1,e0e1,f7f8,c4c5,c0c2,b6b7,c2c1,c5c6,c1a1,c6d6,a1c1,h6h7,c1d1,c7c8,d1d0,d6e6,d0c0,a2c0,f8e8,e6e7,e8e9,c8b8,e9d9,c0a2,d9e9,e1d1,f9e8,h7g7,e9f9,b7c7,e8d7,c7b7,f9f8,b7c7,f8f9,a2c0,f9f8,d1e1,f8f9,b8b9,f9e9,g7g8,e9d9,e1f1,d9e9,c7b7,d7e8,f1f2,e8f9,g8f8,f9e8,f8g8,e8d9,b9c9,d9e8,c9d9,e8d9,g8h8,d9e8,f2f1,e8f9,h8g8,f9e8,c0a2,e8f7,e7d7,e9d9,d7e7,d9d8,f1f2,d8d9,d2e1,d9e9,f2e2,e9d9,e7d7,f7e8,e1f0,e8f9,d7c7,f9e8,g8f8,d9e9,a2c0,e9d9,c0a2,d9e9,c7c8,e9d9,c8d8,d9e9,b7b8 +deef5387;d0e1,h7h2,e1d0,i6i5,d0e1,c9a7,c0e2,b7e7,a0a2,e7e3,a2c1,i5i4,c1b3,g6g5,i3i4,b9d8,b3b9,a9b9,g3g4,h2i2,e0d0,i9i8,e1f2,b9a9,f2e1,i2i3,e2c0,c6c5,g4g5,d8b7,i4i5,e3e4,g5f5,i3i1,f5g5,e4d4,c0e2,a9a8,g5h5,i1f1,a3a4,e6e5,e2c4,b7d8,c4a2,d8b9,h5g5,a8h8,g0i2,c5c4,d0e0,d4h4,e1d0,h4h6,a2c0,f1h1,i2g4,c4d4,g5g6,i8i5,g4i2,f9e8,i2g4,h1a1,g6g7,d4e4,g4e2,h6b6,e2c4,i5h5,g7f7,b6c6,f0e1,h5h0,e1f0,h8h4,a4a5,e9f9,c0e2,h0h2,e0e1,c6h6,e2g4,a1b1,c4a2,b1b6,e1d1,b6b7,a5a6,h4i4,d0e1,b7d7,d1d0,d7d5,e1f2,i4i2,f7f8,f9e9,f0e1,i2i7,a2c0,i7f7,g4i2,f7b7,c0e2,h2h1,a6b6,h1h4,e1f0,h9g7,f0e1,h4h5,c3c4,d5b5,e2g0,e4f4,e1f0,h5h4,b6a6,e8f7,f0e1,h6h9,d0d1,h4i4,g0e2,g7f9,e2g0,b5b2,f8g8,h9h6,c4c5,h6c6,a6b6,b7b6,i2g4,b6b4,g8f8,i4h4,f8e8,f7e8,c5b5,e8f7,e1d0,b4c4,d1e1,h4i4,g0i2,b9d8,e1e0,f4g4,e0f0,d8b7,b5a5,c4c3,i2g0,b2b0,d0e1,c6b6,g0e2,c3c2,f0f1,b0b4,f1f0,b7a9,e1d0,i4i8,e2c0,f9e7,c0a2,b4c4,d0e1,c2a2,a5a6,c4c2,a6b6,e5e4,b6a6,c2c0,a6b6,e7d5,e1d2,i8i2,d2e1,e9e8,e1d2,d5f4,f2e1,i2i6,e1f2,i6i0,f0f1,a2b2,f1e1,b2b6,e1f1,b6a6,f1e1,i0i8,e1e0,f4g6,e0e1,c0f0,e1e0,a6a2,e0d0,a2a0,d0d1,i8i1,f2e1,i1f1 +aa7a232a;i3i4,h7h0,e3e4,h0h2,a0b0,b9a7,g0e2,b7c7,e4e5,c7b7,b0b5,h2h6,b5b7,i6i5,e2g4,i9i6,b7b8,h6h0,f0e1,i6i8,i4i5,g9i7,e1f2,d9e8,c0a2,h9f8,b8a8,e6e5,a8b8,h0h6,b8b9,a7b9,d0e1,h6h5,g4e2,e9d9,i5i6,a6a5,e1d2,i8h8,f2e1,f8h9,c3c4,d9d8,a3a4,g6g5,i6i7,h8h6,e2g4,h9g7,i7h7,a9a8,e0d0,c6c5,e1f0,h5h2,a2c0,a8b8,a4a5,g7e6,g4i2,h2h5,f0e1,h5h0,d0d1,h6g6,h7h8,b8b3,i2g4,b3c3,e1f2,g6i6,h8g8,e6g7,c0a2,i6i9,g4e2,c9a7,e2g0,i9i5,g8h8,g7h9,h8h9,i5i9,a5b5,i9i0,c4c5,c3a3,c5c6,a3a2,b5a5,h0h5,h9g9,i0i5,c6d6,i5i3,f2e1,i3g3,g9f9,h5h8,d6c6,a2a5,f9e9,g3c3,c6d6,a5a4,e1d0,a4b4,e9f9,d8d9,d6d7,h8g8,g0e2,b4e4,e2g0,e4e1,d1e1,g8g6,f9e9,d9e9,e1e2,g6g0,d7e7,g0g2,d2e1,e8d9,e2d2,c3c0,e7e8,e9e8,d2e2,c0c1,e1d2,g2d2,e2f2,e8d8,f2e2,d8d7,e2f2,d2a2,f2e2,c1e1,d0e1,g5g4,e1f0,a2a0,f0e1,a7c5,e1d0,g4f4,e2f2,d7d8,d0e1,c5a7,e1f0,a0a5,f2e2,a7c5,f0e1,b9a7,e2f2,c5e7,f2e2,a7b9,e1d2,e5e4,e2e1,d9e8,e1d1,a5c5,d1e1,c5c2,e1f1,c2c0,d2e1,c0c9,e1f2,c9c4,f1e1,f4f3,e1e0,c4c8,f2e1,b9a7,e0f0,c8b8,f0f1,b8b1,e1f2,e4e3,f1e1,a7c8,e1f1,c8a7,f1f0,b1f1,f2e1,f1i1,f0e0,i1i0,e1f2,f3f2,e0f0,f2e2,f0f1,i0i1,f1f0,i1f1,f0f1,a7b9,f1f0,e2e1 +2c775301;a0a6,b7c7,e0e1,c7b7,a6a7,h7f7,c0e2,b7b3,e2c0,f7e7,e1e2,b3b7,f0e1,b7c7,e2d2,g9i7,a7b9,e7d7,g0e2,d7d6,e1f2,c7b7,b9c9,a9c9,c3c4,d6d4,d2d1,c9c8,e2g4,i7g9,d0e1,d4d2,e1d2,b7b2,e3e4,c8h8,g4e2,h8h3,g3g4,h3h4,c4c5,b2b7,d2e1,e6e5,c0a2,h4h0,e2c4,h0a0,c5c6,b7b2,a3a4,a0a1,d1d2,i9i7,c6b6,a1a2,e1d0,b2b3,d2d1,e5e4,d1e1,i7c7,g4g5,h9g7,c4e2,i6i5,e1d1,a2a4,i3i4,a4a7,e2c4,c7e7,d1d2,f9e8,b6c6,g9i7,d0e1,b3b1,g5g6,a7a4,e1f0,b1b0,c6c7,i5i4,c4a2,b0b9,d2d1,e7e5,d1d2,a4a2,d2d1,e5i5,f2e1,i7g5,e1d0,e8f9,d1e1,a2a4,g6g7,b9b2,c7d7,b2a2,d7c7,a4c4,e1f1,c4a4,f1f2,a4a9,f0e1,a9a5,c7d7,a5f5,f2e2,f5e5,g7f7,g5i7,e1f2,e9e8,e2d2,e8e9,f7e7,f9e8,d2d1,a2c2,f2e1,e4e3,e1f0,c2c5,e7f7,c5c7,d7e7,c7c5,e7e8,e5e8,f0e1,e9f9,d1d2,i4h4,f7f8,e8f8,e1f0,c5b5,f0e1,b5b4,e1f0,b4d4,f0e1,i5c5,e1f0,c5h5,f0e1,h4i4,e1f0,f8f2,d2d1,f2a2,d0e1,a2a7,e1f2,h5h9,d1e1,a7d7,e1f1,d4h4,f1e1,h4d4,e1f1,d4d3,f1e1,h9h1,e1e0,h1h2,e0d0,f9f8,d0d1,d3d5,f0e1,i4h4,e1d0,h2g2,d1d2,e3e2,d2d1,f8e8,d0e1,g2g1,d1d0,g1g6,e1f0,g6g8,f0e1,g8g1,d0e0,e2f2,e0d0,g1g8,e1f0,h4h3,d0d1,d7e7,d1d0,g8g2,d0d1,e8e9,d1d0,f2e2,d0e0,e9f9,f0e1,i7g9,e0d0,e2e1 +77af924a;a3a4,b9c7,c3c4,h7h0,e3e4,h9g7,a0b2,c9e7,b2e2,e7c5,e2f2,b7b8,f2f1,h0h5,c0e2,b8e8,f1h1,e8a8,h1b1,i9i7,b1b3,h5h2,i3i4,f9e8,b3a1,h2h8,a1a2,e8d7,a2b0,g7h9,d0e1,h8e8,b0d0,i7i8,g0i2,a8c8,d0d3,e8g8,d3b3,g8g3,b3c1,c8e8,c1a2,e8c8,a2c3,c5e7,c3d1,d9e8,d1b2,e9f9,b2a2,a6a5,a4a5,a9c9,a2b4,g3d3,b4b2,c9e9,b2a0,g9i7,e2g0,e9d9,e4e5,d3b3,a0d0,g6g5,d0d6,b3b2,e1d0,i7g9,d6d9,e8d9,i2g4,c8g8,e0e1,g8f8,g0i2,b2b3,e1f1,c7b9,e5d5,f8f7,a5b5,d9e8,d5c5,b3b0,i2g0,c6c5,g0i2,f7f4,f1e1,c5c4,e1e2,f9f8,b5c5,b9d8,f0e1,f8f9,e2d2,e8f7,e1f0,c4d4,c5c6,h9g7,i2g0,i8h8,d2d1,f4i4,g0e2,i4i0,d1d2,g5g4,e2c0,g7i8,f0e1,i0i1,c0a2,d4e4,e1f0,i1i5,a2c4,e4d4,c4a2,i5a5,d2e2,b0b7,e2f2,a5e5,f0e1,h8h2,f2f1,b7a7,f1f0,d8b7,f0f1,h2a2,c6b6,a7a4,b6c6,a2a3,c6d6,e5g5,e1d2,d4d3,d6c6,b7c5,c6d6,a4a5,d2e1,g5g6,e1f0,a3a0,f0e1,a5a8,e1f0,a8a1,f1f2,g9i7,f0e1,a1b1,d6d7,b1b7,d7e7,a0c0,e7f7,i8g9,e1d2,b7a7,d2e1,a7a5,e1d2,g9f7,d0e1,a5a3,f2f1,a3a6,e1d0,a6a9,d0e1,i6i5,e1f0,c5e4,d2e1,e4d2,f1f2,d2f1,e1d2,a9a1,d2e1,a1a4,e1d2,a4a2,f2f1,g4h4,d2e1,g6g7,e1d2,a2a7,f1f2,c0e0,d2e1,h4i4,e1d0,g7g2,d0e1,d3e3,e1d0,a7a1,f2f1,e0d0,f1e1,f7h8 +b32fb605;a0a1,e6e5,a1c2,b7b0,c2c6,h7i7,c6e7,c9e7,e0e1,b0b4,e3e4,b4c4,c3c4,i7g7,c0e2,e7c5,g0i2,g7f7,e2g0,f7f1,c4c5,b9d8,c5b5,f1f8,b5b6,d8b9,b6c6,b9a7,i2g4,f8a8,e1e2,a8c8,a3a4,a7b9,g4i2,c8h8,c6c7,h8i8,a4a5,e5e4,a5a6,f9e8,e2e1,h9i7,g0e2,g6g5,i2g0,i7g8,c7d7,i8i7,e1e0,a9a7,f0e1,i9i8,a6b6,e8f7,d7c7,e4e3,e1d2,a7a4,c7c8,g8h6,e2c4,h6g4,b6b7,i8c8,d0e1,c8c5,g0i2,i7i3,c4e2,i6i5,e2c0,c5c0,e1d0,a4a5,i2g0,c0c8,b7b8,a5a4,g3g4,i5i4,g0e2,e3e2,g4g5,a4b4,d2e1,b4b6,e0f0,b6b2,b8a8,g9i7,f0e0,i3h3,g5f5,e9f9,e1f2,c8c0,a8a9,d9e8,f5g5,i7g9,a9b9,e2f2,g5f5,b2b6,f5g5,c0c5,b9a9,f2f1,g5f5,b6c6,a9b9,c5c4,b9c9,c4a4,f5f6,h3d3,d0e1,g9e7,c9b9,d3b3,e1d2,c6e6,d2e1,b3b7,e0d0,a4b4,d0e0,b4c4,b9a9,e7c9,f6f7,b7b2,e0d0,c4e4,f7f8,f9f8,d0e0,f8f9,e0d0,f1e1,a9b9,b2d2,b9c9,e6h6,c9d9,e1d1,d0d1,e4e1,d1e1,d2h2,e1e0,h2a2,d9e9,f9e9,e0f0,h6h7,f0f1,a2g2,f1e1,g2g6,e1f1,e8f9,f1f0,g6c6,f0f1,c6c8,f1f0,h7h9,f0f1,h9i9,f1f0,c8c7,f0f1,c7f7,f1f0,f7f1,f0f1,i9i6,f1f2,e9d9,f2e2,i6e6,e2f2,i4i3,f2f1,e6c6,f1f0,c6a6,f0f1,a6i6,f1e1,i3i2,e1f1,i6c6,f1f0,c6c5,f0f1,c5f5,f1e1,f5h5,e1e2,h5b5,e2f2,b5h5,f2e2,h5h1,e2f2,i2h2,f2e2,h2g2 +7ef7546d;g0i2,b7c7,a0c1,h7h4,i2g0,f9e8,c1i1,i9i7,d0e1,c7f7,e1d0,h4h7,c3c4,h7h5,c4c5,f7h7,i1c1,h7e7,c1g1,e6e5,c0a2,e7b7,a2c4,c6c5,g1b1,c5c4,b1b6,h5h2,b6e6,c9e7,e6d6,b9c7,g3g4,a9a7,d6d2,b7b5,e3e4,b5b0,d2c0,h2h0,g4g5,i7h7,g5h5,c7a8,c0b0,h9g7,e4e5,c4c3,b0b6,a7c7,b6b4,h7h6,e5e6,c7a7,b4c4,a8c7,c4d4,g7e6,d4f4,h0h3,f4g2,e6g7,g2g6,e8d7,f0e1,h3a3,g6g1,a3a2,e1d2,a2c2,g1e2,g7e8,e2e7,g9e7,h5h6,e7c5,g0e2,c2a2,e2g0,c7a8,h6g6,a2c2,i3i4,a8c9,g0i2,c2c1,d0e1,e8c7,i2g0,c3c2,i4i5,a7a8,i5h5,e9e8,g6f6,c1c0,e0d0,a8c8,g0i2,e8e9,d0d1,d9e8,f6f7,c7a8,h5g5,c0e0,i2g0,e0d0,f7e7,e8d9,g5g6,d0c0,e7f7,c8e8,e1f2,a6a5,g6g7,e8d8,g0i2,c9b7,d2e1,c2b2,e1d0,i6i5,f7e7,c0b0,d1d2,b2b1,e7d7,b0a0,f2e1,a0a4,g7g8,a4i4,e1f2,e9e8,d0e1,i4c4,d7d8,b7d8,d2d1,c5a7,i2g4,e8e7,e1d2,b1b0,g4e2,d8e6,g8f8,a7c9,e2c4,e7f7,f8f9,c9a7,f9e9,e6g7,e9d9,a8c9,d1e1,g7h5,e1f1,h5g3,f1f0,g3h1,f0f1,h1g3,f1e1,g3f1,c4a2,a7c5,e1e2,a5a4,a2c0,b0c0,e2e1,c5a7,e1e2,a4b4,d9c9,f1d0,e2e1,d0f1,e1e0,b4c4,d2e1,a7c9,e1f0,f1d0,f0e1,f7e7,e0f0,d0f1,e1d2,f1d2,f2e1,c9a7,e1f2,d2e0,f0f1,e7f7,f1f0,e0f2,f0f1,c4c3,f1e1,c3c2,e1e2,a7c9,e2e1,f2g0,e1e2,i5i4 +94a72e83;i3i4,d9e8,g3g4,a6a5,a3a4,g6g5,g4g5,h7f7,i4i5,f7c7,a4a5,b7b5,c3c4,b5b7,g0i2,a9a5,a0a2,c7d7,e0e1,d7d5,e1e0,d5i5,g5f5,b7b5,i2g4,a5a6,a2e2,b5e5,e2e5,a6a3,e5e4,g9i7,g4e2,e9d9,e0e1,e8f7,f5g5,f7e8,e4c3,a3a6,c3c2,h9g7,c2c1,g7i8,c1a1,a6b6,c4c5,i5c5,a1a5,i6i5,a5a7,d9e9,a7g7,b6b8,g7i8,c5c1,i8i5,e8d7,i5g6,b8b7,g6f6,c1c3,f6i6,b7c7,g5g6,c7b7,e1f1,i7g9,i6i1,f9e8,e3e4,c3c4,f1f2,b7b2,i1i8,b2b1,e4e5,b1g1,e5f5,c9a7,i8e8,e9f9,g6f6,g1g8,e8b8,g8g6,b8b7,i9i7,b7c9,a7c9,f2f1,i7i3,e2c4,i3i0,c0a2,g6g8,f5g5,e6e5,c4e2,f9e9,f1f2,g8a8,f6e6,e5e4,e6d6,i0i2,f2f1,e9d9,d6c6,i2i1,f1f2,b9a7,c6d6,e4f4,a2c4,i1f1,f2f1,d9e9,f1e1,c9e7,d6c6,f4e4,e2g0,e7g5,g0e2,g9e7,e2g0,e4e3,g0e2,a8c8,e1e0,c8h8,c6c7,a7b5,c7b7,e7c5,b7a7,h8d8,d0e1,c5a7,e2g0,g5i7,e0d0,a7c9,e1d2,e9d9,d0d1,d7e8,d1e1,d8d4,e1e0,i7g5,c4e2,e3d3,g0i2,b5a7,e2g4,d4g4,e0d0,a7b9,d0e0,g4b4,i2g4,g5e7,f0e1,b4f4,e1f0,e8f7,d2e1,d3d2,g4e2,d2d1,e2g0,f4c4,g0i2,d9d8,e1d2,e7c5,i2g4,c4b4,f0e1,d1e1,e0f0,b9c7,g4e2,b4e4,e2g0,d8d7,g0e2,e4b4,e2g4,e1d1,f0f1,b4e4,f1f2,e4e7,g4e2,e7e2,f2f1,f7e8,f1f0,c7a6,f0f1,e2h2,d2e1,a6b4,f1f0,b4c2,e1d0,c2b0,d0e1,d1e1 +1f912862;a0a6,i9i8,f0e1,e9e8,e0f0,h7i7,a6b6,e8e7,b6e6,e7d7,c0a2,i7h7,e6c6,h7h8,c6a7,b9a7,a3a4,h8h5,g0i2,h5b5,e1f2,b5c5,a2c4,b7b6,e3e4,b6b2,d0e1,a7c8,g3g4,c5c6,f0f1,c6d6,a4a5,b2b7,i2g0,a9a6,g4g5,d6d5,g5f5,d5d2,g0i2,d2c2,f5e5,d7e7,e1d2,c8d6,e5f5,g6g5,a5b5,i6i5,i2g0,b7c7,b5c5,d6c4,e4e5,a6a1,f1f0,i8c8,e5e6,e7d7,c5c6,a1a9,e6e7,d7e7,d2e1,c2d2,f0e0,g9i7,c6d6,g5g4,g0i2,h9f8,f5g5,e7e8,d6e6,d2d3,e6e7,e8d8,g5f5,a9a6,e7e8,d8e8,c3c4,d3b3,i3i4,c7f7,f5f6,i5i4,f6g6,b3i3,e0d0,i3c3,e1f0,f7b7,g6f6,b7b6,f2e1,b6b3,f6f7,e8e9,c4c5,f8d7,e1d2,c8a8,f7e7,a8h8,c5c6,c3c0,d2e1,h8h9,c6d6,b3b0,d0d1,b0b1,e1d0,i4i3,d6c6,h9h6,f0e1,b1c1,e7d7,e9e8,c6b6,h6h1,b6b7,e8e9,b7a7,g4g3,d7e7,a6a0,e7e8,d9e8,i2g4,a0a5,g4e2,a5a0,e2c4,c0c4,a7a8,c4e4,a8a9,c1c2,a9b9,h1h4,e1f0,h4h0,f0e1,h0h1,b9a9,e4e5,a9b9,h1h5,e1f2,g3g2,f2e1,a0a3,b9a9,h5i5,d1d2,c2c5,d2e2,e8d7,e1f2,a3a0,e2d2,a0d0,d2e2,d0d3,a9b9,c5c4,b9c9,i3h3,c9b9,c4h4,b9a9,i5i4,a9b9,f9e8,f2e1,e5i5,e1f2,i5a5,b9a9,a5d5,e2e1,d5d6,a9b9,d3d1,e1e0,g2g1,b9a9,h4h5,e0f0,d6d2,a9b9,h5f5,f0e0,i7g9,b9c9,i4c4,f2e1,c4f4,e1f2,h3h2,c9d9,d2d9,e0f0,f5c5,f0e0,c5c9,f2e1,f4e4,e0f0,e4e1 +6217713a;f0e1,b7b8,a0b2,b8d8,g0i2,c6c5,b2a4,d8d4,e3e4,b9a7,e0f0,h7b7,i3i4,b7b1,a4a7,d4a4,a7d7,e9e8,d7c7,e8d8,c3c4,i6i5,c7a7,b1b4,a7f7,d8e8,f7d7,b4b1,d7c7,e8d8,c7b7,d8e8,b7b8,e8e7,c4c5,b1b2,b8b2,e6e5,b2b7,e7e8,b7c9,e8d8,i4i5,i9i5,c5d5,i5i8,c9b9,d8e8,c0a2,a4a2,b9d8,e8e9,e1f2,a2e2,d8b8,d9e8,b8b5,e2c2,b5a7,a6a5,a7h7,g9i7,h7g9,i7g9,f2e1,c2c4,i2g4,a9b9,d5e5,c4a4,e5d5,b9b0,e1f2,a4g4,e4e5,i8i2,d5c5,b0b1,c5d5,i2g2,e5e6,a5a4,e6f6,g4f4,f2e1,g2e2,f6f7,e8f7,f0e0,b1d1,e0f0,f9e8,g3g4,e2e7,e1d2,e7e0,f0e0,d1i1,d5e5,i1i9,e5f5,f4f2,e0e1,i9i8,e1d1,f2h2,f5e5,h2e2,g4g5,g6g5,e5f5,i8g8,f5f6,g8h8,f6e6,h8h7,d2e1,e8d9,e1f0,e2i2,d0e1,g9i7,e1d2,f7e8,d1d0,e9f9,d0d1,h7h6,f0e1,h6h7,e1d0,i7g9,d0e1,h7e7,e1f0,i2g2,e6f6,e8f7,d2e1,e7e1,d1e1,a4a3,f6f7,d9e8,e1e2,e8f7,e2e1,g2g1,e1f1,g9e7,f1e1,f9f8,e1e2,g1i1,f0e1,i1g1,e1f2,g1e1,e2e1,e7c5,e1e0,f8f9,e0e1,h9f8,e1e0,f8h7,e0d0,c5e7,d0e0,h7f6,e0f0,a3b3,f0f1,f6d5,f1e1,d5f4,e1d1,b3b2,d1d2,f7e8,f2e1,g5g4,e1f2,f4g2,d2d1,g4g3,d1e1,b2b1,e1e2,e8d9,f2e1,b1a1,e1f0,g2i3,e2e1,f9f8,e1e2,i3g2,e2d2,a1a0,f0e1,f8e8,d2d1,g2e1,d1d2,e1g2,d2e2,e8e9,e2f2,e9e8,f2f1,e7c9,f1f2,g2h0 +2eba9ede;g0e2,g6g5,f0e1,c9e7,e2g4,i9i7,c0a2,h7h0,a0b2,h0h8,g4e2,h8h4,b2b7,h4c4,b7a7,i7f7,e2g0,a9a7,e1f2,f7h7,a2c0,h7h3,e0f0,b9c7,c3c4,h9i7,f0f1,h3h2,c0a2,i7g8,f1e1,h2g2,e3e4,a7a9,e4e5,c7a8,e1e0,e7c5,d0e1,g5g4,g0i2,c5e7,i2g0,a8c7,e1d0,g4f4,d0e1,e6e5,i3i4,a9b9,a3a4,g2g0,e1f0,b9b0,a2c0,g8i9,g3g4,d9e8,c4c5,c7b9,g4g5,i6i5,e0e1,e7c5,g5f5,i5i4,f5f6,g9e7,f6f7,g0g7,c0a2,f4e4,a2c0,e8d7,c0e2,g7g8,f7e7,b0b8,e1f1,i4h4,e7f7,g8d8,e2g4,d8h8,f7f8,b8b5,f2e1,a6a5,a4a5,c5e7,g4i2,h8h9,i2g0,i9h7,f8e8,f9e8,f1f2,h7f8,e1d0,e4f4,a5a6,f4e4,d0e1,f8h7,f2f1,e4f4,a6a7,f4g4,e1d2,e8d9,f0e1,h7g9,f1f2,g4g3,a7b7,h9h7,e1d0,h4i4,f2e2,g3h3,e2f2,h3g3,g0e2,b5b3,e2c0,d7e8,c0e2,h7h4,e2g0,g9i8,b7b8,b3f3,f2e2,h4h0,d2e1,g3g2,e1f0,i4i3,b8a8,f3g3,e2d2,g3g5,g0e2,g5i5,f0e1,g2g1,a8b8,h0h8,e2g0,e8f7,g0i2,i3i2,d2e2,h8h5,b8a8,c6c5,e2d2,h5h2,e1f2,i5g5,a8b8,g5i5,d0e1,c5c4,b8b9,i5i3,b9c9,i3i5,e1d0,h2h0,c9b9,e7g9,b9c9,h0e0,c9d9,e9f9,d2d1,e0e2,d0e1,i5g5,d9e9,f9e9,d1d0,e5e4,d0d1,g5e5,e1f0,e5c5,f0e1,g1f1,e1f0,c4c3,f2e1,i8h6,e1d2,c5d5,f0e1,d5d3,e1d0,d3d5,d0e1,e2d2,e1d2,i2h2,d1d0,d5d7,d0e0,h6g8,d2e1,e9e8,e1f0,g9i7,f0e1,e4f4 +669b99d2;a0a6,h7h8,a6c7,h8f8,f0e1,g9i7,e3e4,b9c7,c0a2,f8h8,i3i4,i9i8,g0e2,h8h7,e1f2,b7b0,d0e1,b0c0,e2g4,h7f7,e0d0,i8f8,e1f0,d9e8,d0e0,f7f0,g4i2,c0c2,a2c0,h9g7,e0f0,f8f2,f0e0,f2f8,c3c4,f8f1,c0e2,c7a8,i4i5,i7g9,e4e5,c9e7,i2g4,f1i1,a3a4,e7g5,e5f5,c2c3,i5i6,i1b1,e0f0,b1b9,i6i7,c3a3,i7h7,b9c9,f5f6,e8d9,h7g7,c9c8,f6g6,a3e3,f0f1,c8h8,g6f6,e3e5,f6f7,a8b6,f7f8,h8h3,c4c5,h3h7,g4i2,b6a4,e2c4,e5d5,i2g4,d5f5,f8g8,c6c5,g7f7,a9a5,g8f8,h7f7,g4i2,a5a8,f8g8,a8d8,c4e2,d8d5,g8g9,f7b7,f1f0,b7a7,g3g4,a7a5,g4g5,d5d2,e2g4,d2d3,f0e0,f5e5,g5f5,d9e8,i2g0,a5b5,g9h9,d3e3,e0d0,b5b3,d0d1,e8d9,h9i9,c5c4,d1d0,f9e8,f5g5,b3c3,d0d1,e8f9,d1d0,e5c5,i9h9,e3h3,g5h5,h3h4,g0e2,c5c8,h5g5,d9e8,g5f5,e8d9,f5f6,c8b8,f6f7,h4h7,h9g9,a4b6,e2g0,c3c2,g4i2,b6c8,f7e7,c2e2,e7f7,b8b1,d0d1,b1b2,f7e7,b2a2,i2g4,e2g2,g0i2,a2c2,e7f7,g2g4,g9f9,e9e8,f7g7,h7h2,f9e9,h2h6,i2g0,c8e9,d1e1,g4g1,e1e0,g1g3,e0d0,g3g0,d0d1,g0c0,g7g8,e9g8,d1d2,c0d0,d2e2,e8f8,e2e1,d0f0,e1d1,c2b2,d1e1,h6i6,e1e2,i6i5,e2d2,i5i6,d2e2,b2d2,e2d2,f0i0,d2e2,d9e8,e2d2,i0i5,d2d1,c4b4,d1e1,i5c5,e1e0,i6i4,e0e1,c5c1,e1e2,c1c7,e2e1,i4d4,e1e2,c7c0,e2e1,c0c1,e1e0,d4d7 +6626c7d6;f0e1,i9i8,a0c1,g9e7,e1d2,h7h8,c1a1,h8e8,d0e1,b9c7,e1f0,e8b8,a1g1,b7b0,e0e1,b8d8,g1e0,c7b9,e0b0,e6e5,c0a2,d8g8,e1f1,g8g7,f1e1,i8e8,g0e2,c9a7,b0b8,a9a8,b8b2,a8b8,b2b3,e8f8,e2g0,b8c8,b3b2,e9e8,b2b6,a6a5,a3a4,f8g8,e1e0,a7c9,d2e1,h9f8,b6c6,g7i7,c6e6,f8e6,g0i2,c9a7,i2g4,e8d8,e1d0,c8c3,g4e2,g8h8,d0e1,i7h7,e2c0,h7h2,e1f2,c3e3,f0e1,e3e1,e0f0,e1b1,c0e2,h2i2,e2g4,h8h3,i3i4,b1d1,a2c0,d1d3,f0f1,d3b3,c0e2,b3b4,a4a5,i2h2,e2c0,e7g5,g4e2,h3h8,f2e1,d9e8,c0a2,b4b6,i4i5,b6b8,i5h5,b8b4,e1f2,h2h0,h5i5,e6g7,e2c4,b4c4,a5a6,h8h9,i5i6,h9h2,g3g4,c4c8,a6b6,e8d9,f2e1,c8c3,a2c0,g7i6,b6c6,h2g2,e1d0,d8e8,c6b6,g2d2,d0e1,b9c7,e1d0,a7c9,b6a6,d2d5,c0e2,e8e9,g4g5,c3c2,g5f5,d5d0,e2c0,h0g0,c0a2,c9e7,a6b6,e7g9,f5e5,i6h4,a2c0,g0h0,b6a6,d0d2,c0a2,c7b9,a6a7,h0f0,a2c4,d2e2,a7a8,b9c7,c4a2,c7a6,e5f5,f0i0,a8b8,c2a2,f5f6,d9e8,b8c8,i0a0,f6f7,e2i2,c8c9,a6b8,f7e7,i2i9,e7d7,a2g2,f1e1,b8a6,d7e7,g2e2,e1f1,e2e0,e7d7,e0e6,d7e7,i9i3,f1f0,e6c6,c9d9,e8d9,f0f1,a6c7,f1f0,a0a6,e7d7,c6c5,d7c7,c5d5,c7c8,a6a2,c8d8,i3i1,d8e8,d9e8,f0e0,g6g5,e0f0,a2g2,f0e0,d5c5,e0d0,i1i4,d0e0,c5c3,e0e1,g2g3,e1d1,c3c0,d1e1,i4i6,e1d1,h4f3 +acfa2789;a0b2,h7h4,b2a2,b9a7,a2c2,b7g7,c2f2,c9e7,f2f9,e9e8,g0e2,h4g4,f9f7,g4f4,f7g7,e8e9,g7h7,g9i7,g3g4,a9a8,h7i7,f4f5,i7g8,a8g8,d0e1,i9i7,e3e4,h9g7,c0a2,f5f9,e0d0,g8f8,e1f2,g7e8,c3c4,f8h8,c4c5,f9f8,e2c0,h8h0,e4e5,f8f4,a2c4,g6g5,c0a2,h0h9,i3i4,f4e4,e5e6,e4e0,i4i5,i7g7,e6d6,e0e2,d6e6,h9h6,a2c0,e8c7,c4e2,c6c5,e6d6,h6h0,e2g0,h0h2,d6c6,h2h8,c6d6,h8h3,g0i2,h3a3,i5i6,a3e3,c0e2,e3e2,d6d7,g7g9,i2g0,a7c8,d0d1,e2b2,d7d8,c7e8,i6i7,g9g6,f2e1,g5g4,e1d2,b2d2,d1e1,d2d1,e1e0,d1d0,e0d0,g6g8,d8e8,e9f9,e8e9,c8e9,f0e1,g8i8,i7i8,g4h4,e1f2,h4i4,g0i2,i4h4,i8i9,c5c4,i2g0,c4d4,d0d1,d9e8,d1e1,d4d3,g0i2,e8d9,e1f1,d3e3,i9h9,f9f8,f1e1,e7c5,e1f1,e3e2,i2g0,c5a7,h9i9,e2f2,f1e1,e9d7,e1d1,h4g4,g0i2,d9e8,d1d0,e8d9,i2g0,a6a5,g0e2,d7f6,e2c0,g4f4,i9h9,f6h7,d0e0,f2g2,c0e2,f4g4,h9g9,g2h2,e2g0,h7f6,g9f9,f8f9,g0i2,f6d5,i2g0,f9f8,e0d0,d5e7,g0i2,h2i2,d0d1,f8e8,d1e1,e8d8,e1e0,e7f5,e0e1,f5e3,e1e0,e3c2,e0f0,c2d0,f0e0,g4h4,e0e1,h4i4,e1e0,a7c9,e0f0,i2i1,f0e0,i4i3,e0f0,d0c2,f0e0,c2d0,e0f0,a5a4,f0e0,d9e8,e0e1,c9a7,e1e0,i3h3,e0f0,h3h2,f0e0,a4b4,e0e1,i1i0,e1e0,i0h0,e0f0,d0b1,f0f1,b4a4,f1e1,e8f9,e1e2,b1d0,e2f2,d8d7 +835d9e26;g3g4,b7b1,e3e4,b1b3,c0e2,b3b4,f0e1,b4b8,a3a4,b8f8,e4e5,f8e8,a4a5,b9a7,g0i2,h7c7,e5d5,h9g7,a5a6,c9e7,d5c5,a9b9,c5d5,e8g8,i2g0,g8c8,e1f0,g7e8,d0e1,b9b4,a6a7,g6g5,a0b2,c8c9,b2c0,i9i8,i3i4,g5g4,e1d0,b4b2,d5c5,g4h4,c0b0,b2e2,g0e2,e7c5,b0a2,i8f8,a2a5,c7e7,a5b3,c5a7,c3c4,g9i7,b3c1,f8f6,c1c2,e8g9,c2a3,c6c5,a3a1,f6f5,a1a7,f5f1,a7a1,g9i8,a1e1,c9c4,e1c2,h4g4,c2a2,c4c0,d0e1,f1f3,a2c2,e7d7,e2c4,d7d0,c2c0,d0d7,e1d2,f3f8,c0d0,f8f0,e0f0,d7h7,d0a0,h7d7,a0c0,d7e7,c0b2,d9e8,b2b6,e6e5,b6b2,g4h4,b2a2,e8d9,a2b0,e7a7,b0e0,a7e7,e0e4,e5e4,c4a2,e9e8,f0f1,h4i4,f1f2,i8g7,d2e1,g7h9,a2c4,i4h4,f2f1,h9f8,f1f0,f8h9,f0e0,c5c4,e0d0,i6i5,e1f0,e7g7,d0d1,e8f8,d1d2,f8e8,f0e1,e8f8,e1f2,f8e8,f2e1,e4d4,e1f2,e8e7,f2e1,g7g4,e1f0,c4c3,d2d1,g4g7,d1d0,g7h7,f0e1,e7f7,d0d1,h7h8,e1f2,i5i4,d1d2,h8c8,d2e2,d9e8,e2e1,c3c2,e1d1,d4d3,d1e1,c2c1,e1e0,i7g9,e0d0,g9i7,d0e0,h9f8,e0f0,c8c5,f0f1,f7e7,f1f0,e8d7,f0f1,i7g5,f1f0,c5d5,f0f1,g5i7,f1f0,e7f7,f0f1,d5c5,f1e1,c5d5,e1f1,f9e8,f1f0,f8h9,f0f1,h9f8,f1f0,d5e5,f0f1,e5e3,f1e1,h4h3,e1f1,c1d1,f1f0,h3i3,f0f1,i7g9,f1f0,e3e4,f0f1,e4g4,f1f0,g4e4,f0e0,e8d9,e0f0,f7e7,f2e1,d1e1 +70b1fbcb;a3a4,b7b5,a0b2,b5b3,i3i4,g6g5,g3g4,c9a7,c0a2,i9i7,g0i2,h7h8,b2b0,h8f8,b0b1,f8f5,i2g0,f5e5,b1e1,e5f5,g0e2,f5f4,i4i5,b9c7,e1d1,b3b8,d1d4,b8d8,e3e4,d8i8,d4d8,f4a4,d8f7,i8f8,f7h7,f8c8,a2c0,a9b9,h7i7,b9b4,i7d7,c8d8,d7c7,b4c4,e2g0,d8d3,e4e5,a6a5,c7a7,d3e3,e5d5,a4b4,a7c6,b4b5,d5e5,g9e7,e5f5,b5c5,c6c5,e3e5,c5d5,c4g4,c3c4,e5e3,d5c5,e3g3,f5f6,h9f8,i5h5,g3g1,c5b7,g4c4,b7b8,c4c3,b8b9,e7c9,b9b8,e6e5,h5h6,g5g4,b8d9,f8d9,g0e2,c3c7,f0e1,c7a7,h6g6,a7a8,e1f0,f9e8,f6e6,e5e4,e2c4,g1g6,e0e1,e4f4,e1e2,g4h4,e6d6,g6f6,c4a2,f6f8,f0e1,f8f6,a2c4,f4e4,c0a2,e4d4,d6e6,f6f1,e1f0,d4e4,e2e1,a8a7,a2c0,a7a8,e6d6,e4e3,c4a2,c9e7,d6c6,f1f8,c0e2,e8d7,e2g4,e7g5,c6c7,g5i7,c7b7,a8d8,e1e0,e9f9,b7b8,i7g9,b8c8,f8h8,e0e1,h8c8,g4e2,c8c0,e1d1,d8g8,d0e1,e3e2,a2c0,a5a4,c0a2,g8c8,e1d2,c8b8,d2e1,a4a3,a2c4,b8g8,e1f2,g8i8,c4a2,a3b3,f2e1,i8g8,e1f2,g8h8,a2c0,h8c8,c0a2,d9e7,a2c0,h4g4,c0a2,g4h4,a2c0,c8b8,f2e1,b8b7,e1d0,e7d5,f0e1,d5f4,e1f0,f4g2,c0a2,b7b9,d0e1,b9b6,e1f2,g9i7,a2c0,h4h3,d1d0,e2f2,c0e2,b6b8,e2c0,b8i8,c0e2,g2h0,e2c4,i8c8,d0e0,c8c4,e0e1,c4c7,e1d1,c7a7,d1e1,a7a3,e1e0,h0f1,f0e1,b3c3,e1d2,f1d2,e0f0,f9f8 +c630fe2c;a0a6,h7e7,c0e2,i9i8,e0e1,e7h7,e2g4,h7h6,g0e2,a9a6,e3e4,i8f8,e1d1,f8g8,i3i4,b7c7,c3c4,h6h3,a3a4,c7c4,i4i5,i6i5,d0e1,g8c8,a4a5,d9e8,e1d0,c4b4,d0e1,h3h2,e1d0,a6a9,a5a6,c8a8,e2c4,g9e7,c4e2,c6c5,e2g0,b4g4,e4e5,e6e5,d1d2,h2h7,f0e1,h7i7,d2e2,i7g7,a6b6,h9i7,e1f0,g7g8,g0i2,g4h4,e2f2,c5c4,f0e1,a8a0,e1d2,c4c3,b6c6,c3d3,i2g0,i7h5,c6b6,b9c7,b6a6,a0a6,d0e1,d3d2,g0i2,d2d1,g3g4,a9b9,g4g5,h4h2,g5f5,g8g9,i2g0,h2g2,g0i2,a6b6,f2e2,b6c6,e2d2,g2g5,i2g0,c9a7,f5f6,g9g8,f6e6,d1d0,g0e2,c7a6,d2d1,b9b2,e2g4,b2i2,g4e2,g8h8,e1d2,c6b6,d2e1,g5g3,e2g0,i2i1,d1d0,g3g5,g0i2,e7c5,d0e0,i1h1,e1f2,h8h6,e0f0,a6b4,f0e0,h5i3,e6f6,g5g4,e0f0,h6h9,f6e6,h9g9,f0e0,h1h3,e6f6,e8d9,e0f0,b6b8,f6g6,b8b7,g6g7,h3h0,f0f1,g4e4,f1e1,g9h9,e1f1,b7b8,f2e1,b4c6,g7f7,b8b3,e1f2,h0h3,f1f0,f9e8,f2e1,b3b7,f7f8,b7g7,e1d0,h9f9,f0e0,g7f7,e0e1,f7f3,f8f9,e9f9,e1e2,f3f4,d0e1,h3b3,e2d2,b3h3,e1f2,c5e7,i2g0,h3h0,g0e2,e8f7,d2d1,h0h7,d1d0,e4a4,e2c4,a4a1,c4e2,f4f2,d0e0,e7g9,e2g0,a1a2,e0e1,a2a3,g0i2,f2f1,e1e0,g9i7,i2g0,i3g4,g0i2,f1f2,i2g4,a3a1,e0e1,a1a6,e1e0,f2a2,e0e1,c6a5,e1e0,h7h5,e0f0,a7c9,g4i2,a5c6,f0f1,a2e2,f1f0,h5h0,f0f1,a6a8,i2g0,h0g0 +1cd71469;a3a4,b7b1,d0e1,h7h5,a0b0,b1b8,a4a5,c6c5,e1f2,h5f5,e3e4,i6i5,b0b4,d9e8,e0e1,e8d9,g0e2,g6g5,g3g4,c9e7,b4c4,c5c4,e1d1,b8b0,c3c4,h9g7,a5b5,g5g4,b5a5,g7i8,f0e1,b0b8,e1d0,f5c5,f2e1,b8b0,e1f2,i8g7,d1e1,c5e5,e4e5,b0d0,i3i4,g7h5,e1e0,g9i7,e2g0,h5i3,e0d0,g4g3,a5a6,e7c5,f2e1,e9e8,d0e0,i9i8,e1f2,e8e9,e0d0,i7g9,d0e0,i8h8,e0e1,i5i4,e1e0,h8e8,a6a7,e8a8,c4c5,g3f3,c5d5,i4h4,e0d0,f3f2,d0d1,g9e7,d1d2,a8c8,d5c5,e6e5,c5c6,e7g5,g0i2,f2g2,c0a2,i3g4,d2e2,g5i7,a2c4,c8c6,c4a2,c6c8,e2d2,f9e8,a2c4,e8f9,i2g4,b9a7,c4e2,c8c1,e2c0,c1g1,c0a2,e5e4,g4e2,a7c6,a2c4,c6a7,e2c0,e4e3,c0e2,d9e8,e2g0,g1g0,d2d1,a9c9,d1e1,g0e0,e1e0,e8d7,e0f0,h4h3,f0f1,e9d9,c4a2,c9c8,a2c0,c8a8,f1e1,h3g3,c0a2,a8g8,e1d1,g8g6,d1d0,a7b9,d0d1,g3f3,d1d0,b9d8,a2c4,f3g3,c4e2,g6a6,e2c0,e3f3,c0a2,f3f2,a2c0,a6a2,c0e2,a2a1,e2g0,a1f1,d0e0,f1e1,e0d0,e1e4,d0d1,f2e2,d1d0,e4i4,g0e2,i7g5,e2g0,i4i6,d0e0,d8f7,e0e1,i6d6,e1f1,d9d8,f1f0,d7e8,g0i2,d6d0,f0f1,f7d6,i2g4,e8d7,g4e2,d0d5,f1e1,g5i7,e2c0,i7g5,e1f1,g2g1,f1e1,d5b5,e1d1,d6f5,d1d0,g5e7,d0d1,b5b8,d1d2,f5h6,c0e2,b8c8,e2g0,d8d9,d2e2,f9e8,g0i2,c8c1,i2g4,c1c8,e2d2,h6g8,d2e2,g8i7,e2f2,c8c2,g4e2,e7g9 +dbdc3acc;c3c4,h7h6,c4c5,b7g7,g3g4,g7g8,c5b5,c6c5,c0e2,g6g5,a0b0,h6h3,b0b9,g8g4,b9c7,f9e8,c7e8,d9e8,e0e1,h3h4,e1d1,g4a4,i3i4,e8f7,e2c4,g9e7,f0e1,f7e8,b5a5,i9i8,g0i2,h4h6,i4i5,h6h8,i5h5,a9a7,e3e4,i8i7,d1d2,i7f7,i2g0,f7f8,g0e2,a6a5,e2c0,c5c4,c0e2,h8h6,a3a4,f8f5,h5h6,e8f9,e2c0,h9i7,d2d1,c4d4,c0a2,i7h9,h6h7,f5f7,e1f2,g5g4,d0e1,f7f2,a2c4,f2a2,e1d0,d4e4,h7i7,a7a8,i7i8,a5a4,i8h8,e4f4,h8h9,a2a3,h9g9,a3e3,c4e2,e3i3,d1d2,i3g3,e2c0,g3g0,g9f9,e9f9,c0a2,g4g3,a2c0,g3g2,c0a2,f4g4,d2d1,g0h0,d0e1,h0a0,e1d0,a8a6,a2c4,c9a7,c4a2,a6b6,a2c0,g4h4,d0e1,b6a6,e1f0,f9f8,d1d2,g2g1,d2e2,h4i4,e2d2,e7g5,f0e1,a0b0,c0e2,b0b1,e1f2,b1b7,e2g4,g5i7,d2e2,a7c9,f2e1,a6c6,e1f0,a4b4,f0e1,i7g5,e2d2,i4h4,d2e2,g5i7,e1f2,f8f9,e2e1,c6c0,g4i2,b7h7,i2g0,h7h5,g0e2,g1g0,e2c4,f9f8,e1f1,c9a7,f1e1,f8f7,e1f1,h5g5,c4a2,g5g9,a2c0,f7e7,c0e2,g9c9,e2g4,c9c8,f2e1,h4g4,f1f2,b4c4,e1d2,c8c6,d2e1,i7g5,e1d2,e6e5,d2e1,a7c9,f2e2,c4d4,e1d0,c6c2,e2e1,c2c7,e1f1,d4d3,d0e1,g0f0,f1f2,g4f4,e1d0,c7a7,f2f1,f0e0,f1f2,a7a6,f2f1,a6a1,d0e1,a1b1,f1f2,f4f3,f2e2,b1a1,e1d0,a1a7,e2e1,a7a5,e1e2,f3e3,e2e1,a5a8,e1f1,a8a1,d0e1,i6i5,f1f2,a1a0,e1d2,a0a1,d2e1,a1e1 +0a7774c6;a0b2,a9a7,e0e1,g9i7,e1d1,i6i5,b2f2,b7b8,c0a2,h7c7,f2f4,b8b6,a2c4,e6e5,f4f7,e9e8,f7e7,e8f8,f0e1,b6b2,e7c7,f9e8,e1f2,b2b1,c7f7,a7f7,a3a4,f8f9,e3e4,f7f5,c4a2,c9a7,g0e2,i5i4,e2g0,b1b6,g3g4,e5e4,g0i2,a6a5,a4a5,b6b5,d0e1,h9f8,e1d0,f5g5,i3i4,a7c9,a5a6,c9a7,a6a7,i7g9,d0e1,b9d8,d1d2,b5b1,a7b7,i9i6,d2e2,b1b5,b7b8,g9i7,c3c4,g5g4,i2g4,d8b9,b8b9,f8h9,e2d2,h9g7,d2d1,b5b3,g4i2,b3d3,d1d0,d3c3,d0d1,g7e6,e1d2,c3a3,d1e1,a3d3,b9c9,d3c3,e1d1,e4f4,i4i5,c3g3,d1e1,g3g1,c9b9,i7g5,e1e2,e8f7,c4c5,e6c5,i2g4,i6i7,a2c4,g5e7,b9a9,i7h7,e2e1,c5e4,i5h5,g1f1,g4e2,f7e8,e1e0,h7g7,a9b9,c6c5,b9a9,f1i1,e0d0,g7g9,d0d1,e8d7,h5i5,e4d2,d1e1,e7c9,i5h5,f9e9,c4a2,i1h1,e1d1,g9g8,d1d0,g8h8,a9b9,e9e8,e2c4,h8h9,b9c9,h9g9,h5g5,g6g5,f2e1,g9g6,a2c0,e8e7,c0e2,h1i1,e1f2,e7f7,e2g4,g6e6,c9d9,d2f1,f2e1,i1i9,d0e0,i9i6,c4e2,e6e2,g4e2,i6i2,e2g4,f4g4,e1f2,d7e8,f2e1,e8d7,d9e9,i2a2,e9d9,a2c2,e1d0,c2a2,d9e9,a2a6,e0f0,d7e8,e9d9,g4g3,d9c9,g3g2,f0e0,a6a4,c9d9,a4a7,d9c9,a7a9,c9b9,f1d0,e0d0,f7e7,b9c9,e8f7,d0d1,a9b9,d1d2,c5c4,c9b9,c4d4,d2d1,g2f2,d1d0,d4d3,b9c9,f2e2,d0e0,g5g4,e0d0,g4h4,d0e0,h4g4,e0d0,d3e3,c9d9,f7e8,d9e9,e2e1,e9d9,e8d9 +d6dd18fd;a0c1,g9e7,g0i2,h9f8,c1a1,f9e8,a1g1,b7b1,e3e4,h7h1,e4e5,b1b6,c0e2,e7c5,e2c4,b6b7,g1g2,i9g9,g2a2,f8h7,d0e1,h1f1,a2b0,b7b1,e1d2,f1c1,i2g4,g9h9,b0b9,b1b8,b9b8,c1i1,b8c6,e8f9,g4i2,i1f1,c6a7,f1f6,a7e7,c9e7,c4a2,a9a8,a2c0,d9e8,e0e1,f6f2,c3c4,f2f6,e5d5,g6g5,c0a2,h7g9,e1e2,h9h1,e2f2,f6f7,d5d6,h1h0,d6e6,f7g7,a2c0,g7g8,e6d6,h0h2,f2f1,h2i2,g3g4,a8a7,g4g5,g8g7,a3a4,g7i7,f1e1,i2f2,e1e0,i6i5,i3i4,a7d7,c4c5,i7g7,i4i5,g9h7,f0e1,h7i9,i5h5,a6a5,e1f0,f2h2,f0e1,e7g5,h5g5,e9d9,c5d5,d7c7,a4a5,h2f2,d5e5,c7a7,g5f5,g7g0,e5e6,g0i0,f5g5,e8f7,d6c6,f9e8,c0a2,a7a5,e1d0,i0d0,e6e7,e8f9,d2e1,f2i2,c6c7,a5a2,e7d7,i2i3,d7d8,d9d8,e1d0,i3i7,g5f5,a2a7,c7b7,i7i0,e0e1,i0h0,f5e5,h0h7,e5d5,a7a9,e1e2,h7h2,e2e1,a9a4,d5e5,a4f4,b7b8,f4b4,e5e6,b4c4,e1e0,i9h7,b8c8,d8e8,e0e1,h7i9,c8c9,c4d4,e6e7,e8d8,e1f1,d4c4,c9d9,h2h1,f1f2,h1c1,e7f7,c1d1,f2e2,c4c8,f7g7,d8d9,g7h7,c8h8,e2f2,i9g8,h7h8,d1i1,h8g8,d9d8,d0e1,i1i0,e1d2,i0h0,f2e2,h0c0,e2f2,c0d0,f2e2,d8d7,g8g9,d0c0,e2e1,d7d8,e1e2,c0c5,g9h9,c5c3,e2e1,d8d7,e1e0,c3e3,e0d0,e3g3,h9i9,g3e3,d0d1,e3e8,i9h9,e8e7,d1d0,e7e4,h9g9,f9e8,g9f9,e4e1,f9e9,e1d1,d0e0,d1g1,e9f9,e8f9,e0d0,g1e1 +e37a7086;c0e2,h9i7,a0b0,b7b8,d0e1,b8b1,e3e4,h7h0,g3g4,b1d1,e0d0,h0f0,b0b4,d1b1,b4b7,b1a1,b7b6,i6i5,d0d1,f0f3,b6b7,a9a7,b7b5,a7b7,b5e5,f9e8,d1d2,f3d3,e5e8,e9e8,e1f0,b7c7,c3c4,c7c8,e2c0,i5i4,g0e2,d3b3,e4e5,i9h9,e5f5,i4i3,c0a2,c8c7,f5e5,b3b4,f0e1,h9h2,c4c5,e8f8,c5c6,h2h8,e1f2,a1g1,g4g5,g1g3,c6d6,h8g8,g5h5,c7c6,e2c0,d9e8,d6d7,b4b2,a3a4,c6c4,h5g5,g3d3,f2e1,c4a4,d7e7,d3d6,g5h5,g8g7,a2c4,b2b0,h5g5,i7h5,e7d7,c9a7,d2d1,g7f7,d7d8,d6d3,e5f5,a6a5,d1d2,f7c7,f5e5,e8f9,d8e8,f8f7,e1f2,h5i7,g5f5,a4c4,f2e1,b0b6,e8d8,d3e3,e1d0,b6c6,e5d5,c7b7,d5c5,b7b0,d8e8,b0a0,d2e2,a5a4,f5e5,b9d8,e5d5,i3h3,c0a2,h3g3,c5b5,e3f3,b5b6,c4e4,e2d2,f3f4,a2c4,c6c7,d0e1,a7c9,e8d8,i7h5,e1f2,f9e8,b6b7,a0i0,d8c8,a4a3,d5e5,i0i3,f2e1,g9i7,d2d1,e4c4,e5d5,f7f8,e1f0,g3f3,c8d8,f8f9,b7b8,c7c5,d5d6,f9e9,d8c8,c5d5,c8d8,f4f0,d1d2,f0d0,d2e2,d0d3,e2e1,i3i4,b8c8,d5e5,d8d9,e9d9,e1d1,i4i1,d1d2,c4f4,d6c6,e8d7,c6d6,f4f6,d6c6,i1g1,c8b8,d3d4,c6c7,e5g5,c7c8,g1e1,b8b9,e1g1,c8d8,d4d8,d2e2,d8e8,e2d2,g5g2,b9a9,g1a1,a9b9,a1a2,d2d1,g2f2,b9c9,d9e9,c9d9,e9d9,d1d0,e8h8,d0e0,h8f8,e0e1,f2b2,e1f1,f3f2,f1e1,f8c8,e1d1,f2g2,d1d2,f6f2,d2d1,c8e8,d1d0,f2f1 +e46c5a8d;e0e1,b7b6,g0e2,h7f7,a0a6,b6b8,e2g0,c6c5,a6c5,f7i7,g3g4,e6e5,c5c6,b8b2,c6g6,b2b0,e1e0,i7i8,g6b6,b9c7,b6d6,b0b5,d6b7,i8h8,b7b5,c7a8,b5a5,h8h6,e3e4,c9e7,e4e5,h6h8,a5b3,e7c9,g0e2,h8i8,b3c1,i8i7,c1d1,a8c7,e2g0,i7e7,e5d5,c7b5,d1d4,h9i7,g4g5,e7e3,d4h4,b5a7,h4i6,d9e8,i6i7,a7c6,d5e5,a9a4,g5h5,a4a9,i7i9,c6d4,e0e1,a9a7,e5f5,a7a4,i9i8,e3e4,i8e8,e9d9,e8e5,d4b3,g0i2,b3a5,h5i5,a4c4,i3i4,e4d4,e5e9,d9d8,e9f9,d8d7,f9c9,c4c9,c0a2,a5c4,i5h5,g9i7,c3c4,d4e4,a2c0,e4f4,h5i5,f4f2,f5e5,f2e2,c0e2,c9i9,e5d5,i9c9,a3a4,c9h9,i2g4,h9g9,e2g0,g9g7,d5e5,i7g9,e1e0,d7e7,a4a5,e7d7,e0e1,g7g8,a5b5,d7d8,g4e2,g9e7,e5e6,g8i8,b5b6,i8i5,e6f6,i5e5,b6a6,e5h5,f6e6,h5a5,e1e0,a5f5,e2g4,f5f8,i4i5,f8f2,d0e1,f2f0,e1f0,e7c5,c4c5,d8d9,g4i2,d9d8,c5b5,d8e8,b5b6,e8f8,f0e1,f8f9,a6a7,f9f8,a7a8,f8f7,b6c6,f7f8,e0d0,f8e8,i5h5,e8e9,e1f0,e9f9,i2g4,f9f8,d0d1,f8e8,c6b6,e8e9,g0e2,e9e8,a8a9,e8e9,e2g0,e9f9,g0i2,f9e9,h5g5,e9e8,a9b9,e8e9,d1e1,e9d9,b6b7,d9d8,e6f6,d8d9,f6g6,d9d8,i2g0,d8d9,e1e0,d9d8,b7a7,d8d7,g0e2,d7d8,a7b7,d8d9,g6g7,d9d8,e2g0,d8d7,g4e2,d7e7,g7f7,e7e8,b7c7,e8d8,f0e1,d8d9,g0i2,d9e9,b9c9,e9f9,e1f2,f9e9,e0e1,e9e8,f7f8,e8e9,c7b7 +3c582def;a3a4,b7c7,a0a1,c7b7,a1f1,b7g7,f1e1,g9i7,e1b1,c6c5,g0e2,c9a7,b1b4,a6a5,b4e4,f9e8,e4g5,e8f9,e2g0,g7c7,e0e1,i7g9,g5i4,h7f7,i4i9,c7d7,i9h9,a9a8,c3c4,d7d3,c0e2,a8d8,h9f9,e9f9,g3g4,f7f1,g0i2,d3c3,i3i4,f1f4,i2g0,f4e4,a4a5,c3c1,g0i2,d8d3,e1e0,c1g1,c4c5,e4f4,c5d5,g6g5,e2g0,d3d2,d5d6,d2d4,g4g5,f4f6,g0e2,f6f4,g5h5,g1g2,a5b5,f4f8,d0e1,g9i7,i2g0,f8b8,g0i2,g2g7,b5a5,d4d3,e2g4,d3d0,e1d0,d9e8,d6e6,b8b5,e0e1,b9d8,e1e2,g7d7,e6f6,d8b9,f6g6,d7e7,e2d2,b5b4,a5a6,b4d4,a6b6,e8d7,b6b7,d4e4,b7a7,e7e6,f0e1,e4e5,a7b7,e5c5,g6f6,c5c1,i4i5,c1c5,f6f7,e6e8,f7f8,f9f8,b7c7,c5c2,e3e4,e8e7,g4e2,c2a2,e2c0,a2a6,e1f0,e7c7,i2g4,a6a4,d0e1,a4c4,c0e2,c4c0,e2c0,c7c1,c0e2,c1c0,g4i2,c0a0,e2c0,a0a1,d2e2,a1d1,e1d2,b9d8,e4e5,d8b9,c0a2,d1e1,a2c4,b9c7,f0e1,c7b9,e5d5,b9c7,e1f0,d7e8,h5h6,f8f9,d5e5,i7g9,i2g0,c7a8,i5i6,e8f7,e5e6,f9f8,i6i7,a8c7,e6f6,f8f9,f6g6,g9i7,c4a2,c7e8,g6f6,e8f6,h6h7,f7e8,h7i7,f6g4,f0e1,g4i3,i7h7,i3g2,h7g7,g2f0,e1f0,e8f7,g0i2,f9f8,f0e1,f8f9,i2g0,f7e8,a2c0,e8d7,g7f7,d7e8,f7e7,e8f7,e1f2,f9f8,d2e1,f8f9,e2d2,f9e9,e7f7,e9f9,f7e7,f9f8,d2d1,f8f9,c0a2,f9f8,a2c4,f8f9,e1f0,f9e9,e7d7,e9e8,c4a2,e8e9,d7e7,e9f9,e7e8 +0d39a92f;i3i4,g9e7,a0a6,a9a7,e0e1,d9e8,a6c5,a7a6,c5c6,b7c7,c6d6,c7c8,d6e4,c8c7,e4f4,h7h5,e1d1,c9a7,d0e1,a6a3,c3c4,c7b7,f4h5,e8f7,h5b5,h9i7,b5b2,e6e5,b2b0,b7b4,c4c5,b4b6,g3g4,f9e8,b0b1,e7g5,d1d0,e8f9,b1c1,b9c7,c0a2,a3d3,d0e0,d3d7,e1f2,e9d9,a2c4,b6e6,g0i2,i9g9,c1a1,e6d6,a1d1,g9h9,c4e2,e5e4,d1d6,d9e9,d6d7,e9e8,e2g0,a7c5,d7c9,e8d8,c9b7,h9i9,g4g5,i9i8,e3e4,i6i5,b7b6,c7b9,b6a6,b9c7,a6a2,d8e8,a2a8,c7a8,e0d0,a8b6,d0d1,b6c4,d1e1,e8d8,g5f5,c5e7,f5e5,e7g5,e1f1,i7h5,e5f5,i8h8,f1e1,c4d6,f5g5,i5i4,e1f1,d8d9,f1e1,g6g5,i2g4,h5g7,e1e0,i4i3,e4e5,g7i8,g0i2,d9e9,f2e1,h8h2,i2g0,i8h6,g0e2,d6c4,e1d0,e9e8,e2c4,h2h1,g4e2,e8e9,d0e1,g5g4,e0d0,h6i8,e2g0,h1h4,g0i2,h4h7,e5f5,h7g7,d0d1,i3h3,e1d0,f9e8,d0e1,g7i7,f5e5,i7i5,e5e6,i5i2,d1d0,h3i3,e1f2,g4g3,d0d1,i8h6,e6d6,i2i1,f2e1,i1i2,e1f2,i2f2,c4e2,f2h2,f0e1,e8d9,e1f2,h2f2,e2g4,e9f9,d6c6,f9e9,g4i2,d9e8,i2g0,f2f0,d1e1,f0f2,c6d6,f2f6,g0e2,g3g2,e2g0,h6f5,e1e2,i3h3,g0i2,g2f2,e2f2,f5h4,f2e2,f6f0,d6d7,h4f5,i2g0,f0g0,e2d2,g0g2,d2d1,e9d9,d1e1,g2f2,d7d8,d9e9,d8d9,e9d9,e1e0,f2c2,e0f0,c2b2,f0f1,f5h6,f1f0,b2b8,f0f1,d9d8,f1f0,h6g4,f0f1,g4h6,f1f2,h6g4,f2e2,b8a8,e2e1,a8c8,e1e2,c8c1 +e9e2fbff;g3g4,i9i7,g0i2,b7b0,a0a2,e6e5,a2b0,h9g7,d0e1,h7h3,e3e4,g7i8,b0c2,f9e8,c0a2,h3a3,e1d2,a3b3,a2c0,e8f9,g4g5,b3b8,c2b2,b8b7,d2e1,i8g7,b2b9,b7b5,e1f2,b5d5,b9b7,a9a7,f2e1,d5d0,c0e2,e9e8,b7e7,e8f8,e7e6,f8f7,e2c4,a7d7,e6c6,d7a7,e0d0,g7h9,i2g0,a7e7,g5f5,f9e8,g0i2,f7f8,c6b6,e7a7,c4e2,i7i9,b6c8,f8f7,e2g4,a7a9,c8c7,f7f8,f5e5,g6g5,i2g0,h9i7,c7e7,e8f7,e7c7,c9e7,c7b7,f7e8,b7i7,i6i5,g0i2,a9a7,i3i4,e8d7,e1d2,g9i7,d0d1,e7c5,d1e1,a7a9,i4i5,a9b9,g4e2,i7g9,e1e0,c5a7,i2g4,d9e8,e5d5,b9b5,d5d6,b5e5,d2e1,e5d5,e2c0,d5d1,d6d7,g5g4,i5h5,f8f7,c0a2,g9e7,e1f2,i9h9,d7e7,f7f8,e7e8,f8f9,e8f8,f9f8,h5g5,h9h4,g5g6,d1d4,c3c4,a7c9,a2c0,d4c4,f2e1,g4f4,e1d0,c4c6,e0e1,f8f9,c0a2,c6c5,g6g7,h4g4,e1e2,c5h5,a2c4,h5g5,e2f2,g5e5,g7h7,e5e7,f0e1,g4i4,h7h8,e7g7,c4a2,i4i5,a2c4,g7g4,h8g8,g4h4,f2e2,h4h9,e2f2,h9i9,f2e2,f4e4,c4a2,i5i6,g8h8,i6i7,e1f2,i7e7,d0e1,e4d4,e2d2,c9a7,a2c0,e7e1,c0a2,i9i6,h8i8,e1e6,i8h8,i6f6,a2c0,d4d3,d2d1,f6h6,d1d0,e6e5,c0a2,d3c3,d0d1,a7c9,a2c0,h6h1,f2e1,e5e3,d1d0,c9e7,h8i8,e3e4,c0a2,c3c2,e1d2,h1h7,a2c4,h7h1,i8i9,c2c1,d2e1,e4c4,e1f0,h1h2,f0e1,c4c7,e1f2,c1d1,d0e0,h2h4,i9h9,d1e1,e0f0,c7c3,h9g9,f9f8,g9f9,f8f9 +345b79c3;c0e2,b7g7,a0b2,h7h5,b2d2,h5f5,d2b1,g7g3,c3c4,b9a7,b1i1,f5g5,i1g2,f9e8,g2g3,a6a5,g3f5,e6e5,f5f7,e8f7,f0e1,e9f9,e0f0,a9b9,f0e0,b9b3,g0i2,e5e4,e2c0,g5i5,c0a2,b3b2,c4c5,b2g2,a2c0,i5h5,a3a4,f9f8,c5d5,h5i5,i2g0,i5h5,i3i4,e4f4,a4a5,a7b9,e1f2,g6g5,d0e1,h5h6,e0d0,f8e8,g0e2,c6c5,a5b5,h6a6,d5e5,a6a7,e3e4,a7c7,e2g0,c9e7,b5b6,f4g4,e5e6,g4g3,g0i2,c7c8,e1f0,g2i2,c0a2,i2f2,d0d1,i9i7,e6e7,e8e7,a2c0,b9a7,f0e1,f2f0,b6c6,i7h7,c0a2,f0c0,a2c4,g3f3,e1d2,h9i7,c4e2,c0c1,d1d0,a7c6,e2c4,c8a8,i4i5,h7h3,e4e5,c1i1,d0e0,i1i3,e0f0,i7h9,c4a2,f7e8,f0e0,e7f7,e0f0,h9g7,e5f5,h3h6,f0e0,g9i7,f5g5,a8a6,a2c4,c6a7,d2e1,h6h5,e1d2,f3g3,g5h5,g7h9,c4a2,a6e6,a2c4,i3i5,h5g5,a7b5,c4a2,e6c6,g5f5,i5g5,e0e1,b5c7,e1e2,g5g9,a2c4,c6c4,e2f2,i6i5,f5f6,f7f8,f6f7,f8f9,f7f8,f9e9,f2f1,g9g4,d2e1,g4g5,f1f0,c4g4,f8e8,d9e8,e1d2,g5d5,f0e0,g4f4,e0d0,e8d7,d0d1,d7e8,d1d0,d5d8,d0d1,h9f8,d1e1,c7a6,e1e2,f4f1,e2e1,i7g9,e1e2,d8d9,e2e1,g3f3,e1f1,e9f9,f1f0,e8d7,f0e0,c5c4,d2e1,a6c7,e0d0,i5i4,d0e0,d7e8,e1d2,d9d8,d2e1,c7b9,e1d0,g9i7,d0e1,f3e3,e1d2,f8h9,d2e1,h9f8,e1f0,i4i3,f0e1,e3e2,e0f0,d8b8,e1d0,b9d8,d0e1,e2f2,e1d0,f2e2,d0e1,b8b2,f0f1,e2e1,f1f0,e8d9 +2a2979b2;c0a2,h7e7,a2c0,h9i7,a0a6,b7c7,a6a5,i7h9,a5c5,a9a6,c5c7,a6a5,g0e2,a5a3,c7e7,g9e7,i3i4,e7c5,d0e1,a3a7,e3e4,a7e7,i4i5,g6g5,c0a2,e7c7,i5h5,h9g7,e2g4,c7e7,h5g5,e7d7,g4i2,d7d3,e1d0,d3d0,e0d0,b9c7,g5f5,g7h9,f5f6,h9i7,e4e5,c9a7,e5e6,c7e6,a2c0,e6d8,f6f7,d8b9,i2g4,i6i5,f7e7,f9e8,g4i2,i9h9,c3c4,h9f9,c4c5,i7g8,e7d7,a7c5,f0e1,f9f7,e1f0,f7f5,d0e0,f5f3,c0e2,f3f1,d7e7,b9a7,e2c0,f1f7,e7f7,a7c8,f7f8,g8i7,c0e2,c8b6,e0d0,e8d7,g3g4,b6c4,f8f9,e9f9,i2g0,i7g6,e2c0,d7e8,c0e2,f9f8,g4g5,e8f7,e2c0,f8f9,g5f5,c5a7,c0a2,g6f8,f5e5,i5i4,g0e2,c4a5,e2g0,f7e8,a2c0,f8e6,f0e1,e8d7,e1f2,d9e8,c0e2,e6d8,e2c4,c6c5,f2e1,c5c4,e1f0,d8b7,d0e0,f9e9,g0i2,a7c9,e0e1,e9d9,e1e0,d9e9,f0e1,c9a7,e0f0,b7d8,e1d0,a7c9,i2g0,d8b7,g0i2,a5b3,f0f1,b3c5,i2g0,c5a4,g0i2,c4c3,e5f5,e8f9,f5g5,i4i3,g5f5,e9e8,i2g0,b7d8,g0i2,i3i2,f5f6,e8e9,f6f7,e9e8,f1f0,d8b7,d0e1,b7d8,f7g7,a4c5,g7g8,i2h2,g8h8,c5a6,e1d2,e8e9,h8h9,c3b3,h9g9,h2i2,d2e1,b3b2,e1d0,i2i1,g9f9,d8f9,f0f1,b2b1,d0e1,e9d9,e1d0,d7e8,f1e1,f9g7,e1f1,a6b4,f1f2,g7f9,d0e1,f9d8,e1d2,e8d7,d2e1,d8b9,e1d2,d7e8,f2e2,e8f9,d2e1,b4a6,e2f2,b9c7,f2f1,b1c1,e1d0,d9d8,f1f2,c1d1,d0e1,a6b8,f2f1,d1e1,f1f0,d8d7 +228812d8;c3c4,a9a7,g3g4,h7h3,c0a2,a7a9,d0e1,b7b1,c4c5,b1b8,a0a1,b8h8,a2c4,g6g5,a1c1,e9e8,c1b3,a9a7,b3d3,g9e7,d3h3,e8d8,g0i2,d8e8,h3g3,e7c5,g3f1,h8h7,f1f7,a7d7,i2g0,h7h5,f7f6,d7f7,i3i4,e6e5,i4i5,e8e9,g0e2,c9e7,i5h5,g5g4,h5h6,g4f4,a3a4,a6a5,f6d6,f7f8,d6f7,e5e4,h6h7,f8f7,e1f2,f7h7,c4a2,b9c7,e3e4,a5a4,f0e1,i6i5,e2c0,c5a7,c0e2,a7c9,e0d0,i9i7,e1f0,c6c5,a2c0,h7h4,d0e0,h4h5,e2g4,i7g7,g4i2,c7d5,f2e1,h5h2,e1d2,g7g3,e0e1,d5c7,e1e0,g3a3,i2g4,c7b5,f0e1,b5c7,g4e2,c7b9,e2g0,f4f3,c0a2,e9e8,a2c0,h2i2,e4e5,f3f2,e5e6,f2e2,e1f0,i2i0,c0e2,a3i3,e2c0,i0g0,e6e7,e8e9,c0e2,g0g1,e0d0,i3c3,d0e0,c3g3,e2g0,h9f8,g0e2,f8g6,d2e1,g1e1,f0e1,b9c7,e2g4,g3g0,e1f0,c7a8,e7e8,d9e8,e0e1,a4b4,g4i2,c5c4,e1f1,g6e7,i2g0,e7d5,g0i2,c4d4,f1e1,d5f4,e1d1,f4h5,d1d2,c9e7,f0e1,e8f7,e1f2,b4c4,d2e2,d4e4,i2g0,e7c5,f2e1,i5i4,g0i2,c4c3,i2g0,h5g7,e1f2,i4h4,g0i2,c5e7,i2g0,a8b6,f2e1,e4e3,e2d2,g7i8,d2d1,h4i4,e1d2,c3d3,d1d0,f9e8,d2e1,b6d5,g0e2,i4h4,e2g4,h4i4,e1f2,i4i3,g4i2,d5f4,i2g4,e8f9,g4i2,f4h3,f2e1,h3i1,i2g4,e7g5,d0d1,f7e8,g4e2,e3f3,d1d0,i3h3,e1f2,d3d2,d0e0,e8d9,e0d0,h3h2,f2e1,i8h6,e2g0,i1g0,e1f2,d2e2,d0e0,h6i4,e0d0,f3f2,d0e0,i4h6,e0f0,e2d2 +9ea1ab09;a0b0,c6c5,f0e1,c5c4,e3e4,h7c7,c0a2,c4b4,a3a4,b7b0,a2c0,b9a7,a4a5,e6e5,i3i4,b4b3,e1f0,b0d0,e0d0,a7b9,c0a2,e9e8,f0e1,e8f8,a2c0,b9a7,c3c4,a9b9,a5b5,h9g7,d0e0,g6g5,b5c5,a7c8,c5d5,c7c6,e1d0,g7h5,c0e2,b9b4,d0e1,b3a3,d5d6,f9e8,e0d0,h5g3,e2g4,c6c7,d0e0,c7i7,e1d0,i7i4,g0i2,b4b1,e4e5,e8f7,e5d5,i4h4,c4c5,c8a9,d6c6,b1b4,e0e1,f7e8,c6b6,a9c8,d5e5,b4b5,b6a6,e8d7,i2g0,h4h2,e5d5,c8a7,c5c6,d9e8,g4e2,h2f2,d5d6,b5b8,a6a7,b8b6,g0i2,f2f7,a7a8,g3h1,e1d1,b6b0,d6e6,h1i3,d0e1,f7g7,i2g4,b0b2,a8b8,i3g4,c6d6,g7f7,e1d0,f8f9,e2g4,b2b0,g4i2,f9e9,d1e1,e9d9,e1e0,i9i8,e0e1,i8i9,i2g0,i9i7,e6e7,d9d8,d6d7,f7d7,e7f7,b0a0,f7g7,i7g7,e1f1,d7d3,f1f0,a3b3,f0f1,a0d0,b8b9,d3d1,b9c9,d8d7,f1e1,d1b1,c9d9,d0g0,d9c9,g7e7,e1f1,g0g4,c9b9,b1b2,b9c9,b2c2,f1f0,c2b2,f0f1,i6i5,c9d9,e7i7,f1f2,g4b4,f2e2,b4g4,e2f2,e8d9,f2f1,b2b0,f1f2,g4d4,f2e2,i7i6,e2e1,b0i0,e1f1,i6g6,f1f2,i0f0,f2f1,d7e7,f1f2,b3a3,f2f1,d4d2,f1f0,g6g7,f0f1,d9e8,f1f0,d2d0,f0f1,d0d4,f1f2,d4c4,f2f1,c4c6,f1f2,e7d7,f2f1,c6c7,f1f2,c7a7,f2e2,a7a9,e2f2,e8d9,f2f1,g5g4,f1e1,g7g6,e1e2,a3b3,e2e1,g6h6,e1e2,a9a3,e2f2,a3a9,f2e2,a9a5,e2f2,h6h8,f2e2,h8h9,e2f2,h9h1,f2e2,g9i7,e2f2,i7g5,f2e2,h1f1 +1fa8749d;g0i2,i6i5,a0c1,b7d7,c0e2,b9c7,c1c0,a9b9,c0b2,f9e8,b2a4,b9b2,a4h4,b2b4,i3i4,b4b9,h4h5,d7d5,h5i3,b9b2,e2g4,c7b9,f0e1,h7h1,e0f0,h1f1,a3a4,b2c2,e3e4,a6a5,i3g2,h9i7,f0e0,i7h5,e0f0,f1h1,f0e0,c2c3,g2g0,d5d2,g4e2,d2i2,g3g4,h5g7,e1d2,g9e7,g0f2,h1a1,f2h1,c6c5,h1d1,e8d7,d0e1,a1b1,d1b0,b9a7,e1f0,c3i3,i4i5,i9g9,f0e1,a7b9,b0b1,i3f3,i5h5,i2d2,b1b9,f3f5,b9b5,d2b2,h5g5,g7f9,b5b6,g9g7,b6a6,a5a4,g5h5,b2a2,e1f2,a2a0,a6c5,a0a2,c5c9,e9e8,c9d9,f5f6,d9d8,e8d8,e2g0,f6f8,h5g5,f8f6,g5g6,e7g9,g6g7,f6g6,e0f0,g6g7,g0i2,g7g4,i2g4,d7e8,f0e0,e8d7,f2e1,g9e7,e0f0,a2i2,f0f1,i2i5,e1f2,i5c5,g4i2,c5g5,e4e5,d8d9,e5d5,g5f5,f2e1,f5f4,e1d2,e7g9,f1f2,a4a3,i2g4,f4e4,f2f1,f9e7,g4i2,a3b3,d2e1,e4g4,d5d6,e7g6,i2g4,g6i7,g4i2,b3b2,d6d7,b2b1,d7d8,d9e9,i2g4,i7h9,d8c8,e6e5,f1f2,b1b0,f2f1,g9e7,f1f0,h9g7,c8d8,g7i6,g4e2,i6h4,e2c4,e7c5,f0f1,h4g2,f1f2,g2f4,c4e2,e5e4,d8e8,e9e8,e1f0,e8d8,e2g4,f4g6,f2f1,b0a0,f1f2,d8d9,f0e1,g6i5,g4i2,i5g6,f2f1,g6f4,e1d0,d9e9,f1f0,e9e8,d0e1,f4e2,f0f1,c5e7,e1f2,e4e3,i2g4,e7c9,f1e1,e2c1,e1d1,c1d3,d1d2,e3f3,d2d1,f3f2,g4e2,e8e7,e2g0,d3f4,g0e2,a0b0,d1e1,f4e2,e1e0,e7f7,e0e1,e2d4,e1d1,c9e7,d1d2,e7c9,d2d1,d4f5,d1d2,f5e3 +3c4e0f15;a0a1,h7h4,a1f1,b7g7,f1f4,g6g5,f4d4,b9a7,f0e1,g9i7,e1d2,e9e8,d4g4,h4h0,g0i2,a7c8,c0e2,g7g4,g3g4,g5g4,e0e1,c8a7,e1d1,g4f4,e3e4,h0h7,e4e5,f4e4,d2e1,e4f4,i3i4,i6i5,e2g0,a6a5,a3a4,i9i8,e5d5,f4e4,d5e5,a7b9,d1d2,h7d7,e5f5,d7g7,f5e5,i8g8,c3c4,e8e9,e5e6,g7a7,i2g4,a9a8,d2e2,g8g9,e1f0,a8b8,e6f6,e4e3,e2f2,d9e8,f6g6,b8b5,f0e1,a7e7,c4c5,e7e5,c5b5,e5d5,g4e2,a5a4,g6h6,b9c7,e2c4,d5c5,g0e2,e8d9,h6g6,c7b9,e2g4,a4b4,g6g7,b4b3,c4e2,d9e8,e2c0,e3f3,f2e2,b9c7,g4i2,c9e7,b5c5,b3c3,e1f2,c6c5,g7g8,h9g7,g8g9,i5i4,e2e1,c7a8,g9f9,e9d9,f9e9,d9e9,c0a2,g7i8,e1f1,i8g7,f2e1,i7g9,i2g4,f3e3,e1f2,e9f9,g4i2,g7i8,a2c4,a8c7,f1f0,c7b5,i2g0,e8f7,c4a2,i8g7,f0f1,g7i8,a2c0,b5a3,c0e2,e7g5,f1f0,e3d3,g0i2,d3d2,e2c4,g9i7,d0e1,i8g7,e1d0,f9f8,f0f1,g7f5,i2g4,i4h4,c4e2,d2c2,e2c4,h4g4,f1f0,a3b5,f0f1,f8e8,f1f0,i7g9,f0f1,c3d3,f2e1,g5e7,e1d2,b5c3,d2e1,g4h4,e1f0,d3e3,c4e2,c2b2,e2g4,c3e2,f1e1,f5d4,g4e2,g9i7,e1d1,d4b5,d1e1,e8e9,e2c4,e9d9,e1f1,c5c4,f1e1,i7g9,e1e0,b5d6,e0e1,d9d8,e1e0,d6b5,f0e1,b2c2,e1f2,c2d2,e0e1,e7c5,e1e0,b5a3,d0e1,d8d7,e0f0,c5e7,e1d2,a3b5,d2e1,e3e2,e1d2,e2d2,f2e1,b5a7,e1d0,d2d1,f0f1,a7c8,f1f2,c8d6,f2e2,e7c9,d0e1,d1e1,e2d2,d7e7 +2ca26625;a0b0,h7h5,f0e1,i9i8,b0b2,h5h0,e1f0,h0h1,b2f2,i8a8,f2f1,h1h8,f1f7,h8f8,g0i2,e9e8,g3g4,f8g8,f7h8,e8e9,c3c4,b7g7,c4c5,b9c7,h8i8,a8f8,a3a4,f8f7,c5c6,f7f3,c0a2,a6a5,a4a5,a9a6,i8i7,f9e8,d0e1,e8d7,c6b6,f3f8,e1d0,c7b9,d0e1,b9c7,a5b5,g7h7,i7h7,a6a7,h7g7,c7b9,e3e4,a7a6,g7d7,e9e8,d7c5,b9c7,c5d7,f8f5,i2g0,f5c5,b5c5,a6a5,e1f2,g8g7,g4g5,a5a6,d7e5,e6e5,g0e2,i6i5,f2e1,a6a4,e1d2,a4a6,b6b7,g9i7,f0e1,c7b9,i3i4,i5i4,b7a7,b9c7,a7a8,g7e7,e0f0,e8e9,g5g6,c7a8,g6f6,a6f6,f0e0,h9g7,e1f2,a8c7,c5b5,c7a6,e2g0,e5e4,d2e1,e4f4,e0d0,g7i8,b5a5,e7e4,e1f0,f6b6,d0d1,e4e0,d1d0,b6b1,a5b5,e0e6,b5c5,b1b5,g0i2,i8g9,a2c0,e6g6,i2g4,b5b3,f2e1,g6f6,g4e2,a6c5,e2g0,i4i3,e1d2,c9a7,c0e2,b3b5,d0e0,g9i8,d2e1,i3h3,e1d2,b5b9,e0d0,c5e4,d2e1,f6f0,e1d2,b9b1,e2c4,e4f6,d2e1,f6e8,e1f2,e8g9,g0e2,b1d1,d0d1,g9h7,e2g0,f0c0,g0e2,c0e0,d1d0,e0f0,e2g4,h7f6,d0d1,f6g4,d1d2,g4h2,c4a2,h2i0,f2e1,e9f9,a2c4,i8g7,e1d0,g7i6,d0e1,h3h2,d2e2,i6g7,e1d2,f0h0,c4a2,h0c0,e2e1,h2g2,e1e2,d9e8,d2e1,f4f3,e2d2,g7h9,a2c4,a7c5,e1d0,c5a7,c4a2,e8d7,d0e1,i7g5,d2e2,f9f8,a2c4,f8f7,e2d2,f7e7,e1f0,d7e8,f0e1,f3f2,e1f2,i0h2,c4a2,h9g7,d2d1,g2f2,a2c4,e8f7,c4a2,g7f9,a2c4,h2f1,c4e2,f2e2 +32776ca5;i3i4,i9i7,e3e4,h9g7,e4e5,g7h9,e5e6,d9e8,f0e1,b7b0,a0a1,b0b5,a1d1,b5f5,e1d2,f5f2,d1a1,h7b7,g0i2,i7e7,a1i1,f2h2,i1c1,e7i7,c0e2,h2h7,c1g1,g6g5,a3a4,h7e7,i4i5,b7b0,d0e1,c6c5,e6f6,e7e3,e0f0,b9a7,g1i0,e8d7,i0h0,h9g7,f6f7,b0b1,h0h6,b1a1,h6h3,g7h9,h3e3,e9d9,i2g4,i7h7,f7g7,i6i5,e2c4,a1a0,f0f1,a0e0,e3e5,e0c0,e5c5,c0i0,c5c8,c9e7,c8e9,d9e9,e1d0,h7h0,f1e1,h0g0,a4a5,g9i7,g7h7,g0h0,h7i7,a7c8,c4a2,h9f8,a5b5,h0d0,b5a5,f8h7,a2c0,i0g0,c0a2,g0i0,a2c0,i0h0,e1e2,h0i0,a5a6,d0c0,e2e1,i0f0,e1d1,e7c9,c3c4,c0b0,g4e2,b0b7,g3g4,c8b6,i7i8,a9b9,i8h8,b6a4,c4c5,f0f7,d1e1,d7e8,e2c0,h7f8,e1d1,g5g4,c5c6,b9a9,c6c7,g4f4,h8h9,f7f6,c7b7,f8h9,d2e1,f6f8,d1d2,c9a7,c0e2,a4b6,b7b8,b6d5,e1d0,e9d9,a6b6,f8b8,e2g4,h9g7,b6b7,f4e4,g4e2,i5i4,d0e1,g7h9,b7c7,b8b3,d2d1,i4i3,e2c4,b3g3,c7b7,a7c5,b7c7,g3g9,c7c8,a9a0,e1f2,g9g6,f2e1,g6e6,c4a2,i3i2,c8d8,d9d8,a2c4,e6g6,e1d0,g6g4,c4a2,e8d9,a2c4,e4e3,d1d2,a0a8,c4e2,a8a2,d2d1,e3e2,d0e1,g4a4,e1f0,a4h4,f0e1,h9f8,e1d2,h4f4,d1d0,a2a9,d2e1,a9a4,d0e0,f8g6,e1d2,e2e1,e0e1,a4a3,e1d1,f4f2,d1d0,a3a6,d0e0,a6a8,e0e1,a8a4,e1e0,a4h4,e0f0,d5f6,f0f1,h4b4,f1e1,b4b1,e1e0,f6e4,d2e1,i2i1,e0f0,g6f4,f0f1,d8e8,f1f0,b1e1 +35a19c3f;a0a2,a9a8,a2e2,d9e8,e2f2,a6a5,d0e1,c9a7,f2e4,b7b4,a3a4,b4b6,e4f6,a8d8,i3i4,b6b0,c0a2,h7h3,f6c6,d8d9,c6a5,h3h1,e1f2,b0c0,a2c4,h9i7,a5f5,b9d8,f5f4,h1h3,g3g4,h3h1,f4d3,i9h9,d3e5,h1h7,e5g6,d8b9,g0e2,h7d7,g6f4,h9h3,f4d5,i6i5,g4g5,d7g7,g5h5,i7h5,d5d4,h5g3,a4a5,h3h8,d4d2,g7e7,e3e4,d9d3,e2g4,g3i2,d2i2,b9c7,e0e1,h8h6,i2h0,c0h0,a5b5,e6e5,e1f1,h6g6,f1e1,d3e3,c4e2,e8d9,b5a5,h0h5,i4i5,c7b5,e1d1,g6d6,d1e1,d6h6,e1d1,h5h0,a5a6,h6h2,e2c4,e9e8,e4e5,h0h1,d1d0,e7b7,d0d1,e3d3,d1e1,h2h6,a6a7,b7b8,e1f1,d3d5,a7b7,h6h9,e5f5,h9h8,f0e1,d5d1,f1f0,e8e9,e1d2,b5c3,g4i2,d1f1,f0f1,c3a2,b7b8,h8h6,i5h5,h1h4,h5h6,a2c3,f2e1,f9e8,h6g6,h4h0,g6g7,c3b1,f1f2,b1d2,c4a2,h0g0,g7g8,d2c0,i2g4,g0i0,g4i2,e8f9,b8b9,d9e8,g8g9,i0g0,f5e5,c0d2,g9f9,e9d9,i2g4,g0h0,e1d0,d2c4,g4i2,h0h1,e5f5,h1h4,a2c4,e8d7,f9e9,d9e9,c4a2,h4g4,b9c9,e9e8,i2g4,e8e9,f5e5,d7e8,g4i2,e8d9,f2e2,e9e8,c9d9,e8d8,e2f2,d8d9,i2g4,d9d8,a2c0,d8d7,d0e1,d7d8,e1d0,d8d9,d0e1,d9e9,f2f1,e9d9,e1d0,d9d8,f1f0,d8e8,e5d5,e8e7,d5d6,e7e8,d6e6,e8e9,e6e7,e9d9,f0e0,d9d8,g4e2,d8d9,c0a2,d9e9,a2c0,e9d9,e0e1,d9e9,c0a2,e9d9,e1e0,d9d8,e2g0,d8d9,g0e2,d9d8,a2c4,d8d9,e2g4,d9e9,c4a2,e9d9,e7f7,d9d8,f7e7,d8d9,a2c0,d9e9,e7e8,e9f9,c0a2 +552331dc;d0e1,b7b4,a0a1,b4b5,c0e2,h7h8,a1a6,i6i5,g3g4,b5e5,a6c6,h8h4,c6c9,e9e8,c9f9,h4h8,f9d9,h8h6,d9h9,h6h5,h9i7,h5h7,e1f2,e5e2,i7i6,b9c7,i6i5,a9a8,e0d0,h7h3,i5i4,h3h5,i4i5,a8a4,i3i4,c7a8,f2e1,g9i7,i5h5,e2c2,h5h2,i9a9,e3e4,e8e7,g4g5,a4a5,h2h6,c2g2,g0i2,a5f5,g5h5,f5g5,c3c4,g5a5,a3a4,g2f2,h6h7,e7e8,d0d1,a5h5,d1d2,f2f4,e4e5,a8c7,e1f2,h5h6,h7h6,i7g9,d2d1,g9i7,h6h7,f4e4,h7c7,e8e9,c7i7,e4h4,i7h9,e9e8,h9h4,a9h9,h4h9,e8e7,h9h2,e6e5,h2h4,e7f7,h4h3,f7f8,c4c5,f8f7,h3d3,g6g5,d3f4,f7e7,i2g4,e7e8,f0e1,e5e4,a4a5,e8e9,g4i2,e9e8,c5d5,e4e3,f4f8,e8e7,d5d6,e3f3,f8g8,e7f7,g8c8,g5g4,e1d2,g4h4,c8c4,f3f2,c4g4,f7e7,g4f4,h4g4,f4f2,e7e8,d2e1,g4f4,f2h1,f4g4,a5b5,g4f4,d1d2,f4e4,h1i3,e8d8,i3b3,d8e8,b5c5,e4d4,b3b8,e8e7,b8i8,d4e4,i8c8,e7f7,i4i5,e4d4,c8f8,f7f8,d2d1,f8f7,e1d0,d4c4,d6e6,c4c3,c5c6,c3c2,c6c7,c2c1,d1e1,f7f8,c7d7,c1c0,e6e7,f8f9,d7d8,c0d0,i2g0,f9e9,i5i6,d0e0,g0i2,e0f0,i2g0,e9f9,d8c8,f0g0,e7f7,g0h0,c8c9,h0g0,i6h6,g0h0,c9d9,h0i0,e1d1,i0h0,h6i6,h0i0,d9e9,f9e9,f7g7,e9f9,d1e1,f9f8,g7f7,f8f9,f7g7,f9f8,e1e0,f8f9,i6i7,f9f8,i7i8,f8f9,i8i9,i0h0,g7h7,f9f8,h7i7,f8f9,i9h9,h0g0,i7h7,g0f0,e0e1,f0e0,h7i7,e0d0,h9i9,d0c0,e1e0,f9f8,i7i8,f8f9,i9h9,c0b0,h9i9,b0c0,i8h8,f9f8,h8g8,f8f7,i9h9,c0d0,e0e1,d0e0,e1e0 +5087d4bd;g0i2,b9c7,c0e2,h7h0,f0e1,h0h1,a0c1,h1h3,i2g0,b7b8,e1d2,a9a8,e3e4,b8b9,c1c2,g9i7,c2b2,i7g9,e0e1,h3h0,b2b9,a8g8,e2c4,h0i0,e1f1,i0d0,b9b3,d0c0,b3b7,c0c1,f1e1,c7b9,g3g4,e6e5,b7a9,f9e8,a3a4,c1c4,g4g5,e8f9,a9c9,e9e8,e1e0,h9i7,c9f9,c4d4,g5f5,i9i8,e0e1,c6c5,f9f6,e8e7,f6f9,e7d7,g0i2,a6a5,f9f8,d7e7,e1f1,d4d8,i2g0,d8d3,f1f2,i8h8,c3c4,g6g5,f5e5,b9a7,f2e2,h8h5,f8c8,a7c8,i3i4,g8h8,g0i2,h8h9,i2g4,h5i5,a4a5,h9i9,e2e1,e7e8,g4e2,c8e7,i4i5,e7c8,e1f1,c8a9,i5i6,d3d7,c4c5,g5g4,e5e6,d7d8,a5b5,i7g8,e2g4,d8d4,c5d5,g8i7,b5a5,d4g4,f1e1,g4i4,e1e0,i4i0,d2e1,i9h9,i6i7,h9h7,d5c5,h7h0,e1f0,i0f0,c5b5,f0f7,e0e1,h0h3,e1e2,h3f3,e2e1,f3h3,i7h7,f7f9,e1f1,h3a3,e6d6,a3f3,f1e1,a9b7,e1e0,f3h3,h7h8,h3h7,h8h9,h7c7,e0d0,c7c5,b5b6,c5f5,d6e6,f5e5,h9i9,e5h5,b6b7,h5h7,b7a7,h7h0,d0d1,h0h9,a5b5,h9h1,d1d0,g9e7,b5b6,f9f7,a7a8,f7f2,i9h9,h1h2,h9i9,e7c9,a8a9,f2f4,d0e0,h2h9,e0f0,h9h6,b6b7,h6f6,f0f1,f6f8,b7a7,e8e9,e4e5,d9e8,e6f6,f4h4,e5e6,h4h2,f6f7,h2d2,a7a8,d2d9,f1f2,d9d1,f7f8,d1d0,f8e8,e9d9,e8e9,d9d8,e9f9,d0e0,f9e9,e0h0,e6f6,h0h9,i9h9,d8e8,a8b8,e8d8,e9f9,d8d7,f9e9,c9e7,e9d9,e7c9,a9b9,d7d8,f6e6,d8d7,h9i9,d7d8,b8c8,d8d7,c8d8,c9a7,i9h9,a7c5,d9e9,d7d8,e6e7,c5a7,e7d7,d8e8,e9d9,a7c9,d9c9,e8e9,f2f1,e9e8,h9g9,e8e9,b9a9,e9e8,g9f9 +12dd8c37;d0e1,h9g7,e1f2,b7b2,c0e2,h7h4,a0b2,h4h9,b2d1,b9a7,e0d0,i9i8,g3g4,e9e8,c3c4,a9b9,d1d3,i8i9,d3c5,b9b7,a3a4,e8e9,c5d5,a7b9,d5e5,f9e8,e5f5,h9h7,e3e4,b7d7,f5d5,h7h6,e4e5,d7d8,d0e0,e8f9,d5d9,e9e8,e5d5,c9a7,c4c5,g9e7,c5c6,h6h5,c6c7,g7i8,d9f9,a6a5,f9f5,i9d9,f5g3,d9h9,c7c8,h5h0,c8d8,e8e9,g3f1,i8g7,d8e8,g7e8,f1d0,e9f9,i3i4,a7c5,a4a5,h9h7,d5c5,b9c7,c5b5,h7h1,d0d7,h1h6,d7d6,c7d9,d6b7,e7c9,b7a9,h6h1,a9c8,h1h7,c8c6,d9f8,c6b8,g6g5,b8d7,h0i0,d7d8,h7e7,d8c8,e7a7,f2e1,i0i1,c8c5,i1i3,e2c0,i3c3,c5e5,c3c2,e5d5,f8h7,d5f5,f9e9,e1f2,a7g7,g0i2,c2c1,f5g5,c1g1,g5g7,g1g7,f2e1,h7i9,e0d0,g7g9,g4g5,e8f6,d0d1,i9h7,b5c5,e9e8,a5b5,e8e9,i2g4,h7f8,c0a2,g9i9,g5f5,f6e8,g4i2,e6e5,d1d2,e9f9,i2g4,c9e7,g4e2,e5e4,e1f2,e4d4,e2g4,i9h9,f2e1,h9h1,e1d0,d4c4,c5d5,h1h7,g4e2,h7g7,e2c4,g7g3,f5g5,g3e3,d2e2,e3e4,b5c5,f8e6,e2d2,e7c5,d5e5,e8g9,f0e1,e4e2,e5f5,c5e7,f5f6,g9h7,d2e2,h7i5,e1f2,e7g9,g5h5,g9e7,f6f7,e6c7,f7e7,f9e9,e2e1,c7b9,i4i5,i6i5,h5h6,b9a7,h6g6,e9d9,e7d7,a7c8,c4e2,c8b6,e1e0,b6d5,a2c0,d5e7,d7e7,d9d8,g6g7,i5i4,g7g8,d8d9,e0e1,d9e9,e1f1,e9d9,d0e1,i4h4,f1f0,h4g4,e2g4,d9e9,g8h8,e9f9,e1d2,f9f8,g4i2,f8f9,e7f7,f9e9,h8h9,e9d9,h9i9,d9d8,i9h9,d8e8,h9i9,e8d8,f2e1,d8d7,c0e2,d7d8,f0e0,d8d7,e0f0,d7d8,e2c0,d8e8,f0f1,e8d8,f1f2,d8e8,f7f8,e8e9,f2f1,e9d9,e1f2,d9e9,c0e2,e9d9,f8e8 +87e27a99;c0a2,b7b2,a0b2,f9e8,b2d2,h7h4,i3i4,h4h7,d0e1,e8d7,a2c0,h7h8,c3c4,h8h5,d2d6,h5h1,c4c5,h1h0,d6d0,c9e7,a3a4,e9f9,d0d2,a9a8,d2f2,f9e9,f2f6,a8h8,f6e4,h0h1,e4h4,h8h6,h4b4,d9e8,g0e2,h6h4,c5b5,h4h5,e1d2,h1g1,b4c2,h5h8,e3e4,e7g5,c2d0,g1a1,e4e5,h8h7,d2e1,h7h6,i4i5,a1d1,i5h5,h9i7,e5d5,i7h9,g3g4,c6c5,g4g5,b9c7,d0d1,i6i5,e2g4,i9i8,d1d3,e8f7,h5h6,g6g5,d5c5,i8e8,d3i3,e8i8,b5b6,e9e8,i3d3,c7b9,d3b3,a6a5,b3h3,b9d8,h3d3,i8i6,e1d2,i6i9,d3i3,h9i7,i3h1,e8e7,h1b1,i7g8,b1b3,g8f6,g4e2,d8b9,b3b2,i9i6,b2b9,f6e4,b9b7,e7e8,a4a5,e4f6,b7a7,i6i9,d2e1,g9i7,e1f2,i9i8,a7b5,i5i4,b5b4,i4i3,b4b1,f6h7,b1d2,i8g8,e0e1,h7g9,c5c6,e8e9,h6h7,f7e8,d2d6,g8f8,e1e0,f8g8,c0a2,e8d9,c6c7,e6e5,h7g7,g8b8,d6c4,b8e8,e2g4,e8e6,c4a4,d7e8,g7g8,e5e4,a4b2,i3h3,a2c4,e4e3,b2c2,e9f9,c2e2,h3i3,g8h8,e6i6,e2e8,e3e2,g4e2,g9i8,f2e1,i6c6,e8g9,f9f8,a5a6,c6b6,g9g8,f8f9,g8i8,b6b8,e0d0,b8b2,e2g4,b2b6,c4e2,b6b1,c7c8,b1b6,a6b6,d9e8,b6a6,e8d9,e2c4,f9e9,i8i7,e9f9,e1d2,i3i2,h8g8,g5g4,c8b8,i2i1,g8g9,f9f8,i7i1,d9e8,d2e1,e8f7,i1i5,g4f4,c4e2,f4f3,i5g6,f8e8,b8a8,f3g3,g6g3,e8f8,g3g1,f7e8,g1g3,e8f9,g3f1,f8e8,e2g4,e8e9,g4e2,f9e8,f1g1,e8f7,d0d1,f7e8,d1d2,e8d7,e2c4,e9e8,g1f1,e8e9,f1h1,e9e8,c4e2,e8f8,e1d0,f8e8,h1h5,e8e7,h5h0,d7e8,a6a7,e8d9,e2g4,e7f7,h0g0,f7f8,g0h2,f8e8,h2i4,e8f8,a8a9,d9e8,i4i9,e8f9,i9f9,f8e8,f9f7 +593a7fe1;i3i4,b7g7,i4i5,i9i7,a3a4,h7h4,a0b0,h4d4,i5i6,i7i8,f0e1,g7g8,b0a2,d4e4,a2c1,e9e8,c1c6,i8i7,c6g6,a9a7,g6g9,g8f8,g3g4,f8g8,c0e2,e4e5,g9d9,a7d7,c3c4,g8h8,e2c0,e5a5,e3e4,a5b5,d9d7,b5h5,e4e5,h5h2,d7d2,h2g2,d2d1,g2e2,e0f0,i7f7,e1f2,b9c7,d1g1,h8g8,i6i7,a6a5,e5d5,e2e4,g1c1,c9e7,c1a2,f7f8,d5d6,e4a4,g0i2,g8g5,a2a4,g5b5,c0e2,h9i7,a4a2,f8f6,a2c1,c7a6,c1d1,b5d5,d1d2,d5e5,f0f1,i7h5,d2a2,e5g5,a2a0,f6f3,a0a5,g5g9,d0e1,e8e9,a5b3,f3f2,e1f2,a6b4,e2c0,e9d9,g4g5,e6e5,c0a2,e5e4,b3a3,e7c9,i2g4,h5i3,a3b3,e4f4,b3c1,i3h1,d6e6,g9g4,e6e7,g4g2,g5f5,h1f2,a2c0,g2g8,c1e2,g8e8,e2c1,e8d8,c1e1,d8d2,e1d3,d9e9,c4c5,d2c2,d3d6,b4d5,f1f0,d5f6,d6f6,f2h3,f5g5,c2c1,e7f7,c9e7,f6d5,c1g1,c0a2,f9e8,d5c3,g1i1,c3e3,i1i9,f7g7,i9i8,a2c0,i8i2,e3c2,i2i8,c0a2,i8g8,c2a1,h3g1,a2c4,g1f3,f0f1,e7g9,g5h5,f3d4,a1a6,g9e7,a6a3,d4c6,g7g8,e7g9,a3i3,e8d7,f1f0,g9e7,i3g3,d7e8,g3e2,c6e5,c5d5,e9d9,e2c1,e5g6,h5i5,e7c5,i5h5,c5e7,c1d3,d9e9,f0e0,g6i5,d3h3,e7c9,h3i1,i5h3,g8f8,f4g4,f8f9,e9d9,i1i8,h3g1,e0f0,g1h3,h5i5,h3g1,i5h5,g1e0,i8g8,e0f2,g8e7,g4g3,f9e9,d9d8,e7e4,c9a7,e4e0,f2g4,c4e2,a7c9,h5g5,g4e3,e0e3,e8d9,e3c2,c9a7,e9d9,g3f3,d5e5,a7c5,g5h5,f3f2,c2c1,c5a7,f0e0,a7c5,e2g4,f2f1,c1e1,f1e1,e0e1,c5a7,e5e6,a7c9,d9c9,d8d7,e1f1,d7d8,f1f0,d8d7,e6d6,d7e7,f0f1,e7e8,h5h6,e8e7,h6g6,e7e8,d6d7,e8e9,c9d9,e9e8,g6h6 +9afba04d;g3g4,h7h0,a0a1,b7f7,a1g1,f7b7,g1i0,b7i7,i0h0,i7e7,h0h1,e7i7,e0e1,d9e8,h1h5,c9a7,g0e2,i7b7,h5i7,e8d7,g4g5,b7b8,i7g6,b8h8,c0a2,e9e8,g6f8,e8f8,e2g0,h8h2,a3a4,g9i7,e1e2,h2h5,c3c4,h5h7,g0i2,h7g7,g5f5,g7e7,f5e5,a6a5,e3e4,e6e5,e2d2,i7g9,e4e5,b9c7,i3i4,g9i7,i4i5,i7g9,c4c5,g9i7,i5i6,a9c9,e5f5,e7e2,f5g5,c9a9,i6i7,e2e5,i7h7,e5e3,c5c6,i9i8,c6d6,i8i7,d6d7,a7c5,g5f5,e3b3,h7h8,b3b1,a2c4,b1g1,f0e1,g1g0,h8g8,f8f7,d7c7,c5e7,e1f0,f9e8,c7d7,i7i2,d2d1,e7g9,g8h8,i2i4,h8g8,g0d0,d7d8,f7e7,d1d2,a9a8,f5e5,i4i5,e5d5,a8d8,d5d6,e8f9,c4a2,i5i6,a4a5,d8f8,a2c4,h9i7,d6c6,f8f7,g8h8,i6i4,h8g8,i7h5,g8g9,f7f6,d2d1,f6f1,d1d2,f9e8,a5b5,f1c1,g9f9,d0e0,b5c5,i4i2,c4e2,i2g2,c6d6,g2g6,d6e6,e7f7,c5d5,e0a0,e2g0,h5i3,e6f6,g6f6,d5c5,c1c4,f9e9,a0g0,c5d5,e8d9,d5d6,c4c3,d6e6,g0g7,e6f6,f7f8,e9d9,c3c8,f0e1,g7b7,d9c9,b7d7,f6g6,c8c1,d2e2,d7d5,c9d9,d5c5,e1f0,c5e5,d9e9,e5e7,e2d2,c1f1,g6h6,i3g4,h6i6,e7i7,i6h6,f1i1,e9f9,i1d1,d2d1,i7h7,h6g6,g4f6,g6h6,h7f7,h6g6,f7c7,f0e1,f6g8,e1d0,f8f7,g6f6,f7e7,f9g9,c7c2,g9h9,c2c9,d0e1,c9g9,f6g6,g9d9,e1f0,g8f6,g6f6,d9b9,h9i9,b9b5,d1d0,e7e8,f6f7,b5f5,f0e1,f5b5,e1f0,b5b1,f7g7,b1g1,g7f7,g1a1,f7e7,e8e9,e7f7,e9e8,f7e7,e8f8,e7f7,f8e8,f0e1,a1d1,i9h9,e8d8,f7g7,d8d9,g7f7,d1d3,f7e7,d3d7,e7d7,d9e9,d0e0,e9e8,e0f0,e8e9,e1d0,e9e8,h9g9,e8e9,d0e1,e9e8,d7c7,e8d8,g9f9,d8e8,c7d7 +c3409e9e;f0e1,h7h3,e3e4,b7b2,a3a4,h3h2,a0a3,h2h7,g0i2,b2b7,e1f2,h9i7,c0e2,h7h2,e2g0,h2h6,a3a1,f9e8,f2e1,h6h2,a1d1,g6g5,e0f0,b7b3,d1a1,b3b2,a1c1,b2c2,e4e5,h2h5,e1d2,c9a7,c1e1,c2c1,e1g1,c1d1,g1f1,d1a1,f1f2,e8f7,a4a5,h5i5,e5e6,a1a2,f2e4,e9f9,i2g4,c6c5,e4f4,f9e9,f4f5,a7c9,f5f3,d9e8,f3f5,b9c7,f0e0,i9h9,f5g5,a9a8,g5g8,e9f9,g8g5,i7g8,g5c5,h9h4,g4e2,h4e4,c5f5,g8f6,e6f6,g9i7,f5h6,i5i4,i3i4,a2c2,a5a6,e4e7,h6h4,a8b8,h4g4,b8b0,g4i3,i7g5,f6f7,b0d0,e0d0,c2e2,i3g4,c7b9,g4b4,e2e3,a6a7,e3f3,b4b1,f3e3,g3g4,e7c7,f7e7,f9e9,b1g1,e3h3,g1c1,c7c8,c1d3,e8f9,c3c4,h3g3,d3d7,c8d8,d7d5,g5e7,d5b4,d8d2,d0e0,d2i2,b4b9,e9e8,g0e2,i2i3,e2c0,i3i0,e0e1,g3d3,e1f1,e7c5,b9b1,c5a7,b1b5,i0i2,b5d5,d3d4,d5c5,e8e7,c5b5,i2h2,g4g5,f9e8,b5f5,e7d7,g5h5,d4d3,f5f2,d7d8,f1f0,d8d9,f2f8,h2a2,f8e8,d3d8,e8h8,d8d7,i4i5,d7d5,h5h6,a2c2,i5h5,d5d4,h8b8,d9e9,b8b2,d4e4,b2a4,e4f4,a4a0,f4f7,f0f1,e9d9,h5i5,c2b2,f1f0,f7f6,c0e2,f6c6,a0a1,b2a2,a1f1,c9e7,f1f4,c6f6,f4e6,a2a0,f0f1,a7c9,e6f6,a0a1,f1f2,e7g9,f6h7,a1f1,f2f1,c9a7,h6i6,a7c5,e2c0,c5e7,h7h2,e7g5,f1f0,d9d8,h2h9,g5i7,i5h5,d8d7,h9i9,d7d8,h5g5,i7g5,f0f1,g5e7,f1f0,e7g5,i9g9,g5e7,g9c9,e7g9,c9g9,d8d7,i6i7,d7e7,g9g0,e7e8,g0g4,e8d8,f0e0,d8d9,c0a2,d9d8,g4g6,d8d7,i7i8,d7d8,g6g3,d8d7,g3f1,d7d8,f1f0,d8d7,f0f1,d7d8,f1a1,d8d7,a2c0,d7d8,c0e2,d8d9,a1b3,d9e9,b3b8,e9f9,b8h8,f9e9,e0f0,e9d9,h8g8 +64835de6;a0a1,c9e7,c0e2,b9c7,a1b1,b7b2,e2c0,i9i8,b1d2,b2b6,d2h2,b6b8,h2h0,g9i7,h0i2,b8b0,i2i6,a9c9,i6e6,c7e8,e6f6,i7g9,f6e6,h9i7,a3a4,b0b9,e6g6,b9b8,c0a2,e7g5,g6f6,a6a5,f6e6,g5e7,e6c5,h7h6,c5h5,c9a9,a4a5,h6h9,h5f5,h9h7,a5b5,e8g7,f5e5,f9e8,g3g4,g7f5,e5e4,f5h4,e4h4,a9a5,d0e1,e8d7,h4h5,e7g5,h5h0,g5e7,e3e4,i7h5,h0h1,b8e8,e1f2,e8b8,c3c4,b8f8,h1e1,f8f5,i3i4,f5d5,e4e5,a5a8,e1a1,i8i6,e5d5,a8d8,g4g5,d8h8,d5e5,c6c5,a1h1,e9f9,b5b6,e7g5,h1i3,h7f7,e0d0,f7f3,e5e6,f3g3,i3g4,i6f6,g4i3,h8e8,f2e1,e8a8,i3g3,h5g3,b6a6,g3e2,e6d6,g9e7,d0e0,f6f1,a6b6,a8i8,e0d0,f1f3,g0e2,f3f7,d6d7,f9e9,e2c0,f7g7,d7d8,g5i7,b6a6,i7g9,d8e8,d9e8,c0e2,g7i7,e1d2,i7h7,i4i5,i8g8,c4c5,h7h5,a2c4,g8g6,c4a2,h5h2,e2c4,h2h4,c5b5,h4g4,i5h5,g4h4,f0e1,g6g2,b5c5,e8f7,c5c6,f7e8,a6a7,g9i7,c6d6,g2h2,h5g5,h4h7,e1f2,h2i2,d6e6,i2i0,d0d1,i0f0,e6e7,h7h1,d2e1,h1f1,g5f5,i7g5,f5g5,f0c0,a2c0,f1g1,g5g6,g1h1,g6g7,h1h0,c4a2,h0c0,e1d2,c0b0,f2e1,b0g0,a7b7,g0g6,e1f2,g6g1,f2e1,e9d9,g7g8,g1g5,b7b8,g5i5,g8f8,e8d7,d1d0,i5i0,d0d1,i0i4,b8a8,d7e8,e1d0,i4c4,a8b8,c4e4,e7f7,e4f4,b8b9,f4f5,f8g8,f5g5,b9c9,d9d8,a2c0,g5e5,f7f8,e5e2,g8h8,e2d2,d1e1,e8d7,c9b9,d2b2,h8g8,b2b8,e1d1,b8a8,g8g9,a8a0,d0e1,a0a5,g9f9,a5f5,c0a2,f5f1,b9a9,f1f2,f9g9,f2d2,d1d2,d8d9,e1f2,d9e9,a9b9,d7e8,f8f9,e8f9,a2c4,e9e8,c4e2,e8e7,e2g0,e7e8,g0e2,e8e9,b9c9,f9e8,f2e1,e8d9,e2c0,d9e8,d2e2 +afa58eba;e3e4,b7b1,c3c4,h9i7,a0c1,h7h2,c1e1,b9a7,e1g2,h2h1,g2h2,c6c5,h2f2,c5c4,f2f9,e9f9,e0e1,h1i1,c0e2,b1b7,a3a4,i7g8,g3g4,i1g1,a4a5,b7b2,e2c0,b2b0,c0e2,a9b9,i3i4,b0b3,g4g5,i9i8,e2c0,c9e7,g5f5,b3b2,c0a2,b2f2,e1e2,b9b6,a5a6,b6b9,e4e5,f2g2,a2c4,g2g3,e5e6,d9e8,e2d2,g3e3,d2d1,e3f3,f5f6,i6i5,g0i2,b9b3,e6e7,i8i9,i2g0,g1g5,a6b6,g5g2,d0e1,b3e3,e7f7,a7c8,e1f2,e3e2,f0e1,f3d3,f7e7,d3b3,i4i5,e2e1,d1d0,e1a1,e7f7,b3b5,g0e2,a1a8,e2g0,g2h2,b6b7,h2h3,b7c7,c8a7,i5h5,h3b3,c4e2,g8e7,h5h6,i9i2,e2g4,e8f7,d0d1,b5c5,d1e1,i2i4,e1e2,a8a9,h6g6,b3b2,e2e1,c5i5,e1d1,a7c8,g4e2,i4c4,d1d2,a9b9,f6f7,c8e9,g6g7,b2b6,f7e7,f9f8,e7f7,f8e8,c7b7,i5i7,b7c7,e9c8,f7e7,e8f8,e7f7,i7f7,g0i2,b6h6,e2g0,h6h8,g0e2,h8g8,e2c0,b9b8,c7c8,f7f5,i2g0,f5f7,g7f7,f8e8,c8b8,g9e7,b8b9,g8g1,b9c9,g1g9,d2d1,c4c0,c9b9,c0c2,f7e7,e8f8,g0i2,c2e2,i2g4,e2d2,d1d2,g9d9,b9c9,d9d7,g4e2,d7b7,d2d1,b7c7,e2c4,c7a7,f2e1,a7a4,e1d2,a4a9,d1d0,a9a1,e7d7,a1a8,d7c7,a8a2,c4a2,f8f9,c9b9,f9e9,c7b7,e9e8,d0d1,e8e9,a2c0,e9f9,c0a2,f9e9,a2c4,e9d9,b7a7,d9e9,b9c9,e9f9,c9d9,f9f8,c4e2,f8e8,e2g4,e8e7,g4e2,e7e8,d2e1,e8f8,d1d0,f8f9,e1f2,f9f8,e2g0,f8f7,g0i2,f7f8,f2e1,f8f9,d9c9,f9e9,d0e0,e9e8,e0d0,e8f8,e1d2,f8f9,d0e0,f9f8,e0d0,f8e8,a7b7,e8e9,i2g4,e9f9,g4i2,f9f8,b7c7,f8f9,c9d9,f9f8,i2g4,f8f7,g4i2,f7f8,i2g0,f8e8,c7d7,e8f8,g0e2,f8e8,d9e9,e8f8,d2e1,f8e8,d7e7,e8f8,e2c0 +fea502af;a0a6,h7c7,a3a4,e9e8,a4a5,c7g7,g0e2,b7f7,a6c5,e8e7,c5f5,e7e8,e3e4,e8e9,f5f1,f7f6,f1f9,e9e8,f9h9,f6f2,h9i9,i6i5,c0a2,f2f4,i9h7,f4f5,h7i5,f5d5,e2g0,g7g3,i5g5,d5d3,g5i6,g3g4,a2c4,d3d1,i6h4,d1f1,h4h0,f1f7,h0h6,g4c4,h6h7,f7a7,h7f8,e8e7,f8f9,e7e8,f9g9,e8f8,g9g7,g6g5,g7i6,a7f7,i6i9,f7c7,i9i5,c4d4,i5i6,f8e8,f0e1,a9a6,a5b5,d4d3,i3i4,d3i3,i6i9,i3d3,i9i7,a6a2,i7h5,d3d6,g0e2,a2a1,e4e5,c7f7,e2c0,e8e7,h5i7,e7e8,c0e2,e8d8,i4i5,g5g4,i7f7,d8e8,b5c5,a1a3,f7g7,e8e9,g7b7,a3a0,e1d2,a0a3,b7a9,a3b3,c5b5,c9a7,c3c4,d6d3,a9d9,e9d9,b5c5,g4f4,e5f5,e6e5,i5h5,b3a3,e2g0,a3c3,f5e5,c3c2,c5c6,d3c3,d0e1,c2d2,c6c7,f4e4,c7c8,d2a2,e0f0,a7c5,e5f5,a2a4,c8d8,d9e9,f0e0,a4a2,f5e5,e9f9,h5i5,a2f2,e1f0,e4e3,e5f5,c3a3,g0i2,f2f1,f5e5,f1f3,i5i6,f3f8,e5f5,f8i8,d8c8,b9c7,c8b8,c7b5,e0d0,a3a7,b8b9,c5e7,d0e0,f9e9,c4c5,e9e8,i2g0,e7g9,i6i7,e8d8,f0e1,i8g8,f5f6,g8f8,c5b5,a7d7,e0d0,e3d3,d0e0,d8d9,e1f2,d7g7,i7h7,g7g3,h7h8,g3g4,h8h9,f8d8,f2e1,d8b8,b5a5,g4a4,a5a6,d3d2,a6b6,a4a7,h9i9,b8e8,g0i2,e8c8,i2g4,a7a6,b6b7,c8c2,e1f2,a6a0,e0e1,c2a2,e1e0,d2e2,f6e6,a0b0,e6e7,e2d2,f2e1,a2c2,e0f0,b0d0,e7d7,c2a2,d7e7,a2a1,e7f7,d9e9,e1d0,a1f1,f0f1,d2c2,f7g7,g9i7,g7h7,i7g9,f1f0,e9e8,g4e2,c2d2,b7a7,d2c2,a7a8,g9e7,h7h8,e7c5,f0e0,e8e9,a8a9,c5e7,h8i8,e7g5,b9c9,c2c1,c9b9,g5e7,e2c4,c1d1,c4a2,d1c1,i8h8,c1c0,a2c0,e9f9,c0e2,f9f8,e2g0,e7c9,b9c9,f8f7,h8g8 +c6d2adf2;g0e2,h7i7,c3c4,i6i5,a0a1,i9i8,c4c5,b7e7,a1i1,i8g8,i1f1,a6a5,f1f9,e9f9,a3a4,e6e5,c5c6,e7c7,g3g4,i5i4,g4g5,i7i5,d0e1,g8e8,a4a5,f9f8,c6c7,g9i7,c7c8,a9a8,i3i4,i5h5,g5f5,h9g7,a5b5,h5h2,f5f6,h2h6,c8c9,a8c8,e1d2,e5e4,f6g6,e8d8,d2e1,b9c7,g6h6,c7a6,c9d9,c8b8,b5b6,d8d9,b6a6,b8b6,a6a7,b6b5,e2g4,f8f7,h6g6,d9d0,e1d0,b5i5,c0e2,i5f5,g6g7,f7e7,a7a8,e4f4,e0e1,f5f8,e2c4,f8e8,a8a9,e8g8,a9b9,f4e4,g7f7,e7e8,f7e7,e8f8,c4e2,g8g9,e3e4,g9f9,e2c4,f9c9,c4e2,c9d9,e2c0,i7g9,c0a2,g9i7,b9a9,d9h9,e7f7,f8e8,f7e7,e8d8,e4e5,i7g9,e7d7,d8e8,a9b9,h9h8,d7e7,e8d8,e1e0,h8h2,g4i2,h2h9,a2c0,h9h4,e7e8,d8e8,i2g0,g9i7,f0e1,h4f4,b9c9,f4f8,g0i2,f8g8,c9b9,g8g1,e1f0,i7g9,c0a2,g1i1,a2c0,g9e7,i2g4,i1g1,g4e2,g1g0,e2g0,e7g5,i4i5,g5e7,e5d5,e8f8,g0i2,e7c9,i2g4,c9e7,i5h5,f8f7,c0a2,f7f8,b9a9,f8f7,a2c4,f7f8,g4i2,f8f9,h5h6,e7c9,c4e2,f9e9,h6g6,c9e7,e2c4,e9e8,c4a2,e8f8,i2g0,f8e8,e0e1,e8d8,e1e2,e7g5,g6f6,d8d7,e2d2,d7d8,g0i2,d8e8,f6e6,e8f8,a9b9,f8f7,f0e1,g5i7,i2g4,i7g5,b9c9,g5e7,a2c0,e7g9,d2d1,g9e7,e1f0,e7c9,d5e5,f7f8,e5f5,f8e8,d0e1,e8e9,e6e7,c9a7,f5g5,a7c9,c0a2,e9f9,a2c0,f9f8,e1d2,f8f9,e7e8,c9a7,g5h5,a7c5,d1d0,c5e7,g4i2,e7c9,i2g0,c9e7,d0d1,e7g9,d1e1,g9i7,h5i5,i7g9,i5h5,g9i7,e1d1,i7g9,e8d8,g9i7,d2e1,f9e9,d8e8,e9e8,g0e2,i7g9,d1d0,g9i7,e1f2,e8f8,d0d1,i7g5,d1d0,f8e8,h5g5,e8f8,g5g6,f8e8,e2g4,e8e9,g6g7,e9f9,g7f7,f9e9,d0d1,e9f9,f7f8,f9e9,c0a2 +a593cbf5;a0a1,b7b3,g3g4,h7h3,a1d1,h3h1,d1h1,g9i7,h1i1,b3b6,f0e1,c6c5,c3c4,e9e8,e1d2,b9c7,i1i2,c9a7,i2i0,i7g5,c0e2,h9f8,e0f0,a7c9,i0i6,c7a8,a3a4,g5e7,i6i4,g6g5,i4i6,b6b4,i6f6,e8d8,e3e4,i9i8,f6f9,d8e8,f9h9,b4b5,h9h8,e8e9,h8a8,a9a8,g0i2,d9e8,c4c5,i8g8,d0e1,b5b3,i3i4,g8g7,c5b5,b3b1,a4a5,e8f9,e1d0,a8e8,d0e1,g7g6,i4i5,f8h9,e1f2,b1b4,a5a6,b4b2,e2g0,h9g7,g4g5,g7i8,i2g4,g6g8,b5c5,b2b3,g5f5,g8f8,c5d5,e8d8,a6b6,b3h3,f5e5,h3a3,i5h5,i8h6,g4e2,a3a8,g0i2,a8a3,i2g4,a3a5,d2e1,a5b5,g4i2,b5e5,b6a6,f8g8,e4e5,g8h8,f0e0,h6g8,e2c0,d8a8,e0d0,a8d8,d5d6,h8h9,d6d7,h9h6,h5h6,c9a7,h6i6,d8e8,a6a7,g8f6,c0e2,e7g9,d0d1,e9d9,a7b7,e8f8,e5f5,f6h7,e2g0,f8f7,f5f6,h7g5,i6i7,f7h7,i7h7,g5e4,h7g7,f9e8,d7d8,d9e9,e1d0,e4c5,d8e8,e9e8,g7f7,c5d3,f2e1,e6e5,g0e2,d3b2,d1d2,b2c4,d2d1,e5e4,f6g6,c4d6,d1d2,e8e9,b7b8,d6b5,d2d1,e9e8,f7f8,e8e9,d1d2,b5d4,e2g0,e4f4,e1f0,d4e2,b8b9,f4f3,f0e1,f3f2,e1f2,e2d4,g6f6,d4f5,b9a9,f5h6,f8g8,e9e8,g8g9,h6f5,i2g4,f5h4,f6g6,h4i6,g9h9,i6g5,h9i9,g5i6,g6f6,i6h8,a9b9,h8f7,d0e1,f7d8,b9a9,d8e6,d2e2,e8e9,a9b9,e9f9,b9c9,f9f8,f6e6,f8e8,c9b9,e8d8,e6f6,d8d9,e1d0,d9d8,f2e1,d8d7,f6e6,d7d8,i9h9,d8d9,g4i2,d9e9,h9i9,e9f9,e6d6,f9f8,i2g4,f8f9,i9h9,f9f8,d6c6,f8f7,h9i9,f7f8,c6b6,f8f9,g0i2,f9f8,b6b7,f8f9,b7b8,f9f8,e1f2,f8f7,b9a9,f7f8,i9h9,f8f9,e2d2,f9e9,f2e1,e9f9,e1f0,f9f8,d0e1,f8f7,i2g0,f7f8,b8c8,f8f9,a9b9,f9e9,h9g9,e9e8,c8d8,e8e9,g0e2 +50939fc1;a0a2,c9e7,a2b0,a9a7,g0i2,b7d7,a3a4,g9i7,b0b8,h7h2,b8c6,i9i8,c6g6,h2h6,g6i6,a6a5,f0e1,h6f6,i6h4,f6f0,e0f0,e9e8,f0e0,d7d2,h4b4,b9d8,b4c4,e6e5,e3e4,a7b7,e1d2,b7b1,c4b4,i8h8,d0e1,b1b4,e4e5,e8f8,g3g4,h8h2,e1d0,b4e4,c0e2,h2f2,c3c4,i7g9,e5f5,a5a4,f5g5,e4g4,c4c5,h9i7,c5b5,f8f7,b5b6,d8e6,e2g4,f2g2,g5f5,e7c5,f5f6,f7e7,f6g6,g2g4,g6g7,g4i4,e0f0,d9e8,g7g8,a4b4,i2g0,b4b3,b6a6,b3b2,g0e2,i4a4,f0e0,b2c2,e2g0,e8d9,a6b6,i7h5,b6b7,c5a7,g8h8,a4g4,h8h9,c2d2,e0e1,d2d1,e1e2,g4g5,b7a7,g5g4,g0i2,g4d4,a7b7,d4b4,b7a7,e7d7,h9g9,b4e4,e2f2,e6f4,i2g4,d9e8,a7b7,e4a4,f2f1,a4b4,g9h9,b4a4,b7a7,a4a0,d0e1,f4g2,e1f2,g2i1,a7a8,h5f4,g4i2,d1d0,a8b8,a0a8,b8a8,f4g6,f2e1,g6e7,f1f0,e8d9,i3i4,d9e8,e1f2,e8f7,i2g0,f7e8,h9g9,e7d5,f2e1,d0c0,g9f9,c0b0,e1d2,i1g0,a8a9,d7e7,d2e1,d5f6,f9e9,f6g8,f0e0,b0c0,a9b9,e8f9,e0f0,g0f2,b9a9,g8h6,e1f2,h6g8,e9f9,g8e9,f9e9,e7d7,i4i5,c0d0,i5h5,d7e7,h5h6,e7f7,h6i6,d0c0,e9d9,f7e7,f2e1,c0d0,f0f1,d0e0,e1f0,e7e8,d9e9,e8e7,a9b9,e7e8,f0e1,e8e7,i6i7,e0f0,e1f2,e7e8,i7h7,e8e7,f1f0,e7f7,h7h8,f7f8,f0f1,f8e8,h8i8,e8d8,b9a9,d8d7,f2e1,d7e7,e9d9,e7d7,i8h8,d7e7,e1d2,e7e8,d2e1,e8e7,d9c9,e7d7,f1f0,d7e7,e1d0,e7e8,a9b9,e8d8,h8i8,d8e8,d0e1,e8e9,f0f1,e9e8,e1d0,e8e9,c9d9,e9d9,f1f0,d9e9,b9c9,e9e8,c9b9,e8e7,b9c9,e7d7,c9b9,d7d8,i8h8,d8d9,h8i8,d9d8,b9c9,d8e8,d0e1,e8e7,i8h8,e7e8,f0f1,e8e7,c9b9,e7d7,e1f2,d7e7,b9c9,e7e8,f1f0,e8f8,c9d9,f8f9,h8g8 +9286bafd;a0a1,b7b2,d0e1,b2d2,g0i2,d2b2,g3g4,h9i7,e3e4,h7e7,e1d2,d9e8,a1f1,e8d7,i3i4,b2b8,f1f2,b8b6,f2f3,g6g5,f3f8,b6b7,f8b8,f9e8,g4g5,b7b3,d2e1,a9a8,b8a6,b9c7,a6b6,e7g7,i2g0,i7g8,b6a8,c7a8,a3a4,g7i7,g0e2,g8h6,e2g4,e9d9,c0a2,c6c5,g4i2,h6i8,g5h5,b3b4,e0d0,c9e7,d0d1,b4b6,a2c4,b6b4,e4e5,e6e5,d1d0,e8f7,i2g0,a8c7,h5i5,f7e8,i5i6,e7c9,i6i7,b4b0,g0i2,i9h9,i7h7,b0f0,a4a5,f0f7,h7g7,g9e7,g7f7,h9h1,a5b5,e8f9,b5a5,h1h6,i2g0,h6g6,f7e7,e5e4,e1f2,i8g9,f2e1,c7a6,g0i2,g6g1,e1f0,g9h7,i2g0,a6c7,g0i2,c7b5,e7f7,f9e8,i2g0,c9a7,f7g7,h7g9,d0e0,g1g3,i4i5,g3g6,i5i6,g6b6,i6i7,a7c9,c4e2,e4e3,a5b5,b6b9,i7h7,d9d8,e2g4,e3e2,g7g8,c5c4,h7g7,e8f7,g7h7,b9a9,g0e2,a9a1,c3c4,a1a0,e0e1,a0e0,e1e0,f7e8,h7g7,d8d9,e0e1,e8f9,g7h7,c9a7,g4i2,g9i8,g8g9,i8g7,e2g4,g7h9,h7i7,h9g7,e1f1,f9e8,g9h9,g7h9,i7i8,h9i7,i8i9,a7c9,f1f2,e8f9,c4c5,f9e8,f2f1,c9a7,i2g0,i7g6,g4i2,g6f8,c5d5,f8h7,b5b6,d9d8,d5e5,e8f9,f1e1,h7i5,e5f5,i5h3,f5e5,h3i1,i9h9,f9e8,e5f5,e8d9,e1f1,i1g0,b6a6,d8e8,f5e5,g0f2,f0e1,e8f8,e1f2,f8f9,a6a7,d9e8,a7b7,f9e9,b7a7,e9f9,i2g4,e8f7,e5f5,f9f8,f2e1,f7e8,e1f0,e8f7,f0e1,f7e8,e1d2,e8f7,f1e1,f7e8,f5f6,e8f9,f6f7,f8f7,e1d1,f7f8,a7b7,f9e8,g4e2,e8f9,b7c7,d7e8,h9g9,e8f7,e2g0,f8e8,c7c8,e8e9,g0e2,f9e8,c8b8,e8d9,d1e1,f7e8,e1e0,e8d7,g9h9,d9e8,b8a8,e9f9,h9g9,f9e9,e0e1,e8f7,a8b8,e9d9,b8a8,d7e8,e1f1,d9e9,e2g4,e9d9,d2e1,e8f9,e1d2,d9e9,a8a9,f9e8,g4e2,e8d9,e2g4,f7e8,f1e1 +0d8f37a9;c0a2,h7f7,a0c1,b7b0,e0e1,f7h7,c3c4,h7a7,c1c2,a7b7,c2b4,b0f0,b4b9,f0f4,g0e2,b7f7,b9a7,e9e8,a7e7,e8f8,e7d9,f8e8,d9d4,f7h7,d4d3,h7h6,d3d4,h6h7,d4d7,i6i5,d7c7,e8d8,g3g4,a9a7,c7d7,d8e8,d7c9,e8f8,c9c8,f8f7,c8c9,h7h8,c9b9,f4f3,e2c0,i9i7,i3i4,f7f8,b9b2,f8e8,a3a4,a7g7,b2i2,g7b7,e1e0,i7i8,i2h2,f3f2,h2f3,h8h1,f3h2,b7g7,h2g2,h9i7,g2g3,a6a5,i4i5,g7g8,g3f3,a5a4,e3e4,f2f0,i5i6,e8d8,f3f7,g9e7,f7g5,i8i9,g5i4,d8e8,i4g3,a4b4,g3h3,b4a4,h3g5,e8e9,i6h6,i9i8,g5h5,h1a1,h5h3,f0f6,h3i1,i8i9,i1d1,f6f8,d1c3,a1e1,c3e2,e7c5,e2g1,f8f4,g1h3,g8c8,c0e2,c8c7,h3b3,i9i8,b3b6,f4f3,b6b7,a4a3,b7b3,i8i9,b3b2,i9g9,h6h7,c7d7,h7h8,e1g1,b2b5,e6e5,b5b1,d7d9,b1e1,g1g2,e1b1,g9h9,e4e5,f9e8,b1d1,d9d5,h8g8,i7h5,d1b0,f3f6,d0e1,h9f9,e1f0,d5d4,f0e1,d4d5,e1d2,h5g7,e0d0,d5d7,g8h8,f9h9,b0c0,f6f9,c0c5,d7a7,d2e1,h9i9,c5c6,g2g1,e5e6,a7c7,e1f0,g1b1,c6c5,c7c6,d0d1,c6c7,d1d2,c7d7,c5d5,b1b2,f0e1,g7e6,e2g0,a3a2,d5d7,e6d8,g0i2,i9i6,d7b6,b2b5,b6i6,b5f5,i6i8,f9h9,i2g0,e8d7,i8i6,d8f7,i6i9,f7g9,h8i8,f5a5,e1f0,e9f9,i9g9,f9f8,d2d1,a5f5,g9a9,h9c9,a9a7,c9b9,a7c6,f5h5,g4g5,b9e9,c6d4,h5h1,d4d7,f8e8,d7a7,h1h0,a7a9,g6g5,a9a2,e9i9,a2d2,e8e9,d2b1,h0h1,b1c1,h1h9,c1a1,h9h1,a1h1,e9e8,h1f1,g5g4,f1e3,e8f8,i8i9,g4g3,d1d2,f8f7,e3d5,g3h3,d5d3,h3i3,d3d6,f7f8,i9h9,i3i2,d6e4,i2h2,e4g5,f8e8,g5g3,e8f8,g3g4,h2i2,g4d4,f8f7,d4d5,i2h2,h9g9,f7f8,g9f9,h2g2,d5f5,f8e8,f5h5,g2h2,h5h4,h2g2,h4g2,e8f8,g2g7 +f884c88f;i3i4,c9a7,g0i2,h7g7,a3a4,g7e7,a0a3,g6g5,a3c4,b9d8,c4f4,e7d7,a4a5,d7f7,f4e4,b7e7,i2g4,e6e5,e4c5,f7f5,c5d3,e7e6,d3c1,a9c9,c1c6,f5f7,c6i6,e6b6,i6c6,i9i8,i4i5,b6b5,c6a7,b5b0,i5i6,f7i7,a7a6,i7e7,a6e6,d8e6,e3e4,b0b5,a5b5,e6g7,i6i7,c9c6,i7i8,e7e4,c3c4,c6c5,i8h8,g7e8,e0e1,e8g7,e1f1,c5c7,b5a5,c7e7,c4c5,e4e2,a5b5,e7c7,g4i2,g9e7,c0e2,c7a7,b5a5,g7i8,c5d5,a7b7,a5b5,g5g4,f0e1,b7d7,d5e5,e7g5,b5c5,d7a7,i2g0,a7d7,f1f0,d7d8,e5d5,g4h4,c5c6,h4g4,g3g4,d8c8,g4g5,c8c6,d5c5,h9f8,e2g4,f9e8,h8i8,c6c7,i8i9,c7h7,g0i2,h7d7,c5b5,d7d2,i9h9,f8h7,e1d2,e8f9,g5f5,e9e8,b5b6,h7i9,b6b7,i9h7,b7c7,h7i9,d2e1,e8f8,e1f2,d9e8,f0f1,i9h7,c7c8,h7f6,f5g5,e8f7,g5h5,f7e8,g4e2,f6g8,e2g4,e8d9,i2g0,f8f7,h5h6,f7e7,c8d8,e7f7,g4e2,f7e7,h9i9,d9e8,f1f0,e8d9,d8e8,g8i7,e2c0,f9e8,d0e1,i7h9,f0f1,e8f9,h6i6,h9f8,i9h9,d9e8,h9g9,f8h9,e1f0,h9i7,c0a2,i7g8,i6h6,g8f6,f0e1,f6g4,g0i2,g4i5,g9h9,e8d7,f1f0,e7e8,f0f1,e8d8,h6g6,i5h7,a2c0,d8d9,e1f0,d9e9,g6h6,d7e8,h9g9,h7f8,c0e2,f8h7,f1e1,e8f7,i2g4,e9e8,h6g6,h7f8,e1d1,f8h9,f2e1,h9g7,g6h6,e8e9,e2g0,e9e8,e1f2,e8e7,d1d2,g7h5,g4e2,h5g3,e2c0,e7e8,g9h9,g3f1,f2e1,f1g3,e1d0,g3f5,d2d1,f5e7,g0e2,e8e9,h6i6,e9e8,c0a2,e8e9,d1e1,e7c6,a2c4,c6b4,h9g9,b4a6,c4a2,f9e8,g9h9,e9d9,e2g4,a6c7,e1e2,c7d5,h9i9,d9d8,g4i2,d5f6,i6i7,d8d9,i7h7,f6d5,h7g7,e8d7,a2c4,d5e7,g7f7,d7e8,f7e7,d9e9,d0e1,e8d9,i2g0,e9f9,e1d0,d9e8,d0e1,e8d9,e1d0,d9e8,f0e1,e8f7,e1f2,f7e8,e7e8 +2be7c7e1;f0e1,b7a7,a0a6,h7f7,a6a4,a7b7,a4i4,b7b6,i4h4,f7a7,i3i4,e6e5,h4h1,h9g7,h1h2,d9e8,g0e2,g7h9,e0f0,e8d9,e1f2,g9i7,e2g0,b6b3,h2g4,i7g5,g4g6,a7g7,c0a2,i9i8,g6i7,a9a8,i7i8,b3b8,g0e2,b8g8,i8g8,h9f8,g8i8,a8b8,i8h6,g7c7,h6g4,b8b2,g4i3,e9e8,i3h3,c7i7,h3h8,e8e9,f0f1,i7a7,h8f8,b9c7,e2c0,b2b4,f8d9,c7d9,g3g4,b4c4,a2c4,e9e8,c0e2,a7a8,c4a2,e5e4,d0e1,e4f4,e1f0,a8a6,g4g5,e8f8,a2c4,a6a7,g5h5,a7i7,f1e1,i7i8,e1f1,i8g8,e2c0,g8g4,c0e2,c6c5,e2g4,f4e4,g4e2,d9c7,f0e1,e4f4,e3e4,c7e6,e1d0,f4g4,e2c0,c9a7,e4e5,g4g3,c4a2,i6i5,d0e1,e6c7,a3a4,g3f3,i4i5,c7d9,a2c4,f3g3,e1d0,f8e8,i5i6,a7c9,c4a2,d9e7,h5h6,e7f5,a4a5,e8d8,e5f5,d8d7,f1f0,d7d8,f5g5,c5c4,f0e0,c4c3,a5b5,g3g2,c0e2,d8d9,h6h7,g2g1,i6i7,c9e7,b5a5,f9e8,a5a6,e7c9,a2c0,g1g0,g5f5,d9d8,e2g0,d8d7,e0f0,c3c2,f5g5,e8d9,g5h5,c9a7,h5g5,a7c5,d0e1,c5e7,c0a2,d9e8,g0e2,e7g9,g5h5,c2d2,h7h8,d7e7,a6b6,e7d7,h5h6,e8f9,h6g6,g9e7,e2c4,f9e8,f0e0,e7g5,i7h7,d7d8,e1d2,e8d9,b6b7,d9e8,h8g8,e8d9,a2c0,d8d7,e0e1,g5i7,c0a2,d7d8,e1e0,i7g9,c4e2,g9i7,e0d0,d8d7,e2c0,d7d8,a2c4,i7g5,g8g9,g5e7,d0d1,d9e8,h7i7,e7g5,b7b8,e8d7,d1d0,d8e8,i7i8,e8e7,g6f6,g5i7,c4e2,i7g5,e2g4,d7e8,d2e1,e8f7,c0a2,f7e8,a2c0,e8d9,b8b9,d9e8,b9a9,g5i7,d0d1,e8d9,i8i9,i7g5,c0a2,e7e8,g4i2,g5i7,e1d2,e8e7,g9f9,e7d7,i2g0,d7d8,f6f7,i7g9,d1d0,g9i7,f9e9,d9e8,d0e0,d8d7,g0i2,e8d9,a2c0,i7g9,d2e1,g9i7,e1d2,i7g5,e0e1,g5e7,e1e2,d9e8,c0a2,d7d8,f7e7,e8d7,a9b9,d7e8,i9h9,e8f7,a2c0,f7e8,i2g0,e8d7,h9i9,d7e8,e7e8,d8d7,b9c9 +b47877fe;e3e4,i9i7,d0e1,i7i9,e1d0,h7h0,g3g4,g9i7,e0e1,h0h5,c0e2,b7f7,a0a6,h5h0,a6a7,f7f6,e4e5,h0h5,a7c6,h5g5,i3i4,a9a8,c6a5,b9a7,e1e0,a7b5,e2c0,i9i8,d0e1,a8b8,e1d2,b8d8,e0d0,i6i5,g0e2,i8e8,e5e6,f6f3,i4i5,b5a7,a5b3,a7b9,b3b1,f3g3,b1b9,e8g8,i5i6,g8i8,e2c4,d8e8,c0e2,e8a8,a3a4,e9e8,i6h6,h9f8,b9b2,g5i5,h6h7,a8c8,c4a2,i5c5,b2c4,c5b5,e2c0,i8h8,d2e1,b5c5,h7i7,g3i3,e6d6,e8e7,d0d1,h8i8,d6e6,e7f7,e1d0,i8i7,d1d2,i7i5,d0e1,c5c6,g4g5,i3d3,g5g6,d3d5,c4c6,i5i6,c6e5,f8d7,c3c4,c8c7,d2e2,i6i3,e6d6,f7f8,e1f2,i3i0,e5f7,f8f7,e2e1,d7e9,e1d1,i0i2,g6h6,c7c5,d6e6,d5g5,e6f6,f7f8,h6g6,g5e5,f0e1,c9a7,e1d0,i2f2,d1e1,e5e7,g6h6,f2h2,f6g6,h2h0,h6h7,h0e0,e1d1,e7e2,d0e1,c5d5,e1d2,d5d6,g6f6,a7c5,a4a5,f9e8,f6g6,d6d2,d1d2,e0d0,d2e2,c5e7,c4c5,d0d2,e2d2,e9g8,c5c6,g8i7,a5b5,e8d7,g6g7,i7h5,h7h8,h5f6,h8h9,f8f9,h9g9,f9e9,c6b6,f6g4,b5a5,e7c9,g7f7,g4e3,b6a6,e3c2,a6b6,c2d0,f7g7,d7e8,a5a6,d0f1,d2e2,c9a7,g7g8,f1h0,e2f2,e8d7,g9h9,d7e8,g8g9,h0f1,a2c4,e8d7,a6a7,f1h0,h9i9,e9e8,b6a6,h0i2,i9h9,e8e9,a7b7,e9e8,b7b8,e8e7,h9i9,i2h4,c0e2,h4g2,g9h9,e7e8,b8c8,g2i1,c8b8,e8e7,c4a2,d7e8,e2g4,e7d7,a6a7,e8f9,b8c8,d7e7,a2c4,i1g2,g4e2,g2i3,f2f1,i3h1,a7b7,h1f0,f1f0,e7d7,c8c9,d7e7,b7b8,e7d7,b8a8,f9e8,e2g4,d7e7,g4e2,e7d7,f0e0,e8f7,e0e1,f7e8,c4a2,e8f9,e1e0,d7e7,e0d0,f9e8,a8b8,e8f9,d0d1,d9e8,b8b9,e8f7,d1e1,f7e8,e1e0,e8d7,e0e1,e7f7,h9g9,d7e8,e2c0,f7f8,c0e2,e8f7,g9h9,f8e8,h9g9,e8e7,c9d9,e7e8,b9c9,e8e7,a2c4,f9e8,c9b9,e8d9,b9c9,e7e8,c9b9,e8e9,c4a2,f7e8,e2c0 +61c382b5;g3g4,b9a7,a3a4,h7i7,a4a5,d9e8,g0e2,i7e7,e0e1,e8d7,c0a2,b7b5,e1f1,b5f5,g4g5,a6a5,d0e1,g6g5,f1f2,a9a8,a0a1,f5f4,e2g4,e7i7,f2f1,h9g7,a1c1,a8c8,e1d2,g7i8,g4e2,c8b8,c1a1,f4b4,a1a5,d7e8,a5a3,c6c5,a3c4,e9d9,e2g4,b4b0,g4e2,a7c6,c4h4,i7i3,h4c4,c6e5,c4a5,b8b6,f0e1,e8f7,e1d0,c9a7,f1e1,b6b2,a2c4,b2b4,a5b5,b0b1,b5b6,e5c6,b6a6,c6a5,a6c6,i3i0,c6c5,b1a1,e2g0,i8g7,c5b7,g7h5,b7f7,f9e8,f7f1,b4b3,f1h0,i0i4,e3e4,i6i5,h0i0,e8f7,i0i5,a1d1,i5i4,f7e8,i4i3,b3b1,e1e2,g9i7,d0e1,a5b3,e1f0,i9h9,i3i2,a7c9,d2e1,d1d2,i2i6,d2d4,i6e6,h5g7,e6a6,b3a1,a6e6,g5g4,e6d4,d9e9,d4c2,h9g9,c2b2,g7h5,b2d2,b1b7,d2d5,e9f9,d5h5,g4f4,h5h3,g9h9,h3h5,b7b8,h5g7,f9e9,g7a7,b8d8,a7c6,c9a7,c6b8,e8f7,g0i2,h9h1,b8a8,a7c5,a8a3,d8d2,e1d2,i7g9,e2f2,a1b3,a3b3,e9e8,b3b6,h1h7,f2f1,h7h6,b6b1,h6h3,b1b0,h3h7,e4e5,h7i7,b0a2,e8d8,a2a8,d8d7,a8f8,d7e7,f8f7,e7f7,c4a2,i7i4,e5d5,g9i7,d5e5,i7g9,e5d5,i4i2,c3c4,i2g2,c4c5,g9e7,f0e1,g2g9,e1d0,g9g7,c5c6,e7c5,c6b6,g7g6,d5d6,g6g2,d2e1,f7f8,b6c6,g2c2,c6b6,c2f2,f1f2,f8e8,b6b7,e8e7,a2c0,f4f3,f2f1,e7f7,c0a2,f3f2,f1f0,c5a7,e1d2,f7f8,b7a7,f8f7,f0e0,f7f8,a2c4,f2e2,c4a2,e2d2,d6c6,f8f7,c6c7,f7f8,e0e1,d2e2,e1e0,e2f2,a2c0,f2f1,c0e2,f1e1,e0e1,f8f7,e2c0,f7f8,e1d1,f8f7,c7d7,f7f8,d0e1,f8e8,d7c7,e8f8,a7b7,f8e8,d1d2,e8e9,e1f0,e9f9,d2d1,f9e9,d1d0,e9e8,c7d7,e8f8,c0e2,f8f9,d7c7,f9f8,d0d1,f8e8,d1e1,e8f8,c7d7,f8e8,b7b8,e8e9,e1e0,e9e8,f0e1,e8f8,e1d0,f8e8,d0e1,e8e9,b8b9,e9d9,e2g4,d9e9,g4i2,e9e8,e0f0,e8e9,i2g0,e9e8,e1f2,e8e9,b9a9,e9d9,f0e0 +17bb8d27;a0c1,b7b5,d0e1,g9i7,c1c2,h7e7,g3g4,b5i5,i3i4,e7e3,e1f2,e3e4,g4g5,e4d4,c2d2,i5h5,d2a2,c6c5,a2a0,c9e7,e0d0,e7g5,f0e1,h5h1,i4i5,d4d6,c0a2,c5c4,g0e2,b9d8,e1f0,e9e8,a0c1,d6c6,f0e1,h1g1,c1g1,i7g9,e1d2,g9e7,e2g4,e8e9,g1e2,a6a5,e2e4,h9i7,g4e2,a9a8,e4d4,a8a9,d2e1,a9a8,d4g4,i7h9,g4i4,d8b9,i4c4,h9f8,c4b6,f8d7,e2c0,d7c9,c0e2,a8g8,a2c4,i9i7,b6e6,f9e8,e6i6,e9f9,e1d2,i7f7,i6i8,c6b6,i8g8,b6e6,f2e1,f7f4,g8g9,f9f8,g9e9,f4f0,d0d1,f0i0,e1f0,i0h0,e9f9,e8f9,e2g0,f8e8,d1e1,h0i0,e1e2,e6b6,g0i2,i0i1,e2f2,g5i7,i2g0,b6b0,c4e2,c9b7,e2c0,b0b4,f0e1,i7g9,i5i6,b4b1,i6h6,b1b2,c0e2,b2b4,e1f0,b7d8,e2g4,b4a4,f2e2,i1e1,e2f2,e7g5,d2e1,a4b4,c3c4,b4b6,f2f1,e8e7,g4i2,b6b4,h6g6,e7d7,e1f2,b9c7,i2g4,b4b8,g6h6,d8f7,f0e1,b8d8,h6i6,d8a8,i6h6,a8e8,g4i2,e8d8,c4c5,d8a8,a3a4,a8e8,e1d0,e8h8,a4a5,c7b9,c5b5,h8g8,a5a6,g9e7,f2e1,e7c5,b5a5,b9c7,i2g4,d7d8,e1d2,d9e8,d0e1,c5e7,a6b6,e7g9,b6b7,f7e9,h6h7,g8i8,b7c7,g5i7,c7b7,i7g5,a5a6,i8i3,b7a7,d8d9,h7i7,e8f7,a7b7,i3i5,f1f2,g5e7,b7c7,i5i1,f2f1,i1i2,g4i2,e7c9,c7b7,c9a7,e1f0,g9e7,i2g4,f7e8,b7b8,e7c9,f1f2,e8f7,i7i8,c9e7,a6a7,e7g5,b8b9,f7e8,g0i2,g5i7,f0e1,e8f7,a7b7,i7g5,e1f0,g5e7,i8h8,e7c9,f2e2,d9d8,b7c7,e9c8,d2e1,f9e8,e1d2,c9a7,c7b7,d8d7,i2g0,c8e7,g0i2,e8f9,f0e1,d7d8,b7a7,f7e8,e1f0,e7c6,h8i8,c6e5,i8i9,e5d3,a7a8,e8f7,i9h9,d3c5,a8b8,d8d9,h9i9,c5b7,f0e1,b7a5,b9a9,a5c6,b8a8,c6a7,e1d0,a7b5,i9h9,b5a3,a8b8,f7e8,e2e1,a3c4,h9i9,c4e3,i2g0,e8d7,e1e0,e3f1,a9b9,f9e8,e0f0,e8f9,d2e1,d9e9,f0f1,e9e8,b8a8,e8d8,f1f2,d8d9,b9c9,d9e9,g4i2,d7e8,f2e2 +5aee51b1;a3a4,h7f7,g0e2,g9e7,a4a5,b7b6,a0a4,b6b7,a4c5,f7g7,c5g5,h9f8,g3g4,f8h9,a5a6,i6i5,a6a7,g7g5,e3e4,c6c5,e4e5,i9i6,i3i4,b7b8,a7b7,b8f8,i4i5,a9a0,e5e6,g5h5,e6e7,f8f6,b7c7,f6f5,g4g5,i6i5,g5f5,h5h0,f0e1,h0h6,e2g4,i5i3,f5e5,h9i7,e0f0,i3d3,c3c4,d3d2,e1d2,a0b0,c7c8,b0b8,d0e1,b8b0,e1d0,h6h8,c0a2,h8d8,e7e8,d9e8,e5e6,b9a7,f0f1,d8d3,d0e1,d3d8,e1f2,e8d9,e6d6,a7c6,c8c9,d8b8,d6e6,b0b1,f2e1,b1d1,e6f6,b8b4,g4i2,b4b2,a2c0,b2b0,c9b9,b0a0,b9a9,a0a4,a9b9,a4a5,i2g4,d1e1,f1f2,i7h9,g4e2,c6a7,f6g6,a5a0,g6f6,a0a3,e2g4,e1a1,f6f7,a7b5,b9c9,a3g3,f7g7,g3g2,g7f7,a1h1,g4i2,h1h0,c9b9,h0h6,i2g4,g2h2,d2e1,h6a6,c0e2,h9f8,e1d0,a6a3,c4c5,a3a1,b9c9,a1a5,c5b5,a5a3,f7f8,h2h6,b5c5,h6g6,f8f9,e9e8,d0e1,a3a0,e2c4,a0g0,c9d9,g0f0,e1f0,g6b6,f2f1,b6i6,c5b5,e8d8,f0e1,i6e6,e1d2,e6e1,g4e2,d8d7,f1f2,e1i1,f9g9,i1i3,e2g4,i3a3,g9f9,a3b3,c4e2,b3b1,b5a5,b1e1,f9e9,d7d8,e2c0,e1d1,a5a6,d1i1,c0e2,d8e8,e2c4,e8d8,c4e2,i1i4,e2g0,i4i6,g0i2,i6c6,a6b6,d8e8,e9f9,c6c8,b6a6,c8c6,i2g0,c6h6,d9e9,h6d6,g4i2,e8e7,f9g9,d6h6,d2e1,h6b6,e1d2,b6e6,d2e1,e6e2,e9d9,e7d7,g0e2,d7d8,i2g4,d8e8,e1d0,e8e7,e2c0,e7d7,c0a2,d7d8,g4i2,d8d7,i2g4,d7e7,d9c9,e7d7,a6b6,d7e7,g9f9,e7d7,b6b7,d7d8,a2c4,d8e8,b7a7,e8e7,g4i2,e7e8,f9e9,e8e7,i2g0,e7d7,e9f9,d7e7,g0i2,e7d7,f9e9,d7e7,d0e1,e7e8,a7a8,e8d8,c9b9,d8e8,e9d9,e8d8,d9c9,d8e8,a8b8,e8e7,e1d2,e7d7,c4a2,d7d8,a2c4,d8e8,f2f1,e8e7,c4e2,e7d7,e2c4,d7d8,c9d9,d8d9,b8c8,d9e9,f1f0,e9e8,i2g4,e8e9,f0f1,e9e8,b9a9,e8e7,c8d8,e7d7,d8c8,d7e7,c4e2,e7d7,f1f2,d7e7,e2c4,e7d7,c4a2,d7e7,g4i2,e7d7,f2e2 +e0a2c0d8;e0e1,g9e7,g0e2,b7b1,e2c4,h9g7,e1e2,e7c5,e2e1,h7h8,e3e4,c5e7,i3i4,e7g5,e1e0,i9g9,f0e1,c9e7,c4e2,h8h5,e1f0,c6c5,e2c4,h5h8,c0e2,h8a8,f0e1,b9d8,a0a1,a9c9,e1f0,b1b0,d0e1,b0f0,e2g4,f0f8,a1a6,a8a9,e4e5,g7e8,e5f5,g9g8,f5e5,a9a7,a6c7,c9b9,c7b7,f8f7,b7c9,b9c9,e5e6,c9a9,c4e2,a9a8,i4i5,g8g7,e6d6,a7d7,d6e6,d7d0,a3a4,a8a9,e0d0,d8e6,e1f2,a9a8,f2e1,e6d4,d0e0,d4e2,e1d0,a8a6,g4i2,f7f2,e0e1,f2f5,g3g4,e2d4,i5i6,f5f3,e1f1,a6a5,g4g5,d4f5,c3c4,f5d6,i6i7,g7f7,d0e1,f7f8,g5f5,a5b5,e1f0,g6g5,i7h7,f8f7,c4c5,b5a5,c5d5,a5a7,f1f2,e8c7,f2f1,f3g3,h7i7,c7a6,d5d6,g3b3,f0e1,a7a9,f1f0,b3h3,i7h7,d9e8,h7g7,a9a7,d6c6,h3h0,i2g4,h0h2,g7g8,h2h9,g8h8,a6b4,e1d2,b4d5,c6b6,a7a4,h8h9,f7f6,g4i2,a4a7,f0f1,g5g4,h9i9,e8d7,d2e1,f6f8,e1d2,a7a2,i2g4,a2a0,f1e1,d7e8,b6c6,a0a4,f5f6,a4a1,e1e0,e8f7,g4e2,d5f4,e0f0,f4e2,f0e0,a1i1,c6b6,i1g1,b6b7,g1g8,d2e1,g8g9,e0f0,f8b8,b7b8,e7c5,f6e6,g9h9,f0f1,h9h1,f1f2,h1h7,e6f6,h7h6,b8a8,h6g6,a8a9,g6g9,f6e6,g9g4,e1d0,g4g7,a9b9,e9e8,d0e1,e2d4,f2f1,g7g0,e1f2,d4f3,e6d6,g0a0,i9h9,e8f8,d6d7,a0a2,f2e1,a2a3,e1f0,c5a7,h9i9,a3a2,d7e7,f3e5,f0e1,e5g6,e7f7,f8e8,f7f8,g6f8,b9c9,a2c2,e1f2,c2d2,f2e1,d2h2,e1f2,h2h8,f2e1,h8h0,e1d2,h0d0,i9h9,d0b0,h9g9,b0b8,c9b9,a7c5,b9c9,f8h9,g9f9,e8d8,f1f0,b8b3,f9e9,b3a3,c9b9,a3a5,f0f1,h9i7,b9a9,a5a2,f1f2,c5a7,f2f1,i7h9,d2e1,a2c2,e1d2,c2c6,f1e1,d8d7,a9b9,c6g6,b9a9,g6f6,e1d1,f6f4,d1d0,h9g7,d0e0,f4e4,e0f0,g7h9,f0f1,e4b4,d2e1,b4b1,e9d9,b1b9,a9b9,a7c9,e1d0,c9a7,d9c9,d7e7,c9d9,h9g7,d0e1,g7h9,f1f0,h9f8,e1d2,e7e8,d9c9,e8e9,d2e1,e9f9,c9d9,a7c9,b9c9 +5f3ca528;g3g4,h9g7,a0c1,g9i7,c1a1,e9e8,d0e1,g7h9,a1a6,b9a7,a6c6,c9e7,c6c5,a7b5,g4g5,a9c9,c5b7,b5c7,c3c4,c9b9,b7b9,h9g7,b9b7,h7h6,e1d0,i9h9,g5g6,e8e9,b7b3,h6h3,a3a4,c7b5,b3d4,i7g9,f0e1,i6i5,c4c5,h3h0,g0e2,g7i6,g6g7,g9i7,i3i4,i7g5,d4h4,h0h1,h4g6,h1f1,g6e7,g5e7,g7g8,f1f2,e2g4,f9e8,i4i5,e7g5,c5d5,f2c2,d5d6,c2b2,e1d2,i6h8,i5i6,b5a7,g8f8,b2b5,i6h6,b5b6,c0e2,b6b3,d2e1,b3b5,d6c6,b5b7,f8g8,e8f9,e0f0,b7d7,g8g9,e9e8,g4i2,d7d3,e1f2,d3d5,g9h9,d5f5,f0e0,h8f7,e2g4,f5f4,h9g9,a7b5,g9f9,b5a7,h6i6,e8f8,g4e2,e6e5,d0e1,a7b9,a4a5,b9a7,e1d2,f4e4,f9g9,f8e8,i2g4,e4b4,g4i2,f7d8,c6d6,b4f4,i6h6,f4f6,d6e6,e5e4,e2g0,a7c6,a5a6,f6f8,g9h9,g5e7,e6e7,e8e9,e7f7,d8f7,h9g9,f7g5,g9f9,e9e8,a6a7,f8f3,f9e9,c6b8,h6h7,g5e6,e3e4,f3h3,e0e1,e6d4,g0e2,h3b3,e1d1,b3f3,h7h8,f3i3,a7b7,d4b5,h8i8,i3f3,b7a7,b8d7,i2g4,f3i3,i8i9,i3i8,e9d9,d7f6,e2c4,b5c3,d1d0,f6g8,f2e1,i8i0,i9h9,g8i7,g4e2,e8f8,d9e9,c3b1,d0e0,i7h5,e4e5,i0i8,h9i9,h5i7,e5d5,f8f7,d5e5,i8e8,a7b7,f7f8,b7c7,b1d2,e0d0,d2b1,d0e0,i7g6,e1d2,e8a8,e9d9,a8a3,i9h9,b1c3,d9e9,a3a1,e0d0,a1b1,c7d7,b1b0,d7c7,b0c0,d2e1,g6h8,e1d2,h8f9,e2g0,c0b0,d0e0,c3b1,g0i2,b0c0,d2e1,f8f7,e9f9,b1a3,c7b7,c0c1,e0d0,c1a1,f9g9,f7e7,c4e2,a1b1,b7a7,b1b3,e1f0,b3f3,a7a8,f3f1,e5d5,f1h1,e2c0,h1c1,f0e1,c1d1,d0e0,a3b1,e0f0,e7d7,g9f9,d1d2,f9g9,d2b2,e1d0,d7d8,d5e5,b2b6,e5f5,d8e8,f0f1,b6f6,f5f6,b1d0,f1f2,e8e7,f6g6,d0e2,g9f9,e7d7,f9g9,e2d0,a8b8,d7d8,b8a8,d8e8,i2g4,d0c2,h9i9,c2a1,g9f9,a1c2,g6g7,e8d8,g7h7,d8d7,h7g7,d7d8,g4e2,c2b0,f9g9,b0c2,g7f7,c2e1,a8a9,e1g2,f7e7,g2f4,e2g0,f4e2,g9f9,e2d0,c0a2,d0f1,f2f1,d8d9,e7d7 +4d1ef4a0;e0e1,h7h0,a3a4,h0h4,a0a6,f9e8,g0i2,b7h7,c0e2,h7e7,a6c6,h4b4,e2c4,e7h7,c6b4,e6e5,g3g4,e8f7,b4b2,h7i7,b2a0,a9a7,c4e2,i6i5,a0a1,i7g7,c3c4,e9e8,e2g0,i9i7,a1a3,a7a6,e1f1,i7i6,a3c3,g7i7,f0e1,g6g5,c3b3,i7h7,b3b5,a6e6,e1f2,h7g7,b5a5,i5i4,a5g5,i4i3,g5f5,e6e7,f5f3,e7e6,f2e1,g9i7,f3g5,g7g6,g5g6,e8d8,g6i7,e6e9,i7h9,i6i5,h9h2,i5i9,h2h6,d8e8,e1f0,i9i7,h6i4,e9i9,i4i3,e5e4,i3i5,c9e7,i5i7,e7g5,i7i5,e8d8,f0e1,d8d7,i5h5,i9i7,g4g5,b9a7,h5i5,i7g7,i5i6,g7g9,g5h5,g9h9,i6g7,d7d8,g7h9,a7b9,h9h7,f7e8,e3e4,e8f7,a4a5,d9e8,h7h9,e8d9,h5g5,d9e8,h9i9,e8f9,i9i8,d8d9,e1d2,f9e8,e4e5,b9d8,i8i4,e8d7,f1f0,d8b9,i4i6,d9d8,a5b5,f7e8,b5c5,b9c7,i6g7,c7b5,g7g6,b5c7,f0e0,c7a6,g6e7,a6c7,e7g6,c7a8,g6i5,e8d9,e5d5,a8c7,g5h5,d9e8,d2e1,d8d9,d5d6,c7a8,i5h3,d9d8,h5g5,e8f7,h3h2,a8c9,e1d2,f7e8,d2e1,e8d9,g5f5,d7e8,f5e5,e8f7,h2f1,c9d7,f1g1,f7e8,e1d2,e8f9,g1g2,d8e8,g2h4,d7e9,h4h6,e8f8,e5d5,e9d7,h6i6,f8f7,d2e1,f7f8,i6i9,d7c9,d6e6,c9d7,i9i7,f9e8,i7i8,f8f9,c5c6,f9e9,c6c7,d7e5,i8i7,e5f3,e1f0,f3h2,d5e5,h2f1,c4c5,e8f9,i7i4,f1e3,i4g3,e3g4,c7c8,g4e3,g3i4,f9e8,i2g4,e3g4,i4i1,g4i3,g0e2,i3g2,c8c9,e8d7,e5d5,g2i3,c5b5,d9e8,i1a1,e8f9,a1a6,i3h5,b5b6,h5f6,a6a8,f6h7,a8c7,h7f8,c7c6,d7e8,d5e5,e8f7,c6b4,f8h9,b4i4,h9i7,i4i5,f9e8,b6a6,e9f9,e5f5,e8d7,a6a7,i7g8,i5i3,f9f8,c9b9,g8i7,i3i5,i7g6,i5i3,g6e7,i3i5,d7e8,i5i8,f8f9,i8e8,e7c6,e8d6,c6a7,e2g0,a7b5,d6d3,b5d4,b9c9,d4b3,e6d6,f7e8,d3b3,f9f8,c9d9,e8f7,b3b1,f8f9,b1a1,f7e8,d0e1,e8d9,a1a0,f9f8,f5f6,d9e8,a0a1,e8f9,a1a0,f8e8,a0a4,e8e9,f6f7,e9e8,a4a8,e8e9,a8d8,f9e8,d8b7,e8d7,d6d7,e9e8,g0i2,e8e9,b7d8 +cc536f1c;a0c1,a9a7,c1c2,b9c7,c2f2,h7h5,f2d2,h9g7,g0e2,g7i8,c0a2,b7b0,d0e1,b0b4,d2c4,b4b5,c4c6,b5f5,a2c0,g6g5,e2g0,f5c5,c0a2,d9e8,g3g4,a7b7,e1d0,i9h9,e0e1,h5h0,g0e2,h0h4,e2c0,h4h2,c6i6,h2g2,e1f1,a6a5,i6i4,c7d9,a3a4,i8g7,i4i9,c5b5,f1e1,g2h2,e3e4,h2f2,c3c4,b5b4,i9i6,f2d2,i6i9,h9h6,g4g5,d9e7,c0e2,b7b9,e2g0,h6h2,a2c0,h2h7,i9i5,h7h3,i5h5,d2d6,e1e0,h3h1,h5h9,e8d7,c0e2,g7i6,h9g9,e9e8,g5f5,i6g5,g9g8,e7g8,e4e5,b9b7,e2c0,d6d2,e5e6,b7a7,f0e1,h1e1,d0e1,d2f2,e1f2,e8d8,a4a5,b4b5,e6d6,a7a5,e0d0,g8i7,g0i2,b5d5,i2g0,d5e5,f2e1,a5c5,i3i4,e5e4,d6c6,i7g8,c6d6,e4e7,c4c5,e7e9,d6c6,e9e7,e1d2,g5f7,c6c7,e7e2,f5e5,f7g9,d0d1,e2h2,d2e1,h2b2,c5d5,b2b7,d1d2,g8i7,d2e2,g9h7,d5c5,b7b5,c0a2,b5b9,c5c6,h7g9,g0i2,c9a7,a2c0,g9f7,e1d2,f7h6,c7b7,a7c9,b7b8,h6f5,e5e6,i7h5,c6c7,f5g7,i4i5,d8d9,e2f2,g7i8,c7c8,h5i3,c0e2,c9e7,c8c9,d9d8,c9d9,i3g2,i2g4,g2f0,b8a8,b9a9,d9c9,f0e2,c9d9,e2f4,a8b8,d7e8,e6f6,i8g7,d9e9,f4d5,f2f1,a9a3,b8b9,d8d7,f1f2,a3a7,e9f9,g7h5,f6g6,d5b6,g4e2,h5i7,e2c0,d7d8,d2e1,e8f7,e1d0,a7a9,b9c9,e7g9,g6g7,a9a0,i5i6,b6c4,c0a2,i7h9,f2e2,h9i7,a2c4,a0a4,i6i7,a4a6,c4a2,d8d7,c9d9,a6c6,e2e1,c6e6,d9e9,e6e5,a2c0,e5h5,c0e2,h5g5,e9d9,g5a5,f9e9,a5a4,i7h7,d7e7,e9f9,a4h4,e1f1,h4h0,e2c0,h0h1,c0e2,h1h4,d9e9,h4h3,f1e1,h3h2,h7h8,f7e8,f9g9,e8f9,g9h9,h2i2,g7g8,e7e8,e9d9,e8e7,g8f8,i2i9,d9e9,i9i3,h9i9,i3i4,e1e0,i4c4,e9f9,c4c1,f9g9,c1c7,d0e1,c7c5,g9h9,c5b5,f8f9,b5a5,h8i8,a5d5,e2g4,d5e5,e1d0,e5e1,d0e1,e7d7,g4e2,d7d8,h9g9,d8e8,e2g4,e8e7,e0f0,e7e8,f9e9,e8e9,g4e2,e9d9,i8h8,d9d8,e2c4,d8e8,f0f1,e8e9,e1d0,e9e8,h8g8,e8e7,g9h9,e7d7,d0e1,d7d8,e1d0,d8d7,f1e1,d7d8,g8f8,d8d7,f8e8 +9313d3a5;f0e1,b7b2,g0i2,h7h2,a0a2,c9e7,e1d2,h9g7,i3i4,a9a7,a2b2,c6c5,b2b5,i6i5,b5a7,g9i7,d0e1,e6e5,a7g7,h2e2,c0e2,b9d8,e0d0,d8b7,e1f0,a6a5,g7f9,e9e8,i2g0,b7a9,d0e0,i7g5,f9f4,i9h9,e3e4,h9h4,i4i5,h4h7,f4f9,h7h3,g3g4,h3h9,f9h8,e8e9,h8e8,d9e8,e4e5,h9h7,e5f5,h7h0,e0d0,h0h3,d2e1,e9f9,g0i2,a9b7,e1f2,e8d7,f5e5,h3c3,i5h5,g5i7,h5h6,c3c0,d0d1,c0c1,d1d0,c1a1,e5f5,a1g1,e2c0,g1e1,h6h7,e1i1,f5e5,g6g5,h7i7,a5a4,i2g0,a4b4,g4g5,i1i5,g5f5,i5i1,c0e2,i1i5,f5g5,b7d8,g5h5,b4b3,e5d5,d8f7,h5i5,e7c9,i5h5,f7d6,f2e1,f9e9,d0d1,b3c3,a3a4,c9e7,g0i2,d7e8,e1f2,e9f9,d1d2,f9e9,a4a5,e8d9,d5e5,d6b7,h5i5,b7c9,i7h7,e9e8,e2c0,e8f8,h7i7,c9a8,i2g0,c3c2,d2e2,e7g5,i5i6,a8c7,g0i2,c7e6,e5d5,d9e8,f2e1,c2d2,e1d2,c5c4,e2e1,c4d4,i7h7,d4d3,c0a2,g5i7,a2c4,d3c3,i2g0,e8f9,g0e2,f9e8,d5c5,f8f7,a5b5,e6c7,c5c6,c3d3,e1e0,e8d7,c6c7,d7e8,h7h8,f7f8,e2c0,f8f7,c4a2,e8d7,b5a5,d3d2,f0e1,i7g5,c7d7,f7f8,h8h9,d2e2,a5b5,f8e8,c0e2,g5e7,d7c7,e7g9,c7c8,e8e7,e1d0,e7e8,c8d8,e8d8,e2g0,g9i7,i6i7,d8d9,a2c4,d9d8,g0i2,d8d9,b5a5,d9d8,c4e2,d8e8,i2g0,e8d8,i7h7,d8d9,h9g9,d9e9,e0f0,e9d9,e2g4,d9d8,h7g7,d8e8,g9f9,e8e7,d0e1,e7d7,f9e9,d7e7,g4e2,e7d7,f0f1,d7e7,e2g4,e7e8,g0i2,e8e9,g7h7,e9d9,h7h8,d9e9,a5b5,e9d9,e1d2,d9e9,i2g0,e9d9,b5c5,d9d8,g4i2,d8d9,h8h9,d9e9,c5d5,e9d9,f1f2,d9d8,d5e5,d8d9,h9g9,d9d8,f2e2,d8e8,e2e1,e8f8,e5d5,f8f7,g9h9,f7f8,d5d6,f8f9,e1e0,f9f8,i2g4,f8f9,e0e1,f9f8,d6c6,f8f9,h9g9,f9f8,e1e0,f8f7,d2e1,f7e7,c6b6,e7d7,e1d0,d7d8,b6b7,d8d9,b7c7,d9d8,g9h9,d8d9,g0i2,d9d8,h9g9,d8d9,c7b7,d9d8,g9h9,d8d7,h9g9,d7d8,b7a7,d8d9,e0f0,d9d8,a7b7,d8d7,i2g0,d7d8,f0f1,d8e8,g0i2,e8e9,g9f9,e9e8,b7c7,e8d8,i2g0,d8e8,g4i2,e8d8,f1f0,d8e8,c7d7 +c8224343;g3g4,h7g7,c0e2,f9e8,a0b2,e8d7,b2b3,i6i5,b3c1,b7b8,c1d3,e6e5,d3c5,i9i6,c5e4,c9e7,f0e1,g7h7,e4a4,b8b7,e1f2,i6h6,a4a5,d9e8,a5c6,h6h1,e0f0,h7h2,g4g5,b9a7,c6c5,b7b2,d0e1,b2b6,c5b5,a9b9,b5c5,h2h5,e1d2,b9b7,c5b5,e8d9,g5f5,h5g5,b5b1,h1h0,b1d1,h0g0,e2g0,d7e8,d1b0,g9i7,b0c0,e8f9,f0f1,b6f6,c0a1,h9g7,a1a2,f6e6,f5f6,i5i4,d2e1,b7b4,f6g6,b4c4,e1f0,a6a5,f2e1,g5g4,a2i2,g7i8,i2i4,c4c7,g0i2,f9e8,e1f2,e7c5,i4g5,c7c8,g5h5,e8f7,f1e1,e5e4,e1f1,c8e8,h5h4,e8b8,h4h5,b8b2,a3a4,i8g7,h5d5,b2b4,f1e1,i7g5,d5c7,g5i7,c7b7,b4b2,b7b2,a7b5,c3c4,b5c3,b2b5,c3d1,b5b6,d9e8,e1d1,g4g2,b6b2,e6f6,b2a2,e9f9,f2e1,f6b6,a2b4,e4f4,b4b6,c5e7,b6a8,i7g5,a8b6,f4f3,b6b2,g5i7,b2a2,f9e9,g6h6,g7i6,e3e4,f3f2,a2g2,e7c9,i3i4,c9e7,i2g0,i7g9,g2g5,f2g2,d1d0,e8d7,g5i6,g9i7,d0d1,f7e8,i4i5,i7g9,d1d0,e9f9,i6h8,f9e9,h6h7,g2f2,d0e0,e8f9,h8a8,e7c9,e1f2,g9i7,e0e1,i7g9,e4e5,f9e8,a4a5,c9a7,e5d5,g9e7,a8d8,e9f9,a5a6,a7c5,e1e2,e7g9,g0i2,c5a7,f0e1,f9e9,d8a8,a7c9,e1d2,g9e7,e2e1,e9f9,a8a9,f9f8,h7h8,f8f7,c4c5,c9a7,a9f9,e8f9,i2g4,a7c5,h8i8,e7g5,i8h8,d7e8,e1f1,e8d9,i5i6,d9e8,d2e1,c5a7,g4i2,f7f8,d5e5,f8f7,i6h6,e8d9,e1d0,a7c9,i2g4,c9a7,e5d5,f7f8,h6g6,g5i7,d0e1,f8e8,h8i8,i7g9,d5c5,e8e9,f1f0,e9e8,f0e0,e8e7,g4i2,f9e8,a6a7,e7f7,g6g7,f7e7,c5b5,e7d7,a7a8,e8f9,g7f7,g9e7,e1f0,e7g9,f7g7,d7d8,b5c5,g9i7,c5b5,f9e8,b5b6,e8f9,b6a6,f9e8,e0e1,i7g5,g7h7,d8d7,e1f1,g5i7,i8i9,d7d8,i2g0,d8d7,g0i2,d7d8,h7i7,e8f9,a6a7,d9e8,f0e1,d8d9,e1d2,d9e9,a7b7,e9d9,a8b8,e8d7,b8b9,f9e8,i7i8,e8f7,d2e1,d9d8,i2g0,f7e8,f1f0,e8f9,b7c7,d8d9,b9c9,d9e9,e1d0,e9e8,c9b9,e8f8,c7d7,f8e8,f0f1,e8f8,i9h9,f9e8,i8h8,e8d9,b9c9,f8f9,h9g9,f9e9,c9d9,e9d9,g9h9,d9e9,d7e7,e9f9,e7e8 +225b9f92;c3c4,b7b3,a0a1,b3b0,c4c5,h7h6,a1g1,g9i7,g1i2,i6i5,g0e2,h9f8,e0e1,b0b3,i2f2,h6h4,e1e0,b3c3,f2h3,h4e4,e0e1,a6a5,c5d5,c9a7,h3i5,c3g3,i5i4,b9d8,d5e5,e9e8,e1f1,e4d4,f0e1,e6e5,i4i6,g6g5,e1f2,g3g0,i6i7,d4f4,f2e1,f4a4,i7i8,e8e7,c0a2,a4b4,i8f8,b4c4,a2c4,d8b9,f8f3,a9a8,f3f5,e7e8,f5f7,i9h9,e1f0,h9h4,f7g5,h4f4,g5f3,e5e4,f3f4,g0d0,e2g4,d0d7,f4f3,b9d8,f3f7,d7d2,c4a2,d8b7,f7f2,a7c5,f2h3,a8d8,a2c0,d2d0,h3i1,d8d4,i1g1,d4b4,c0e2,a5a4,g1g0,e8e7,f1f2,c5a7,g0i1,b4c4,e3e4,d0d3,e2c0,c4c0,i1g0,b7d8,f2f1,d3d0,i3i4,c6c5,f1f2,d0d1,a3a4,d1d0,g0g3,e7e8,f2e2,d0d3,a4a5,c0c4,e2f2,e8e7,g4e2,d3b3,g3h1,b3g3,h1h2,d8b7,h2h7,e7e8,h7d7,g3a3,d7b6,a3b3,f0e1,b3i3,b6b4,c4c1,b4d5,c1d1,e1f0,i3i2,d5d7,d1d5,d7a7,d5d1,a7b7,e8e9,b7b8,f9e8,b8b5,e8d7,e2g4,i2g2,g4e2,d1d2,b5b9,e9e8,e4e5,g2g3,e5f5,d2c2,b9b7,g3g6,b7c5,c2c1,a5a6,e8e9,c5c7,g6g3,c7b9,e9e8,f5g5,c1f1,f2f1,e8e7,e2g4,g3d3,b9b2,e7e8,b2d2,d3e3,d2d1,e3i3,d1d4,i3i2,d4c6,i2i1,c6c4,i1i0,c4b4,e8e7,b4b1,i0i2,b1a3,i2b2,a3g3,b2b4,g3g1,b4b8,g1g2,b8b5,g2g3,b5d5,g3f3,d5d3,f1f2,d3d0,f3h3,d0d6,f2f1,d6b6,h3f3,b6b2,g5h5,b2c2,a6a7,c2f2,f3f2,e7e8,f2f3,e8d8,f3h4,d9e8,f1e1,e8d9,h4h2,d7e8,g4e2,d8d7,h5g5,e8f9,e1f1,d9e8,h2f2,d7e7,a7a8,e8f7,f2f3,f9e8,f3g3,e7d7,g3g1,d7d8,f1e1,e8f9,g1i0,d8d7,i0i1,f9e8,e1f1,e8f9,i4i5,d7e7,e2g4,f9e8,i1g2,e8d9,g2f4,e7e8,f4f5,e8e9,f5f2,e9e8,f2h3,e8d8,h3i3,d8d7,a8a9,d7e7,i3h5,e7d7,g4i2,d9e8,g5g6,e8f9,h5c5,d7d8,c5a5,d8e8,g6g7,e8e7,i2g4,f9e8,a5c6,e7d7,f0e1,e8f9,c6g6,d7d8,g6e7,f7e8,e7g6,d8d7,g6e5,d7d8,i5i6,e8d7,g7h7,d8d9,i6i7,f9e8,g4e2,e8f7,e5f7,d7e8,f7f4,e8f7,f4e6,f7e8,e6a6,e8f7,a9b9,d9e9,f1f2,e9d9,a6b4,d9d8,b4b6,d8d9,e1f0,d9e9,e2c0,e9e8,b6d7 +1569186f;a0a6,g6g5,c0a2,h7h6,c3c4,b7i7,a6a7,e6e5,g0e2,g9e7,a7a9,i6i5,a9a6,d9e8,e3e4,e8f7,a6c6,f9e8,c4c5,b9a7,c6f6,h6h8,f6f2,a7c6,c5c6,e8f9,e2g0,h8h5,f2c2,h5h3,e0e1,e7g9,c6c7,h3h1,c2d4,h1i1,d4d8,i7i3,d8h8,c9a7,h8i6,i9i7,i6i3,a7c9,c7c8,i7i6,e1d1,f9e8,i3h5,i5i4,h5h7,i6h6,h7h8,h6d6,d1e1,c9e7,c8d8,d6f6,e4e5,i1g1,g0i2,f6c6,e1e2,e8d9,h8h1,g1b1,h1g1,c6a6,e2e1,g9i7,i2g0,b1b5,e5f5,b5b1,g1e2,a6e6,e2e6,e9f9,e6e4,g5g4,e4f4,f9e9,f4d4,f7e8,d4b5,b1c1,b5e5,c1a1,e5b5,a1a3,g3g4,a3e3,d8e8,e9f9,e8e9,f9f8,e9d9,e3c3,b5a3,e7c5,e1e2,c3e3,f5e5,i7g5,a3b5,c5e7,b5c7,e3b3,d9e9,b3d3,c7c8,f8f7,g0i2,e7c5,d0e1,d3d4,e5e6,d4d2,a2c0,h9g7,c8a8,d2d5,a8b6,d5d6,b6b3,d6d1,b3a3,d1d9,a3a4,d9a9,e1f2,a9a5,a4b2,f7f8,b2b4,g7f5,b4b5,f8e8,g4g5,i4h4,b5b3,c5e7,e9d9,e7c9,b3b9,a5c5,b9b7,c5c4,b7a7,h4i4,a7a3,f5d6,a3d3,c4h4,d3f4,d6b7,i2g4,h4h9,e2d2,h9h1,f4g2,h1d1,f0e1,d1c1,g2i1,e8f8,e1f0,c9a7,e6f6,c1c8,g4e2,c8b8,i1f1,f8e8,d2d1,b7c5,d1e1,c5b3,f1i1,b3a5,e1e0,a5c6,f6g6,b8b4,f0e1,i4i3,g6h6,a7c9,e2g0,b4b1,g5f5,c6b4,d9e9,b4c6,i1i2,b1a1,i2g3,a1a5,g3g7,e8f8,h6h7,a5a0,c0e2,a0a4,g7d7,c6e7,f5f6,f8e8,e2c0,e7d5,e0f0,d5f6,d7a7,a4g4,a7a6,e8f8,e9d9,i3h3,a6b4,f6d5,b4a6,d5b6,a6a7,g4g9,a7c6,g9g3,e1d2,b6a8,f0f1,c9a7,d9e9,g3g7,c6g6,a7c9,g6g7,c9e7,g7e7,h3g3,e7e8,f8f7,e8b8,g3g2,b8b7,f7f8,b7b6,a8c7,b6b5,c7a8,b5b3,g2h2,b3c1,h2i2,c1c9,a8b6,e9f9,i2i1,c9c1,i1h1,c1e2,b6d5,h7h8,d5c7,e2d0,h1h0,d0e0,f8f7,h8i8,c7e8,d2e1,e8c7,f1f0,c7b5,c0a2,b5c3,i8h8,c3e2,a2c4,f7f8,c4a2,f8f9,e0d0,f9f8,d0e0,f8f7,a2c0,h0g0,e0g0,e2d4,g0g5,f7e7,g5g2,e7e8,g2g5,e8d8,g5g3,d4f5,g3d3,d8e8,d3c5,e8d8,c5b3,f5e3,b3b7,d8e8,b7b2,e3g2,b2g2,e8d8,g2g7,d8d9,c0e2,d9d8,g7g9,d8d7,g9h7,d7d8,h7h1,d8e8,e2g0,e8e7,h1h5,e7d7,h5i7,d7d8,i7c7 +4fb15d8a;a3a4,a6a5,f0e1,h7h3,e0f0,f9e8,a0a5,g6g5,a5c6,b7c7,a4a5,h3h7,c6e5,a9a8,e5b5,b9a7,b5b1,a8d8,b1b6,h7h6,a5b5,h6h5,g3g4,d8d0,e1d0,c7c0,f0f1,i9i7,b6e6,g9e7,e6i6,i7h7,i6e6,c0c2,e6a6,h9g7,g4g5,h5h4,d0e1,e8d7,b5b6,h4h1,c3c4,a7c8,a6a3,h1h4,g0i2,d9e8,c4c5,h4h3,e1f0,c8a9,f0e1,h3h4,a3a5,g7h9,a5a8,h4a4,a8a6,c2c1,e1d0,a4a5,c5d5,c1a1,a6a7,a1a2,a7c6,a2b2,c6e5,h7i7,e5e4,b2e2,b6a6,a5b5,e4c4,a9b7,i2g4,b5b6,c4c8,b7d8,d0e1,e8f9,f1f2,i7g7,d5e5,e9d9,g5f5,b6b9,c8c1,g7g5,c1a1,b9a9,a1c1,d9e9,e1d2,h9f8,d2e1,e2a2,c1c5,f8h9,c5c3,d8b9,c3d5,h9g7,d5e7,c9e7,f2e2,f9e8,f5g5,g7f9,a6a7,a2a5,g5f5,f9g7,a7b7,a5f5,b7a7,f5f2,i3i4,e8f9,e1f2,a9a8,e5d5,e7g5,g4i2,g7e6,a7b7,b9d8,e2e1,a8a1,d5c5,a1a0,c5b5,a0a8,e1e0,a8a2,b5c5,a2d2,i2g4,g5i7,e3e4,d2d4,c5b5,d4b4,g4i2,b4b1,e0d0,b1h1,i2g4,e6c5,e4e5,h1h2,g4e2,i7g5,e5f5,h2h3,e2c0,h3b3,i4i5,b3g3,f5g5,g3a3,b5c5,a3b3,g5f5,b3e3,i5h5,e3i3,h5i5,i3i2,c5c6,f9e8,c6b6,i2i1,b6a6,i1i4,d0d1,e9f9,b7c7,d8b9,c7c8,b9c7,c8c9,i4i0,c9d9,f9f8,d1e1,c7b9,e1d1,i0d0,d1d2,f8f7,d9c9,d0g0,i5h5,g0g6,c9b9,f7f8,b9c9,g6g0,h5i5,f8f9,c9b9,g0i0,c0e2,i0i3,f2e1,f9e9,i5h5,i3i2,e2g0,i2f2,e1f2,e9d9,a6b6,e8f9,d2e2,d9d8,b9a9,d8d9,h5g5,f9e8,g5h5,d9e9,b6b7,e9d9,f5g5,d9e9,b7a7,e9d9,f2e1,e8f7,a7a8,d9d8,e2d2,f7e8,h5i5,e8f9,g5h5,f9e8,g0i2,e8f9,i2g4,d8e8,h5g5,e8e9,g5h5,e9d9,i5i6,f9e8,d2d1,e8f7,d1d0,f7e8,d0d1,e8f9,h5i5,f9e8,g4i2,d9d8,e1d0,d8d9,i5h5,e8f9,h5g5,f9e8,g5h5,d9d8,h5i5,e8f9,a8b8,f9e8,b8b9,e8d9,i2g4,d9e8,g4i2,d8d9,i6h6,e8f7,h6g6,d9d8,b9c9,f7e8,i2g4,e8d9,d1d2,d9e8,g4e2,e8f7,a9b9,f7e8,i5i6,e8f9,e2g0,f9e8,i6i7,e8d9,i7h7,d9e8,g6f6,e8f9,b9a9,f9e8,f6f7,e8f9,c9d9,d8e8,a9b9,e8d8,f7f8,f9e8,f8g8,d8d9,b9a9,d9e9,a9b9,e8d9,d0e1,e9f9,g8f8,f9e9,e1d0,d9e8,b9a9,e8d9,d0e1,d7e8,h7i7,e8d7,g0i2,d7e8,a9b9,e8f7,f8f9,e9f9,b9c9,f9e9,i2g4,e9f9,g4e2,f9e9,e1f0,d9e8,e2c0,e8f9,d2d1,f7e8,d1e1 +58330aca;g3g4,b7b5,c3c4,c6c5,c0e2,b5b0,e2c0,g6g5,e0e1,b0b5,i3i4,f9e8,c4c5,h7h5,g0i2,g5g4,a0a6,e8f9,a6d6,a9a6,d6i6,h5i5,c5c6,b5h5,c0e2,e9e8,i6h6,h5a5,e3e4,a5a4,h6h0,a6c6,e1e0,c6c7,h0h2,a4a5,h2g2,i9i7,g2e1,a5a9,i2g4,i5d5,e1b1,c7a7,b1f1,d5d1,f1h2,d1d5,h2h9,i7i9,h9g7,a7f7,g4i2,d5h5,g7h7,f7f5,h7e7,e8d8,e7a7,f5f7,a7a9,f7f5,d0e1,f5c5,i2g4,h5f5,e1d0,c5c7,d0e1,c7c4,a9a5,f5f8,e1d0,d8e8,g4i2,c4a4,e2g4,a4d4,a5b5,d4d1,b5b9,d1f1,f0e1,c9a7,b9b2,e8d8,i4i5,f8f2,i5h5,a7c5,h5g5,f2f7,b2b3,d9e8,b3a5,f7e7,g5g6,e7i7,e1f2,f1e1,f2e1,i7c7,a5c5,e6e5,c5c4,g9e7,g6h6,c7a7,e1f0,i9i2,f0e1,d8d9,e1f0,a7a6,c4c8,a6a4,e0e1,a4b4,h6g6,i2i9,c8b8,d9e9,b8b4,i9i7,e1e2,i7i2,g4i2,e5e4,i2g0,e4d4,b4b8,e7g9,b8a6,g9e7,g6g7,e9d9,d0e1,e8d7,a3a4,d4e4,a6f6,e7c9,f6f8,d9e9,f8f7,e9e8,f7g9,e4e3,e2f2,e3e2,g0e2,e8e7,g7g8,e7e8,g8h8,c9a7,e1d0,a7c5,g9g5,e8d8,g5g2,c5a7,e2c4,f9e8,g2g0,d8d9,g0g5,a7c9,g5g1,c9e7,g1a1,e7c9,h8i8,e8f9,f2e2,d7e8,a1a0,e8d7,c4a2,f9e8,a0c1,d9d8,c1c7,e8f9,c7a7,d7e8,a7a8,d8d9,a8b8,d9e9,b8a8,e9d9,a8a9,d9d8,e2f2,d8d7,i8h8,e8f7,h8i8,f7e8,a9f9,e8f7,f9h8,d7e7,h8c8,e7d7,c8h8,f7e8,a4a5,e8d9,a5a6,d7e7,h8i6,c9a7,d0e1,e7e8,a6a7,e8e9,a7a8,e9e8,i6i0,e8e7,i0h0,e7d7,h0h4,d7d8,h4h2,d8e8,h2f1,e8e7,a8b8,e7e8,a2c4,e8e9,c4e2,e9e8,e2g4,e8e9,f1h1,d9e8,h1h2,e8d9,h2i0,d9e8,b8b9,e8d9,i0i2,d9e8,e1d2,e8f7,i2i5,e9e8,i5f5,e8e9,f5h4,f7e8,h4h3,e8d9,i8h8,d9e8,h3h4,e8d9,h4h2,d9e8,h2g2,e8f7,g2h4,e9f9,h4h1,f9e9,h1b1,e9d9,f0e1,d9d8,b1c3,d8d7,c3a3,d7e7,e1d0,f7e8,a3c4,e8d9,c4a3,e7d7,f2f1,d9e8,a3e3,e8f7,e3e1,d7d8,g4i2,d8d9,f1f0,d9d8,e1g0,d8d7,g0i0,d7d8,i0g0,d8d7,i2g4,d7e7,g0g1,e7d7,g1e1,d7d8,e1f3,d8d9,d0e1,d9d8,b9c9,f7e8,f3e3,d8d7,e3e6,e8d9,e6a6,d9e8,a6f6,d7e7,f6i6,e8f7,f0f1,e7d7,i6i7,d7d8,i7f7,d8e8,f7f4,e8e9,f4f5,e9e8,f5e5,e8d8,e5a5,d8d7,a5a4,d7e7,a4c4,e7d7,e1f2,d7d8,c9d9,d8d9,c4c1,d9e9,f1f0,e9d9,c1c7,d9d8,c7a6,d8e8,f2e1,e8e7,e1f2,e7f7,a6f6,f7e7,h8g8 +58ef878d;g0e2,b7d7,a0b0,d7d6,b0b3,d6d4,b3a5,d4d7,a5h5,h7f7,h5h4,a9a8,h4h2,g9i7,h2f2,i7g9,e2g0,d7e7,a3a4,i9i8,d0e1,e6e5,g0e2,a6a5,a4a5,a8c8,f2h3,f7f3,h3g1,e7g7,g1f1,i8f8,g3g4,g7f7,f1f7,c6c5,g4g5,i6i5,f7g7,c5c4,g7d7,c8d8,d7d6,h9i7,e2c4,f8f6,d6f6,b9a7,c4a2,d8d2,a5a6,d2b2,f6b6,b2f2,b6a4,e9e8,a4a7,e8e9,a7a9,f3f6,i3i4,f2f4,e3e4,f6f8,a6b6,i7h5,a9a4,i5i4,a4b4,f8e8,e4e5,e8d8,g5f5,f4f2,e5d5,f2f4,b4b5,d9e8,b5a3,d8c8,a3b5,i4h4,b5b2,e8d9,b2d1,d9e8,f5e5,c8a8,d1d4,a8b8,d5d6,g9e7,e1d0,h5f6,d4f3,f4f5,b6a6,b8b1,f3d3,f5f0,e0e1,f0f5,a6a7,f5h5,d3h3,b1c1,h3i3,h5h8,i3i8,e8f7,d6d7,f7e8,e5e6,e8d9,d7e7,h4g4,e7e8,f9e8,a2c4,h8h5,i8i6,e8f9,e1f1,f9e8,c0a2,g4f4,e6f6,h5c5,f6e6,e8f7,i6i1,c1b1,e6f6,b1b5,i1h1,c5e5,h1f2,b5c5,d0e1,e5e4,a7a8,c5c7,e1d0,d9e8,f6g6,e4e3,f2g2,e3e6,g6f6,e6e5,g2g4,e5e3,f1f2,e3e1,g4g7,e8f9,g7g8,e9d9,g8g5,e1f1,f2e2,f9e8,d0e1,c7d7,g5g4,d7d8,e1f0,d8d4,e2d2,e8f9,c4e2,f1d1,d2d1,d4d8,d1d2,f4g4,a8a9,f7e8,e2c4,d8d4,a9b9,d4d5,a2c0,d5d7,c4e2,d7d3,f6e6,d3d5,c0a2,c9a7,c3c4,d5d7,e2c0,d7d3,b9c9,d9d8,c0e2,g4f4,a2c0,d3d7,e6f6,d7d6,f0e1,e8d7,e1f2,d6e6,c9d9,f4e4,d9c9,e4e3,f6f7,e3e2,d2d1,e6b6,c9d9,b6b1,f7g7,e2e1,d1e1,b1b5,d9e9,b5b3,g7g8,b3b4,e9f9,b4b7,g8f8,b7b5,f8e8,d8d9,e1d1,b5d5,e8d8,d5d8,d1e1,d8i8,c0a2,i8i3,e1e2,i3c3,f9g9,a7c5,f2e1,c3f3,g9h9,f3a3,a2c0,a3a0,e2d2,d9e9,c0e2,e9f9,c4c5,a0d0,e2c4,f9e9,c4a2,d0e0,a2c0,e0f0,c5c6,f0f2,c6c7,f2f8,c0e2,f8b8,c7b7,b8g8,e1f2,g8a8,b7a7,e9e8,h9g9,a8a9,g9h9,a9e9,a7b7,e8d8,e2g4,e9f9,d2e2,f9f3,e2d2,f3d3,d2e2,d3c3,e2d2,c3c6,f2e1,c6c5,h9g9,d8d9,d2d1,d9e9,e1d2,e9d9,b7a7,c5c3,d2e1,c3c4,d1d0,c4c3,g4i2,c3g3,i2g4,g3e3,g9f9,e3b3,d0e0,b3b4,g4e2,d9d8,e2g4,b4b7,g4e2,b7c7,e2g0,c7b7,e0d0,b7b8,f9e9,b8b4,e1f2,b4b0,a7b7,b0g0,b7c7,g0g6,e9d9,g6g4,d0d1,g4e4,d1d2,e4i4,c7d7,d8d9,d2e2,i4i9,d7c7,i9i5,c7b7,i5i3,f2e1,i3h3,b7c7,h3h7,e1f2,h7d7,e2e1,d7d0,c7d7,d0d3,e1e2,d3c3,f2e1,c3c7,e1d0,c7a7,d0e1,a7a5,e2f2,a5e5,d7c7,e5i5,f2f1,i5i2,e1f0,i2d2,c7d7,d2f2,f1e1,f2e2,e1e2 \ No newline at end of file diff --git a/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/AbstractPieceType.kt b/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/AbstractPieceType.kt index a263854ba..8c0b7dafd 100644 --- a/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/AbstractPieceType.kt +++ b/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/AbstractPieceType.kt @@ -16,6 +16,12 @@ enum class AbstractPieceType( HORSE('n', '傌', '馬'), CHARIOT('r', '俥', '車'), CANNON('c', '炮', '砲'), - SOLDIER('p', '兵', '卒', 5); + SOLDIER('p', '兵', '卒', 5), + + /** + * The super-chariot (Banner) in Manchu chess: combines the powers of the chariot, horse, and cannon. + * Represented by 'm'/'M' in the FEN. Only used as Red's piece in the Manchu variant. + */ + SUPER_CHARIOT('m', '俥', '車', 1); } diff --git a/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Board.kt b/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Board.kt index 210c79fb1..9c1980922 100644 --- a/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Board.kt +++ b/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Board.kt @@ -5,10 +5,13 @@ import io.elephantchess.xiangqi.Color.BLACK import io.elephantchess.xiangqi.Color.RED import io.elephantchess.xiangqi.HalfMove.Companion.parseMoveFromUci import io.elephantchess.xiangqi.Position.Companion.getAllPositions +import io.elephantchess.xiangqi.Variant.MANCHU +import io.elephantchess.xiangqi.Variant.XIANGQI class Board( initFen: String = DEFAULT_START_FEN, private val keepHistory: Boolean = false, + val variant: Variant = inferVariantFromFen(initFen), ) { private val content: Array> = Array(WIDTH) { Array(HEIGHT) { null } } @@ -230,6 +233,34 @@ class Board( result } + SUPER_CHARIOT -> { + // The super-chariot combines the powers of chariot, horse, and cannon. + val chariotMoves = piecePosition.getOrthogonalLines().flatMap { line -> filterChariotLine(line) } + val cannonMoves = piecePosition.getOrthogonalLines().flatMap { line -> filterCannonLine(line) } + val horseMoves = mutableListOf() + val top = piecePosition.getTop() + val bottom = piecePosition.getBottom() + val left = piecePosition.getLeft() + val right = piecePosition.getRight() + if (top.existsOnBoard() && !hasPieceAt(top)) { + horseMoves += top.getTopLeft() + horseMoves += top.getTopRight() + } + if (bottom.existsOnBoard() && !hasPieceAt(bottom)) { + horseMoves += bottom.getBottomLeft() + horseMoves += bottom.getBottomRight() + } + if (left.existsOnBoard() && !hasPieceAt(left)) { + horseMoves += left.getTopLeft() + horseMoves += left.getBottomLeft() + } + if (right.existsOnBoard() && !hasPieceAt(right)) { + horseMoves += right.getTopRight() + horseMoves += right.getBottomRight() + } + (chariotMoves + cannonMoves + horseMoves).distinct() + } + ELEPHANT -> { val result = mutableListOf() if (!hasPieceAt(piecePosition.getTopLeft())) { @@ -418,7 +449,7 @@ class Board( } fun copy(keepHistory: Boolean = false): Board { - val copy = Board(keepHistory = keepHistory) + val copy = Board(keepHistory = keepHistory, variant = variant) copy.clear() (0 until WIDTH).forEach { x -> (0 until HEIGHT).forEach { y -> @@ -482,9 +513,29 @@ class Board( const val WIDTH = 9 const val HEIGHT = 10 const val DEFAULT_START_FEN = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 0" + + /** + * Manchu chess start FEN. Black pieces are set up as in standard xiangqi. + * Red has only: general, 2 advisors, 2 elephants, 5 soldiers, and the super-chariot (M) at a1. + * The super-chariot ('M'/'m') combines the powers of the chariot, horse, and cannon. + */ + const val MANCHU_START_FEN = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/9/9/M1BAKAB2 w - - 0 0" + const val PRINT_SEPARATOR = " " const val UCI_MOVE_REGEX_PATTERN = "[a-i][0-9][a-i][0-9]" + fun defaultStartFen(variant: Variant): String = when (variant) { + XIANGQI -> DEFAULT_START_FEN + MANCHU -> MANCHU_START_FEN + } + + /** + * Infers the chess variant from a FEN string: if the piece section contains 'M' (super-chariot), + * it is Manchu chess; otherwise standard Xiangqi. + */ + fun inferVariantFromFen(fen: String): Variant = + if (fen.substringBefore(' ').contains('M')) MANCHU else XIANGQI + fun validateFen(fen: String) { try { Board(fen) diff --git a/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Variant.kt b/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Variant.kt new file mode 100644 index 000000000..f3c0ef39b --- /dev/null +++ b/xiangqi-core/src/main/kotlin/io/elephantchess/xiangqi/Variant.kt @@ -0,0 +1,35 @@ +package io.elephantchess.xiangqi + +/** + * The chess variant being played. + */ +enum class Variant { + + XIANGQI, + + /** + * Manchu chess (also known as Yitong). Similar to xiangqi but Red's pieces are + * reduced to: general, 2 advisors, 2 elephants, 5 soldiers, and a super-chariot (Banner) + * that combines the powers of the chariot, horse, and cannon. + * + * @see Wikipedia: Manchu chess + */ + MANCHU; + + /** + * The Chinese character commonly used to represent this variant. + */ + fun chineseChar(): String = when (this) { + XIANGQI -> "象" + MANCHU -> "统" + } + + /** + * The Fairy Stockfish variant name for this variant (used with `setoption name UCI_Variant value `). + */ + fun fairyStockfishVariantName(): String = when (this) { + XIANGQI -> "xiangqi" + MANCHU -> "manchu" + } + +} From 7f50e2cc2f7be4a83dee73ad44935a1590cfc818 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 09:27:07 +0000 Subject: [PATCH 09/12] Merge latest master and resolve new conflicts Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/e6a414b4-0176-46c5-a136-8332b7dbb689 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../services/PlayerVsPlayerGameDaoService.kt | 13 ++++ .../services/PlayerVsPlayerGameService.kt | 12 +++- .../services/PlayerVsPlayerGameServiceTest.kt | 61 +++++++++++++++++++ .../resources/templates/about/changelog.html | 4 ++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt index 214bbf81d..186886aed 100644 --- a/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt +++ b/webapp-dao/src/main/kotlin/io/elephantchess/db/services/PlayerVsPlayerGameDaoService.kt @@ -272,6 +272,19 @@ class PlayerVsPlayerGameDaoService(private val dslContext: DSLContext) { .awaitSingleValue() } + suspend fun countCreatedGamesByUser( + userId: String, + timeControlCategory: TimeControlCategory, + ): Int { + return dslContext + .selectCount() + .from(GAME) + .where(GAME.INVITER.eq(userId)) + .and(GAME.GAME_STATUS.eq(CREATED)) + .and(GAME.TIME_CONTROL_CATEGORY.eq(timeControlCategory)) + .awaitSingleValue() ?: 0 + } + suspend fun countLiveGames(duration: Duration): Int { return dslContext .selectCount() diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt index 6f8d3e685..d50c54625 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt @@ -285,11 +285,20 @@ class PlayerVsPlayerGameService( throw BadRequestException("Guest users are not allowed to create correspondence games") } + // limit the number of CREATED games a user can have with the same settings + val timeControlCategory = TimeControlCategory.fromSeconds(request.timeControlBase) + val createdGamesCount = pvpGameDaoService.countCreatedGamesByUser( + userId.id, + timeControlCategory, + ) + if (createdGamesCount >= MAX_CREATED_GAMES_PER_SETTINGS) { + throw BadRequestException("You already have $MAX_CREATED_GAMES_PER_SETTINGS pending games with the same settings") + } + // if user is a guest, always allow guests to join // it should be disabled by UI already, so it's additional backend validation val allowGuests = userId.userType == UserType.GUEST || request.allowGuests - val timeControlCategory = TimeControlCategory.fromSeconds(request.timeControlBase) val userRating = getUserRating(userId.id, timeControlCategory, request.variant) // if such a game already exists, join it instead of creating a new one @@ -1104,6 +1113,7 @@ class PlayerVsPlayerGameService( val NOTIFICATIONS_OFFLINE_FOR = 2.minutes const val MESSAGE_LENGTH_LIMIT = 200 + const val MAX_CREATED_GAMES_PER_SETTINGS = 3 /** * Maximum age of a typing event we are willing to surface to a client. diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index 73f88f0f1..89cee6256 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -610,6 +610,67 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertTrue(afterUpdateEntry.isPreAnalyzed) } + /** + * User should not be able to create more than 3 CREATED PvP games with the same settings. + */ + @Test + fun createGameLimitTest01() = runTest { + val request = CreateGameRequest( + inviterColor = RED, + isRated = true, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = false, + alwaysVisibleInLobby = false, + privateInvite = true + ) + + pvpGameService.createGame(userId1, request) + pvpGameService.createGame(userId1, request) + pvpGameService.createGame(userId1, request) + + assertEquals(3, countGameByStatus(CREATED)) + + val e = assertFailsWith { + pvpGameService.createGame(userId1, request) + } + assertEquals("You already have 3 pending games with the same settings", e.message) + + assertEquals(3, countGameByStatus(CREATED)) + } + + /** + * Limit is per time category: different category should allow new games, but different color should not. + */ + @Test + fun createGameLimitTest02() = runTest { + val request = CreateGameRequest( + inviterColor = RED, + isRated = true, + timeControlBase = 30.minutes.inWholeSeconds.toInt(), + timeControlIncrement = null, + timeControlMode = TimeControlMode.GAME_TIME, + allowGuests = false, + alwaysVisibleInLobby = false, + privateInvite = true + ) + + pvpGameService.createGame(userId1, request) + pvpGameService.createGame(userId1, request) + pvpGameService.createGame(userId1, request) + + // different color but same time category: should still be rejected + assertFailsWith { + pvpGameService.createGame(userId1, request.copy(inviterColor = BLACK)) + } + + // different time category (BLITZ vs RAPID): should succeed + pvpGameService.createGame(userId1, request.copy(timeControlBase = 3.minutes.inWholeSeconds.toInt())) + assertEquals(4, countGameByStatus(CREATED)) + assertEquals(4, countGameByStatus(CREATED)) + } + /** * Two Manchu games with compatible colors should be matched together. */ diff --git a/webapp/src/main/resources/templates/about/changelog.html b/webapp/src/main/resources/templates/about/changelog.html index 1f7f9410b..fdcf0d539 100644 --- a/webapp/src/main/resources/templates/about/changelog.html +++ b/webapp/src/main/resources/templates/about/changelog.html @@ -44,6 +44,10 @@

    2026-05-25

    player-specific queries (issue #444) +
  • + Limit users to 3 new PvP games per time category + (issue #339) +
  • 2026-05-24

      From 953783650c7d8ab96719fe38916b95a473697e50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 09:28:31 +0000 Subject: [PATCH 10/12] Remove duplicate assertion from merged game limit test Agent-Logs-Url: https://github.com/benckx/elephantchess/sessions/e6a414b4-0176-46c5-a136-8332b7dbb689 Co-authored-by: benckx <8626080+benckx@users.noreply.github.com> --- .../servicelayer/services/PlayerVsPlayerGameServiceTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index 89cee6256..4123f5b28 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -668,7 +668,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)) - assertEquals(4, countGameByStatus(CREATED)) } /** From 751156371b363b87c46f2f83c18fa54d01d66010 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 07:55:01 +0000 Subject: [PATCH 11/12] Move pre-analyzed pane before chat pane --- webapp/src/main/resources/public/js/userdata/my-games.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/main/resources/public/js/userdata/my-games.js b/webapp/src/main/resources/public/js/userdata/my-games.js index 3f74877e5..8cf00d802 100644 --- a/webapp/src/main/resources/public/js/userdata/my-games.js +++ b/webapp/src/main/resources/public/js/userdata/my-games.js @@ -284,9 +284,9 @@ class MyGamesPage extends InfiniteScrollPage { variantPane, timeControlPane, middlePane, + preAnalyzedIndicatorPane, chatIndicatorPane, ratingDeltaIndicatorPane, - preAnalyzedIndicatorPane, outcomeIndicatorPane, rightPane ); From c1f20cd9ffe033dc7ec689773af25f1eaa192271 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:28:37 +0000 Subject: [PATCH 12/12] Fix CI compile failure in PlayerVsPlayerGameServiceTest --- .../servicelayer/services/PlayerVsPlayerGameServiceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index aec748a7f..55aa572b5 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -663,7 +663,7 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { @Test fun listUserGamesPreAnalyzedStatusTest() = runTest { - val gameId = createAndJoinGame(RED) + val gameId = createAndJoinGame(userId1, userId2, inviterColor = RED) val beforeUpdateEntry = pvpGameService