refactor(tests): improve CommandInvoker tests for clarity and coverage
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -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
|
||||
|
||||
+97
-111
@@ -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
|
||||
|
||||
+10
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user