feat: Add tests for bot behavior including king position, illegal moves, and game events
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -71,3 +71,9 @@ class GameContextTest extends AnyFunSuite with Matchers:
|
|||||||
test("withResult clears result"):
|
test("withResult clears result"):
|
||||||
val ctx = GameContext.initial.withResult(Some(GameResult.Win(Color.Black)))
|
val ctx = GameContext.initial.withResult(Some(GameResult.Win(Color.Black)))
|
||||||
ctx.withResult(None).result shouldBe None
|
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
|
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.game.{BotParticipant, GameContext, Human}
|
||||||
|
import de.nowchess.api.move.{Move, MoveType}
|
||||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||||
import de.nowchess.bot.bots.ClassicalBot
|
import de.nowchess.bot.bots.ClassicalBot
|
||||||
import de.nowchess.bot.{BotController, BotDifficulty}
|
import de.nowchess.bot.{BotController, BotDifficulty}
|
||||||
@@ -12,6 +14,14 @@ import org.scalatest.matchers.should.Matchers
|
|||||||
|
|
||||||
import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
|
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:
|
class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
test("GameEngine can play against a ClassicalBot"):
|
test("GameEngine can play against a ClassicalBot"):
|
||||||
@@ -114,3 +124,126 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
|||||||
// Game should not be ended (checkmate/stalemate)
|
// Game should not be ended (checkmate/stalemate)
|
||||||
engine.context.moves.nonEmpty should be(true)
|
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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user