feat: Add tests for bot behavior including king position, illegal moves, and game events
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-16 21:55:55 +02:00
parent 8ecfbd6266
commit 5a66a057b3
2 changed files with 140 additions and 1 deletions
@@ -71,3 +71,9 @@ class GameContextTest extends AnyFunSuite with Matchers:
test("withResult clears result"):
val ctx = GameContext.initial.withResult(Some(GameResult.Win(Color.Black)))
ctx.withResult(None).result shouldBe None
test("kingSquare returns white king position"):
GameContext.initial.kingSquare(Color.White) shouldBe Some(Square(File.E, Rank.R1))
test("kingSquare returns black king position"):
GameContext.initial.kingSquare(Color.Black) shouldBe Some(Square(File.E, Rank.R8))
@@ -1,7 +1,9 @@
package de.nowchess.chess.engine
import de.nowchess.api.board.Color
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, Rank, Square}
import de.nowchess.api.bot.Bot
import de.nowchess.api.game.{BotParticipant, GameContext, Human}
import de.nowchess.api.move.{Move, MoveType}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.bot.bots.ClassicalBot
import de.nowchess.bot.{BotController, BotDifficulty}
@@ -12,6 +14,14 @@ import org.scalatest.matchers.should.Matchers
import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
private class NoMoveBot extends Bot:
def name: String = "nomove"
def nextMove(context: GameContext): Option[Move] = None
private class FixedMoveBot(move: Move) extends Bot:
def name: String = "fixed"
def nextMove(context: GameContext): Option[Move] = Some(move)
class GameEngineWithBotTest extends AnyFunSuite with Matchers:
test("GameEngine can play against a ClassicalBot"):
@@ -114,3 +124,126 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
// Game should not be ended (checkmate/stalemate)
engine.context.moves.nonEmpty should be(true)
test("startGame triggers bot when the starting player is a bot"):
val bot = new FixedMoveBot(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)))
val engine = GameEngine(
GameContext.initial,
DefaultRules,
Map(Color.White -> BotParticipant(bot), Color.Black -> Human(PlayerInfo(PlayerId("p2"), "Player 2"))),
)
val movesMade = new AtomicInteger(0)
engine.subscribe(new Observer:
def onGameEvent(event: GameEvent): Unit = event match
case _: MoveExecutedEvent => movesMade.incrementAndGet()
case _ => ()
)
engine.startGame()
Thread.sleep(500)
movesMade.get() should be >= 1
test("applyBotMove fires InvalidMoveEvent when bot move destination is illegal"):
val illegalMove = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R3), MoveType.Normal())
val bot = new FixedMoveBot(illegalMove)
val engine = GameEngine(
GameContext.initial,
DefaultRules,
Map(Color.White -> Human(PlayerInfo(PlayerId("p1"), "Player 1")), Color.Black -> BotParticipant(bot)),
)
val invalidCount = new AtomicInteger(0)
engine.subscribe(new Observer:
def onGameEvent(event: GameEvent): Unit = event match
case _: InvalidMoveEvent => invalidCount.incrementAndGet()
case _ => ()
)
engine.processUserInput("e2e4")
Thread.sleep(1000)
invalidCount.get() should be >= 1
test("applyBotMove fires InvalidMoveEvent when bot move source square is invalid"):
val invalidMove = Move(Square(File.E, Rank.R5), Square(File.E, Rank.R6), MoveType.Normal())
val bot = new FixedMoveBot(invalidMove)
val engine = GameEngine(
GameContext.initial,
DefaultRules,
Map(Color.White -> Human(PlayerInfo(PlayerId("p1"), "Player 1")), Color.Black -> BotParticipant(bot)),
)
val invalidCount = new AtomicInteger(0)
engine.subscribe(new Observer:
def onGameEvent(event: GameEvent): Unit = event match
case _: InvalidMoveEvent => invalidCount.incrementAndGet()
case _ => ()
)
engine.processUserInput("e2e4")
Thread.sleep(1000)
invalidCount.get() should be >= 1
test("handleBotNoMove fires CheckmateEvent when position is checkmate"):
// White king at A1 in check from Qb2; Rb8 protects queen so king can't capture it
val board = Board(Map(
Square(File.A, Rank.R1) -> Piece.WhiteKing,
Square(File.B, Rank.R2) -> Piece.BlackQueen,
Square(File.B, Rank.R8) -> Piece.BlackRook,
Square(File.H, Rank.R8) -> Piece.BlackKing,
))
val ctx = GameContext.initial.copy(
board = board,
turn = Color.White,
castlingRights = CastlingRights(false, false, false, false),
enPassantSquare = None,
halfMoveClock = 0,
moves = List.empty,
)
val engine = GameEngine(ctx, DefaultRules, Map(Color.White -> BotParticipant(new NoMoveBot), Color.Black -> Human(PlayerInfo(PlayerId("p2"), "Player 2"))))
val checkmateCount = new AtomicInteger(0)
engine.subscribe(new Observer:
def onGameEvent(event: GameEvent): Unit = event match
case _: CheckmateEvent => checkmateCount.incrementAndGet()
case _ => ()
)
engine.startGame()
Thread.sleep(1000)
checkmateCount.get() should be >= 1
test("handleBotNoMove fires DrawEvent when position is stalemate"):
// White king at A1 not in check but has no legal moves (queen at B3 covers A2, B1, B2)
val board = Board(Map(
Square(File.A, Rank.R1) -> Piece.WhiteKing,
Square(File.B, Rank.R3) -> Piece.BlackQueen,
Square(File.H, Rank.R8) -> Piece.BlackKing,
))
val ctx = GameContext.initial.copy(
board = board,
turn = Color.White,
castlingRights = CastlingRights(false, false, false, false),
enPassantSquare = None,
halfMoveClock = 0,
moves = List.empty,
)
val engine = GameEngine(ctx, DefaultRules, Map(Color.White -> BotParticipant(new NoMoveBot), Color.Black -> Human(PlayerInfo(PlayerId("p2"), "Player 2"))))
val drawCount = new AtomicInteger(0)
engine.subscribe(new Observer:
def onGameEvent(event: GameEvent): Unit = event match
case _: DrawEvent => drawCount.incrementAndGet()
case _ => ()
)
engine.startGame()
Thread.sleep(1000)
drawCount.get() should be >= 1
test("handleBotNoMove does nothing when position is neither checkmate nor stalemate"):
val engine = GameEngine(
GameContext.initial,
DefaultRules,
Map(Color.White -> BotParticipant(new NoMoveBot), Color.Black -> Human(PlayerInfo(PlayerId("p2"), "Player 2"))),
)
val unexpectedEvents = new AtomicInteger(0)
engine.subscribe(new Observer:
def onGameEvent(event: GameEvent): Unit = event match
case _: CheckmateEvent => unexpectedEvents.incrementAndGet()
case _: DrawEvent => unexpectedEvents.incrementAndGet()
case _ => ()
)
engine.startGame()
Thread.sleep(500)
unexpectedEvents.get() shouldBe 0