Add chess resign and undo actions
All checks were successful
Deploy Blog Backend / test (push) Successful in 1m20s
Deploy Blog Backend / deploy (push) Successful in 1m27s

This commit is contained in:
박원엽
2026-06-19 16:41:03 +09:00
parent bd08af5d59
commit 5686d3d5ad
3 changed files with 230 additions and 0 deletions

View File

@@ -107,6 +107,55 @@ class ChessGameService(
return ChessGameResponse.from(updated, maiaResponse.move)
}
@Transactional
fun resign(memberId: Long, gameId: String): ChessGameResponse {
val session = getPlayableSession(memberId, gameId)
require(session.status == "IN_PROGRESS") { "This chess game is already finished." }
val result = session.playerColor.resignationResult()
val updated = session.copy(
status = "RESIGNED",
result = result,
pgn = session.pgn.withPgnResult(result),
updatedAt = Instant.now(clock)
)
saveSession(updated)
return ChessGameResponse.from(updated)
}
@Transactional
fun undoMove(memberId: Long, gameId: String): ChessGameResponse {
val session = getPlayableSession(memberId, gameId)
require(session.status != "RESIGNED") { "Resigned games cannot be undone." }
val lastPlayerMoveIndex = session.moves.indices
.lastOrNull { sideForMoveIndex(it) == session.playerColor }
?: throw IllegalArgumentException("There is no player move to undo.")
val revertedMoves = session.moves.take(lastPlayerMoveIndex)
val labels = playerLabels(session.playerColor, session.rating, session.model)
val state = maiaEngine.getState(
MaiaStateRequest(
moves = revertedMoves,
white = labels.white,
black = labels.black
)
)
val updated = session.copy(
fen = state.fen,
turn = ChessSide.from(state.turn),
moves = revertedMoves,
status = state.status,
result = state.result,
pgn = state.pgn,
updatedAt = Instant.now(clock)
)
saveSession(updated)
return ChessGameResponse.from(updated)
}
private fun getPlayableSession(memberId: Long, gameId: String): ChessGameSession {
val session = chessGameStore.findById(gameId)
if (session != null) {
@@ -128,6 +177,28 @@ class ChessGameService(
require(session.memberId == memberId) { "Chess game not found." }
}
private fun sideForMoveIndex(index: Int): ChessSide {
return if (index % 2 == 0) ChessSide.WHITE else ChessSide.BLACK
}
private fun ChessSide.resignationResult(): String {
return if (this == ChessSide.WHITE) "0-1" else "1-0"
}
private fun String.withPgnResult(result: String): String {
val withHeader = if (contains(Regex("""\[Result\s+"[^"]*"]"""))) {
replace(Regex("""\[Result\s+"[^"]*"]"""), """[Result "$result"]""")
} else {
"""[Result "$result"]""" + "\n" + this
}
val trailingResult = Regex("""(1-0|0-1|1/2-1/2|\*)\s*$""")
return if (trailingResult.containsMatchIn(withHeader)) {
trailingResult.replace(withHeader, result)
} else {
"${withHeader.trimEnd()} $result"
}
}
private fun playerLabels(playerColor: ChessSide, rating: Int, model: String): PlayerLabels {
val maiaName = "Maia3-$model-$rating"
return if (playerColor == ChessSide.WHITE) {