feat(grpc): implement gRPC services for game context management and move handling

This commit is contained in:
2026-04-25 10:21:58 +02:00
parent 9a39cd6916
commit b6be0cd249
28 changed files with 1687 additions and 145 deletions
@@ -0,0 +1,87 @@
syntax = "proto3";
option java_package = "de.nowchess.io.proto";
option java_multiple_files = true;
option java_outer_classname = "ChessTypesProto";
enum ProtoColor {
WHITE = 0;
BLACK = 1;
}
enum ProtoPieceType {
PAWN = 0;
KNIGHT = 1;
BISHOP = 2;
ROOK = 3;
QUEEN = 4;
KING = 5;
}
enum ProtoMoveKind {
QUIET = 0;
CAPTURE = 1;
CASTLE_KINGSIDE = 2;
CASTLE_QUEENSIDE = 3;
EN_PASSANT = 4;
PROMO_QUEEN = 5;
PROMO_ROOK = 6;
PROMO_BISHOP = 7;
PROMO_KNIGHT = 8;
}
enum ProtoGameResultKind {
ONGOING = 0;
WIN_CHECKMATE_W = 1;
WIN_CHECKMATE_B = 2;
WIN_RESIGN_W = 3;
WIN_RESIGN_B = 4;
WIN_TIME_W = 5;
WIN_TIME_B = 6;
DRAW_STALEMATE = 7;
DRAW_INSUFFICIENT = 8;
DRAW_FIFTY_MOVE = 9;
DRAW_THREEFOLD = 10;
DRAW_AGREEMENT = 11;
}
message ProtoPiece {
ProtoColor color = 1;
ProtoPieceType piece_type = 2;
}
message ProtoSquarePiece {
string square = 1;
ProtoPiece piece = 2;
}
message ProtoMove {
string from = 1;
string to = 2;
ProtoMoveKind move_kind = 3;
}
message ProtoCastlingRights {
bool white_king_side = 1;
bool white_queen_side = 2;
bool black_king_side = 3;
bool black_queen_side = 4;
}
message ProtoGameContext {
repeated ProtoSquarePiece board = 1;
ProtoColor turn = 2;
ProtoCastlingRights castling_rights = 3;
string en_passant_square = 4;
int32 half_move_clock = 5;
repeated ProtoMove moves = 6;
ProtoGameResultKind result = 7;
repeated ProtoSquarePiece initial_board = 8;
}
message ProtoPostMoveStatus {
bool is_checkmate = 1;
bool is_stalemate = 2;
bool is_insufficient_material = 3;
bool is_check = 4;
bool is_threefold_repetition = 5;
}
@@ -0,0 +1,31 @@
syntax = "proto3";
option java_package = "de.nowchess.io.proto";
option java_multiple_files = true;
option java_outer_classname = "IoServiceProto";
import "chess_types.proto";
message ProtoImportFenRequest {
string fen = 1;
}
message ProtoImportPgnRequest {
string pgn = 1;
}
message ProtoCombinedExport {
string fen = 1;
string pgn = 2;
}
message ProtoStringResult {
string value = 1;
}
service IoService {
rpc ImportFen (ProtoImportFenRequest) returns (ProtoGameContext);
rpc ImportPgn (ProtoImportPgnRequest) returns (ProtoGameContext);
rpc ExportCombined (ProtoGameContext) returns (ProtoCombinedExport);
rpc ExportFen (ProtoGameContext) returns (ProtoStringResult);
rpc ExportPgn (ProtoGameContext) returns (ProtoStringResult);
}
@@ -1,6 +1,9 @@
quarkus:
http:
port: 8081
grpc:
server:
use-separate-server: false
application:
name: nowchess-io
smallrye-openapi:
@@ -0,0 +1,50 @@
package de.nowchess.io.grpc
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import de.nowchess.io.proto.*
import io.grpc.stub.StreamObserver
import io.grpc.Status
import io.quarkus.grpc.GrpcService
import scala.jdk.CollectionConverters.*
// scalafix:off DisableSyntax.throw
@GrpcService
class IoGrpcService extends IoServiceGrpc.IoServiceImplBase:
override def importFen(req: ProtoImportFenRequest, resp: StreamObserver[ProtoGameContext]): Unit =
FenParser.parseFen(req.getFen) match
case Left(err) =>
resp.onError(Status.INVALID_ARGUMENT.withDescription(err.message).asRuntimeException())
case Right(ctx) =>
respond(resp, IoProtoMapper.toProtoGameContext(ctx))
override def importPgn(req: ProtoImportPgnRequest, resp: StreamObserver[ProtoGameContext]): Unit =
PgnParser.importGameContext(req.getPgn) match
case Left(err) =>
resp.onError(Status.INVALID_ARGUMENT.withDescription(err.message).asRuntimeException())
case Right(ctx) =>
respond(resp, IoProtoMapper.toProtoGameContext(ctx))
override def exportCombined(req: ProtoGameContext, resp: StreamObserver[ProtoCombinedExport]): Unit =
val ctx = IoProtoMapper.fromProtoGameContext(req)
respond(
resp,
ProtoCombinedExport
.newBuilder()
.setFen(FenExporter.exportGameContext(ctx))
.setPgn(PgnExporter.exportGameContext(ctx))
.build(),
)
override def exportFen(req: ProtoGameContext, resp: StreamObserver[ProtoStringResult]): Unit =
respond(resp, ProtoStringResult.newBuilder().setValue(FenExporter.exportGameContext(IoProtoMapper.fromProtoGameContext(req))).build())
override def exportPgn(req: ProtoGameContext, resp: StreamObserver[ProtoStringResult]): Unit =
respond(resp, ProtoStringResult.newBuilder().setValue(PgnExporter.exportGameContext(IoProtoMapper.fromProtoGameContext(req))).build())
private def respond[T](obs: StreamObserver[T], value: T): Unit =
obs.onNext(value)
obs.onCompleted()
// scalafix:on DisableSyntax.throw
@@ -0,0 +1,142 @@
package de.nowchess.io.grpc
import de.nowchess.api.board.*
import de.nowchess.api.board.{CastlingRights as DomainCastlingRights}
import de.nowchess.api.game.{DrawReason, GameContext, GameResult, WinReason}
import de.nowchess.api.move.{Move as DomainMove, MoveType, PromotionPiece}
import de.nowchess.io.proto.*
import scala.jdk.CollectionConverters.*
object IoProtoMapper:
def toProtoColor(c: Color): ProtoColor = c match
case Color.White => ProtoColor.WHITE
case Color.Black => ProtoColor.BLACK
def fromProtoColor(c: ProtoColor): Color = c match
case ProtoColor.WHITE => Color.White
case _ => Color.Black
def toProtoPieceType(pt: PieceType): ProtoPieceType = pt match
case PieceType.Pawn => ProtoPieceType.PAWN
case PieceType.Knight => ProtoPieceType.KNIGHT
case PieceType.Bishop => ProtoPieceType.BISHOP
case PieceType.Rook => ProtoPieceType.ROOK
case PieceType.Queen => ProtoPieceType.QUEEN
case PieceType.King => ProtoPieceType.KING
def fromProtoPieceType(pt: ProtoPieceType): PieceType = pt match
case ProtoPieceType.PAWN => PieceType.Pawn
case ProtoPieceType.KNIGHT => PieceType.Knight
case ProtoPieceType.BISHOP => PieceType.Bishop
case ProtoPieceType.ROOK => PieceType.Rook
case ProtoPieceType.QUEEN => PieceType.Queen
case _ => PieceType.King
def toProtoMoveKind(mt: MoveType): ProtoMoveKind = mt match
case MoveType.Normal(false) => ProtoMoveKind.QUIET
case MoveType.Normal(true) => ProtoMoveKind.CAPTURE
case MoveType.CastleKingside => ProtoMoveKind.CASTLE_KINGSIDE
case MoveType.CastleQueenside => ProtoMoveKind.CASTLE_QUEENSIDE
case MoveType.EnPassant => ProtoMoveKind.EN_PASSANT
case MoveType.Promotion(PromotionPiece.Queen) => ProtoMoveKind.PROMO_QUEEN
case MoveType.Promotion(PromotionPiece.Rook) => ProtoMoveKind.PROMO_ROOK
case MoveType.Promotion(PromotionPiece.Bishop) => ProtoMoveKind.PROMO_BISHOP
case MoveType.Promotion(PromotionPiece.Knight) => ProtoMoveKind.PROMO_KNIGHT
def fromProtoMoveKind(k: ProtoMoveKind): MoveType = k match
case ProtoMoveKind.QUIET => MoveType.Normal(false)
case ProtoMoveKind.CAPTURE => MoveType.Normal(true)
case ProtoMoveKind.CASTLE_KINGSIDE => MoveType.CastleKingside
case ProtoMoveKind.CASTLE_QUEENSIDE => MoveType.CastleQueenside
case ProtoMoveKind.EN_PASSANT => MoveType.EnPassant
case ProtoMoveKind.PROMO_QUEEN => MoveType.Promotion(PromotionPiece.Queen)
case ProtoMoveKind.PROMO_ROOK => MoveType.Promotion(PromotionPiece.Rook)
case ProtoMoveKind.PROMO_BISHOP => MoveType.Promotion(PromotionPiece.Bishop)
case ProtoMoveKind.PROMO_KNIGHT => MoveType.Promotion(PromotionPiece.Knight)
case _ => MoveType.Normal(false)
def toProtoMove(m: DomainMove): ProtoMove =
ProtoMove.newBuilder().setFrom(m.from.toString).setTo(m.to.toString).setMoveKind(toProtoMoveKind(m.moveType)).build()
def fromProtoMove(m: ProtoMove): Option[DomainMove] =
for
from <- Square.fromAlgebraic(m.getFrom)
to <- Square.fromAlgebraic(m.getTo)
yield DomainMove(from, to, fromProtoMoveKind(m.getMoveKind))
def toProtoBoard(board: Board): java.util.List[ProtoSquarePiece] =
board.pieces.map { (sq, piece) =>
ProtoSquarePiece
.newBuilder()
.setSquare(sq.toString)
.setPiece(ProtoPiece.newBuilder().setColor(toProtoColor(piece.color)).setPieceType(toProtoPieceType(piece.pieceType)).build())
.build()
}.toSeq.asJava
def fromProtoBoard(pieces: java.util.List[ProtoSquarePiece]): Board =
Board(pieces.asScala.flatMap(sp => Square.fromAlgebraic(sp.getSquare).map(_ -> Piece(fromProtoColor(sp.getPiece.getColor), fromProtoPieceType(sp.getPiece.getPieceType)))).toMap)
def toProtoResultKind(r: Option[GameResult]): ProtoGameResultKind = r match
case None => ProtoGameResultKind.ONGOING
case Some(GameResult.Win(Color.White, WinReason.Checkmate)) => ProtoGameResultKind.WIN_CHECKMATE_W
case Some(GameResult.Win(Color.Black, WinReason.Checkmate)) => ProtoGameResultKind.WIN_CHECKMATE_B
case Some(GameResult.Win(Color.White, WinReason.Resignation)) => ProtoGameResultKind.WIN_RESIGN_W
case Some(GameResult.Win(Color.Black, WinReason.Resignation)) => ProtoGameResultKind.WIN_RESIGN_B
case Some(GameResult.Win(Color.White, WinReason.TimeControl)) => ProtoGameResultKind.WIN_TIME_W
case Some(GameResult.Win(Color.Black, WinReason.TimeControl)) => ProtoGameResultKind.WIN_TIME_B
case Some(GameResult.Draw(DrawReason.Stalemate)) => ProtoGameResultKind.DRAW_STALEMATE
case Some(GameResult.Draw(DrawReason.InsufficientMaterial)) => ProtoGameResultKind.DRAW_INSUFFICIENT
case Some(GameResult.Draw(DrawReason.FiftyMoveRule)) => ProtoGameResultKind.DRAW_FIFTY_MOVE
case Some(GameResult.Draw(DrawReason.ThreefoldRepetition)) => ProtoGameResultKind.DRAW_THREEFOLD
case Some(GameResult.Draw(DrawReason.Agreement)) => ProtoGameResultKind.DRAW_AGREEMENT
def fromProtoResultKind(k: ProtoGameResultKind): Option[GameResult] = k match
case ProtoGameResultKind.ONGOING => None
case ProtoGameResultKind.WIN_CHECKMATE_W => Some(GameResult.Win(Color.White, WinReason.Checkmate))
case ProtoGameResultKind.WIN_CHECKMATE_B => Some(GameResult.Win(Color.Black, WinReason.Checkmate))
case ProtoGameResultKind.WIN_RESIGN_W => Some(GameResult.Win(Color.White, WinReason.Resignation))
case ProtoGameResultKind.WIN_RESIGN_B => Some(GameResult.Win(Color.Black, WinReason.Resignation))
case ProtoGameResultKind.WIN_TIME_W => Some(GameResult.Win(Color.White, WinReason.TimeControl))
case ProtoGameResultKind.WIN_TIME_B => Some(GameResult.Win(Color.Black, WinReason.TimeControl))
case ProtoGameResultKind.DRAW_STALEMATE => Some(GameResult.Draw(DrawReason.Stalemate))
case ProtoGameResultKind.DRAW_INSUFFICIENT => Some(GameResult.Draw(DrawReason.InsufficientMaterial))
case ProtoGameResultKind.DRAW_FIFTY_MOVE => Some(GameResult.Draw(DrawReason.FiftyMoveRule))
case ProtoGameResultKind.DRAW_THREEFOLD => Some(GameResult.Draw(DrawReason.ThreefoldRepetition))
case ProtoGameResultKind.DRAW_AGREEMENT => Some(GameResult.Draw(DrawReason.Agreement))
case _ => None
def toProtoGameContext(ctx: GameContext): ProtoGameContext =
ProtoGameContext
.newBuilder()
.addAllBoard(toProtoBoard(ctx.board))
.setTurn(toProtoColor(ctx.turn))
.setCastlingRights(
ProtoCastlingRights
.newBuilder()
.setWhiteKingSide(ctx.castlingRights.whiteKingSide)
.setWhiteQueenSide(ctx.castlingRights.whiteQueenSide)
.setBlackKingSide(ctx.castlingRights.blackKingSide)
.setBlackQueenSide(ctx.castlingRights.blackQueenSide)
.build(),
)
.setEnPassantSquare(ctx.enPassantSquare.map(_.toString).getOrElse(""))
.setHalfMoveClock(ctx.halfMoveClock)
.addAllMoves(ctx.moves.map(toProtoMove).asJava)
.setResult(toProtoResultKind(ctx.result))
.addAllInitialBoard(toProtoBoard(ctx.initialBoard))
.build()
def fromProtoGameContext(p: ProtoGameContext): GameContext =
val cr = p.getCastlingRights
GameContext(
board = fromProtoBoard(p.getBoardList),
turn = fromProtoColor(p.getTurn),
castlingRights = DomainCastlingRights(cr.getWhiteKingSide, cr.getWhiteQueenSide, cr.getBlackKingSide, cr.getBlackQueenSide),
enPassantSquare = Option(p.getEnPassantSquare).filter(_.nonEmpty).flatMap(Square.fromAlgebraic),
halfMoveClock = p.getHalfMoveClock,
moves = p.getMovesList.asScala.flatMap(fromProtoMove).toList,
result = fromProtoResultKind(p.getResult),
initialBoard = fromProtoBoard(p.getInitialBoardList),
)