feat: Improve code formatting and readability in AlphaBetaSearch and related tests
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -5,8 +5,8 @@ import de.nowchess.api.move.PromotionPiece
|
||||
|
||||
object Parser:
|
||||
|
||||
/** Parses UCI move notation: "e2e4" (4 chars) or "e7e8q" (5 chars with promotion piece suffix).
|
||||
* The promotion suffix is q=Queen, r=Rook, b=Bishop, n=Knight. Returns None for invalid input.
|
||||
/** Parses UCI move notation: "e2e4" (4 chars) or "e7e8q" (5 chars with promotion piece suffix). The promotion suffix
|
||||
* is q=Queen, r=Rook, b=Bishop, n=Knight. Returns None for invalid input.
|
||||
*/
|
||||
def parseMove(input: String): Option[(Square, Square, Option[PromotionPiece])] =
|
||||
val trimmed = input.trim.toLowerCase
|
||||
|
||||
@@ -19,13 +19,16 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
class GameEngine(
|
||||
val initialContext: GameContext = GameContext.initial,
|
||||
val ruleSet: RuleSet = DefaultRules,
|
||||
val participants: Map[Color, Participant] = Map(Color.White -> Human(PlayerInfo(PlayerId("p1"), "Player 1")), Color.Black -> Human(PlayerInfo(PlayerId("p2"), "Player 2"))),
|
||||
val participants: Map[Color, Participant] = Map(
|
||||
Color.White -> Human(PlayerInfo(PlayerId("p1"), "Player 1")),
|
||||
Color.Black -> Human(PlayerInfo(PlayerId("p2"), "Player 2")),
|
||||
),
|
||||
) extends Observable:
|
||||
// Ensure that initialBoard is set correctly for threefold repetition detection
|
||||
private val contextWithInitialBoard = if initialContext.moves.isEmpty && initialContext.board != initialContext.initialBoard then
|
||||
initialContext.copy(initialBoard = initialContext.board)
|
||||
else
|
||||
initialContext
|
||||
private val contextWithInitialBoard =
|
||||
if initialContext.moves.isEmpty && initialContext.board != initialContext.initialBoard then
|
||||
initialContext.copy(initialBoard = initialContext.board)
|
||||
else initialContext
|
||||
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||
private var currentContext: GameContext = contextWithInitialBoard
|
||||
private val invoker = new CommandInvoker()
|
||||
@@ -109,10 +112,15 @@ class GameEngine(
|
||||
notifyObservers(InvalidMoveEvent(currentContext, "Illegal move."))
|
||||
case _ if isPromotionMove(piece, to) =>
|
||||
if promotionPiece.isEmpty then
|
||||
notifyObservers(InvalidMoveEvent(currentContext, "Promotion piece required: append q, r, b, or n to the move."))
|
||||
notifyObservers(
|
||||
InvalidMoveEvent(currentContext, "Promotion piece required: append q, r, b, or n to the move."),
|
||||
)
|
||||
else
|
||||
candidates.find(_.moveType == MoveType.Promotion(promotionPiece.get)) match
|
||||
case None => notifyObservers(InvalidMoveEvent(currentContext, "Error completing promotion: no matching legal move."))
|
||||
case None =>
|
||||
notifyObservers(
|
||||
InvalidMoveEvent(currentContext, "Error completing promotion: no matching legal move."),
|
||||
)
|
||||
case Some(move) => executeMove(move)
|
||||
case move :: _ =>
|
||||
executeMove(move)
|
||||
@@ -159,7 +167,7 @@ class GameEngine(
|
||||
result
|
||||
|
||||
private def applyReplayMove(move: Move): Either[String, Unit] =
|
||||
val legal = ruleSet.legalMoves(currentContext)(move.from)
|
||||
val legal = ruleSet.legalMoves(currentContext)(move.from)
|
||||
val candidate = move.moveType match
|
||||
case MoveType.Promotion(pp) => legal.find(m => m.to == move.to && m.moveType == MoveType.Promotion(pp))
|
||||
case _ => legal.find(_.to == move.to)
|
||||
@@ -174,10 +182,9 @@ class GameEngine(
|
||||
|
||||
/** Load an arbitrary board position, clearing all history and undo/redo state. */
|
||||
def loadPosition(newContext: GameContext): Unit = synchronized {
|
||||
val contextWithInitialBoard = if newContext.moves.isEmpty then
|
||||
newContext.copy(initialBoard = newContext.board)
|
||||
else
|
||||
newContext
|
||||
val contextWithInitialBoard =
|
||||
if newContext.moves.isEmpty then newContext.copy(initialBoard = newContext.board)
|
||||
else newContext
|
||||
currentContext = contextWithInitialBoard
|
||||
invoker.clear()
|
||||
notifyObservers(BoardResetEvent(currentContext))
|
||||
@@ -235,7 +242,8 @@ class GameEngine(
|
||||
else if ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext))
|
||||
|
||||
if currentContext.halfMoveClock >= 100 then notifyObservers(FiftyMoveRuleAvailableEvent(currentContext))
|
||||
if ruleSet.isThreefoldRepetition(currentContext) then notifyObservers(ThreefoldRepetitionAvailableEvent(currentContext))
|
||||
if ruleSet.isThreefoldRepetition(currentContext) then
|
||||
notifyObservers(ThreefoldRepetitionAvailableEvent(currentContext))
|
||||
else requestBotMoveIfNeeded()
|
||||
|
||||
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
initialShouldFailOnUndo: Boolean = false,
|
||||
initialShouldFailOnExecute: Boolean = false,
|
||||
) extends Command:
|
||||
val shouldFailOnUndo = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnUndo)
|
||||
val shouldFailOnExecute = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnExecute)
|
||||
val shouldFailOnUndo = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnUndo)
|
||||
val shouldFailOnExecute = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnExecute)
|
||||
override def execute(): Boolean = !shouldFailOnExecute.get()
|
||||
override def undo(): Boolean = !shouldFailOnUndo.get()
|
||||
override def description: String = "Conditional fail"
|
||||
|
||||
@@ -251,10 +251,10 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
engine.processUserInput("f3g1")
|
||||
observer.clear()
|
||||
|
||||
engine.processUserInput("f6g8") // 3rd occurrence of initial position
|
||||
engine.processUserInput("f6g8") // 3rd occurrence of initial position
|
||||
|
||||
observer.hasEvent[ThreefoldRepetitionAvailableEvent] shouldBe true
|
||||
engine.context.result shouldBe None // claimable, not automatic
|
||||
engine.context.result shouldBe None // claimable, not automatic
|
||||
|
||||
test("draw claim via threefold repetition ends game with DrawEvent"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
@@ -268,7 +268,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
engine.processUserInput("g1f3")
|
||||
engine.processUserInput("g8f6")
|
||||
engine.processUserInput("f3g1")
|
||||
engine.processUserInput("f6g8") // threefold now available
|
||||
engine.processUserInput("f6g8") // threefold now available
|
||||
|
||||
observer.clear()
|
||||
engine.processUserInput("draw")
|
||||
|
||||
@@ -15,17 +15,17 @@ 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
|
||||
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)
|
||||
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"):
|
||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||
val engine = GameEngine(
|
||||
GameContext.initial,
|
||||
DefaultRules,
|
||||
@@ -99,7 +99,7 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
movesMade.get() should be >= 1
|
||||
|
||||
test("GameEngine plays valid bot moves"):
|
||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||
val engine = GameEngine(
|
||||
GameContext.initial,
|
||||
DefaultRules,
|
||||
@@ -125,17 +125,18 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
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 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.subscribe(
|
||||
new Observer:
|
||||
def onGameEvent(event: GameEvent): Unit = event match
|
||||
case _: MoveExecutedEvent => movesMade.incrementAndGet()
|
||||
case _ => (),
|
||||
)
|
||||
engine.startGame()
|
||||
Thread.sleep(500)
|
||||
@@ -143,17 +144,18 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
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 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.subscribe(
|
||||
new Observer:
|
||||
def onGameEvent(event: GameEvent): Unit = event match
|
||||
case _: InvalidMoveEvent => invalidCount.incrementAndGet()
|
||||
case _ => (),
|
||||
)
|
||||
engine.processUserInput("e2e4")
|
||||
Thread.sleep(1000)
|
||||
@@ -161,17 +163,18 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
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 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.subscribe(
|
||||
new Observer:
|
||||
def onGameEvent(event: GameEvent): Unit = event match
|
||||
case _: InvalidMoveEvent => invalidCount.incrementAndGet()
|
||||
case _ => (),
|
||||
)
|
||||
engine.processUserInput("e2e4")
|
||||
Thread.sleep(1000)
|
||||
@@ -179,12 +182,14 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
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 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,
|
||||
@@ -193,12 +198,17 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
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 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.subscribe(
|
||||
new Observer:
|
||||
def onGameEvent(event: GameEvent): Unit = event match
|
||||
case _: CheckmateEvent => checkmateCount.incrementAndGet()
|
||||
case _ => (),
|
||||
)
|
||||
engine.startGame()
|
||||
Thread.sleep(1000)
|
||||
@@ -206,11 +216,13 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
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 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,
|
||||
@@ -219,12 +231,17 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
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 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.subscribe(
|
||||
new Observer:
|
||||
def onGameEvent(event: GameEvent): Unit = event match
|
||||
case _: DrawEvent => drawCount.incrementAndGet()
|
||||
case _ => (),
|
||||
)
|
||||
engine.startGame()
|
||||
Thread.sleep(1000)
|
||||
@@ -237,13 +254,13 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
||||
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.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