feat(rule): Rules as a microservice (#39)
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:
2026-04-22 10:09:35 +02:00
parent ffeb3ce338
commit 093134d36c
54 changed files with 1655 additions and 76 deletions
@@ -0,0 +1,109 @@
package de.nowchess.chess.adapter
import de.nowchess.api.board.{File, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType}
import de.nowchess.chess.client.{RuleMoveRequest, RuleServiceClient, RuleSquareRequest}
import io.quarkus.test.InjectMock
import io.quarkus.test.junit.QuarkusTest
import jakarta.inject.Inject
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.junit.jupiter.api.{BeforeEach, DisplayName, Test}
import org.junit.jupiter.api.Assertions.*
import org.mockito.Mockito.{verify, when}
import scala.compiletime.uninitialized
// scalafix:off
@QuarkusTest
@DisplayName("RuleSetRestAdapter")
class RuleSetRestAdapterTest:
@Inject
var adapter: RuleSetRestAdapter = uninitialized
@InjectMock
@RestClient
var client: RuleServiceClient = uninitialized
private val ctx = GameContext.initial
private val sq = Square(File.E, Rank.R2)
private val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal(false))
@BeforeEach
def setup(): Unit =
when(client.candidateMoves(RuleSquareRequest(ctx, sq.toString))).thenReturn(List(move))
when(client.legalMoves(RuleSquareRequest(ctx, sq.toString))).thenReturn(List(move))
when(client.allLegalMoves(ctx)).thenReturn(List(move))
when(client.isCheck(ctx)).thenReturn(false)
when(client.isCheckmate(ctx)).thenReturn(false)
when(client.isStalemate(ctx)).thenReturn(false)
when(client.isInsufficientMaterial(ctx)).thenReturn(false)
when(client.isFiftyMoveRule(ctx)).thenReturn(false)
when(client.isThreefoldRepetition(ctx)).thenReturn(false)
when(client.applyMove(RuleMoveRequest(ctx, move))).thenReturn(ctx)
@Test
@DisplayName("candidateMoves delegates to client")
def testCandidateMoves(): Unit =
val result = adapter.candidateMoves(ctx)(sq)
assertEquals(List(move), result)
verify(client).candidateMoves(RuleSquareRequest(ctx, sq.toString))
@Test
@DisplayName("legalMoves delegates to client")
def testLegalMoves(): Unit =
val result = adapter.legalMoves(ctx)(sq)
assertEquals(List(move), result)
verify(client).legalMoves(RuleSquareRequest(ctx, sq.toString))
@Test
@DisplayName("allLegalMoves delegates to client")
def testAllLegalMoves(): Unit =
val result = adapter.allLegalMoves(ctx)
assertEquals(List(move), result)
verify(client).allLegalMoves(ctx)
@Test
@DisplayName("isCheck delegates to client")
def testIsCheck(): Unit =
assertFalse(adapter.isCheck(ctx))
verify(client).isCheck(ctx)
@Test
@DisplayName("isCheckmate delegates to client")
def testIsCheckmate(): Unit =
assertFalse(adapter.isCheckmate(ctx))
verify(client).isCheckmate(ctx)
@Test
@DisplayName("isStalemate delegates to client")
def testIsStalemate(): Unit =
assertFalse(adapter.isStalemate(ctx))
verify(client).isStalemate(ctx)
@Test
@DisplayName("isInsufficientMaterial delegates to client")
def testIsInsufficientMaterial(): Unit =
assertFalse(adapter.isInsufficientMaterial(ctx))
verify(client).isInsufficientMaterial(ctx)
@Test
@DisplayName("isFiftyMoveRule delegates to client")
def testIsFiftyMoveRule(): Unit =
assertFalse(adapter.isFiftyMoveRule(ctx))
verify(client).isFiftyMoveRule(ctx)
@Test
@DisplayName("isThreefoldRepetition delegates to client")
def testIsThreefoldRepetition(): Unit =
assertFalse(adapter.isThreefoldRepetition(ctx))
verify(client).isThreefoldRepetition(ctx)
@Test
@DisplayName("applyMove delegates to client")
def testApplyMove(): Unit =
val result = adapter.applyMove(ctx)(move)
assertEquals(ctx, result)
verify(client).applyMove(RuleMoveRequest(ctx, move))
// scalafix:on
@@ -13,7 +13,7 @@ object EngineTestHelpers:
new GameEngine(ruleSet = DefaultRules)
def makeEngineWithBoard(board: Board, turn: Color = Color.White): GameEngine =
GameEngine(initialContext = GameContext.initial.withBoard(board).withTurn(turn))
GameEngine(initialContext = GameContext.initial.withBoard(board).withTurn(turn), ruleSet = DefaultRules)
def loadFen(engine: GameEngine, fen: String): Unit =
engine.loadGame(FenParser, fen)
@@ -1,5 +1,6 @@
package de.nowchess.chess.engine
import de.nowchess.rules.sets.DefaultRules
import scala.collection.mutable
import de.nowchess.api.board.Color
import de.nowchess.api.game.{DrawReason, GameResult}
@@ -18,7 +19,7 @@ import org.scalatest.matchers.should.Matchers
class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
test("White offers draw"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -32,7 +33,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected DrawOfferEvent, but got $other")
test("Black accepts White's draw offer"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -49,7 +50,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected DrawEvent, but got $other")
test("Black declines White's draw offer"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -65,7 +66,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected DrawOfferDeclinedEvent, but got $other")
test("Black offers draw"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -79,7 +80,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected DrawOfferEvent, but got $other")
test("White accepts Black's draw offer"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -96,7 +97,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected DrawEvent, but got $other")
test("Cannot accept draw when no offer pending"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -110,7 +111,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("Cannot decline draw when no offer pending"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -124,7 +125,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("Cannot offer draw when game is already over"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -147,7 +148,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("Cannot accept your own draw offer"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -163,7 +164,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("Cannot decline your own draw offer"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -179,7 +180,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("Cannot make second draw offer when one is already pending"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -195,7 +196,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("Draw offer is cleared when game ends by resignation (accept)"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -215,7 +216,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("Draw offer is cleared when game ends by resignation (decline)"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
@@ -235,22 +236,22 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("pendingDrawOfferBy returns None initially"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
engine.pendingDrawOfferBy shouldBe None
test("pendingDrawOfferBy returns White after White offers"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
engine.offerDraw(Color.White)
engine.pendingDrawOfferBy shouldBe Some(Color.White)
test("pendingDrawOfferBy returns None after draw is accepted"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
engine.offerDraw(Color.White)
engine.acceptDraw(Color.Black)
engine.pendingDrawOfferBy shouldBe None
test("applyDraw sets draw result when game not over"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
engine.applyDraw(DrawReason.Agreement)
@@ -263,7 +264,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
fail(s"Expected DrawEvent, but got $other")
test("applyDraw does nothing when game already over"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
// End the game with checkmate
@@ -276,7 +277,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
observer.events should have length 0
test("claimDraw with fifty-move rule when at half-move 100"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
// Play moves to reach fifty-move rule claim
@@ -288,7 +289,7 @@ class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
// This is hard to do naturally; skip for now if not critical
test("claimDraw when game already over"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new DrawOfferMockObserver()
engine.subscribe(observer)
// End the game with checkmate
@@ -1,5 +1,6 @@
package de.nowchess.chess.engine
import de.nowchess.rules.sets.DefaultRules
import scala.collection.mutable
import de.nowchess.api.board.Color
import de.nowchess.api.game.DrawReason
@@ -11,7 +12,7 @@ import org.scalatest.matchers.should.Matchers
class GameEngineGameEndingTest extends AnyFunSuite with Matchers:
test("GameEngine handles Checkmate (Fool's Mate)"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new EndingMockObserver()
engine.subscribe(observer)
@@ -31,7 +32,7 @@ class GameEngineGameEndingTest extends AnyFunSuite with Matchers:
fail(s"Expected CheckmateEvent, but got $other")
test("GameEngine handles check detection"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new EndingMockObserver()
engine.subscribe(observer)
@@ -53,7 +54,7 @@ class GameEngineGameEndingTest extends AnyFunSuite with Matchers:
// Wait, let's just use Sam Loyd's 10-move stalemate:
// 1. e3 a5 2. Qh5 Ra6 3. Qxa5 h5 4. h4 Rah6 5. Qxc7 f6 6. Qxd7+ Kf7 7. Qxb7 Qd3 8. Qxb8 Qh7 9. Qxc8 Kg6 10. Qe6
test("GameEngine handles Stalemate via 10-move known sequence"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new EndingMockObserver()
engine.subscribe(observer)
@@ -5,7 +5,7 @@ import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, MoveRedoneEvent, Observer}
import de.nowchess.api.io.GameContextImport
import de.nowchess.rules.RuleSet
import de.nowchess.api.rules.RuleSet
import de.nowchess.rules.sets.DefaultRules
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -21,7 +21,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
events
test("accessors expose redo availability and command history"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
engine.canRedo shouldBe false
engine.commandHistory shouldBe empty
@@ -30,7 +30,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
engine.commandHistory.nonEmpty shouldBe true
test("processUserInput handles undo redo empty and malformed commands"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val events = captureEvents(engine)
engine.processUserInput("")
@@ -44,7 +44,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
} should be >= 3
test("processUserInput emits Illegal move for syntactically valid but illegal target"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val events = captureEvents(engine)
engine.processUserInput("e2e5")
@@ -56,14 +56,14 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
test("loadGame returns Left when importer fails"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val failingImporter = new GameContextImport:
def importGameContext(input: String): Either[String, GameContext] = Left("boom")
engine.loadGame(failingImporter, "ignored") shouldBe Left("boom")
test("loadPosition replaces context clears history and notifies reset"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val events = captureEvents(engine)
engine.processUserInput("e2e4")
@@ -78,7 +78,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
} shouldBe true
test("redo event includes captured piece description when replaying a capture"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val events = captureEvents(engine)
EngineTestHelpers.loadFen(engine, "4k3/8/8/8/8/8/4K3/R6r w - - 0 1")
@@ -145,13 +145,13 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
test("loadGame replay executes non-promotion moves through default replay branch"):
val normalMove = Move(sq("e2"), sq("e4"), MoveType.Normal())
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
engine.replayMoves(List(normalMove), engine.context) shouldBe Right(())
engine.context.moves.lastOption shouldBe Some(normalMove)
test("replayMoves skips later moves after the first move triggers an error"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val saved = engine.context
val illegalPromotion = Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Queen))
val trailingMove = Move(sq("e2"), sq("e4"))
@@ -160,19 +160,19 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
engine.context shouldBe saved
test("normalMoveNotation handles missing source piece"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val result = engine.normalMoveNotation(Move(sq("e3"), sq("e4")), Board.initial, isCapture = false)
result shouldBe "e4"
test("pieceNotation default branch returns empty string"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val result = engine.pieceNotation(PieceType.Pawn)
result shouldBe ""
test("observerCount reflects subscribe and unsubscribe operations"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new Observer:
def onGameEvent(event: GameEvent): Unit = ()
@@ -1,5 +1,11 @@
package de.nowchess.chess.engine
import de.nowchess.rules.sets.DefaultRules
import scala.collection.mutable
import de.nowchess.api.board.{Board, Color}
import de.nowchess.api.game.GameContext
import de.nowchess.chess.observer.{GameEvent, Observer, PgnLoadedEvent}
import de.nowchess.io.pgn.PgnParser
import de.nowchess.chess.observer.{GameEvent, Observer}
import de.nowchess.io.fen.FenParser
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
@@ -11,7 +17,7 @@ import scala.collection.mutable
class GameEngineLoadGameTest extends AnyFunSuite with Matchers:
test("loadGame with PgnParser: loads valid PGN and enables undo/redo"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val pgn = "[Event \"Test\"]\n\n1. e4 e5\n"
val result = engine.loadGame(PgnParser, pgn)
result shouldBe Right(())
@@ -19,7 +25,7 @@ class GameEngineLoadGameTest extends AnyFunSuite with Matchers:
engine.canUndo shouldBe true
test("loadGame with FenParser: loads position without replaying moves"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val fen = "8/4P3/4k3/8/8/8/8/8 w - - 0 1"
val result = engine.loadGame(FenParser, fen)
result shouldBe Right(())
@@ -27,7 +33,7 @@ class GameEngineLoadGameTest extends AnyFunSuite with Matchers:
engine.canUndo shouldBe false
test("exportGame with PgnExporter: exports current game as PGN"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
engine.processUserInput("e2e4")
engine.processUserInput("e7e5")
val pgn = engine.exportGame(PgnExporter)
@@ -1,6 +1,7 @@
package de.nowchess.chess.engine
import de.nowchess.api.board.{Board, Color, File, Rank, Square}
import de.nowchess.rules.sets.DefaultRules
import de.nowchess.api.game.GameContext
import de.nowchess.io.fen.FenParser
import de.nowchess.chess.observer.*
@@ -37,7 +38,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
.withTurn(Color.White)
.withCastlingRights(castlingRights)
val engine = new GameEngine(ctx)
val engine = new GameEngine(ctx, DefaultRules)
val events = captureEvents(engine)
// White castles queenside: e1c1
@@ -65,7 +66,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
.withEnPassantSquare(epSquare)
.withCastlingRights(de.nowchess.api.board.CastlingRights(false, false, false, false))
val engine = new GameEngine(ctx)
val engine = new GameEngine(ctx, DefaultRules)
val events = captureEvents(engine)
// White pawn on e5 captures en passant to d6
@@ -96,7 +97,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
.withTurn(Color.White)
.withCastlingRights(de.nowchess.api.board.CastlingRights(false, false, false, false))
val engine = new GameEngine(ctx)
val engine = new GameEngine(ctx, DefaultRules)
val events = captureEvents(engine)
engine.processUserInput("e7e8b")
@@ -117,7 +118,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
.withTurn(Color.White)
.withCastlingRights(de.nowchess.api.board.CastlingRights(false, false, false, false))
val engine = new GameEngine(ctx)
val engine = new GameEngine(ctx, DefaultRules)
val events = captureEvents(engine)
// King moves e1 -> f1
@@ -14,7 +14,7 @@ import de.nowchess.chess.observer.{
MoveExecutedEvent,
Observer,
}
import de.nowchess.rules.RuleSet
import de.nowchess.api.rules.RuleSet
import de.nowchess.rules.sets.DefaultRules
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -29,7 +29,7 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
events
private def engineWith(board: Board, turn: Color = Color.White): GameEngine =
new GameEngine(initialContext = GameContext.initial.withBoard(board).withTurn(turn))
new GameEngine(initialContext = GameContext.initial.withBoard(board).withTurn(turn), ruleSet = DefaultRules)
test("processUserInput without promotion suffix fires InvalidMoveEvent when pawn reaches back rank") {
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
@@ -1,5 +1,6 @@
package de.nowchess.chess.engine
import de.nowchess.rules.sets.DefaultRules
import scala.collection.mutable
import de.nowchess.api.board.Color
import de.nowchess.api.game.GameResult
@@ -10,7 +11,7 @@ import org.scalatest.matchers.should.Matchers
class GameEngineResignTest extends AnyFunSuite with Matchers:
test("White resigns"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new ResignMockObserver()
engine.subscribe(observer)
@@ -25,7 +26,7 @@ class GameEngineResignTest extends AnyFunSuite with Matchers:
fail(s"Expected ResignEvent, but got $other")
test("Black resigns"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new ResignMockObserver()
engine.subscribe(observer)
@@ -40,7 +41,7 @@ class GameEngineResignTest extends AnyFunSuite with Matchers:
fail(s"Expected ResignEvent, but got $other")
test("Cannot resign when game is already over"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new ResignMockObserver()
engine.subscribe(observer)
@@ -64,7 +65,7 @@ class GameEngineResignTest extends AnyFunSuite with Matchers:
fail(s"Expected InvalidMoveEvent, but got $other")
test("resign() without color resigns side to move"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new ResignMockObserver()
engine.subscribe(observer)
@@ -73,7 +74,7 @@ class GameEngineResignTest extends AnyFunSuite with Matchers:
engine.context.result shouldBe Some(GameResult.Win(Color.Black))
test("resign() without color does nothing when game already over"):
val engine = new GameEngine()
val engine = new GameEngine(ruleSet = DefaultRules)
val observer = new ResignMockObserver()
engine.subscribe(observer)
@@ -0,0 +1,87 @@
package de.nowchess.chess.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.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)
// ── SquareSerializer ──────────────────────────────────────────────
test("SquareSerializer writes square as string"):
mapper.writeValueAsString(e4) shouldBe """"e4""""
// ── SquareDeserializer ────────────────────────────────────────────
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])
@@ -1,6 +1,7 @@
package de.nowchess.chess.registry
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.rules.sets.DefaultRules
import de.nowchess.chess.engine.GameEngine
import io.quarkus.test.junit.QuarkusTest
import jakarta.inject.Inject
@@ -20,14 +21,24 @@ class GameRegistryImplTest:
@Test
@DisplayName("store saves entry")
def testStore(): Unit =
val entry = GameEntry("g1", GameEngine(), PlayerInfo(PlayerId("p1"), "P1"), PlayerInfo(PlayerId("p2"), "P2"))
val entry = GameEntry(
"g1",
GameEngine(ruleSet = DefaultRules),
PlayerInfo(PlayerId("p1"), "P1"),
PlayerInfo(PlayerId("p2"), "P2"),
)
registry.store(entry)
assertTrue(registry.get("g1").isDefined)
@Test
@DisplayName("get returns stored entry")
def testGet(): Unit =
val entry = GameEntry("g2", GameEngine(), PlayerInfo(PlayerId("p1"), "P1"), PlayerInfo(PlayerId("p2"), "P2"))
val entry = GameEntry(
"g2",
GameEngine(ruleSet = DefaultRules),
PlayerInfo(PlayerId("p1"), "P1"),
PlayerInfo(PlayerId("p2"), "P2"),
)
registry.store(entry)
val retrieved = registry.get("g2")
assertTrue(retrieved.isDefined)
@@ -41,7 +52,12 @@ class GameRegistryImplTest:
@Test
@DisplayName("update modifies existing entry")
def testUpdate(): Unit =
val entry = GameEntry("g3", GameEngine(), PlayerInfo(PlayerId("p1"), "P1"), PlayerInfo(PlayerId("p2"), "P2"))
val entry = GameEntry(
"g3",
GameEngine(ruleSet = DefaultRules),
PlayerInfo(PlayerId("p1"), "P1"),
PlayerInfo(PlayerId("p2"), "P2"),
)
registry.store(entry)
val updated = entry.copy(resigned = true)
registry.update(updated)
@@ -1,11 +1,13 @@
package de.nowchess.chess.resource
import de.nowchess.api.board.Square
import de.nowchess.api.dto.*
import de.nowchess.api.game.GameContext
import de.nowchess.chess.client.IoServiceClient
import de.nowchess.chess.client.{IoServiceClient, RuleMoveRequest, RuleServiceClient, RuleSquareRequest}
import de.nowchess.chess.exception.BadRequestException
import de.nowchess.io.fen.FenExporter
import de.nowchess.io.pgn.PgnParser
import de.nowchess.rules.sets.DefaultRules
import io.quarkus.test.InjectMock
import io.quarkus.test.junit.QuarkusTest
import jakarta.inject.Inject
@@ -14,6 +16,7 @@ import org.junit.jupiter.api.{BeforeEach, DisplayName, Test}
import org.junit.jupiter.api.Assertions.*
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.when
import org.mockito.invocation.InvocationOnMock
import scala.compiletime.uninitialized
@@ -29,6 +32,10 @@ class GameResourceIntegrationTest:
@RestClient
var ioClient: IoServiceClient = uninitialized
@InjectMock
@RestClient
var ruleClient: RuleServiceClient = uninitialized
@BeforeEach
def setupMocks(): Unit =
when(ioClient.importFen(any())).thenReturn(GameContext.initial)
@@ -37,6 +44,32 @@ class GameResourceIntegrationTest:
)
when(ioClient.exportFen(any())).thenReturn(FenExporter.exportGameContext(GameContext.initial))
when(ioClient.exportPgn(any())).thenReturn("1. e4 c5")
when(ruleClient.legalMoves(any())).thenAnswer((inv: InvocationOnMock) =>
val req = inv.getArgument[RuleSquareRequest](0)
DefaultRules.legalMoves(req.context)(Square.fromAlgebraic(req.square).get),
)
when(ruleClient.allLegalMoves(any())).thenAnswer((inv: InvocationOnMock) =>
DefaultRules.allLegalMoves(inv.getArgument[GameContext](0)),
)
when(ruleClient.applyMove(any())).thenAnswer((inv: InvocationOnMock) =>
val req = inv.getArgument[RuleMoveRequest](0)
DefaultRules.applyMove(req.context)(req.move),
)
when(ruleClient.isCheck(any())).thenAnswer((inv: InvocationOnMock) =>
DefaultRules.isCheck(inv.getArgument[GameContext](0)),
)
when(ruleClient.isCheckmate(any())).thenAnswer((inv: InvocationOnMock) =>
DefaultRules.isCheckmate(inv.getArgument[GameContext](0)),
)
when(ruleClient.isStalemate(any())).thenAnswer((inv: InvocationOnMock) =>
DefaultRules.isStalemate(inv.getArgument[GameContext](0)),
)
when(ruleClient.isInsufficientMaterial(any())).thenAnswer((inv: InvocationOnMock) =>
DefaultRules.isInsufficientMaterial(inv.getArgument[GameContext](0)),
)
when(ruleClient.isThreefoldRepetition(any())).thenAnswer((inv: InvocationOnMock) =>
DefaultRules.isThreefoldRepetition(inv.getArgument[GameContext](0)),
)
@Test
@DisplayName("createGame returns 201")