Add Maia chess gameplay and deployment
Some checks failed
Deploy Blog Backend / test (push) Successful in 2m6s
Deploy Blog Backend / deploy (push) Failing after 1m8s

This commit is contained in:
박원엽
2026-06-19 14:49:20 +09:00
parent b47258ea53
commit 3042dc898e
26 changed files with 1682 additions and 32 deletions

View File

@@ -0,0 +1,176 @@
package me.wypark.blogbackend.domain.chess
import me.wypark.blogbackend.api.dto.ChessGameCreateRequest
import me.wypark.blogbackend.api.dto.ChessGamePgnResponse
import me.wypark.blogbackend.api.dto.ChessGameResponse
import me.wypark.blogbackend.api.dto.ChessGameStatsResponse
import me.wypark.blogbackend.api.dto.ChessGameSummaryResponse
import me.wypark.blogbackend.api.dto.ChessMoveRequest
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Clock
import java.time.Instant
import java.util.UUID
@Service
@Transactional(readOnly = true)
class ChessGameService(
private val chessGameStore: ChessGameStore,
private val chessGameHistoryStore: ChessGameHistoryStore,
private val maiaEngine: MaiaEngine,
private val clock: Clock
) {
@Transactional
fun createGame(memberId: Long, request: ChessGameCreateRequest): ChessGameResponse {
val playerColor = ChessSide.from(request.playerColor)
val now = Instant.now(clock)
val labels = playerLabels(playerColor, request.rating, request.model)
val initialState = maiaEngine.getState(
MaiaStateRequest(
moves = emptyList(),
white = labels.white,
black = labels.black
)
)
var session = ChessGameSession(
gameId = UUID.randomUUID().toString(),
memberId = memberId,
rating = request.rating,
playerColor = playerColor,
model = request.model,
temperature = request.temperature,
topP = request.topP,
fen = initialState.fen,
turn = ChessSide.from(initialState.turn),
moves = emptyList(),
status = initialState.status,
result = initialState.result,
pgn = initialState.pgn,
createdAt = now,
updatedAt = now
)
var maiaMove: String? = null
if (playerColor == ChessSide.BLACK) {
val maiaResponse = maiaEngine.playMove(session.toMaiaPlayRequest())
maiaMove = maiaResponse.move
session = session.applyEngineResponse(maiaResponse, now)
}
saveSession(session)
return ChessGameResponse.from(session, maiaMove)
}
fun getGames(memberId: Long, pageable: Pageable): Page<ChessGameSummaryResponse> {
return chessGameHistoryStore.findAllByMemberId(memberId, pageable)
.map { ChessGameSummaryResponse.from(it) }
}
fun getStats(memberId: Long): ChessGameStatsResponse {
return ChessGameStatsResponse.from(chessGameHistoryStore.getStats(memberId))
}
fun getGame(memberId: Long, gameId: String): ChessGameResponse {
val session = chessGameStore.findById(gameId)
if (session != null) {
requireOwnedBy(session, memberId)
return ChessGameResponse.from(session)
}
val record = chessGameHistoryStore.findByGameIdAndMemberId(gameId, memberId)
?: throw IllegalArgumentException("Chess game not found.")
return ChessGameResponse.from(record)
}
fun getPgn(memberId: Long, gameId: String): ChessGamePgnResponse {
val game = getGame(memberId, gameId)
return ChessGamePgnResponse(gameId = game.gameId, pgn = game.pgn)
}
@Transactional
fun playMove(memberId: Long, gameId: String, request: ChessMoveRequest): ChessGameResponse {
val session = getPlayableSession(memberId, gameId)
require(session.status == "IN_PROGRESS") { "This chess game is already finished." }
require(session.turn == session.playerColor) { "It is not the player's turn." }
val movesAfterPlayer = session.moves + request.move
val maiaResponse = maiaEngine.playMove(
session.toMaiaPlayRequest(moves = movesAfterPlayer)
)
val updated = session.applyEngineResponse(maiaResponse, Instant.now(clock), movesAfterPlayer)
saveSession(updated)
return ChessGameResponse.from(updated, maiaResponse.move)
}
private fun getPlayableSession(memberId: Long, gameId: String): ChessGameSession {
val session = chessGameStore.findById(gameId)
if (session != null) {
requireOwnedBy(session, memberId)
return session
}
val record = chessGameHistoryStore.findByGameIdAndMemberId(gameId, memberId)
?: throw IllegalArgumentException("Chess game not found.")
return record.toSession()
}
private fun saveSession(session: ChessGameSession) {
chessGameStore.save(session)
chessGameHistoryStore.save(session)
}
private fun requireOwnedBy(session: ChessGameSession, memberId: Long) {
require(session.memberId == memberId) { "Chess game not found." }
}
private fun playerLabels(playerColor: ChessSide, rating: Int, model: String): PlayerLabels {
val maiaName = "Maia3-$model-$rating"
return if (playerColor == ChessSide.WHITE) {
PlayerLabels(white = "Player", black = maiaName)
} else {
PlayerLabels(white = maiaName, black = "Player")
}
}
private fun ChessGameSession.toMaiaPlayRequest(
moves: List<String> = this.moves
): MaiaPlayRequest {
val labels = playerLabels(playerColor, rating, model)
return MaiaPlayRequest(
moves = moves,
rating = rating,
model = model,
temperature = temperature,
topP = topP,
white = labels.white,
black = labels.black
)
}
private fun ChessGameSession.applyEngineResponse(
response: MaiaPlayResponse,
now: Instant,
movesBeforeMaia: List<String> = moves
): ChessGameSession {
val nextMoves = response.move?.let { movesBeforeMaia + it } ?: movesBeforeMaia
return copy(
fen = response.fen,
turn = ChessSide.from(response.turn),
moves = nextMoves,
status = response.status,
result = response.result,
pgn = response.pgn,
updatedAt = now
)
}
private data class PlayerLabels(
val white: String,
val black: String
)
}