feat(grpc): implement gRPC services for game context management and move handling
This commit is contained in:
@@ -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),
|
||||
)
|
||||
Reference in New Issue
Block a user