refactor(tests): improve CommandInvoker tests for clarity and coverage
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-05 22:44:53 +02:00
parent 03c3b90d06
commit d2c22337aa
14 changed files with 358 additions and 712 deletions
@@ -163,13 +163,15 @@ class GameEngine(
moves.foreach: move =>
if error.isEmpty then
handleParsedMove(move.from, move.to)
move.moveType match
move.moveType match {
case MoveType.Promotion(pp) =>
if pendingPromotion.isDefined then
completePromotion(pp)
else
error = Some(s"Promotion required for move ${move.from}${move.to}")
case _ => ()
}
error match
case Some(err) =>
currentContext = savedContext
@@ -27,136 +27,122 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
previousContext = Some(GameContext.initial)
)
test("CommandInvoker.execute() with failing command returns false"):
test("execute rejects failing commands and keeps history unchanged"):
val invoker = new CommandInvoker()
val cmd = FailingCommand()
invoker.execute(cmd) shouldBe false
invoker.history.size shouldBe 0
invoker.getCurrentIndex shouldBe -1
test("CommandInvoker.execute() does not add failed command to history"):
val invoker = new CommandInvoker()
val failingCmd = FailingCommand()
val successCmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(failingCmd) shouldBe false
invoker.history.size shouldBe 0
invoker.execute(successCmd) shouldBe true
invoker.history.size shouldBe 1
invoker.history(0) shouldBe successCmd
invoker.history.head shouldBe successCmd
test("CommandInvoker.undo() returns false when currentIndex < 0"):
val invoker = new CommandInvoker()
invoker.undo() shouldBe false
test("undo redo and history trimming cover all command state transitions"):
{
val invoker = new CommandInvoker()
invoker.undo() shouldBe false
invoker.canUndo shouldBe false
invoker.undo() shouldBe false
}
test("CommandInvoker.undo() returns false when empty history"):
val invoker = new CommandInvoker()
invoker.canUndo shouldBe false
invoker.undo() shouldBe false
{
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.undo()
invoker.undo()
invoker.undo() shouldBe false
}
test("CommandInvoker.undo() returns false when currentIndex >= history size"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.undo()
invoker.undo()
invoker.undo()
{
val invoker = new CommandInvoker()
val failingUndoCmd = ConditionalFailCommand(shouldFailOnUndo = true)
invoker.execute(failingUndoCmd) shouldBe true
invoker.canUndo shouldBe true
invoker.undo() shouldBe false
invoker.getCurrentIndex shouldBe 0
}
test("CommandInvoker.undo() returns false when command.undo() fails"):
val invoker = new CommandInvoker()
val failingCmd = ConditionalFailCommand(shouldFailOnUndo = true)
invoker.execute(failingCmd) shouldBe true
invoker.canUndo shouldBe true
invoker.undo() shouldBe false
invoker.getCurrentIndex shouldBe 0
{
val invoker = new CommandInvoker()
val successUndoCmd = ConditionalFailCommand()
invoker.execute(successUndoCmd) shouldBe true
invoker.undo() shouldBe true
invoker.getCurrentIndex shouldBe -1
}
test("CommandInvoker.undo() returns true when command.undo() succeeds"):
val invoker = new CommandInvoker()
val successCmd = ConditionalFailCommand(shouldFailOnUndo = false)
invoker.execute(successCmd) shouldBe true
invoker.undo() shouldBe true
invoker.getCurrentIndex shouldBe -1
{
val invoker = new CommandInvoker()
invoker.redo() shouldBe false
}
test("CommandInvoker.redo() returns false when nothing to redo"):
val invoker = new CommandInvoker()
invoker.redo() shouldBe false
{
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd)
invoker.canRedo shouldBe false
invoker.redo() shouldBe false
}
test("CommandInvoker.redo() returns false when at end of history"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd)
invoker.canRedo shouldBe false
invoker.redo() shouldBe false
{
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val redoFailCmd = ConditionalFailCommand()
invoker.execute(cmd1)
invoker.execute(redoFailCmd)
invoker.undo()
invoker.canRedo shouldBe true
redoFailCmd.shouldFailOnExecute = true
invoker.redo() shouldBe false
invoker.getCurrentIndex shouldBe 0
}
test("CommandInvoker.redo() returns false when currentIndex + 1 >= size"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.canRedo shouldBe false
invoker.redo() shouldBe false
{
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd) shouldBe true
invoker.undo() shouldBe true
invoker.redo() shouldBe true
invoker.getCurrentIndex shouldBe 0
}
test("CommandInvoker.redo() returns false when command.execute() fails"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val redoFailCmd = ConditionalFailCommand(shouldFailOnExecute = false)
invoker.execute(cmd1)
invoker.execute(redoFailCmd)
invoker.undo()
invoker.canRedo shouldBe true
redoFailCmd.shouldFailOnExecute = true
invoker.redo() shouldBe false
invoker.getCurrentIndex shouldBe 0
{
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
val cmd3 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.undo()
invoker.canRedo shouldBe true
invoker.execute(cmd3)
invoker.canRedo shouldBe false
invoker.history.size shouldBe 2
invoker.history(1) shouldBe cmd3
}
test("CommandInvoker.redo() returns true when command.execute() succeeds"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd) shouldBe true
invoker.undo() shouldBe true
invoker.redo() shouldBe true
invoker.getCurrentIndex shouldBe 0
test("CommandInvoker.execute() discards redo history via while loop"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
val cmd3 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.undo()
invoker.canRedo shouldBe true
invoker.execute(cmd3)
invoker.canRedo shouldBe false
invoker.history.size shouldBe 2
invoker.history(1) shouldBe cmd3
test("CommandInvoker.execute() discards multiple redo commands"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
val cmd3 = createMoveCommand(sq(File.G, Rank.R1), sq(File.F, Rank.R3))
val cmd4 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.execute(cmd3)
invoker.execute(cmd4)
invoker.undo()
invoker.undo()
invoker.canRedo shouldBe true
val newCmd = createMoveCommand(sq(File.B, Rank.R2), sq(File.B, Rank.R4))
invoker.execute(newCmd)
invoker.history.size shouldBe 3
invoker.canRedo shouldBe false
test("CommandInvoker.execute() with no redo history (while condition false)"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.canRedo shouldBe false
val cmd3 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
invoker.execute(cmd3)
invoker.history.size shouldBe 3
{
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
val cmd3 = createMoveCommand(sq(File.G, Rank.R1), sq(File.F, Rank.R3))
val cmd4 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.execute(cmd3)
invoker.execute(cmd4)
invoker.undo()
invoker.undo()
invoker.canRedo shouldBe true
val newCmd = createMoveCommand(sq(File.B, Rank.R2), sq(File.B, Rank.R4))
invoker.execute(newCmd)
invoker.history.size shouldBe 3
invoker.canRedo shouldBe false
}
@@ -17,63 +17,31 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
previousContext = Some(GameContext.initial)
)
test("CommandInvoker executes a command and adds it to history"):
test("execute appends commands and updates index"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd) shouldBe true
invoker.history.size shouldBe 1
invoker.getCurrentIndex shouldBe 0
test("CommandInvoker executes multiple commands in sequence"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
invoker.execute(cmd1) shouldBe true
invoker.execute(cmd2) shouldBe true
invoker.history.size shouldBe 2
invoker.getCurrentIndex shouldBe 1
test("CommandInvoker.canUndo returns false when empty"):
val invoker = new CommandInvoker()
invoker.canUndo shouldBe false
test("CommandInvoker.canUndo returns true after execution"):
test("undo and redo update index and availability flags"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.canUndo shouldBe false
invoker.execute(cmd)
invoker.canUndo shouldBe true
test("CommandInvoker.undo decrements current index"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd)
invoker.getCurrentIndex shouldBe 0
invoker.undo() shouldBe true
invoker.getCurrentIndex shouldBe -1
test("CommandInvoker.canRedo returns true after undo"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd)
invoker.undo()
invoker.canRedo shouldBe true
test("CommandInvoker.redo re-executes a command"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd)
invoker.undo() shouldBe true
invoker.redo() shouldBe true
invoker.getCurrentIndex shouldBe 0
test("CommandInvoker.canUndo returns false when at beginning"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd)
invoker.undo()
invoker.canUndo shouldBe false
test("CommandInvoker clear removes all history"):
test("clear removes full history and resets index"):
val invoker = new CommandInvoker()
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
invoker.execute(cmd)
@@ -81,24 +49,7 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
invoker.history.size shouldBe 0
invoker.getCurrentIndex shouldBe -1
test("CommandInvoker discards all history when executing after undoing all"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
val cmd3 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
invoker.execute(cmd1)
invoker.execute(cmd2)
invoker.undo()
invoker.undo()
invoker.getCurrentIndex shouldBe -1
invoker.canRedo shouldBe true
invoker.execute(cmd3)
invoker.canRedo shouldBe false
invoker.history.size shouldBe 1
invoker.history(0) shouldBe cmd3
invoker.getCurrentIndex shouldBe 0
test("CommandInvoker discards redo history when executing mid-history"):
test("execute after undo discards redo history"):
val invoker = new CommandInvoker()
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
@@ -111,6 +62,6 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
invoker.execute(cmd3)
invoker.canRedo shouldBe false
invoker.history.size shouldBe 2
invoker.history(0) shouldBe cmd1
invoker.history.head shouldBe cmd1
invoker.history(1) shouldBe cmd3
invoker.getCurrentIndex shouldBe 1
@@ -6,37 +6,19 @@ import org.scalatest.matchers.should.Matchers
class CommandTest extends AnyFunSuite with Matchers:
test("QuitCommand can be created"):
test("QuitCommand properties and behavior"):
val cmd = QuitCommand()
cmd shouldNot be(null)
test("QuitCommand execute returns true"):
val cmd = QuitCommand()
cmd.execute() shouldBe true
test("QuitCommand undo returns false (cannot undo quit)"):
val cmd = QuitCommand()
cmd.undo() shouldBe false
test("QuitCommand description"):
val cmd = QuitCommand()
cmd.description shouldBe "Quit game"
test("ResetCommand with no prior state"):
val cmd = ResetCommand()
cmd.execute() shouldBe true
cmd.undo() shouldBe false
test("ResetCommand behavior depends on previousContext"):
val noState = ResetCommand()
noState.execute() shouldBe true
noState.undo() shouldBe false
noState.description shouldBe "Reset board"
test("ResetCommand with prior state can undo"):
val cmd = ResetCommand(previousContext = Some(GameContext.initial))
cmd.execute() shouldBe true
cmd.undo() shouldBe true
test("ResetCommand with no context cannot undo"):
val cmd = ResetCommand(previousContext = None)
cmd.execute() shouldBe true
cmd.undo() shouldBe false
test("ResetCommand description"):
val cmd = ResetCommand()
cmd.description shouldBe "Reset board"
val withState = ResetCommand(previousContext = Some(GameContext.initial))
withState.execute() shouldBe true
withState.undo() shouldBe true
@@ -9,51 +9,31 @@ class MoveCommandTest extends AnyFunSuite with Matchers:
private def sq(f: File, r: Rank): Square = Square(f, r)
test("MoveCommand with no moveResult defaults to None"):
test("MoveCommand defaults to empty optional state and false execute/undo"):
val cmd = MoveCommand(from = sq(File.E, Rank.R2), to = sq(File.E, Rank.R4))
cmd.moveResult shouldBe None
cmd.execute() shouldBe false
test("MoveCommand with no previousContext defaults to None"):
val cmd = MoveCommand(from = sq(File.E, Rank.R2), to = sq(File.E, Rank.R4))
cmd.previousContext shouldBe None
cmd.execute() shouldBe false
cmd.undo() shouldBe false
test("MoveCommand description is always returned"):
val cmd = MoveCommand(from = sq(File.E, Rank.R2), to = sq(File.E, Rank.R4))
cmd.description shouldBe "Move from e2 to e4"
test("MoveCommand execute returns false when moveResult is None"):
val cmd = MoveCommand(from = sq(File.A, Rank.R1), to = sq(File.B, Rank.R3))
cmd.execute() shouldBe false
test("MoveCommand undo returns false when previousContext is None"):
val cmd = MoveCommand(
from = sq(File.E, Rank.R2),
to = sq(File.E, Rank.R4),
moveResult = Some(MoveResult.Successful(GameContext.initial, None)),
previousContext = None
)
cmd.undo() shouldBe false
test("MoveCommand execute returns true when moveResult is defined"):
val cmd = MoveCommand(
test("MoveCommand execute/undo succeed when state is present"):
val executable = MoveCommand(
from = sq(File.E, Rank.R2),
to = sq(File.E, Rank.R4),
moveResult = Some(MoveResult.Successful(GameContext.initial, None))
)
cmd.execute() shouldBe true
executable.execute() shouldBe true
test("MoveCommand undo returns true when previousContext is defined"):
val cmd = MoveCommand(
val undoable = MoveCommand(
from = sq(File.E, Rank.R2),
to = sq(File.E, Rank.R4),
moveResult = Some(MoveResult.Successful(GameContext.initial, None)),
previousContext = Some(GameContext.initial)
)
cmd.undo() shouldBe true
undoable.undo() shouldBe true
test("MoveCommand should be immutable - fields cannot be mutated after creation"):
test("MoveCommand is immutable and preserves equality/hash semantics"):
val cmd1 = MoveCommand(from = sq(File.E, Rank.R2), to = sq(File.E, Rank.R4))
val result = MoveResult.Successful(GameContext.initial, None)
@@ -68,24 +48,23 @@ class MoveCommandTest extends AnyFunSuite with Matchers:
cmd2.moveResult shouldBe Some(result)
cmd2.previousContext shouldBe Some(GameContext.initial)
test("MoveCommand equals and hashCode respect immutability"):
val cmd1 = MoveCommand(
val eq1 = MoveCommand(
from = sq(File.E, Rank.R2),
to = sq(File.E, Rank.R4),
moveResult = None,
previousContext = None
)
val cmd2 = MoveCommand(
val eq2 = MoveCommand(
from = sq(File.E, Rank.R2),
to = sq(File.E, Rank.R4),
moveResult = None,
previousContext = None
)
cmd1 shouldBe cmd2
cmd1.hashCode shouldBe cmd2.hashCode
eq1 shouldBe eq2
eq1.hashCode shouldBe eq2.hashCode
val hash1 = cmd1.hashCode
val hash2 = cmd1.hashCode
val hash1 = eq1.hashCode
val hash2 = eq1.hashCode
hash1 shouldBe hash2
@@ -17,7 +17,7 @@ class GameEngineCoverageRegressionTest extends AnyFunSuite with Matchers:
private def captureEvents(engine: GameEngine): collection.mutable.ListBuffer[GameEvent] =
val events = collection.mutable.ListBuffer[GameEvent]()
engine.subscribe(new Observer { def onGameEvent(event: GameEvent): Unit = events += event })
engine.subscribe((event: GameEvent) => events += event)
events
test("accessors expose redo availability and command history"):
@@ -141,6 +141,15 @@ class GameEngineCoverageRegressionTest extends AnyFunSuite with Matchers:
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 saved = engine.context
val illegalPromotion = Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Queen))
val trailingMove = Move(sq("e2"), sq("e4"))
engine.replayMoves(List(illegalPromotion, trailingMove), saved) shouldBe Left("Promotion required for move e2e1")
engine.context shouldBe saved
test("normalMoveNotation handles missing source piece"):
val engine = new GameEngine()
@@ -12,47 +12,31 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
// ── Observer wiring ────────────────────────────────────────────
test("subscribe adds observer to notification list"):
test("observer subscribe and unsubscribe behavior"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
engine.processUserInput("e2e4")
observer.hasEvent[MoveExecutedEvent] shouldBe true
test("unsubscribe removes observer from notification list"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
val countBeforeUnsubscribe = observer.eventCount
engine.subscribe(observer)
engine.unsubscribe(observer)
engine.processUserInput("e2e4")
observer.eventCount shouldBe 0
observer.eventCount shouldBe countBeforeUnsubscribe
// ── Initial state ──────────────────────────────────────────────
test("initial game context has standard starting position"):
test("initial engine state is standard"):
val engine = EngineTestHelpers.makeEngine()
engine.board.pieceAt(Square(File.E, Rank.R1)) shouldBe Some(Piece.WhiteKing)
test("initial turn is white"):
val engine = EngineTestHelpers.makeEngine()
engine.turn shouldBe Color.White
// ── Quit command ──────────────────────────────────────────────
test("quit command does not error"):
test("quit aliases and reset keep engine responsive"):
val engine = EngineTestHelpers.makeEngine()
engine.processUserInput("quit")
// Quit just returns, no event
test("q alias for quit works"):
val engine = EngineTestHelpers.makeEngine()
engine.processUserInput("q")
// Quit just returns, no event
// ── Reset command ──────────────────────────────────────────────
test("reset command returns game to initial position"):
val engine = EngineTestHelpers.makeEngine()
engine.processUserInput("e2e4")
engine.reset()
@@ -62,20 +46,16 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
// ── Turn toggling ──────────────────────────────────────────────
test("turn toggles to black after valid white move"):
test("turn toggles across valid move sequence"):
val engine = EngineTestHelpers.makeEngine()
engine.processUserInput("e2e4")
engine.turn shouldBe Color.Black
test("turn toggles back to white after black move"):
val engine = EngineTestHelpers.makeEngine()
engine.processUserInput("e2e4")
engine.processUserInput("e7e5")
engine.turn shouldBe Color.White
// ── Invalid moves (minimal) ────────────────────────────────────
test("move with no piece at source fails"):
test("invalid move forms trigger InvalidMoveEvent and keep turn where relevant"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
@@ -85,20 +65,10 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
observer.hasEvent[InvalidMoveEvent] shouldBe true
engine.turn shouldBe Color.White // turn unchanged
test("move with opponent piece fails"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
engine.processUserInput("e7e5") // try to move black pawn on white's turn
observer.hasEvent[InvalidMoveEvent] shouldBe true
test("illegal move fails (pawn backward)"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
engine.processUserInput("e2e4")
engine.processUserInput("e5e4") // pawn backward
@@ -106,8 +76,15 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
// ── Undo/Redo ────────────────────────────────────────────────
test("undo restores previous position"):
test("undo redo success and empty-history failures"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
engine.undo()
observer.hasEvent[InvalidMoveEvent] shouldBe true
observer.clear()
engine.processUserInput("e2e4")
engine.undo()
@@ -115,37 +92,17 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
engine.board.pieceAt(Square(File.E, Rank.R2)) shouldBe Some(Piece.WhitePawn)
engine.turn shouldBe Color.White
test("redo replays last undone move"):
val engine = EngineTestHelpers.makeEngine()
engine.processUserInput("e2e4")
engine.undo()
engine.redo()
engine.board.pieceAt(Square(File.E, Rank.R4)) shouldBe Some(Piece.WhitePawn)
engine.turn shouldBe Color.Black
test("undo on empty history triggers InvalidMoveEvent"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
engine.undo()
observer.hasEvent[InvalidMoveEvent] shouldBe true
test("redo on empty future history triggers InvalidMoveEvent"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
observer.clear()
engine.redo()
observer.hasEvent[InvalidMoveEvent] shouldBe true
// ── Fifty-move rule ────────────────────────────────────────────
test("FiftyMoveRuleAvailableEvent fired when half-move clock reaches 100"):
test("fifty-move event and draw claim success/failure"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
@@ -159,13 +116,6 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
observer.hasEvent[FiftyMoveRuleAvailableEvent] shouldBe true
// ── Draw claiming ──────────────────────────────────────────────
test("draw claim succeeds when available"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// Load position with sufficient move history for draw claim
EngineTestHelpers.loadFen(engine, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 100 1")
observer.clear()
@@ -174,12 +124,9 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
observer.hasEvent[DrawClaimedEvent] shouldBe true
test("draw claim fails when not available"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// Initial position has no draw available
observer.clear()
engine.reset()
engine.processUserInput("draw")
observer.hasEvent[InvalidMoveEvent] shouldBe true