@@ -22,8 +22,15 @@ scoverage {
|
||||
scoverageVersion.set(versions["SCOVERAGE"]!!)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDir("build/classes/java/quarkus-generated-sources/grpc")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<ScalaCompile> {
|
||||
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
||||
dependsOn("quarkusGenerateCode")
|
||||
}
|
||||
|
||||
val quarkusPlatformGroupId: String by project
|
||||
@@ -44,6 +51,8 @@ dependencies {
|
||||
}
|
||||
|
||||
implementation(project(":modules:api"))
|
||||
implementation(project(":modules:json"))
|
||||
implementation(project(":modules:security"))
|
||||
|
||||
implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
||||
implementation("io.quarkus:quarkus-rest")
|
||||
@@ -52,6 +61,7 @@ dependencies {
|
||||
implementation("io.quarkus:quarkus-rest-client")
|
||||
implementation("io.quarkus:quarkus-rest-jackson")
|
||||
implementation("io.quarkus:quarkus-config-yaml")
|
||||
implementation("io.quarkus:quarkus-grpc")
|
||||
implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
|
||||
implementation("io.quarkus:quarkus-smallrye-jwt")
|
||||
implementation("io.quarkus:quarkus-smallrye-health")
|
||||
@@ -106,3 +116,16 @@ tasks.reportScoverage {
|
||||
tasks.jar {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
tasks.withType(ScalaCompile::class).configureEach {
|
||||
if (name == "compileScoverageScala") {
|
||||
source = source.asFileTree.matching {
|
||||
exclude("**/grpc/*.scala")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("compileScoverageJava").configure {
|
||||
dependsOn(tasks.named("quarkusGenerateCode"))
|
||||
dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
syntax = "proto3";
|
||||
option java_package = "de.nowchess.rules.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,38 @@
|
||||
syntax = "proto3";
|
||||
option java_package = "de.nowchess.rules.proto";
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "RuleServiceProto";
|
||||
|
||||
import "chess_types.proto";
|
||||
|
||||
message ProtoSquareRequest {
|
||||
ProtoGameContext context = 1;
|
||||
string square = 2;
|
||||
}
|
||||
|
||||
message ProtoMoveRequest {
|
||||
ProtoGameContext context = 1;
|
||||
ProtoMove move = 2;
|
||||
}
|
||||
|
||||
message ProtoMoveList {
|
||||
repeated ProtoMove moves = 1;
|
||||
}
|
||||
|
||||
message ProtoBoolResult {
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
service RuleService {
|
||||
rpc CandidateMoves (ProtoSquareRequest) returns (ProtoMoveList);
|
||||
rpc LegalMoves (ProtoSquareRequest) returns (ProtoMoveList);
|
||||
rpc AllLegalMoves (ProtoGameContext) returns (ProtoMoveList);
|
||||
rpc IsCheck (ProtoGameContext) returns (ProtoBoolResult);
|
||||
rpc IsCheckmate (ProtoGameContext) returns (ProtoBoolResult);
|
||||
rpc IsStalemate (ProtoGameContext) returns (ProtoBoolResult);
|
||||
rpc IsInsufficientMaterial (ProtoGameContext) returns (ProtoBoolResult);
|
||||
rpc IsFiftyMoveRule (ProtoGameContext) returns (ProtoBoolResult);
|
||||
rpc IsThreefoldRepetition (ProtoGameContext) returns (ProtoBoolResult);
|
||||
rpc ApplyMove (ProtoMoveRequest) returns (ProtoGameContext);
|
||||
rpc PostMoveStatus (ProtoGameContext) returns (ProtoPostMoveStatus);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"reflection": [
|
||||
{ "type": "scala.Tuple1[]" },
|
||||
{ "type": "scala.Tuple2[]" },
|
||||
{ "type": "scala.Tuple3[]" },
|
||||
{ "type": "scala.Tuple4[]" },
|
||||
{ "type": "scala.Tuple5[]" },
|
||||
{ "type": "scala.Tuple6[]" },
|
||||
{ "type": "scala.Tuple7[]" },
|
||||
{ "type": "scala.Tuple8[]" },
|
||||
{ "type": "scala.Tuple9[]" },
|
||||
{ "type": "scala.Tuple10[]" },
|
||||
{ "type": "scala.Tuple11[]" },
|
||||
{ "type": "scala.Tuple12[]" },
|
||||
{ "type": "scala.Tuple13[]" },
|
||||
{ "type": "scala.Tuple14[]" },
|
||||
{ "type": "scala.Tuple15[]" },
|
||||
{ "type": "scala.Tuple16[]" },
|
||||
{ "type": "scala.Tuple17[]" },
|
||||
{ "type": "scala.Tuple18[]" },
|
||||
{ "type": "scala.Tuple19[]" },
|
||||
{ "type": "scala.Tuple20[]" },
|
||||
{ "type": "scala.Tuple21[]" },
|
||||
{ "type": "scala.Tuple22[]" },
|
||||
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
quarkus:
|
||||
http:
|
||||
port: 8082
|
||||
grpc:
|
||||
server:
|
||||
use-separate-server: false
|
||||
application:
|
||||
name: rule-service
|
||||
|
||||
nowchess:
|
||||
internal:
|
||||
secret: 123abc
|
||||
|
||||
@@ -2,11 +2,8 @@ package de.nowchess.rules.config
|
||||
|
||||
import com.fasterxml.jackson.core.Version
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.move.MoveType
|
||||
import de.nowchess.rules.json.*
|
||||
import de.nowchess.json.ChessJacksonModule
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@@ -19,11 +16,4 @@ class JacksonConfig extends ObjectMapperCustomizer:
|
||||
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||
// scalafix:on DisableSyntax.null
|
||||
})
|
||||
val mod = new SimpleModule()
|
||||
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||
mod.addSerializer(classOf[Square], new SquareSerializer())
|
||||
mod.addDeserializer(classOf[Square], new SquareDeserializer())
|
||||
mod.addSerializer(classOf[MoveType], new MoveTypeSerializer())
|
||||
mod.addDeserializer(classOf[MoveType], new MoveTypeDeserializer())
|
||||
mapper.registerModule(mod)
|
||||
mapper.registerModule(new ChessJacksonModule())
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package de.nowchess.rules.grpc
|
||||
|
||||
import de.nowchess.api.board.{CastlingRights as DomainCastlingRights, *}
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult, WinReason}
|
||||
import de.nowchess.api.grpc.ProtoMapperBase
|
||||
import de.nowchess.api.move.{Move as DomainMove, MoveType}
|
||||
import de.nowchess.rules.proto.*
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
object ProtoMapper
|
||||
extends ProtoMapperBase[
|
||||
ProtoColor,
|
||||
ProtoPieceType,
|
||||
ProtoMoveKind,
|
||||
ProtoMove,
|
||||
ProtoSquarePiece,
|
||||
java.util.List[ProtoSquarePiece],
|
||||
ProtoCastlingRights,
|
||||
ProtoGameResultKind,
|
||||
ProtoGameContext,
|
||||
]:
|
||||
private val (colorTo, colorFrom) = ProtoMapperBase.colorConversions(ProtoColor.WHITE, ProtoColor.BLACK)
|
||||
private val (pieceTypeTo, pieceTypeFrom) = ProtoMapperBase.pieceTypeConversions(
|
||||
ProtoPieceType.PAWN,
|
||||
ProtoPieceType.KNIGHT,
|
||||
ProtoPieceType.BISHOP,
|
||||
ProtoPieceType.ROOK,
|
||||
ProtoPieceType.QUEEN,
|
||||
ProtoPieceType.KING,
|
||||
)
|
||||
private val (moveKindTo, moveKindFrom) = ProtoMapperBase.moveKindConversions(
|
||||
ProtoMoveKind.QUIET,
|
||||
ProtoMoveKind.CAPTURE,
|
||||
ProtoMoveKind.CASTLE_KINGSIDE,
|
||||
ProtoMoveKind.CASTLE_QUEENSIDE,
|
||||
ProtoMoveKind.EN_PASSANT,
|
||||
ProtoMoveKind.PROMO_QUEEN,
|
||||
ProtoMoveKind.PROMO_ROOK,
|
||||
ProtoMoveKind.PROMO_BISHOP,
|
||||
ProtoMoveKind.PROMO_KNIGHT,
|
||||
)
|
||||
|
||||
override def toProtoColor(c: Color): ProtoColor = colorTo(c)
|
||||
override def fromProtoColor(c: ProtoColor): Color = colorFrom(c)
|
||||
override def toProtoPieceType(pt: PieceType): ProtoPieceType = pieceTypeTo(pt)
|
||||
override def fromProtoPieceType(pt: ProtoPieceType): PieceType = pieceTypeFrom(pt)
|
||||
override def toProtoMoveKind(mt: MoveType): ProtoMoveKind = moveKindTo(mt)
|
||||
override def fromProtoMoveKind(k: ProtoMoveKind): MoveType = moveKindFrom(k)
|
||||
|
||||
override def toProtoMove(m: DomainMove): ProtoMove =
|
||||
ProtoMove
|
||||
.newBuilder()
|
||||
.setFrom(m.from.toString)
|
||||
.setTo(m.to.toString)
|
||||
.setMoveKind(toProtoMoveKind(m.moveType))
|
||||
.build()
|
||||
|
||||
override def fromProtoMove(m: ProtoMove): Option[DomainMove] =
|
||||
for
|
||||
from <- Square.fromAlgebraic(m.getFrom)
|
||||
to <- Square.fromAlgebraic(m.getTo)
|
||||
yield DomainMove(from, to, fromProtoMoveKind(m.getMoveKind))
|
||||
|
||||
override def toProtoSquarePiece(sq: Square, piece: Piece): ProtoSquarePiece =
|
||||
ProtoSquarePiece
|
||||
.newBuilder()
|
||||
.setSquare(sq.toString)
|
||||
.setPiece(
|
||||
ProtoPiece
|
||||
.newBuilder()
|
||||
.setColor(toProtoColor(piece.color))
|
||||
.setPieceType(toProtoPieceType(piece.pieceType))
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
override def fromProtoSquarePiece(sp: ProtoSquarePiece): Option[(Square, Piece)] =
|
||||
Square
|
||||
.fromAlgebraic(sp.getSquare)
|
||||
.map(_ -> Piece(fromProtoColor(sp.getPiece.getColor), fromProtoPieceType(sp.getPiece.getPieceType)))
|
||||
|
||||
override def toProtoBoard(board: Board): java.util.List[ProtoSquarePiece] =
|
||||
board.pieces
|
||||
.map((sq, piece) => toProtoSquarePiece(sq, piece))
|
||||
.toSeq
|
||||
.asJava
|
||||
|
||||
override def fromProtoBoard(pieces: java.util.List[ProtoSquarePiece]): Board =
|
||||
Board(
|
||||
pieces.asScala
|
||||
.flatMap(fromProtoSquarePiece)
|
||||
.toMap,
|
||||
)
|
||||
|
||||
override 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
|
||||
|
||||
override 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
|
||||
|
||||
override def toProtoCastlingRights(cr: DomainCastlingRights): ProtoCastlingRights =
|
||||
ProtoCastlingRights
|
||||
.newBuilder()
|
||||
.setWhiteKingSide(cr.whiteKingSide)
|
||||
.setWhiteQueenSide(cr.whiteQueenSide)
|
||||
.setBlackKingSide(cr.blackKingSide)
|
||||
.setBlackQueenSide(cr.blackQueenSide)
|
||||
.build()
|
||||
|
||||
override def fromProtoCastlingRights(pcr: ProtoCastlingRights): DomainCastlingRights =
|
||||
DomainCastlingRights(pcr.getWhiteKingSide, pcr.getWhiteQueenSide, pcr.getBlackKingSide, pcr.getBlackQueenSide)
|
||||
|
||||
override def toProtoGameContext(ctx: GameContext): ProtoGameContext =
|
||||
ProtoGameContext
|
||||
.newBuilder()
|
||||
.addAllBoard(toProtoBoard(ctx.board))
|
||||
.setTurn(toProtoColor(ctx.turn))
|
||||
.setCastlingRights(toProtoCastlingRights(ctx.castlingRights))
|
||||
.setEnPassantSquare(ctx.enPassantSquare.map(_.toString).getOrElse(""))
|
||||
.setHalfMoveClock(ctx.halfMoveClock)
|
||||
.addAllMoves(ctx.moves.map(toProtoMove).asJava)
|
||||
.setResult(toProtoResultKind(ctx.result))
|
||||
.addAllInitialBoard(toProtoBoard(ctx.initialBoard))
|
||||
.build()
|
||||
|
||||
override def fromProtoGameContext(p: ProtoGameContext): GameContext =
|
||||
GameContext(
|
||||
board = fromProtoBoard(p.getBoardList),
|
||||
turn = fromProtoColor(p.getTurn),
|
||||
castlingRights = fromProtoCastlingRights(p.getCastlingRights),
|
||||
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),
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
package de.nowchess.rules.grpc
|
||||
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.rules.proto.*
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import io.grpc.stub.StreamObserver
|
||||
import io.grpc.{Status, StatusRuntimeException}
|
||||
import io.quarkus.grpc.GrpcService
|
||||
|
||||
// scalafix:off DisableSyntax.throw
|
||||
@GrpcService
|
||||
class RuleGrpcService extends RuleServiceGrpc.RuleServiceImplBase:
|
||||
|
||||
private def parseSquare(s: String): Square =
|
||||
Square
|
||||
.fromAlgebraic(s)
|
||||
.getOrElse(
|
||||
throw Status.INVALID_ARGUMENT.withDescription(s"Invalid square: $s").asRuntimeException(),
|
||||
)
|
||||
|
||||
override def candidateMoves(req: ProtoSquareRequest, resp: StreamObserver[ProtoMoveList]): Unit =
|
||||
val ctx = ProtoMapper.fromProtoGameContext(req.getContext)
|
||||
val sq = parseSquare(req.getSquare)
|
||||
val moves = DefaultRules.candidateMoves(ctx)(sq)
|
||||
resp.onNext(
|
||||
ProtoMoveList
|
||||
.newBuilder()
|
||||
.addAllMoves(toJavaMoveList(moves))
|
||||
.build(),
|
||||
)
|
||||
resp.onCompleted()
|
||||
|
||||
override def legalMoves(req: ProtoSquareRequest, resp: StreamObserver[ProtoMoveList]): Unit =
|
||||
val ctx = ProtoMapper.fromProtoGameContext(req.getContext)
|
||||
val sq = parseSquare(req.getSquare)
|
||||
val moves = DefaultRules.legalMoves(ctx)(sq)
|
||||
respond(resp, ProtoMoveList.newBuilder().addAllMoves(toJavaMoveList(moves)).build())
|
||||
|
||||
override def allLegalMoves(req: ProtoGameContext, resp: StreamObserver[ProtoMoveList]): Unit =
|
||||
val moves = DefaultRules.allLegalMoves(ProtoMapper.fromProtoGameContext(req))
|
||||
respond(resp, ProtoMoveList.newBuilder().addAllMoves(toJavaMoveList(moves)).build())
|
||||
|
||||
override def isCheck(req: ProtoGameContext, resp: StreamObserver[ProtoBoolResult]): Unit =
|
||||
respond(resp, boolResult(DefaultRules.isCheck(ProtoMapper.fromProtoGameContext(req))))
|
||||
|
||||
override def isCheckmate(req: ProtoGameContext, resp: StreamObserver[ProtoBoolResult]): Unit =
|
||||
respond(resp, boolResult(DefaultRules.isCheckmate(ProtoMapper.fromProtoGameContext(req))))
|
||||
|
||||
override def isStalemate(req: ProtoGameContext, resp: StreamObserver[ProtoBoolResult]): Unit =
|
||||
respond(resp, boolResult(DefaultRules.isStalemate(ProtoMapper.fromProtoGameContext(req))))
|
||||
|
||||
override def isInsufficientMaterial(req: ProtoGameContext, resp: StreamObserver[ProtoBoolResult]): Unit =
|
||||
respond(resp, boolResult(DefaultRules.isInsufficientMaterial(ProtoMapper.fromProtoGameContext(req))))
|
||||
|
||||
override def isFiftyMoveRule(req: ProtoGameContext, resp: StreamObserver[ProtoBoolResult]): Unit =
|
||||
respond(resp, boolResult(DefaultRules.isFiftyMoveRule(ProtoMapper.fromProtoGameContext(req))))
|
||||
|
||||
override def isThreefoldRepetition(req: ProtoGameContext, resp: StreamObserver[ProtoBoolResult]): Unit =
|
||||
respond(resp, boolResult(DefaultRules.isThreefoldRepetition(ProtoMapper.fromProtoGameContext(req))))
|
||||
|
||||
override def applyMove(req: ProtoMoveRequest, resp: StreamObserver[ProtoGameContext]): Unit =
|
||||
val ctx = ProtoMapper.fromProtoGameContext(req.getContext)
|
||||
val move = ProtoMapper
|
||||
.fromProtoMove(req.getMove)
|
||||
.getOrElse(
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Invalid move").asRuntimeException(),
|
||||
)
|
||||
respond(resp, ProtoMapper.toProtoGameContext(DefaultRules.applyMove(ctx)(move)))
|
||||
|
||||
override def postMoveStatus(req: ProtoGameContext, resp: StreamObserver[ProtoPostMoveStatus]): Unit =
|
||||
val status = DefaultRules.postMoveStatus(ProtoMapper.fromProtoGameContext(req))
|
||||
respond(
|
||||
resp,
|
||||
ProtoPostMoveStatus
|
||||
.newBuilder()
|
||||
.setIsCheckmate(status.isCheckmate)
|
||||
.setIsStalemate(status.isStalemate)
|
||||
.setIsInsufficientMaterial(status.isInsufficientMaterial)
|
||||
.setIsCheck(status.isCheck)
|
||||
.setIsThreefoldRepetition(status.isThreefoldRepetition)
|
||||
.build(),
|
||||
)
|
||||
|
||||
private def boolResult(v: Boolean): ProtoBoolResult = ProtoBoolResult.newBuilder().setValue(v).build()
|
||||
|
||||
private def respond[T](obs: StreamObserver[T], value: T): Unit =
|
||||
obs.onNext(value)
|
||||
obs.onCompleted()
|
||||
|
||||
private def toJavaMoveList(moves: List[de.nowchess.api.move.Move]): java.util.List[ProtoMove] =
|
||||
import scala.jdk.CollectionConverters.*
|
||||
moves.map(ProtoMapper.toProtoMove).asJava
|
||||
// scalafix:on DisableSyntax.throw
|
||||
@@ -1,19 +0,0 @@
|
||||
package de.nowchess.rules.json
|
||||
|
||||
import com.fasterxml.jackson.core.{JsonParseException, JsonParser}
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.fasterxml.jackson.databind.{DeserializationContext, JsonDeserializer}
|
||||
import de.nowchess.api.move.{MoveType, PromotionPiece}
|
||||
|
||||
class MoveTypeDeserializer extends JsonDeserializer[MoveType]:
|
||||
// scalafix:off DisableSyntax.throw
|
||||
override def deserialize(p: JsonParser, ctx: DeserializationContext): MoveType =
|
||||
val node = p.getCodec.readTree[ObjectNode](p)
|
||||
node.get("type").asText() match
|
||||
case "normal" => MoveType.Normal(node.get("isCapture").asBoolean(false))
|
||||
case "castleKingside" => MoveType.CastleKingside
|
||||
case "castleQueenside" => MoveType.CastleQueenside
|
||||
case "enPassant" => MoveType.EnPassant
|
||||
case "promotion" => MoveType.Promotion(PromotionPiece.valueOf(node.get("piece").asText()))
|
||||
case t => throw new JsonParseException(p, s"Unknown move type: $t")
|
||||
// scalafix:on DisableSyntax.throw
|
||||
@@ -1,23 +0,0 @@
|
||||
package de.nowchess.rules.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
|
||||
import de.nowchess.api.move.MoveType
|
||||
|
||||
class MoveTypeSerializer extends JsonSerializer[MoveType]:
|
||||
override def serialize(value: MoveType, gen: JsonGenerator, provider: SerializerProvider): Unit =
|
||||
gen.writeStartObject()
|
||||
value match
|
||||
case MoveType.Normal(isCapture) =>
|
||||
gen.writeStringField("type", "normal")
|
||||
gen.writeBooleanField("isCapture", isCapture)
|
||||
case MoveType.CastleKingside =>
|
||||
gen.writeStringField("type", "castleKingside")
|
||||
case MoveType.CastleQueenside =>
|
||||
gen.writeStringField("type", "castleQueenside")
|
||||
case MoveType.EnPassant =>
|
||||
gen.writeStringField("type", "enPassant")
|
||||
case MoveType.Promotion(piece) =>
|
||||
gen.writeStringField("type", "promotion")
|
||||
gen.writeStringField("piece", piece.toString)
|
||||
gen.writeEndObject()
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.nowchess.rules.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.{DeserializationContext, JsonDeserializer}
|
||||
import de.nowchess.api.board.Square
|
||||
|
||||
class SquareDeserializer extends JsonDeserializer[Square]:
|
||||
override def deserialize(p: JsonParser, ctx: DeserializationContext): Square =
|
||||
Square.fromAlgebraic(p.getText).orNull
|
||||
@@ -1,8 +0,0 @@
|
||||
package de.nowchess.rules.json
|
||||
|
||||
import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer}
|
||||
import de.nowchess.api.board.Square
|
||||
|
||||
class SquareKeyDeserializer extends KeyDeserializer:
|
||||
override def deserializeKey(key: String, ctx: DeserializationContext): AnyRef =
|
||||
Square.fromAlgebraic(key).orNull
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.nowchess.rules.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
|
||||
import de.nowchess.api.board.Square
|
||||
|
||||
class SquareKeySerializer extends JsonSerializer[Square]:
|
||||
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
|
||||
gen.writeFieldName(value.toString)
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.nowchess.rules.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
|
||||
import de.nowchess.api.board.Square
|
||||
|
||||
class SquareSerializer extends JsonSerializer[Square]:
|
||||
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
|
||||
gen.writeString(value.toString)
|
||||
@@ -4,6 +4,8 @@ import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.rules.dto.*
|
||||
import de.nowchess.api.rules.PostMoveStatus
|
||||
import de.nowchess.security.InternalOnly
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.ws.rs.*
|
||||
@@ -11,6 +13,7 @@ import jakarta.ws.rs.core.MediaType
|
||||
|
||||
@Path("/api/rules")
|
||||
@ApplicationScoped
|
||||
@InternalOnly
|
||||
class RuleSetResource:
|
||||
private val rules = DefaultRules
|
||||
|
||||
@@ -88,3 +91,10 @@ class RuleSetResource:
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def applyMove(req: ContextMoveRequest): GameContext =
|
||||
rules.applyMove(req.context)(req.move)
|
||||
|
||||
@POST
|
||||
@Path("/post-move-status")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def postMoveStatus(ctx: GameContext): PostMoveStatus =
|
||||
rules.postMoveStatus(ctx)
|
||||
|
||||
@@ -3,60 +3,463 @@ package de.nowchess.rules.sets
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
import de.nowchess.api.rules.{PostMoveStatus, RuleSet}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/** Standard chess rules implementation. Handles move generation, validation, check/checkmate/stalemate detection.
|
||||
/** Standard chess rules — optimized hot path.
|
||||
*
|
||||
* Internal representation: Array[Int](64), indexed by file + rank*8. Piece encoding: 0=empty, +1..+6=white
|
||||
* (P/N/B/R/Q/K), -1..-6=black. Move generation uses pre-computed ray/jump tables and an integer-encoded move word to
|
||||
* avoid heap allocation in tight loops. Check detection uses make/unmake on the mutable array instead of copying the
|
||||
* immutable Board map.
|
||||
*/
|
||||
// scalafix:off DisableSyntax.var
|
||||
// scalafix:off DisableSyntax.return
|
||||
object DefaultRules extends RuleSet:
|
||||
|
||||
/** Represents a position for threefold repetition (board state + turn + castling + en passant). */
|
||||
private case class Position(
|
||||
board: Board,
|
||||
turn: Color,
|
||||
castlingRights: CastlingRights,
|
||||
enPassantSquare: Option[Square],
|
||||
)
|
||||
// ─── Piece constants ──────────────────────────────────────────────────────
|
||||
private val PAWN = 1; private val KNIGHT = 2; private val BISHOP = 3
|
||||
private val ROOK = 4; private val QUEEN = 5; private val KING = 6
|
||||
|
||||
// ── Direction vectors ──────────────────────────────────────────────
|
||||
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
|
||||
private val BishopDirs: List[(Int, Int)] = List((1, 1), (1, -1), (-1, 1), (-1, -1))
|
||||
private val QueenDirs: List[(Int, Int)] = RookDirs ++ BishopDirs
|
||||
private val KnightJumps: List[(Int, Int)] =
|
||||
List((2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2))
|
||||
private inline def idx(f: Int, r: Int): Int = f + (r << 3)
|
||||
private inline def fileOf(sq: Int): Int = sq & 7
|
||||
private inline def rankOf(sq: Int): Int = sq >> 3
|
||||
private inline def isEmpty(p: Int): Boolean = p == 0
|
||||
private inline def isWhitePiece(p: Int): Boolean = p > 0
|
||||
private inline def pieceType(p: Int): Int = if p > 0 then p else -p
|
||||
|
||||
// ── Pawn configuration helpers ─────────────────────────────────────
|
||||
private def pawnForward(color: Color): Int = if color == Color.White then 1 else -1
|
||||
private def pawnStartRank(color: Color): Int = if color == Color.White then 1 else 6
|
||||
private def pawnPromoRank(color: Color): Int = if color == Color.White then 7 else 0
|
||||
private def encodePiece(c: Color, pt: PieceType): Int =
|
||||
val raw = pt match
|
||||
case PieceType.Pawn => PAWN; case PieceType.Knight => KNIGHT
|
||||
case PieceType.Bishop => BISHOP; case PieceType.Rook => ROOK
|
||||
case PieceType.Queen => QUEEN; case PieceType.King => KING
|
||||
if c == Color.White then raw else -raw
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────
|
||||
// ─── Pre-computed tables ──────────────────────────────────────────────────
|
||||
|
||||
private val KNIGHT_TARGETS: Array[Array[Int]] = Array.tabulate(64) { sq =>
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
Array((2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2)).collect {
|
||||
case (df, dr) if f + df >= 0 && f + df < 8 && r + dr >= 0 && r + dr < 8 => idx(f + df, r + dr)
|
||||
}
|
||||
}
|
||||
|
||||
private val KING_TARGETS: Array[Array[Int]] = Array.tabulate(64) { sq =>
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
Array((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)).collect {
|
||||
case (df, dr) if f + df >= 0 && f + df < 8 && r + dr >= 0 && r + dr < 8 => idx(f + df, r + dr)
|
||||
}
|
||||
}
|
||||
|
||||
// Directions 0-3: rook (N,S,E,W); 4-7: bishop (NE,NW,SE,SW)
|
||||
private val DIR_VECS: Array[(Int, Int)] =
|
||||
Array((0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (-1, 1), (1, -1), (-1, -1))
|
||||
|
||||
// RAY_TABLES(sq)(d) = squares along direction d from sq, nearest first
|
||||
private val RAY_TABLES: Array[Array[Array[Int]]] = Array.tabulate(64, 8) { (sq, d) =>
|
||||
val (df, dr) = DIR_VECS(d)
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
val buf = new scala.collection.mutable.ArrayBuffer[Int](7)
|
||||
var nf = f + df; var nr = r + dr
|
||||
while nf >= 0 && nf < 8 && nr >= 0 && nr < 8 do
|
||||
buf += idx(nf, nr); nf += df; nr += dr
|
||||
buf.toArray
|
||||
}
|
||||
|
||||
// PAWN_ATTACK_SOURCES(colorIdx)(target) = squares from which a pawn of that color attacks target.
|
||||
// White pawn (fwd=+1) at (f±1, r-1) attacks (f, r) → sources are at rank r-1.
|
||||
// Black pawn (fwd=-1) at (f±1, r+1) attacks (f, r) → sources are at rank r+1.
|
||||
private val PAWN_ATTACK_SOURCES: Array[Array[Array[Int]]] = Array.tabulate(2) { colorIdx =>
|
||||
val fwd = if colorIdx == 0 then 1 else -1
|
||||
Array.tabulate(64) { sq =>
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
Array(-1, 1).collect {
|
||||
case df if f + df >= 0 && f + df < 8 && r - fwd >= 0 && r - fwd < 8 => idx(f + df, r - fwd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-computed castling square indices (no runtime string parsing)
|
||||
private val A1 = idx(0, 0); private val B1 = idx(1, 0); private val C1 = idx(2, 0)
|
||||
private val D1 = idx(3, 0); private val E1 = idx(4, 0); private val F1 = idx(5, 0)
|
||||
private val G1 = idx(6, 0); private val H1 = idx(7, 0)
|
||||
private val A8 = idx(0, 7); private val B8 = idx(1, 7); private val C8 = idx(2, 7)
|
||||
private val D8 = idx(3, 7); private val E8 = idx(4, 7); private val F8 = idx(5, 7)
|
||||
private val G8 = idx(6, 7); private val H8 = idx(7, 7)
|
||||
|
||||
// Thread-local mutable board and move buffer — zero heap allocation in hot loops
|
||||
private val tlBoard = ThreadLocal.withInitial[Array[Int]](() => new Array[Int](64))
|
||||
// 320 slots: theoretical max ~218 chess moves, promotion bursts add 4 per pawn-on-7th
|
||||
private val tlMoves = ThreadLocal.withInitial[Array[Int]](() => new Array[Int](320))
|
||||
|
||||
// ─── Move word encoding ───────────────────────────────────────────────────
|
||||
// bits 0-5: from square, bits 6-11: to square, bits 12-15: move kind
|
||||
private val KIND_QUIET = 0; private val KIND_CAPTURE = 1; private val KIND_EP = 2
|
||||
private val KIND_CASTLEK = 3; private val KIND_CASTLEQ = 4
|
||||
private val KIND_PROMO_Q = 5; private val KIND_PROMO_R = 6
|
||||
private val KIND_PROMO_B = 7; private val KIND_PROMO_N = 8
|
||||
|
||||
private inline def encMove(from: Int, to: Int, kind: Int): Int = from | (to << 6) | (kind << 12)
|
||||
private inline def moveFrom(m: Int): Int = m & 63
|
||||
private inline def moveTo(m: Int): Int = (m >> 6) & 63
|
||||
private inline def moveKind(m: Int): Int = m >> 12
|
||||
|
||||
// ─── Board ↔ Array[Int] ──────────────────────────────────────────────────
|
||||
|
||||
private def fillBoard(board: Board, arr: Array[Int]): Unit =
|
||||
java.util.Arrays.fill(arr, 0)
|
||||
board.pieces.foreach { (sq, piece) =>
|
||||
arr(idx(sq.file.ordinal, sq.rank.ordinal)) = encodePiece(piece.color, piece.pieceType)
|
||||
}
|
||||
|
||||
private def toSquare(sq: Int): Square =
|
||||
Square(File.values(fileOf(sq)), Rank.values(rankOf(sq)))
|
||||
|
||||
// ─── Attack detection (reverse lookup from target) ────────────────────────
|
||||
// Cast rays/jumps FROM the target to find attackers — O(directions × ray_length) vs O(64 × ray_length)
|
||||
|
||||
private def isAttackedByColor(arr: Array[Int], target: Int, byWhite: Boolean): Boolean =
|
||||
val sign = if byWhite then 1 else -1
|
||||
val colorIdx = if byWhite then 0 else 1
|
||||
|
||||
// Pawn
|
||||
val pawnSrcs = PAWN_ATTACK_SOURCES(colorIdx)(target); var i = 0
|
||||
while i < pawnSrcs.length do
|
||||
if arr(pawnSrcs(i)) == sign * PAWN then return true
|
||||
i += 1
|
||||
|
||||
// Knight
|
||||
val knightSrcs = KNIGHT_TARGETS(target); i = 0
|
||||
while i < knightSrcs.length do
|
||||
if arr(knightSrcs(i)) == sign * KNIGHT then return true
|
||||
i += 1
|
||||
|
||||
// King
|
||||
val kingSrcs = KING_TARGETS(target); i = 0
|
||||
while i < kingSrcs.length do
|
||||
if arr(kingSrcs(i)) == sign * KING then return true
|
||||
i += 1
|
||||
|
||||
// Rook/Queen on rook rays (directions 0-3)
|
||||
val rays = RAY_TABLES(target); i = 0
|
||||
while i < 4 do
|
||||
val ray = rays(i); var j = 0
|
||||
while j < ray.length do
|
||||
val p = arr(ray(j))
|
||||
if p != 0 then
|
||||
if p == sign * ROOK || p == sign * QUEEN then return true
|
||||
j = ray.length // blocked
|
||||
j += 1
|
||||
i += 1
|
||||
|
||||
// Bishop/Queen on bishop rays (directions 4-7)
|
||||
i = 4
|
||||
while i < 8 do
|
||||
val ray = rays(i); var j = 0
|
||||
while j < ray.length do
|
||||
val p = arr(ray(j))
|
||||
if p != 0 then
|
||||
if p == sign * BISHOP || p == sign * QUEEN then return true
|
||||
j = ray.length // blocked
|
||||
j += 1
|
||||
i += 1
|
||||
|
||||
false
|
||||
|
||||
private def findKing(arr: Array[Int], whiteKing: Boolean): Int =
|
||||
val king = if whiteKing then KING else -KING; var sq = 0
|
||||
while sq < 64 do
|
||||
if arr(sq) == king then return sq
|
||||
sq += 1
|
||||
-1
|
||||
|
||||
// ─── Make/unmake for check validation ────────────────────────────────────
|
||||
// Applies move on mutable arr, tests check, undoes — no Map copy.
|
||||
|
||||
private def leavesKingInCheck(arr: Array[Int], move: Int, whiteMoved: Boolean): Boolean =
|
||||
val from = moveFrom(move); val to = moveTo(move); val kind = moveKind(move)
|
||||
val savedFrom = arr(from); val savedTo = arr(to)
|
||||
var epSq = -1
|
||||
var rookFrom = -1; var savedRookPiece = 0; var rookTo = -1
|
||||
|
||||
kind match
|
||||
case KIND_EP =>
|
||||
epSq = idx(fileOf(to), rankOf(from))
|
||||
arr(to) = savedFrom; arr(from) = 0; arr(epSq) = 0
|
||||
|
||||
case KIND_CASTLEK =>
|
||||
rookFrom = if whiteMoved then H1 else H8
|
||||
rookTo = if whiteMoved then F1 else F8
|
||||
savedRookPiece = arr(rookFrom)
|
||||
arr(to) = savedFrom; arr(from) = 0; arr(rookTo) = savedRookPiece; arr(rookFrom) = 0
|
||||
|
||||
case KIND_CASTLEQ =>
|
||||
rookFrom = if whiteMoved then A1 else A8
|
||||
rookTo = if whiteMoved then D1 else D8
|
||||
savedRookPiece = arr(rookFrom)
|
||||
arr(to) = savedFrom; arr(from) = 0; arr(rookTo) = savedRookPiece; arr(rookFrom) = 0
|
||||
|
||||
case k if k >= KIND_PROMO_Q =>
|
||||
val promoted = k match
|
||||
case KIND_PROMO_Q => if whiteMoved then QUEEN else -QUEEN
|
||||
case KIND_PROMO_R => if whiteMoved then ROOK else -ROOK
|
||||
case KIND_PROMO_B => if whiteMoved then BISHOP else -BISHOP
|
||||
case _ => if whiteMoved then KNIGHT else -KNIGHT
|
||||
arr(to) = promoted; arr(from) = 0
|
||||
|
||||
case _ =>
|
||||
arr(to) = savedFrom; arr(from) = 0
|
||||
|
||||
val kingSq = findKing(arr, whiteMoved)
|
||||
val inCheck = kingSq >= 0 && isAttackedByColor(arr, kingSq, !whiteMoved)
|
||||
|
||||
// Undo
|
||||
arr(from) = savedFrom; arr(to) = savedTo
|
||||
if epSq >= 0 then arr(epSq) = if whiteMoved then -PAWN else PAWN
|
||||
if rookFrom >= 0 then
|
||||
arr(rookFrom) = savedRookPiece
|
||||
arr(rookTo) = 0
|
||||
|
||||
inCheck
|
||||
|
||||
// ─── Move generation ─────────────────────────────────────────────────────
|
||||
|
||||
private def generateAll(arr: Array[Int], isWhite: Boolean, ctx: GameContext, buf: Array[Int]): Int =
|
||||
var n = 0; var sq = 0
|
||||
while sq < 64 do
|
||||
val p = arr(sq)
|
||||
if !isEmpty(p) && isWhitePiece(p) == isWhite then n = generatePiece(arr, sq, pieceType(p), isWhite, ctx, buf, n)
|
||||
sq += 1
|
||||
n
|
||||
|
||||
private def generatePiece(
|
||||
arr: Array[Int],
|
||||
sq: Int,
|
||||
pt: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
n: Int,
|
||||
): Int =
|
||||
if pt == PAWN then generatePawnMoves(arr, sq, isWhite, ctx, buf, n)
|
||||
else if pt == KNIGHT then generateJumps(arr, sq, isWhite, KNIGHT_TARGETS(sq), buf, n)
|
||||
else if pt == BISHOP then generateRays(arr, sq, isWhite, buf, n, rookRays = false)
|
||||
else if pt == ROOK then generateRays(arr, sq, isWhite, buf, n, rookRays = true)
|
||||
else if pt == QUEEN then
|
||||
val n2 = generateRays(arr, sq, isWhite, buf, n, rookRays = true)
|
||||
generateRays(arr, sq, isWhite, buf, n2, rookRays = false)
|
||||
else generateKingMoves(arr, sq, isWhite, ctx, buf, n)
|
||||
|
||||
private def generateJumps(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
targets: Array[Int],
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
var n = start; var i = 0
|
||||
while i < targets.length do
|
||||
val to = targets(i); val tgt = arr(to)
|
||||
if isEmpty(tgt) then
|
||||
buf(n) = encMove(from, to, KIND_QUIET); n += 1
|
||||
else if isWhitePiece(tgt) != isWhite then
|
||||
buf(n) = encMove(from, to, KIND_CAPTURE); n += 1
|
||||
i += 1
|
||||
n
|
||||
|
||||
private def generateRays(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
rookRays: Boolean,
|
||||
): Int =
|
||||
var n = start
|
||||
val rays = RAY_TABLES(from)
|
||||
val d0 = if rookRays then 0 else 4
|
||||
val d1 = if rookRays then 4 else 8
|
||||
var d = d0
|
||||
while d < d1 do
|
||||
val ray = rays(d); var j = 0
|
||||
while j < ray.length do
|
||||
val to = ray(j); val tgt = arr(to)
|
||||
if isEmpty(tgt) then
|
||||
buf(n) = encMove(from, to, KIND_QUIET); n += 1
|
||||
else
|
||||
if isWhitePiece(tgt) != isWhite then
|
||||
buf(n) = encMove(from, to, KIND_CAPTURE); n += 1
|
||||
j = ray.length
|
||||
j += 1
|
||||
d += 1
|
||||
n
|
||||
|
||||
private def generateKingMoves(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
val n = generateJumps(arr, from, isWhite, KING_TARGETS(from), buf, start)
|
||||
generateCastlingMoves(arr, from, isWhite, ctx, buf, n)
|
||||
|
||||
private def generateCastlingMoves(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
var n = start
|
||||
val cr = ctx.castlingRights
|
||||
if isWhite && from == E1 then
|
||||
if cr.whiteKingSide && isEmpty(arr(F1)) && isEmpty(arr(G1)) &&
|
||||
arr(E1) == KING && arr(H1) == ROOK &&
|
||||
!isAttackedByColor(arr, E1, false) &&
|
||||
!isAttackedByColor(arr, F1, false) &&
|
||||
!isAttackedByColor(arr, G1, false)
|
||||
then
|
||||
buf(n) = encMove(E1, G1, KIND_CASTLEK); n += 1
|
||||
if cr.whiteQueenSide && isEmpty(arr(D1)) && isEmpty(arr(C1)) && isEmpty(arr(B1)) &&
|
||||
arr(E1) == KING && arr(A1) == ROOK &&
|
||||
!isAttackedByColor(arr, E1, false) &&
|
||||
!isAttackedByColor(arr, D1, false) &&
|
||||
!isAttackedByColor(arr, C1, false)
|
||||
then
|
||||
buf(n) = encMove(E1, C1, KIND_CASTLEQ); n += 1
|
||||
else if !isWhite && from == E8 then
|
||||
if cr.blackKingSide && isEmpty(arr(F8)) && isEmpty(arr(G8)) &&
|
||||
arr(E8) == -KING && arr(H8) == -ROOK &&
|
||||
!isAttackedByColor(arr, E8, true) &&
|
||||
!isAttackedByColor(arr, F8, true) &&
|
||||
!isAttackedByColor(arr, G8, true)
|
||||
then
|
||||
buf(n) = encMove(E8, G8, KIND_CASTLEK); n += 1
|
||||
if cr.blackQueenSide && isEmpty(arr(D8)) && isEmpty(arr(C8)) && isEmpty(arr(B8)) &&
|
||||
arr(E8) == -KING && arr(A8) == -ROOK &&
|
||||
!isAttackedByColor(arr, E8, true) &&
|
||||
!isAttackedByColor(arr, D8, true) &&
|
||||
!isAttackedByColor(arr, C8, true)
|
||||
then
|
||||
buf(n) = encMove(E8, C8, KIND_CASTLEQ); n += 1
|
||||
n
|
||||
|
||||
private def generatePawnMoves(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
var n = start
|
||||
val f = fileOf(from); val r = rankOf(from)
|
||||
val fwd = if isWhite then 1 else -1
|
||||
val startRank = if isWhite then 1 else 6
|
||||
val promoRank = if isWhite then 7 else 0
|
||||
val r1 = r + fwd
|
||||
|
||||
if r1 >= 0 && r1 < 8 then
|
||||
val to1 = idx(f, r1)
|
||||
if isEmpty(arr(to1)) then
|
||||
if r1 == promoRank then
|
||||
buf(n) = encMove(from, to1, KIND_PROMO_Q); n += 1
|
||||
buf(n) = encMove(from, to1, KIND_PROMO_R); n += 1
|
||||
buf(n) = encMove(from, to1, KIND_PROMO_B); n += 1
|
||||
buf(n) = encMove(from, to1, KIND_PROMO_N); n += 1
|
||||
else
|
||||
buf(n) = encMove(from, to1, KIND_QUIET); n += 1
|
||||
if r == startRank then
|
||||
val to2 = idx(f, r + fwd * 2)
|
||||
if isEmpty(arr(to2)) then
|
||||
buf(n) = encMove(from, to2, KIND_QUIET); n += 1
|
||||
|
||||
var di = 0
|
||||
while di < 2 do
|
||||
val nf = f + (if di == 0 then -1 else 1)
|
||||
if nf >= 0 && nf < 8 then
|
||||
val to = idx(nf, r1)
|
||||
val tgt = arr(to)
|
||||
if !isEmpty(tgt) && isWhitePiece(tgt) != isWhite then
|
||||
if r1 == promoRank then
|
||||
buf(n) = encMove(from, to, KIND_PROMO_Q); n += 1
|
||||
buf(n) = encMove(from, to, KIND_PROMO_R); n += 1
|
||||
buf(n) = encMove(from, to, KIND_PROMO_B); n += 1
|
||||
buf(n) = encMove(from, to, KIND_PROMO_N); n += 1
|
||||
else
|
||||
buf(n) = encMove(from, to, KIND_CAPTURE); n += 1
|
||||
di += 1
|
||||
|
||||
ctx.enPassantSquare.foreach { epSq =>
|
||||
val epI = idx(epSq.file.ordinal, epSq.rank.ordinal)
|
||||
val epF = fileOf(epI); val epR = rankOf(epI)
|
||||
if epR == r1 && (epF == f - 1 || epF == f + 1) then
|
||||
buf(n) = encMove(from, epI, KIND_EP); n += 1
|
||||
}
|
||||
n
|
||||
|
||||
// ─── Decode integer move word → API Move ─────────────────────────────────
|
||||
|
||||
private def decodeMoveToApi(m: Int): Move =
|
||||
val fromSq = toSquare(moveFrom(m)); val toSq = toSquare(moveTo(m))
|
||||
moveKind(m) match
|
||||
case KIND_QUIET => Move(fromSq, toSq)
|
||||
case KIND_CAPTURE => Move(fromSq, toSq, MoveType.Normal(isCapture = true))
|
||||
case KIND_EP => Move(fromSq, toSq, MoveType.EnPassant)
|
||||
case KIND_CASTLEK => Move(fromSq, toSq, MoveType.CastleKingside)
|
||||
case KIND_CASTLEQ => Move(fromSq, toSq, MoveType.CastleQueenside)
|
||||
case KIND_PROMO_Q => Move(fromSq, toSq, MoveType.Promotion(PromotionPiece.Queen))
|
||||
case KIND_PROMO_R => Move(fromSq, toSq, MoveType.Promotion(PromotionPiece.Rook))
|
||||
case KIND_PROMO_B => Move(fromSq, toSq, MoveType.Promotion(PromotionPiece.Bishop))
|
||||
case _ => Move(fromSq, toSq, MoveType.Promotion(PromotionPiece.Knight))
|
||||
|
||||
// ─── Public RuleSet API ───────────────────────────────────────────────────
|
||||
|
||||
override def candidateMoves(context: GameContext)(square: Square): List[Move] =
|
||||
context.board.pieceAt(square).fold(List.empty[Move]) { piece =>
|
||||
if piece.color != context.turn then List.empty[Move]
|
||||
else
|
||||
piece.pieceType match
|
||||
case PieceType.Pawn => pawnCandidates(context, square, piece.color)
|
||||
case PieceType.Knight => knightCandidates(context, square, piece.color)
|
||||
case PieceType.Bishop => slidingMoves(context, square, piece.color, BishopDirs)
|
||||
case PieceType.Rook => slidingMoves(context, square, piece.color, RookDirs)
|
||||
case PieceType.Queen => slidingMoves(context, square, piece.color, QueenDirs)
|
||||
case PieceType.King => kingCandidates(context, square, piece.color)
|
||||
}
|
||||
val arr = new Array[Int](64)
|
||||
fillBoard(context.board, arr)
|
||||
val sqI = idx(square.file.ordinal, square.rank.ordinal)
|
||||
val piece = arr(sqI)
|
||||
if isEmpty(piece) || isWhitePiece(piece) != (context.turn == Color.White) then return Nil
|
||||
val buf = new Array[Int](64)
|
||||
val n = generatePiece(arr, sqI, pieceType(piece), context.turn == Color.White, context, buf, 0)
|
||||
(0 until n).map(i => decodeMoveToApi(buf(i))).toList
|
||||
|
||||
override def legalMoves(context: GameContext)(square: Square): List[Move] =
|
||||
candidateMoves(context)(square).filter { move =>
|
||||
!leavesKingInCheck(context, move)
|
||||
}
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val sqI = idx(square.file.ordinal, square.rank.ordinal)
|
||||
val piece = arr(sqI)
|
||||
val isWhite = context.turn == Color.White
|
||||
if isEmpty(piece) || isWhitePiece(piece) != isWhite then return Nil
|
||||
val buf = tlMoves.get()
|
||||
val n = generatePiece(arr, sqI, pieceType(piece), isWhite, context, buf, 0)
|
||||
val result = new scala.collection.mutable.ListBuffer[Move]()
|
||||
var i = 0
|
||||
while i < n do
|
||||
if !leavesKingInCheck(arr, buf(i), isWhite) then result += decodeMoveToApi(buf(i))
|
||||
i += 1
|
||||
result.toList
|
||||
|
||||
override def allLegalMoves(context: GameContext): List[Move] =
|
||||
Square.all.flatMap(sq => legalMoves(context)(sq)).toList
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val isWhite = context.turn == Color.White
|
||||
val buf = tlMoves.get()
|
||||
val n = generateAll(arr, isWhite, context, buf)
|
||||
val result = new scala.collection.mutable.ListBuffer[Move]()
|
||||
var i = 0
|
||||
while i < n do
|
||||
if !leavesKingInCheck(arr, buf(i), isWhite) then result += decodeMoveToApi(buf(i))
|
||||
i += 1
|
||||
result.toList
|
||||
|
||||
override def isCheck(context: GameContext): Boolean =
|
||||
kingSquare(context.board, context.turn)
|
||||
.fold(false)(sq => isAttackedBy(context.board, sq, context.turn.opposite))
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val isWhite = context.turn == Color.White
|
||||
val kingSq = findKing(arr, isWhite)
|
||||
kingSq >= 0 && isAttackedByColor(arr, kingSq, !isWhite)
|
||||
|
||||
override def isCheckmate(context: GameContext): Boolean =
|
||||
isCheck(context) && allLegalMoves(context).isEmpty
|
||||
@@ -71,295 +474,10 @@ object DefaultRules extends RuleSet:
|
||||
context.halfMoveClock >= 100
|
||||
|
||||
override def isThreefoldRepetition(context: GameContext): Boolean =
|
||||
val currentPosition = Position(
|
||||
board = context.board,
|
||||
turn = context.turn,
|
||||
castlingRights = context.castlingRights,
|
||||
enPassantSquare = context.enPassantSquare,
|
||||
)
|
||||
val currentPosition = Position(context.board, context.turn, context.castlingRights, context.enPassantSquare)
|
||||
countPositionOccurrences(context, currentPosition) >= 3
|
||||
|
||||
private def countPositionOccurrences(context: GameContext, targetPosition: Position): Int =
|
||||
try
|
||||
val initialCtx = GameContext(
|
||||
board = context.initialBoard,
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights.Initial,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty,
|
||||
initialBoard = context.initialBoard,
|
||||
)
|
||||
|
||||
def positionOf(ctx: GameContext): Position =
|
||||
Position(
|
||||
board = ctx.board,
|
||||
turn = ctx.turn,
|
||||
castlingRights = ctx.castlingRights,
|
||||
enPassantSquare = ctx.enPassantSquare,
|
||||
)
|
||||
|
||||
val initialCount = if positionOf(initialCtx) == targetPosition then 1 else 0
|
||||
|
||||
context.moves
|
||||
.foldLeft((initialCtx, initialCount)) { case ((tempCtx, count), move) =>
|
||||
val nextCtx = applyMove(tempCtx)(move)
|
||||
val nextCount = if positionOf(nextCtx) == targetPosition then count + 1 else count
|
||||
(nextCtx, nextCount)
|
||||
}
|
||||
._2
|
||||
catch
|
||||
case _: Exception =>
|
||||
// If replay fails, conservatively count only the current position (never triggers a draw)
|
||||
1
|
||||
|
||||
// ── Sliding pieces (Bishop, Rook, Queen) ───────────────────────────
|
||||
|
||||
private def slidingMoves(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dirs: List[(Int, Int)],
|
||||
): List[Move] =
|
||||
dirs.flatMap(dir => castRay(context.board, from, color, dir))
|
||||
|
||||
private def castRay(
|
||||
board: Board,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dir: (Int, Int),
|
||||
): List[Move] =
|
||||
@tailrec
|
||||
def loop(sq: Square, acc: List[Move]): List[Move] =
|
||||
sq.offset(dir._1, dir._2) match
|
||||
case None => acc
|
||||
case Some(next) =>
|
||||
board.pieceAt(next) match
|
||||
case None => loop(next, Move(from, next) :: acc)
|
||||
case Some(p) if p.color != color => Move(from, next, MoveType.Normal(isCapture = true)) :: acc
|
||||
case Some(_) => acc
|
||||
loop(from, Nil).reverse
|
||||
|
||||
// ── Knight ─────────────────────────────────────────────────────────
|
||||
|
||||
private def knightCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
KnightJumps.flatMap { (df, dr) =>
|
||||
from.offset(df, dr).flatMap { to =>
|
||||
context.board.pieceAt(to) match
|
||||
case Some(p) if p.color == color => None
|
||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||
case None => Some(Move(from, to))
|
||||
}
|
||||
}
|
||||
|
||||
// ── King ───────────────────────────────────────────────────────────
|
||||
|
||||
private def kingCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
val steps = QueenDirs.flatMap { (df, dr) =>
|
||||
from.offset(df, dr).flatMap { to =>
|
||||
context.board.pieceAt(to) match
|
||||
case Some(p) if p.color == color => None
|
||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||
case None => Some(Move(from, to))
|
||||
}
|
||||
}
|
||||
steps ++ castlingCandidates(context, from, color)
|
||||
|
||||
// ── Castling ───────────────────────────────────────────────────────
|
||||
|
||||
private case class CastlingMove(
|
||||
kingFromAlg: String,
|
||||
kingToAlg: String,
|
||||
middleAlg: String,
|
||||
rookFromAlg: String,
|
||||
moveType: MoveType,
|
||||
)
|
||||
|
||||
private def castlingCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
color match
|
||||
case Color.White => whiteCastles(context, from)
|
||||
case Color.Black => blackCastles(context, from)
|
||||
|
||||
private def whiteCastles(context: GameContext, from: Square): List[Move] =
|
||||
val expected = Square.fromAlgebraic("e1").getOrElse(from)
|
||||
if from != expected then List.empty
|
||||
else
|
||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.whiteKingSide,
|
||||
CastlingMove("e1", "g1", "f1", "h1", MoveType.CastleKingside),
|
||||
)
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.whiteQueenSide,
|
||||
CastlingMove("e1", "c1", "d1", "a1", MoveType.CastleQueenside),
|
||||
)
|
||||
moves.toList
|
||||
|
||||
private def blackCastles(context: GameContext, from: Square): List[Move] =
|
||||
val expected = Square.fromAlgebraic("e8").getOrElse(from)
|
||||
if from != expected then List.empty
|
||||
else
|
||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.blackKingSide,
|
||||
CastlingMove("e8", "g8", "f8", "h8", MoveType.CastleKingside),
|
||||
)
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.blackQueenSide,
|
||||
CastlingMove("e8", "c8", "d8", "a8", MoveType.CastleQueenside),
|
||||
)
|
||||
moves.toList
|
||||
|
||||
private def queensideBSquare(kingToAlg: String): List[String] =
|
||||
kingToAlg match
|
||||
case "c1" => List("b1")
|
||||
case "c8" => List("b8")
|
||||
case _ => List.empty
|
||||
|
||||
private def addCastleMove(
|
||||
context: GameContext,
|
||||
moves: scala.collection.mutable.ListBuffer[Move],
|
||||
castlingRight: Boolean,
|
||||
castlingMove: CastlingMove,
|
||||
): Unit =
|
||||
if castlingRight then
|
||||
val clearSqs = (List(castlingMove.middleAlg, castlingMove.kingToAlg) ++ queensideBSquare(castlingMove.kingToAlg))
|
||||
.flatMap(Square.fromAlgebraic)
|
||||
if squaresEmpty(context.board, clearSqs) then
|
||||
for
|
||||
kf <- Square.fromAlgebraic(castlingMove.kingFromAlg)
|
||||
km <- Square.fromAlgebraic(castlingMove.middleAlg)
|
||||
kt <- Square.fromAlgebraic(castlingMove.kingToAlg)
|
||||
rf <- Square.fromAlgebraic(castlingMove.rookFromAlg)
|
||||
do
|
||||
val color = context.turn
|
||||
val kingPresent = context.board.pieceAt(kf).exists(p => p.color == color && p.pieceType == PieceType.King)
|
||||
val rookPresent = context.board.pieceAt(rf).exists(p => p.color == color && p.pieceType == PieceType.Rook)
|
||||
val squaresSafe =
|
||||
!isAttackedBy(context.board, kf, color.opposite) &&
|
||||
!isAttackedBy(context.board, km, color.opposite) &&
|
||||
!isAttackedBy(context.board, kt, color.opposite)
|
||||
|
||||
if kingPresent && rookPresent && squaresSafe then moves += Move(kf, kt, castlingMove.moveType)
|
||||
|
||||
private def squaresEmpty(board: Board, squares: List[Square]): Boolean =
|
||||
squares.forall(sq => board.pieceAt(sq).isEmpty)
|
||||
|
||||
// ── Pawn ───────────────────────────────────────────────────────────
|
||||
|
||||
private def pawnCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
val fwd = pawnForward(color)
|
||||
val startRank = pawnStartRank(color)
|
||||
val promoRank = pawnPromoRank(color)
|
||||
|
||||
val single = from.offset(0, fwd).filter(to => context.board.pieceAt(to).isEmpty)
|
||||
val double = Option
|
||||
.when(from.rank.ordinal == startRank) {
|
||||
from.offset(0, fwd).flatMap { mid =>
|
||||
Option
|
||||
.when(context.board.pieceAt(mid).isEmpty) {
|
||||
from.offset(0, fwd * 2).filter(to => context.board.pieceAt(to).isEmpty)
|
||||
}
|
||||
.flatten
|
||||
}
|
||||
}
|
||||
.flatten
|
||||
|
||||
val diagonalCaptures = List(-1, 1).flatMap { df =>
|
||||
from.offset(df, fwd).flatMap { to =>
|
||||
context.board.pieceAt(to).filter(_.color != color).map(_ => to)
|
||||
}
|
||||
}
|
||||
|
||||
val epCaptures: List[Move] = context.enPassantSquare.toList.flatMap { epSq =>
|
||||
List(-1, 1).flatMap { df =>
|
||||
from.offset(df, fwd).filter(_ == epSq).map { to =>
|
||||
Move(from, epSq, MoveType.EnPassant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def toMoves(dest: Square, isCapture: Boolean): List[Move] =
|
||||
if dest.rank.ordinal == promoRank then
|
||||
List(
|
||||
PromotionPiece.Queen,
|
||||
PromotionPiece.Rook,
|
||||
PromotionPiece.Bishop,
|
||||
PromotionPiece.Knight,
|
||||
).map(pt => Move(from, dest, MoveType.Promotion(pt)))
|
||||
else List(Move(from, dest, MoveType.Normal(isCapture = isCapture)))
|
||||
|
||||
val stepSquares = single.toList ++ double.toList
|
||||
val stepMoves = stepSquares.flatMap(dest => toMoves(dest, isCapture = false))
|
||||
val captureMoves = diagonalCaptures.flatMap(dest => toMoves(dest, isCapture = true))
|
||||
stepMoves ++ captureMoves ++ epCaptures
|
||||
|
||||
// ── Check detection ────────────────────────────────────────────────
|
||||
|
||||
private def kingSquare(board: Board, color: Color): Option[Square] =
|
||||
Square.all.find(sq => board.pieceAt(sq).exists(p => p.color == color && p.pieceType == PieceType.King))
|
||||
|
||||
private def isAttackedBy(board: Board, target: Square, attacker: Color): Boolean =
|
||||
Square.all.exists { sq =>
|
||||
board.pieceAt(sq).fold(false) { p =>
|
||||
p.color == attacker && squareAttacks(board, sq, p, target)
|
||||
}
|
||||
}
|
||||
|
||||
private def squareAttacks(board: Board, from: Square, piece: Piece, target: Square): Boolean =
|
||||
val fwd = pawnForward(piece.color)
|
||||
piece.pieceType match
|
||||
case PieceType.Pawn =>
|
||||
from.offset(-1, fwd).contains(target) || from.offset(1, fwd).contains(target)
|
||||
case PieceType.Knight =>
|
||||
KnightJumps.exists((df, dr) => from.offset(df, dr).contains(target))
|
||||
case PieceType.Bishop => rayReaches(board, from, BishopDirs, target)
|
||||
case PieceType.Rook => rayReaches(board, from, RookDirs, target)
|
||||
case PieceType.Queen => rayReaches(board, from, QueenDirs, target)
|
||||
case PieceType.King =>
|
||||
QueenDirs.exists((df, dr) => from.offset(df, dr).contains(target))
|
||||
|
||||
private def rayReaches(board: Board, from: Square, dirs: List[(Int, Int)], target: Square): Boolean =
|
||||
dirs.exists { dir =>
|
||||
@tailrec
|
||||
def loop(sq: Square): Boolean = sq.offset(dir._1, dir._2) match
|
||||
case None => false
|
||||
case Some(next) if next == target => true
|
||||
case Some(next) if board.pieceAt(next).isEmpty => loop(next)
|
||||
case Some(_) => false
|
||||
loop(from)
|
||||
}
|
||||
|
||||
private def leavesKingInCheck(context: GameContext, move: Move): Boolean =
|
||||
val nextBoard = context.board.applyMove(move)
|
||||
val nextContext = context.withBoard(nextBoard)
|
||||
isCheck(nextContext)
|
||||
|
||||
// ── Move application ───────────────────────────────────────────────
|
||||
// ─── applyMove (immutable GameContext update — acceptable for real moves) ─
|
||||
|
||||
override def applyMove(context: GameContext)(move: Move): GameContext =
|
||||
val color = context.turn
|
||||
@@ -389,6 +507,8 @@ object DefaultRules extends RuleSet:
|
||||
.withHalfMoveClock(newClock)
|
||||
.withMove(move)
|
||||
|
||||
// ─── Move application helpers ─────────────────────────────────────────────
|
||||
|
||||
private def applyCastle(board: Board, color: Color, kingside: Boolean): Board =
|
||||
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
||||
val (kingFrom, kingTo, rookFrom, rookTo) =
|
||||
@@ -396,15 +516,10 @@ object DefaultRules extends RuleSet:
|
||||
else (Square(File.E, rank), Square(File.C, rank), Square(File.A, rank), Square(File.D, rank))
|
||||
val king = board.pieceAt(kingFrom).getOrElse(Piece(color, PieceType.King))
|
||||
val rook = board.pieceAt(rookFrom).getOrElse(Piece(color, PieceType.Rook))
|
||||
board
|
||||
.removed(kingFrom)
|
||||
.removed(rookFrom)
|
||||
.updated(kingTo, king)
|
||||
.updated(rookTo, rook)
|
||||
board.removed(kingFrom).removed(rookFrom).updated(kingTo, king).updated(rookTo, rook)
|
||||
|
||||
private def applyEnPassant(board: Board, move: Move): Board =
|
||||
val capturedRank = move.from.rank // the captured pawn is on the same rank as the moving pawn
|
||||
val capturedSquare = Square(move.to.file, capturedRank)
|
||||
val capturedSquare = Square(move.to.file, move.from.rank)
|
||||
board.applyMove(move).removed(capturedSquare)
|
||||
|
||||
private def applyPromotion(board: Board, move: Move, color: Color, pp: PromotionPiece): Board =
|
||||
@@ -420,14 +535,10 @@ object DefaultRules extends RuleSet:
|
||||
val isKingMove = piece.exists(_.pieceType == PieceType.King)
|
||||
val isRookMove = piece.exists(_.pieceType == PieceType.Rook)
|
||||
|
||||
// Helper to check if a square is a rook's starting square
|
||||
val whiteKingsideRook = Square(File.H, Rank.R1)
|
||||
val whiteQueensideRook = Square(File.A, Rank.R1)
|
||||
val blackKingsideRook = Square(File.H, Rank.R8)
|
||||
val blackQueensideRook = Square(File.A, Rank.R8)
|
||||
val whiteKingsideRook = Square(File.H, Rank.R1); val whiteQueensideRook = Square(File.A, Rank.R1)
|
||||
val blackKingsideRook = Square(File.H, Rank.R8); val blackQueensideRook = Square(File.A, Rank.R8)
|
||||
|
||||
val afterKingMove = if isKingMove then rights.revokeColor(color) else rights
|
||||
|
||||
val afterRookMove =
|
||||
if !isRookMove then afterKingMove
|
||||
else
|
||||
@@ -438,7 +549,6 @@ object DefaultRules extends RuleSet:
|
||||
case `blackQueensideRook` => afterKingMove.revokeQueenSide(Color.Black)
|
||||
case _ => afterKingMove
|
||||
|
||||
// Also revoke if a rook is captured
|
||||
move.to match
|
||||
case `whiteKingsideRook` => afterRookMove.revokeKingSide(Color.White)
|
||||
case `whiteQueensideRook` => afterRookMove.revokeQueenSide(Color.White)
|
||||
@@ -447,16 +557,14 @@ object DefaultRules extends RuleSet:
|
||||
case _ => afterRookMove
|
||||
|
||||
private def computeEnPassantSquare(board: Board, move: Move): Option[Square] =
|
||||
val piece = board.pieceAt(move.from)
|
||||
val isDoublePawnPush = piece.exists(_.pieceType == PieceType.Pawn) &&
|
||||
val isDoublePawnPush = board.pieceAt(move.from).exists(_.pieceType == PieceType.Pawn) &&
|
||||
math.abs(move.to.rank.ordinal - move.from.rank.ordinal) == 2
|
||||
if isDoublePawnPush then
|
||||
// EP square is the square the pawn passed through
|
||||
val epRankOrd = (move.from.rank.ordinal + move.to.rank.ordinal) / 2
|
||||
Some(Square(move.from.file, Rank.values(epRankOrd)))
|
||||
else None
|
||||
|
||||
// ── Insufficient material ──────────────────────────────────────────
|
||||
// ─── Insufficient material ────────────────────────────────────────────────
|
||||
|
||||
private def squareColor(sq: Square): Int = (sq.file.ordinal + sq.rank.ordinal) % 2
|
||||
|
||||
@@ -465,7 +573,50 @@ object DefaultRules extends RuleSet:
|
||||
nonKings match
|
||||
case Nil => true
|
||||
case List((_, p)) if p.pieceType == PieceType.Bishop || p.pieceType == PieceType.Knight => true
|
||||
case bishops if bishops.forall { case (_, p) => p.pieceType == PieceType.Bishop } =>
|
||||
// All non-king pieces are bishops: draw only if they all share the same square color
|
||||
case bishops if bishops.forall { case (_, p) => p.pieceType == PieceType.Bishop } =>
|
||||
bishops.map { case (sq, _) => squareColor(sq) }.distinct.sizeIs == 1
|
||||
case _ => false
|
||||
|
||||
// ─── Threefold repetition ─────────────────────────────────────────────────
|
||||
|
||||
private case class Position(
|
||||
board: Board,
|
||||
turn: Color,
|
||||
castlingRights: CastlingRights,
|
||||
enPassantSquare: Option[Square],
|
||||
)
|
||||
|
||||
private def countPositionOccurrences(context: GameContext, target: Position): Int =
|
||||
try
|
||||
val initialCtx = GameContext(
|
||||
board = context.initialBoard,
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights.Initial,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty,
|
||||
initialBoard = context.initialBoard,
|
||||
)
|
||||
|
||||
def positionOf(ctx: GameContext): Position =
|
||||
Position(ctx.board, ctx.turn, ctx.castlingRights, ctx.enPassantSquare)
|
||||
|
||||
val initialCount = if positionOf(initialCtx) == target then 1 else 0
|
||||
|
||||
context.moves
|
||||
.foldLeft((initialCtx, initialCount)) { case ((tempCtx, count), move) =>
|
||||
val nextCtx = applyMove(tempCtx)(move)
|
||||
val nextCount = if positionOf(nextCtx) == target then count + 1 else count
|
||||
(nextCtx, nextCount)
|
||||
}
|
||||
._2
|
||||
catch case _: Exception => 1
|
||||
|
||||
override def postMoveStatus(context: GameContext): PostMoveStatus =
|
||||
PostMoveStatus(
|
||||
isCheckmate = isCheckmate(context),
|
||||
isStalemate = isStalemate(context),
|
||||
isInsufficientMaterial = isInsufficientMaterial(context),
|
||||
isCheck = isCheck(context),
|
||||
isThreefoldRepetition = isThreefoldRepetition(context),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
nowchess:
|
||||
internal:
|
||||
secret: test-secret
|
||||
auth:
|
||||
enabled: false
|
||||
@@ -11,7 +11,7 @@ import org.scalatest.matchers.should.Matchers
|
||||
class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def contextFromFen(fen: String): GameContext =
|
||||
FenParser.parseFen(fen).fold(err => fail(err), identity)
|
||||
FenParser.parseFen(fen).fold(err => fail(err.message), identity)
|
||||
|
||||
private def sq(alg: String): Square =
|
||||
Square.fromAlgebraic(alg).getOrElse(fail(s"Invalid square in test: $alg"))
|
||||
|
||||
@@ -1,102 +1,31 @@
|
||||
package de.nowchess.rules.json
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.move.{MoveType, PromotionPiece}
|
||||
import de.nowchess.api.move.MoveType
|
||||
import de.nowchess.rules.config.JacksonConfig
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class JsonSerializersTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private val mapper: ObjectMapper =
|
||||
val m = new ObjectMapper()
|
||||
val mod = new SimpleModule()
|
||||
m.registerModule(DefaultScalaModule)
|
||||
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||
mod.addSerializer(classOf[Square], new SquareSerializer())
|
||||
mod.addDeserializer(classOf[Square], new SquareDeserializer())
|
||||
mod.addSerializer(classOf[MoveType], new MoveTypeSerializer())
|
||||
mod.addDeserializer(classOf[MoveType], new MoveTypeDeserializer())
|
||||
m.registerModule(mod)
|
||||
val m = new ObjectMapper()
|
||||
new JacksonConfig().customize(m)
|
||||
m
|
||||
|
||||
private val e4 = Square(File.E, Rank.R4)
|
||||
test("customize enables Option serialization via DefaultScalaModule"):
|
||||
mapper.writeValueAsString(None) shouldBe "null"
|
||||
mapper.writeValueAsString(Some("hello")) shouldBe """"hello""""
|
||||
|
||||
// ── SquareKeySerializer ───────────────────────────────────────────
|
||||
test("customize registers SquareSerializer"):
|
||||
mapper.writeValueAsString(Square(File.E, Rank.R4)) shouldBe """"e4""""
|
||||
|
||||
test("SquareKeySerializer writes square as map field name"):
|
||||
mapper.writeValueAsString(Map(e4 -> "piece")) shouldBe """{"e4":"piece"}"""
|
||||
test("customize registers SquareDeserializer"):
|
||||
mapper.readValue(""""e4"""", classOf[Square]) shouldBe Square(File.E, Rank.R4)
|
||||
|
||||
// ── SquareKeyDeserializer ─────────────────────────────────────────
|
||||
|
||||
// scalafix:off DisableSyntax.null
|
||||
test("SquareKeyDeserializer returns square for valid key"):
|
||||
new SquareKeyDeserializer().deserializeKey("e4", null) shouldBe e4
|
||||
|
||||
test("SquareKeyDeserializer returns null for invalid key"):
|
||||
new SquareKeyDeserializer().deserializeKey("z9", null) shouldBe null
|
||||
// scalafix:on DisableSyntax.null
|
||||
|
||||
// ── SquareSerializer/Deserializer ─────────────────────────────────
|
||||
|
||||
test("SquareSerializer writes square as string"):
|
||||
mapper.writeValueAsString(e4) shouldBe """"e4""""
|
||||
|
||||
test("SquareDeserializer reads valid square string"):
|
||||
mapper.readValue(""""e4"""", classOf[Square]) shouldBe e4
|
||||
|
||||
// scalafix:off DisableSyntax.null
|
||||
test("SquareDeserializer returns null for invalid square string"):
|
||||
mapper.readValue(""""z9"""", classOf[Square]) shouldBe null
|
||||
// scalafix:on DisableSyntax.null
|
||||
|
||||
// ── MoveTypeSerializer ────────────────────────────────────────────
|
||||
|
||||
test("MoveTypeSerializer serializes Normal non-capture"):
|
||||
mapper.writeValueAsString(MoveType.Normal(false)) shouldBe """{"type":"normal","isCapture":false}"""
|
||||
|
||||
test("MoveTypeSerializer serializes Normal capture"):
|
||||
mapper.writeValueAsString(MoveType.Normal(true)) shouldBe """{"type":"normal","isCapture":true}"""
|
||||
|
||||
test("MoveTypeSerializer serializes CastleKingside"):
|
||||
test("customize registers MoveTypeSerializer"):
|
||||
mapper.writeValueAsString(MoveType.CastleKingside) shouldBe """{"type":"castleKingside"}"""
|
||||
|
||||
test("MoveTypeSerializer serializes CastleQueenside"):
|
||||
mapper.writeValueAsString(MoveType.CastleQueenside) shouldBe """{"type":"castleQueenside"}"""
|
||||
|
||||
test("MoveTypeSerializer serializes EnPassant"):
|
||||
mapper.writeValueAsString(MoveType.EnPassant) shouldBe """{"type":"enPassant"}"""
|
||||
|
||||
test("MoveTypeSerializer serializes Promotion"):
|
||||
mapper.writeValueAsString(MoveType.Promotion(PromotionPiece.Queen)) shouldBe
|
||||
"""{"type":"promotion","piece":"Queen"}"""
|
||||
|
||||
// ── MoveTypeDeserializer ──────────────────────────────────────────
|
||||
|
||||
test("MoveTypeDeserializer deserializes normal non-capture"):
|
||||
mapper.readValue("""{"type":"normal","isCapture":false}""", classOf[MoveType]) shouldBe
|
||||
MoveType.Normal(false)
|
||||
|
||||
test("MoveTypeDeserializer deserializes normal capture"):
|
||||
mapper.readValue("""{"type":"normal","isCapture":true}""", classOf[MoveType]) shouldBe
|
||||
MoveType.Normal(true)
|
||||
|
||||
test("MoveTypeDeserializer deserializes castleKingside"):
|
||||
mapper.readValue("""{"type":"castleKingside"}""", classOf[MoveType]) shouldBe MoveType.CastleKingside
|
||||
|
||||
test("MoveTypeDeserializer deserializes castleQueenside"):
|
||||
mapper.readValue("""{"type":"castleQueenside"}""", classOf[MoveType]) shouldBe MoveType.CastleQueenside
|
||||
|
||||
test("MoveTypeDeserializer deserializes enPassant"):
|
||||
test("customize registers MoveTypeDeserializer"):
|
||||
mapper.readValue("""{"type":"enPassant"}""", classOf[MoveType]) shouldBe MoveType.EnPassant
|
||||
|
||||
test("MoveTypeDeserializer deserializes promotion"):
|
||||
mapper.readValue("""{"type":"promotion","piece":"Rook"}""", classOf[MoveType]) shouldBe
|
||||
MoveType.Promotion(PromotionPiece.Rook)
|
||||
|
||||
test("MoveTypeDeserializer throws for unknown type"):
|
||||
an[Exception] should be thrownBy
|
||||
mapper.readValue("""{"type":"unknown"}""", classOf[MoveType])
|
||||
|
||||
Reference in New Issue
Block a user