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:
@@ -6,3 +6,5 @@ quarkus:
|
||||
rest-client:
|
||||
io-service:
|
||||
url: http://localhost:8081
|
||||
rule-service:
|
||||
url: http://localhost:8082
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.nowchess.chess.adapter
|
||||
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.chess.client.{RuleMoveRequest, RuleServiceClient, RuleSquareRequest}
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@ApplicationScoped
|
||||
class RuleSetRestAdapter extends RuleSet:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject
|
||||
@RestClient
|
||||
var client: RuleServiceClient = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
def candidateMoves(ctx: GameContext)(sq: Square): List[Move] =
|
||||
client.candidateMoves(RuleSquareRequest(ctx, sq.toString))
|
||||
|
||||
def legalMoves(ctx: GameContext)(sq: Square): List[Move] =
|
||||
client.legalMoves(RuleSquareRequest(ctx, sq.toString))
|
||||
|
||||
def allLegalMoves(ctx: GameContext): List[Move] =
|
||||
client.allLegalMoves(ctx)
|
||||
|
||||
def isCheck(ctx: GameContext): Boolean =
|
||||
client.isCheck(ctx)
|
||||
|
||||
def isCheckmate(ctx: GameContext): Boolean =
|
||||
client.isCheckmate(ctx)
|
||||
|
||||
def isStalemate(ctx: GameContext): Boolean =
|
||||
client.isStalemate(ctx)
|
||||
|
||||
def isInsufficientMaterial(ctx: GameContext): Boolean =
|
||||
client.isInsufficientMaterial(ctx)
|
||||
|
||||
def isFiftyMoveRule(ctx: GameContext): Boolean =
|
||||
client.isFiftyMoveRule(ctx)
|
||||
|
||||
def isThreefoldRepetition(ctx: GameContext): Boolean =
|
||||
client.isThreefoldRepetition(ctx)
|
||||
|
||||
def applyMove(ctx: GameContext)(move: Move): GameContext =
|
||||
client.applyMove(RuleMoveRequest(ctx, move))
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.nowchess.chess.client
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.MediaType
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||
|
||||
case class RuleSquareRequest(context: GameContext, square: String)
|
||||
case class RuleMoveRequest(context: GameContext, move: Move)
|
||||
|
||||
@Path("/api/rules")
|
||||
@RegisterRestClient(configKey = "rule-service")
|
||||
trait RuleServiceClient:
|
||||
|
||||
@POST
|
||||
@Path("/candidate-moves")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def candidateMoves(req: RuleSquareRequest): List[Move]
|
||||
|
||||
@POST
|
||||
@Path("/legal-moves")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def legalMoves(req: RuleSquareRequest): List[Move]
|
||||
|
||||
@POST
|
||||
@Path("/all-legal-moves")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def allLegalMoves(ctx: GameContext): List[Move]
|
||||
|
||||
@POST
|
||||
@Path("/is-check")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def isCheck(ctx: GameContext): Boolean
|
||||
|
||||
@POST
|
||||
@Path("/is-checkmate")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def isCheckmate(ctx: GameContext): Boolean
|
||||
|
||||
@POST
|
||||
@Path("/is-stalemate")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def isStalemate(ctx: GameContext): Boolean
|
||||
|
||||
@POST
|
||||
@Path("/is-insufficient-material")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def isInsufficientMaterial(ctx: GameContext): Boolean
|
||||
|
||||
@POST
|
||||
@Path("/is-fifty-move-rule")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def isFiftyMoveRule(ctx: GameContext): Boolean
|
||||
|
||||
@POST
|
||||
@Path("/is-threefold-repetition")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def isThreefoldRepetition(ctx: GameContext): Boolean
|
||||
|
||||
@POST
|
||||
@Path("/apply-move")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def applyMove(req: RuleMoveRequest): GameContext
|
||||
@@ -5,6 +5,8 @@ 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.chess.json.{MoveTypeDeserializer, MoveTypeSerializer, SquareDeserializer, SquareSerializer}
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@@ -17,7 +19,11 @@ class JacksonConfig extends ObjectMapperCustomizer:
|
||||
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||
// scalafix:on DisableSyntax.null
|
||||
})
|
||||
val squareModule = new SimpleModule()
|
||||
squareModule.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||
squareModule.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||
mapper.registerModule(squareModule)
|
||||
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)
|
||||
|
||||
@@ -8,8 +8,7 @@ import de.nowchess.chess.controller.Parser
|
||||
import de.nowchess.chess.observer.*
|
||||
import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult}
|
||||
import de.nowchess.api.io.{GameContextExport, GameContextImport}
|
||||
import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
@@ -18,7 +17,7 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
*/
|
||||
class GameEngine(
|
||||
val initialContext: GameContext = GameContext.initial,
|
||||
val ruleSet: RuleSet = DefaultRules,
|
||||
val ruleSet: RuleSet,
|
||||
val participants: Map[Color, Participant] = Map(
|
||||
Color.White -> Human(PlayerInfo(PlayerId("p1"), "Player 1")),
|
||||
Color.Black -> Human(PlayerInfo(PlayerId("p2"), "Player 2")),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.nowchess.chess.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
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.nowchess.chess.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()
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.nowchess.chess.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
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.nowchess.chess.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)
|
||||
@@ -6,6 +6,7 @@ import de.nowchess.api.dto.*
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||
import de.nowchess.chess.adapter.RuleSetRestAdapter
|
||||
import de.nowchess.chess.client.IoServiceClient
|
||||
import de.nowchess.chess.controller.Parser
|
||||
import de.nowchess.chess.engine.GameEngine
|
||||
@@ -36,6 +37,9 @@ class GameResource:
|
||||
@Inject
|
||||
@RestClient
|
||||
var ioClient: IoServiceClient = uninitialized
|
||||
|
||||
@Inject
|
||||
var ruleSetAdapter: RuleSetRestAdapter = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
|
||||
@@ -103,7 +107,7 @@ class GameResource:
|
||||
dto.fold(default)(d => PlayerInfo(PlayerId(d.id), d.displayName))
|
||||
|
||||
private def newEntry(ctx: GameContext, white: PlayerInfo, black: PlayerInfo): GameEntry =
|
||||
GameEntry(registry.generateId(), GameEngine(initialContext = ctx), white, black)
|
||||
GameEntry(registry.generateId(), GameEngine(initialContext = ctx, ruleSet = ruleSetAdapter), white, black)
|
||||
|
||||
private def applyMoveInput(engine: GameEngine, uci: String): Option[String] =
|
||||
val error = new AtomicReference[Option[String]](None)
|
||||
@@ -138,6 +142,7 @@ class GameResource:
|
||||
val black = playerInfoFrom(req.black, DefaultBlack)
|
||||
val entry = newEntry(GameContext.initial, white, black)
|
||||
registry.store(entry)
|
||||
println(s"Created game ${entry.gameId}")
|
||||
created(toGameFullDto(entry))
|
||||
|
||||
@GET
|
||||
|
||||
@@ -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)
|
||||
|
||||
+21
-20
@@ -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)
|
||||
|
||||
|
||||
+12
-12
@@ -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)
|
||||
|
||||
+34
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user