feat(rule): Rules as a microservice (#39)
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Co-authored-by: LQ63 <lkhermann@web.de> Co-authored-by: TeamCity <teamcity@service.local> Reviewed-on: #39
This commit was merged in pull request #39.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package de.nowchess.rules.config
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.move.MoveType
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class JacksonConfigTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private val mapper: ObjectMapper =
|
||||
val m = new ObjectMapper()
|
||||
new JacksonConfig().customize(m)
|
||||
m
|
||||
|
||||
test("customize enables Option serialization via DefaultScalaModule"):
|
||||
mapper.writeValueAsString(None) shouldBe "null"
|
||||
mapper.writeValueAsString(Some("hello")) shouldBe """"hello""""
|
||||
|
||||
test("customize registers SquareSerializer"):
|
||||
mapper.writeValueAsString(Square(File.E, Rank.R4)) shouldBe """"e4""""
|
||||
|
||||
test("customize registers MoveTypeSerializer"):
|
||||
mapper.writeValueAsString(MoveType.CastleKingside) shouldBe """{"type":"castleKingside"}"""
|
||||
|
||||
test("customize registers SquareDeserializer"):
|
||||
mapper.readValue(""""e4"""", classOf[Square]) shouldBe Square(File.E, Rank.R4)
|
||||
|
||||
test("customize registers MoveTypeDeserializer"):
|
||||
mapper.readValue("""{"type":"enPassant"}""", classOf[MoveType]) shouldBe MoveType.EnPassant
|
||||
@@ -0,0 +1,102 @@
|
||||
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 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)
|
||||
m
|
||||
|
||||
private val e4 = Square(File.E, Rank.R4)
|
||||
|
||||
// ── SquareKeySerializer ───────────────────────────────────────────
|
||||
|
||||
test("SquareKeySerializer writes square as map field name"):
|
||||
mapper.writeValueAsString(Map(e4 -> "piece")) shouldBe """{"e4":"piece"}"""
|
||||
|
||||
// ── 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"):
|
||||
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"):
|
||||
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])
|
||||
@@ -0,0 +1,301 @@
|
||||
package de.nowchess.rules.resource
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
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
|
||||
import de.nowchess.rules.config.JacksonConfig
|
||||
import de.nowchess.rules.dto.{ContextMoveRequest, ContextSquareRequest}
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import io.quarkus.test.junit.QuarkusTest
|
||||
import io.restassured.RestAssured
|
||||
import io.restassured.http.ContentType
|
||||
import org.hamcrest.Matchers.*
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@QuarkusTest
|
||||
class RuleSetResourceTest:
|
||||
|
||||
private val mapper: ObjectMapper =
|
||||
val m = new ObjectMapper()
|
||||
new JacksonConfig().customize(m)
|
||||
m
|
||||
|
||||
private val rules = DefaultRules
|
||||
|
||||
private def request() = RestAssured.`given`()
|
||||
|
||||
private def toJson(value: AnyRef): String = mapper.writeValueAsString(value)
|
||||
|
||||
private def contextSquareBody(ctx: GameContext, square: String): String =
|
||||
toJson(ContextSquareRequest(ctx, square))
|
||||
|
||||
private def contextMoveBody(ctx: GameContext, move: Move): String =
|
||||
toJson(ContextMoveRequest(ctx, move))
|
||||
|
||||
// ── all-legal-moves ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def allLegalMoves_initialPositionHas20Moves(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial))
|
||||
.when()
|
||||
.post("/api/rules/all-legal-moves")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("size()", is(20))
|
||||
|
||||
// ── legal-moves ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def legalMoves_e2PawnHas2Moves(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(contextSquareBody(GameContext.initial, "e2"))
|
||||
.when()
|
||||
.post("/api/rules/legal-moves")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("size()", is(2))
|
||||
|
||||
// ── candidate-moves ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def candidateMoves_e2PawnHas2Candidates(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(contextSquareBody(GameContext.initial, "e2"))
|
||||
.when()
|
||||
.post("/api/rules/candidate-moves")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("size()", is(2))
|
||||
|
||||
// ── is-check ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def isCheck_falseForInitialPosition(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial))
|
||||
.when()
|
||||
.post("/api/rules/is-check")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("false"))
|
||||
|
||||
@Test
|
||||
def isCheck_trueWhenKingAttacked(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(buildCheckContext()))
|
||||
.when()
|
||||
.post("/api/rules/is-check")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("true"))
|
||||
|
||||
// ── is-checkmate ──────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def isCheckmate_falseForInitialPosition(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial))
|
||||
.when()
|
||||
.post("/api/rules/is-checkmate")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("false"))
|
||||
|
||||
@Test
|
||||
def isCheckmate_trueForFoolsMate(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(buildFoolsMate()))
|
||||
.when()
|
||||
.post("/api/rules/is-checkmate")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("true"))
|
||||
|
||||
// ── is-stalemate ──────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def isStalemate_falseForInitialPosition(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial))
|
||||
.when()
|
||||
.post("/api/rules/is-stalemate")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("false"))
|
||||
|
||||
@Test
|
||||
def isStalemate_trueForStalematePosition(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(buildStalemateContext()))
|
||||
.when()
|
||||
.post("/api/rules/is-stalemate")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("true"))
|
||||
|
||||
// ── is-insufficient-material ──────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def isInsufficientMaterial_falseForInitialPosition(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial))
|
||||
.when()
|
||||
.post("/api/rules/is-insufficient-material")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("false"))
|
||||
|
||||
@Test
|
||||
def isInsufficientMaterial_trueForKingsOnly(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(buildKingsOnlyContext()))
|
||||
.when()
|
||||
.post("/api/rules/is-insufficient-material")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("true"))
|
||||
|
||||
// ── is-fifty-move-rule ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def isFiftyMoveRule_falseForInitialPosition(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial))
|
||||
.when()
|
||||
.post("/api/rules/is-fifty-move-rule")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("false"))
|
||||
|
||||
@Test
|
||||
def isFiftyMoveRule_trueWhenClockAt100(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial.copy(halfMoveClock = 100)))
|
||||
.when()
|
||||
.post("/api/rules/is-fifty-move-rule")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("true"))
|
||||
|
||||
// ── is-threefold-repetition ───────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def isThreefoldRepetition_falseForInitialPosition(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(GameContext.initial))
|
||||
.when()
|
||||
.post("/api/rules/is-threefold-repetition")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("false"))
|
||||
|
||||
@Test
|
||||
def isThreefoldRepetition_trueAfterRepeatedMoves(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(buildThreefoldContext()))
|
||||
.when()
|
||||
.post("/api/rules/is-threefold-repetition")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body(is("true"))
|
||||
|
||||
// ── apply-move ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def applyMove_updatesContext(): Unit =
|
||||
val move = rules
|
||||
.legalMoves(GameContext.initial)(Square(File.E, Rank.R2))
|
||||
.find(_.to == Square(File.E, Rank.R4))
|
||||
.get
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(contextMoveBody(GameContext.initial, move))
|
||||
.when()
|
||||
.post("/api/rules/apply-move")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("turn", is("Black"))
|
||||
|
||||
// ── error handling ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
def invalidSquare_returns400(): Unit =
|
||||
request()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(toJson(ContextSquareRequest(GameContext.initial, "z9")))
|
||||
.when()
|
||||
.post("/api/rules/legal-moves")
|
||||
.`then`()
|
||||
.statusCode(400)
|
||||
|
||||
// ── position builders ─────────────────────────────────────────────
|
||||
|
||||
private def buildCheckContext(): 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 buildFoolsMate(): GameContext =
|
||||
val moves = List(("f2", "f3"), ("e7", "e5"), ("g2", "g4"), ("d8", "h4"))
|
||||
moves.foldLeft(GameContext.initial) { (ctx, fromTo) =>
|
||||
val from = Square.fromAlgebraic(fromTo._1).get
|
||||
val to = Square.fromAlgebraic(fromTo._2).get
|
||||
rules.legalMoves(ctx)(from).find(_.to == to).fold(ctx)(rules.applyMove(ctx))
|
||||
}
|
||||
|
||||
private def buildStalemateContext(): 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 buildKingsOnlyContext(): 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 buildThreefoldContext(): 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(ctx: GameContext, from: Square, to: Square): GameContext =
|
||||
rules.legalMoves(ctx)(from).find(_.to == to).fold(ctx)(rules.applyMove(ctx))
|
||||
val ctx1 = mv(GameContext.initial, g1, f3)
|
||||
val ctx2 = mv(ctx1, g8, f6)
|
||||
val ctx3 = mv(ctx2, f3, g1)
|
||||
val ctx4 = mv(ctx3, f6, g8)
|
||||
val ctx5 = mv(ctx4, g1, f3)
|
||||
val ctx6 = mv(ctx5, g8, f6)
|
||||
val ctx7 = mv(ctx6, f3, g1)
|
||||
mv(ctx7, f6, g8)
|
||||
@@ -0,0 +1,153 @@
|
||||
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
|
||||
import de.nowchess.rules.dto.{ContextMoveRequest, ContextSquareRequest}
|
||||
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 ctxSq(g: GameContext, sq: String) = ContextSquareRequest(g, sq)
|
||||
private def ctxMv(g: GameContext, m: Move) = ContextMoveRequest(g, 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(GameContext.initial) should have size 20
|
||||
|
||||
// ── legalMoves ────────────────────────────────────────────────────
|
||||
|
||||
test("legalMoves returns 2 moves for e2 pawn"):
|
||||
resource.legalMoves(ctxSq(GameContext.initial, "e2")) 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")) 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(GameContext.initial) shouldBe false
|
||||
|
||||
test("isCheck returns true when king is attacked"):
|
||||
resource.isCheck(checkContext()) shouldBe true
|
||||
|
||||
// ── isCheckmate ───────────────────────────────────────────────────
|
||||
|
||||
test("isCheckmate returns false for initial position"):
|
||||
resource.isCheckmate(GameContext.initial) shouldBe false
|
||||
|
||||
test("isCheckmate returns true for Fool's mate"):
|
||||
resource.isCheckmate(foolsMate()) shouldBe true
|
||||
|
||||
// ── isStalemate ───────────────────────────────────────────────────
|
||||
|
||||
test("isStalemate returns false for initial position"):
|
||||
resource.isStalemate(GameContext.initial) shouldBe false
|
||||
|
||||
test("isStalemate returns true for stalemate position"):
|
||||
resource.isStalemate(stalemateContext()) shouldBe true
|
||||
|
||||
// ── isInsufficientMaterial ────────────────────────────────────────
|
||||
|
||||
test("isInsufficientMaterial returns false for initial position"):
|
||||
resource.isInsufficientMaterial(GameContext.initial) shouldBe false
|
||||
|
||||
test("isInsufficientMaterial returns true for kings only"):
|
||||
resource.isInsufficientMaterial(kingsOnlyContext()) shouldBe true
|
||||
|
||||
// ── isFiftyMoveRule ───────────────────────────────────────────────
|
||||
|
||||
test("isFiftyMoveRule returns false for initial position"):
|
||||
resource.isFiftyMoveRule(GameContext.initial) shouldBe false
|
||||
|
||||
test("isFiftyMoveRule returns true when halfMoveClock is 100"):
|
||||
resource.isFiftyMoveRule(GameContext.initial.copy(halfMoveClock = 100)) shouldBe true
|
||||
|
||||
// ── isThreefoldRepetition ─────────────────────────────────────────
|
||||
|
||||
test("isThreefoldRepetition returns false for initial position"):
|
||||
resource.isThreefoldRepetition(GameContext.initial) shouldBe false
|
||||
|
||||
test("isThreefoldRepetition returns true after repeated moves"):
|
||||
resource.isThreefoldRepetition(threefoldContext()) 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 Color.Black
|
||||
Reference in New Issue
Block a user