test(rules): Rules as a microservice

Added tests to rules
This commit is contained in:
LQ63
2026-04-21 18:08:33 +02:00
parent 5a186db349
commit c09a618dba
3 changed files with 180 additions and 0 deletions
@@ -0,0 +1,14 @@
package de.nowchess.rules.config
import com.fasterxml.jackson.databind.ObjectMapper
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JacksonConfigTest extends AnyFunSuite with Matchers:
test("customize registers DefaultScalaModule enabling Option serialization"):
val config = new JacksonConfig()
val mapper = new ObjectMapper()
config.customize(mapper)
mapper.writeValueAsString(None) shouldBe "null"
mapper.writeValueAsString(Some("hello")) shouldBe """"hello""""
@@ -138,6 +138,12 @@ class DtoMapperTest extends AnyFunSuite with Matchers:
val ctx = GameContext.initial.copy(enPassantSquare = Some(Square(File.E, Rank.R3)))
DtoMapper.fromGameContext(ctx).enPassantSquare shouldBe Some("e3")
test("toGameContext round-trips a valid en passant square"):
val ctx = GameContext.initial.copy(enPassantSquare = Some(Square(File.E, Rank.R3)))
DtoMapper.toGameContext(DtoMapper.fromGameContext(ctx)).map(_.enPassantSquare) shouldBe Right(
Some(Square(File.E, Rank.R3)),
)
// ── fromMove ──────────────────────────────────────────────────────
test("fromMove converts all move types"):
@@ -0,0 +1,160 @@
package de.nowchess.rules.resource
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType}
import de.nowchess.rules.dto.{ContextMoveRequest, ContextRequest, ContextSquareRequest, DtoMapper}
import de.nowchess.rules.sets.DefaultRules
import jakarta.ws.rs.BadRequestException
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class RuleSetResourceUnitTest extends AnyFunSuite with Matchers:
private val resource = new RuleSetResource()
private val rules = DefaultRules
private def ctx(g: GameContext) = ContextRequest(DtoMapper.fromGameContext(g))
private def ctxSq(g: GameContext, sq: String) = ContextSquareRequest(DtoMapper.fromGameContext(g), sq)
private def ctxMv(g: GameContext, m: Move) = ContextMoveRequest(DtoMapper.fromGameContext(g), DtoMapper.fromMove(m))
// ── position builders ─────────────────────────────────────────────
private def checkContext(): GameContext =
val board = Board(Map(
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King),
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King),
Square(File.E, Rank.R3) -> Piece(Color.Black, PieceType.Rook),
))
GameContext(board, Color.White, CastlingRights.None, None, 0, List.empty, initialBoard = board)
private def foolsMate(): GameContext =
val moves = List(("f2", "f3"), ("e7", "e5"), ("g2", "g4"), ("d8", "h4"))
moves.foldLeft(GameContext.initial) { (c, ft) =>
val from = Square.fromAlgebraic(ft._1).get
val to = Square.fromAlgebraic(ft._2).get
rules.legalMoves(c)(from).find(_.to == to).fold(c)(rules.applyMove(c))
}
private def stalemateContext(): GameContext =
val board = Board(Map(
Square(File.H, Rank.R8) -> Piece(Color.Black, PieceType.King),
Square(File.F, Rank.R7) -> Piece(Color.White, PieceType.Queen),
Square(File.G, Rank.R6) -> Piece(Color.White, PieceType.King),
))
GameContext(board, Color.Black, CastlingRights.None, None, 0, List.empty, initialBoard = board)
private def kingsOnlyContext(): GameContext =
val board = Board(Map(
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King),
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King),
))
GameContext(board, Color.White, CastlingRights.None, None, 0, List.empty, initialBoard = board)
private def threefoldContext(): GameContext =
val g1 = Square(File.G, Rank.R1)
val f3 = Square(File.F, Rank.R3)
val g8 = Square(File.G, Rank.R8)
val f6 = Square(File.F, Rank.R6)
def mv(c: GameContext, from: Square, to: Square): GameContext =
rules.legalMoves(c)(from).find(_.to == to).fold(c)(rules.applyMove(c))
val c1 = mv(GameContext.initial, g1, f3)
val c2 = mv(c1, g8, f6)
val c3 = mv(c2, f3, g1)
val c4 = mv(c3, f6, g8)
val c5 = mv(c4, g1, f3)
val c6 = mv(c5, g8, f6)
val c7 = mv(c6, f3, g1)
mv(c7, f6, g8)
// ── allLegalMoves ─────────────────────────────────────────────────
test("allLegalMoves returns 20 moves for initial position"):
resource.allLegalMoves(ctx(GameContext.initial)).moves should have size 20
// ── legalMoves ────────────────────────────────────────────────────
test("legalMoves returns 2 moves for e2 pawn"):
resource.legalMoves(ctxSq(GameContext.initial, "e2")).moves should have size 2
test("legalMoves throws BadRequestException for invalid square"):
an[BadRequestException] should be thrownBy
resource.legalMoves(ctxSq(GameContext.initial, "z9"))
// ── candidateMoves ────────────────────────────────────────────────
test("candidateMoves returns moves for e2 pawn"):
resource.candidateMoves(ctxSq(GameContext.initial, "e2")).moves should not be empty
test("candidateMoves throws BadRequestException for invalid square"):
an[BadRequestException] should be thrownBy
resource.candidateMoves(ctxSq(GameContext.initial, "z9"))
// ── isCheck ───────────────────────────────────────────────────────
test("isCheck returns false for initial position"):
resource.isCheck(ctx(GameContext.initial)).result shouldBe false
test("isCheck returns true when king is attacked"):
resource.isCheck(ctx(checkContext())).result shouldBe true
test("isCheck throws BadRequestException for invalid context"):
an[BadRequestException] should be thrownBy
resource.isCheck(ctx(GameContext.initial).copy(context =
DtoMapper.fromGameContext(GameContext.initial).copy(turn = "Red"),
))
// ── isCheckmate ───────────────────────────────────────────────────
test("isCheckmate returns false for initial position"):
resource.isCheckmate(ctx(GameContext.initial)).result shouldBe false
test("isCheckmate returns true for Fool's mate"):
resource.isCheckmate(ctx(foolsMate())).result shouldBe true
// ── isStalemate ───────────────────────────────────────────────────
test("isStalemate returns false for initial position"):
resource.isStalemate(ctx(GameContext.initial)).result shouldBe false
test("isStalemate returns true for stalemate position"):
resource.isStalemate(ctx(stalemateContext())).result shouldBe true
// ── isInsufficientMaterial ────────────────────────────────────────
test("isInsufficientMaterial returns false for initial position"):
resource.isInsufficientMaterial(ctx(GameContext.initial)).result shouldBe false
test("isInsufficientMaterial returns true for kings only"):
resource.isInsufficientMaterial(ctx(kingsOnlyContext())).result shouldBe true
// ── isFiftyMoveRule ───────────────────────────────────────────────
test("isFiftyMoveRule returns false for initial position"):
resource.isFiftyMoveRule(ctx(GameContext.initial)).result shouldBe false
test("isFiftyMoveRule returns true when halfMoveClock is 100"):
resource.isFiftyMoveRule(ctx(GameContext.initial.copy(halfMoveClock = 100))).result shouldBe true
// ── isThreefoldRepetition ─────────────────────────────────────────
test("isThreefoldRepetition returns false for initial position"):
resource.isThreefoldRepetition(ctx(GameContext.initial)).result shouldBe false
test("isThreefoldRepetition returns true after repeated moves"):
resource.isThreefoldRepetition(ctx(threefoldContext())).result shouldBe true
// ── applyMove ─────────────────────────────────────────────────────
test("applyMove returns updated context with switched turn"):
val move = rules
.legalMoves(GameContext.initial)(Square(File.E, Rank.R2))
.find(_.to == Square(File.E, Rank.R4))
.get
resource.applyMove(ctxMv(GameContext.initial, move)).turn shouldBe "Black"
test("applyMove throws BadRequestException for invalid move"):
val badMove = DtoMapper.fromMove(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)))
.copy(moveType = "unknown")
an[BadRequestException] should be thrownBy
resource.applyMove(ContextMoveRequest(DtoMapper.fromGameContext(GameContext.initial), badMove))