feat: NCS-25 Add linters to keep quality up (#27)
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #27 Reviewed-by: Leon Hermann <lq@blackhole.local> Co-authored-by: Janis <janis.e.20@gmx.de> Co-committed-by: Janis <janis.e.20@gmx.de>
This commit was merged in pull request #27.
This commit is contained in:
+26
-23
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.chess.command
|
||||
|
||||
import de.nowchess.api.board.{Square, File, Rank}
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -10,13 +10,16 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
private def sq(f: File, r: Rank): Square = Square(f, r)
|
||||
|
||||
private case class FailingCommand() extends Command:
|
||||
override def execute(): Boolean = false
|
||||
override def undo(): Boolean = false
|
||||
override def execute(): Boolean = false
|
||||
override def undo(): Boolean = false
|
||||
override def description: String = "Failing command"
|
||||
|
||||
private case class ConditionalFailCommand(var shouldFailOnUndo: Boolean = false, var shouldFailOnExecute: Boolean = false) extends Command:
|
||||
override def execute(): Boolean = !shouldFailOnExecute
|
||||
override def undo(): Boolean = !shouldFailOnUndo
|
||||
private case class ConditionalFailCommand(
|
||||
var shouldFailOnUndo: Boolean = false,
|
||||
var shouldFailOnExecute: Boolean = false,
|
||||
) extends Command:
|
||||
override def execute(): Boolean = !shouldFailOnExecute
|
||||
override def undo(): Boolean = !shouldFailOnUndo
|
||||
override def description: String = "Conditional fail"
|
||||
|
||||
private def createMoveCommand(from: Square, to: Square, executeSucceeds: Boolean = true): MoveCommand =
|
||||
@@ -24,12 +27,12 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
from = from,
|
||||
to = to,
|
||||
moveResult = if executeSucceeds then Some(MoveResult.Successful(GameContext.initial, None)) else None,
|
||||
previousContext = Some(GameContext.initial)
|
||||
previousContext = Some(GameContext.initial),
|
||||
)
|
||||
|
||||
test("execute rejects failing commands and keeps history unchanged"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = FailingCommand()
|
||||
val cmd = FailingCommand()
|
||||
invoker.execute(cmd) shouldBe false
|
||||
invoker.history.size shouldBe 0
|
||||
invoker.getCurrentIndex shouldBe -1
|
||||
@@ -52,8 +55,8 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
|
||||
{
|
||||
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 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()
|
||||
@@ -62,7 +65,7 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
{
|
||||
val invoker = new CommandInvoker()
|
||||
val invoker = new CommandInvoker()
|
||||
val failingUndoCmd = ConditionalFailCommand(shouldFailOnUndo = true)
|
||||
invoker.execute(failingUndoCmd) shouldBe true
|
||||
invoker.canUndo shouldBe true
|
||||
@@ -71,7 +74,7 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
{
|
||||
val invoker = new CommandInvoker()
|
||||
val invoker = new CommandInvoker()
|
||||
val successUndoCmd = ConditionalFailCommand()
|
||||
invoker.execute(successUndoCmd) shouldBe true
|
||||
invoker.undo() shouldBe true
|
||||
@@ -85,15 +88,15 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
|
||||
{
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
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 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)
|
||||
@@ -106,7 +109,7 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
|
||||
{
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
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
|
||||
@@ -115,9 +118,9 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
|
||||
{
|
||||
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))
|
||||
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()
|
||||
@@ -130,10 +133,10 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
|
||||
{
|
||||
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))
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.chess.command
|
||||
|
||||
import de.nowchess.api.board.{Square, File, Rank}
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -14,12 +14,12 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
from = from,
|
||||
to = to,
|
||||
moveResult = Some(MoveResult.Successful(GameContext.initial, None)),
|
||||
previousContext = Some(GameContext.initial)
|
||||
previousContext = Some(GameContext.initial),
|
||||
)
|
||||
|
||||
test("execute appends commands and updates index"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
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
|
||||
@@ -31,7 +31,7 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
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))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.canUndo shouldBe false
|
||||
invoker.execute(cmd)
|
||||
invoker.canUndo shouldBe true
|
||||
@@ -43,7 +43,7 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
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))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd)
|
||||
invoker.clear()
|
||||
invoker.history.size shouldBe 0
|
||||
@@ -51,9 +51,9 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
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))
|
||||
val cmd3 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
|
||||
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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.chess.command
|
||||
|
||||
import de.nowchess.api.board.{Square, File, Rank}
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@@ -21,7 +21,7 @@ class MoveCommandTest extends AnyFunSuite with Matchers:
|
||||
val executable = MoveCommand(
|
||||
from = sq(File.E, Rank.R2),
|
||||
to = sq(File.E, Rank.R4),
|
||||
moveResult = Some(MoveResult.Successful(GameContext.initial, None))
|
||||
moveResult = Some(MoveResult.Successful(GameContext.initial, None)),
|
||||
)
|
||||
executable.execute() shouldBe true
|
||||
|
||||
@@ -29,7 +29,7 @@ class MoveCommandTest extends AnyFunSuite with Matchers:
|
||||
from = sq(File.E, Rank.R2),
|
||||
to = sq(File.E, Rank.R4),
|
||||
moveResult = Some(MoveResult.Successful(GameContext.initial, None)),
|
||||
previousContext = Some(GameContext.initial)
|
||||
previousContext = Some(GameContext.initial),
|
||||
)
|
||||
undoable.undo() shouldBe true
|
||||
|
||||
@@ -39,7 +39,7 @@ class MoveCommandTest extends AnyFunSuite with Matchers:
|
||||
val result = MoveResult.Successful(GameContext.initial, None)
|
||||
val cmd2 = cmd1.copy(
|
||||
moveResult = Some(result),
|
||||
previousContext = Some(GameContext.initial)
|
||||
previousContext = Some(GameContext.initial),
|
||||
)
|
||||
|
||||
cmd1.moveResult shouldBe None
|
||||
@@ -52,14 +52,14 @@ class MoveCommandTest extends AnyFunSuite with Matchers:
|
||||
from = sq(File.E, Rank.R2),
|
||||
to = sq(File.E, Rank.R4),
|
||||
moveResult = None,
|
||||
previousContext = None
|
||||
previousContext = None,
|
||||
)
|
||||
|
||||
val eq2 = MoveCommand(
|
||||
from = sq(File.E, Rank.R2),
|
||||
to = sq(File.E, Rank.R4),
|
||||
moveResult = None,
|
||||
previousContext = None
|
||||
previousContext = None,
|
||||
)
|
||||
|
||||
eq1 shouldBe eq2
|
||||
|
||||
@@ -27,7 +27,7 @@ object EngineTestHelpers:
|
||||
private val _events = mutable.ListBuffer[GameEvent]()
|
||||
|
||||
def events: mutable.ListBuffer[GameEvent] = _events
|
||||
def eventCount: Int = _events.length
|
||||
def eventCount: Int = _events.length
|
||||
def hasEvent[T <: GameEvent](implicit ct: scala.reflect.ClassTag[T]): Boolean =
|
||||
_events.exists(ct.runtimeClass.isInstance(_))
|
||||
def getEvent[T <: GameEvent](implicit ct: scala.reflect.ClassTag[T]): Option[T] =
|
||||
|
||||
+38
-29
@@ -2,7 +2,7 @@ package de.nowchess.chess.engine
|
||||
|
||||
import scala.collection.mutable
|
||||
import de.nowchess.api.board.{Board, Color}
|
||||
import de.nowchess.chess.observer.{Observer, GameEvent, CheckDetectedEvent, CheckmateEvent, StalemateEvent}
|
||||
import de.nowchess.chess.observer.{CheckDetectedEvent, CheckmateEvent, GameEvent, Observer, StalemateEvent}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
@@ -10,82 +10,91 @@ import org.scalatest.matchers.should.Matchers
|
||||
class GameEngineGameEndingTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("GameEngine handles Checkmate (Fool's Mate)"):
|
||||
val engine = new GameEngine()
|
||||
val engine = new GameEngine()
|
||||
val observer = new EndingMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
|
||||
// Play Fool's mate
|
||||
engine.processUserInput("f2f3")
|
||||
engine.processUserInput("e7e5")
|
||||
engine.processUserInput("g2g4")
|
||||
|
||||
|
||||
observer.events.clear()
|
||||
engine.processUserInput("d8h4")
|
||||
|
||||
// Verify CheckmateEvent (engine also fires MoveExecutedEvent before CheckmateEvent)
|
||||
observer.events.last shouldBe a[CheckmateEvent]
|
||||
|
||||
|
||||
val event = observer.events.last.asInstanceOf[CheckmateEvent]
|
||||
event.winner shouldBe Color.Black
|
||||
|
||||
|
||||
// Board should be reset after checkmate
|
||||
engine.board shouldBe Board.initial
|
||||
engine.turn shouldBe Color.White
|
||||
|
||||
test("GameEngine handles check detection"):
|
||||
val engine = new GameEngine()
|
||||
val engine = new GameEngine()
|
||||
val observer = new EndingMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
|
||||
// Play a simple check
|
||||
engine.processUserInput("e2e4")
|
||||
engine.processUserInput("e7e5")
|
||||
engine.processUserInput("f1c4")
|
||||
engine.processUserInput("g8f6")
|
||||
|
||||
|
||||
observer.events.clear()
|
||||
engine.processUserInput("c4f7") // Check!
|
||||
|
||||
|
||||
val checkEvents = observer.events.collect { case e: CheckDetectedEvent => e }
|
||||
checkEvents.size shouldBe 1
|
||||
checkEvents.head.context.turn shouldBe Color.Black // Black is now in check
|
||||
|
||||
|
||||
// Shortest known stalemate is 19 moves. Here is a faster one:
|
||||
// e3 a5 Qh5 Ra6 Qxa5 h5 h4 Rah6 Qxc7 f6 Qxd7+ Kf7 Qxb7 Qd3 Qxb8 Qh7 Qxc8 Kg6 Qe6
|
||||
// Wait, let's just use Sam Loyd's 10-move stalemate:
|
||||
// 1. e3 a5 2. Qh5 Ra6 3. Qxa5 h5 4. h4 Rah6 5. Qxc7 f6 6. Qxd7+ Kf7 7. Qxb7 Qd3 8. Qxb8 Qh7 9. Qxc8 Kg6 10. Qe6
|
||||
test("GameEngine handles Stalemate via 10-move known sequence"):
|
||||
val engine = new GameEngine()
|
||||
val engine = new GameEngine()
|
||||
val observer = new EndingMockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
|
||||
val moves = List(
|
||||
"e2e3", "a7a5",
|
||||
"d1h5", "a8a6",
|
||||
"h5a5", "h7h5",
|
||||
"h2h4", "a6h6",
|
||||
"a5c7", "f7f6",
|
||||
"c7d7", "e8f7",
|
||||
"d7b7", "d8d3",
|
||||
"b7b8", "d3h7",
|
||||
"b8c8", "f7g6",
|
||||
"c8e6"
|
||||
"e2e3",
|
||||
"a7a5",
|
||||
"d1h5",
|
||||
"a8a6",
|
||||
"h5a5",
|
||||
"h7h5",
|
||||
"h2h4",
|
||||
"a6h6",
|
||||
"a5c7",
|
||||
"f7f6",
|
||||
"c7d7",
|
||||
"e8f7",
|
||||
"d7b7",
|
||||
"d8d3",
|
||||
"b7b8",
|
||||
"d3h7",
|
||||
"b8c8",
|
||||
"f7g6",
|
||||
"c8e6",
|
||||
)
|
||||
|
||||
|
||||
moves.dropRight(1).foreach(engine.processUserInput)
|
||||
|
||||
|
||||
observer.events.clear()
|
||||
engine.processUserInput(moves.last)
|
||||
|
||||
|
||||
val stalemateEvents = observer.events.collect { case e: StalemateEvent => e }
|
||||
stalemateEvents.size shouldBe 1
|
||||
|
||||
|
||||
// Board should be reset after stalemate
|
||||
engine.board shouldBe Board.initial
|
||||
engine.turn shouldBe Color.White
|
||||
|
||||
private class EndingMockObserver extends Observer:
|
||||
val events = mutable.ListBuffer[GameEvent]()
|
||||
|
||||
|
||||
override def onGameEvent(event: GameEvent): Unit =
|
||||
events += event
|
||||
events += event
|
||||
|
||||
+18
-21
@@ -92,12 +92,12 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = legalMoves(context)(square)
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] =
|
||||
if square == sq("e2") then List(promotionMove) else List.empty
|
||||
def allLegalMoves(context: GameContext): List[Move] = List(promotionMove)
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = false
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def allLegalMoves(context: GameContext): List[Move] = List(promotionMove)
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = false
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = DefaultRules.applyMove(context)(move)
|
||||
|
||||
val engine = new GameEngine(ruleSet = permissiveRules)
|
||||
@@ -112,14 +112,14 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
||||
val promotionMove = Move(sq("e2"), sq("e8"), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val noLegalMoves = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = List.empty
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = List.empty
|
||||
def allLegalMoves(context: GameContext): List[Move] = List.empty
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = false
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = List.empty
|
||||
def allLegalMoves(context: GameContext): List[Move] = List.empty
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = false
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
|
||||
val engine = new GameEngine(ruleSet = noLegalMoves)
|
||||
engine.processUserInput("e2e4")
|
||||
@@ -137,21 +137,20 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("loadGame replay executes non-promotion moves through default replay branch"):
|
||||
val normalMove = Move(sq("e2"), sq("e4"), MoveType.Normal())
|
||||
val engine = new GameEngine()
|
||||
val engine = new GameEngine()
|
||||
|
||||
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 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"))
|
||||
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()
|
||||
val result = engine.normalMoveNotation(Move(sq("e3"), sq("e4")), Board.initial, isCapture = false)
|
||||
@@ -174,5 +173,3 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
||||
engine.observerCount shouldBe 1
|
||||
engine.unsubscribe(observer)
|
||||
engine.observerCount shouldBe 0
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.nowchess.chess.engine
|
||||
import scala.collection.mutable
|
||||
import de.nowchess.api.board.{Board, Color}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.chess.observer.{Observer, GameEvent, PgnLoadedEvent}
|
||||
import de.nowchess.chess.observer.{GameEvent, Observer, PgnLoadedEvent}
|
||||
import de.nowchess.io.pgn.PgnParser
|
||||
import de.nowchess.io.fen.FenParser
|
||||
import de.nowchess.io.pgn.PgnExporter
|
||||
@@ -15,7 +15,7 @@ class GameEngineLoadGameTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("loadGame with PgnParser: loads valid PGN and enables undo/redo"):
|
||||
val engine = new GameEngine()
|
||||
val pgn = "[Event \"Test\"]\n\n1. e4 e5\n"
|
||||
val pgn = "[Event \"Test\"]\n\n1. e4 e5\n"
|
||||
val result = engine.loadGame(PgnParser, pgn)
|
||||
result shouldBe Right(())
|
||||
engine.context.moves.size shouldBe 2
|
||||
@@ -23,7 +23,7 @@ class GameEngineLoadGameTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("loadGame with FenParser: loads position without replaying moves"):
|
||||
val engine = new GameEngine()
|
||||
val fen = "8/4P3/4k3/8/8/8/8/8 w - - 0 1"
|
||||
val fen = "8/4P3/4k3/8/8/8/8/8 w - - 0 1"
|
||||
val result = engine.loadGame(FenParser, fen)
|
||||
result shouldBe Right(())
|
||||
engine.context.moves.isEmpty shouldBe true
|
||||
|
||||
@@ -9,11 +9,11 @@ import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
/** Tests that exercise moveToPgn branches not covered by other test files:
|
||||
* - CastleQueenside (line 223)
|
||||
* - EnPassant notation (lines 224-225) and computeCaptured EnPassant (lines 254-255)
|
||||
* - Promotion(Bishop) notation (line 230)
|
||||
* - King normal move notation (line 246)
|
||||
*/
|
||||
* - CastleQueenside (line 223)
|
||||
* - EnPassant notation (lines 224-225) and computeCaptured EnPassant (lines 254-255)
|
||||
* - Promotion(Bishop) notation (line 230)
|
||||
* - King normal move notation (line 246)
|
||||
*/
|
||||
class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def captureEvents(engine: GameEngine): collection.mutable.ListBuffer[GameEvent] =
|
||||
@@ -28,10 +28,10 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
||||
val board = FenParser.parseBoard("k7/8/8/8/8/8/8/R3K3").get
|
||||
// Castling rights: white queen-side only (no king-side rook present)
|
||||
val castlingRights = de.nowchess.api.board.CastlingRights(
|
||||
whiteKingSide = false,
|
||||
whiteKingSide = false,
|
||||
whiteQueenSide = true,
|
||||
blackKingSide = false,
|
||||
blackQueenSide = false
|
||||
blackKingSide = false,
|
||||
blackQueenSide = false,
|
||||
)
|
||||
val ctx = GameContext.initial
|
||||
.withBoard(board)
|
||||
@@ -43,7 +43,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
// White castles queenside: e1c1
|
||||
engine.processUserInput("e1c1")
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be (true)
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be(true)
|
||||
|
||||
events.clear()
|
||||
engine.undo()
|
||||
@@ -55,7 +55,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("undo after en passant emits MoveUndoneEvent with file-x-destination notation"):
|
||||
// White pawn on e5, black pawn on d5 (just double-pushed), en passant square d6
|
||||
val board = FenParser.parseBoard("k7/8/8/3pP3/8/8/8/7K").get
|
||||
val board = FenParser.parseBoard("k7/8/8/3pP3/8/8/8/7K").get
|
||||
val epSquare = Square.fromAlgebraic("d6")
|
||||
val ctx = GameContext.initial
|
||||
.withBoard(board)
|
||||
@@ -68,12 +68,12 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
// White pawn on e5 captures en passant to d6
|
||||
engine.processUserInput("e5d6")
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be (true)
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be(true)
|
||||
|
||||
// Verify the captured pawn was found (computeCaptured EnPassant branch)
|
||||
val moveEvt = events.collect { case e: MoveExecutedEvent => e }.head
|
||||
moveEvt.capturedPiece shouldBe defined
|
||||
moveEvt.capturedPiece.get should include ("Black")
|
||||
moveEvt.capturedPiece.get should include("Black")
|
||||
|
||||
events.clear()
|
||||
engine.undo()
|
||||
@@ -117,11 +117,11 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
// King moves e1 -> f1
|
||||
engine.processUserInput("e1f1")
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be (true)
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be(true)
|
||||
|
||||
events.clear()
|
||||
engine.undo()
|
||||
|
||||
val evt = events.collect { case e: MoveUndoneEvent => e }.head
|
||||
evt.pgnNotation should startWith ("K")
|
||||
evt.pgnNotation should include ("f1")
|
||||
evt.pgnNotation should startWith("K")
|
||||
evt.pgnNotation should include("f1")
|
||||
|
||||
@@ -10,7 +10,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
// ── Checkmate ───────────────────────────────────────────────────
|
||||
|
||||
test("checkmate ends game with CheckmateEvent"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -24,7 +24,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
observer.hasEvent[CheckmateEvent] shouldBe true
|
||||
|
||||
test("checkmate with white winner"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -45,20 +45,29 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
// ── Stalemate ───────────────────────────────────────────────────
|
||||
|
||||
test("stalemate ends game with StalemateEvent"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
val moves = List(
|
||||
"e2e3", "a7a5",
|
||||
"d1h5", "a8a6",
|
||||
"h5a5", "h7h5",
|
||||
"h2h4", "a6h6",
|
||||
"a5c7", "f7f6",
|
||||
"c7d7", "e8f7",
|
||||
"d7b7", "d8d3",
|
||||
"b7b8", "d3h7",
|
||||
"b8c8", "f7g6"
|
||||
"e2e3",
|
||||
"a7a5",
|
||||
"d1h5",
|
||||
"a8a6",
|
||||
"h5a5",
|
||||
"h7h5",
|
||||
"h2h4",
|
||||
"a6h6",
|
||||
"a5c7",
|
||||
"f7f6",
|
||||
"c7d7",
|
||||
"e8f7",
|
||||
"d7b7",
|
||||
"d8d3",
|
||||
"b7b8",
|
||||
"d3h7",
|
||||
"b8c8",
|
||||
"f7g6",
|
||||
)
|
||||
moves.foreach(engine.processUserInput)
|
||||
observer.clear()
|
||||
@@ -68,21 +77,30 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
observer.hasEvent[StalemateEvent] shouldBe true
|
||||
|
||||
test("stalemate when king has no moves and no pieces"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
val moves = List(
|
||||
"e2e3", "a7a5",
|
||||
"d1h5", "a8a6",
|
||||
"h5a5", "h7h5",
|
||||
"h2h4", "a6h6",
|
||||
"a5c7", "f7f6",
|
||||
"c7d7", "e8f7",
|
||||
"d7b7", "d8d3",
|
||||
"b7b8", "d3h7",
|
||||
"b8c8", "f7g6",
|
||||
"c8e6"
|
||||
"e2e3",
|
||||
"a7a5",
|
||||
"d1h5",
|
||||
"a8a6",
|
||||
"h5a5",
|
||||
"h7h5",
|
||||
"h2h4",
|
||||
"a6h6",
|
||||
"a5c7",
|
||||
"f7f6",
|
||||
"c7d7",
|
||||
"e8f7",
|
||||
"d7b7",
|
||||
"d8d3",
|
||||
"b7b8",
|
||||
"d3h7",
|
||||
"b8c8",
|
||||
"f7g6",
|
||||
"c8e6",
|
||||
)
|
||||
|
||||
moves.foreach(engine.processUserInput)
|
||||
@@ -93,7 +111,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
// ── Check detection ────────────────────────────────────────────
|
||||
|
||||
test("check detected after move puts king in check"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -108,7 +126,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
observer.hasEvent[CheckDetectedEvent] shouldBe true
|
||||
|
||||
test("check by knight"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -122,7 +140,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
// ── Fifty-move rule ────────────────────────────────────────────
|
||||
|
||||
test("fifty-move rule triggers when half-move clock reaches 100"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -155,7 +173,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
// ── Draw claim ────────────────────────────────────────────────
|
||||
|
||||
test("draw can be claimed when fifty-move rule is available"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -167,7 +185,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
||||
observer.hasEvent[DrawClaimedEvent] shouldBe true
|
||||
|
||||
test("draw cannot be claimed when not available"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
|
||||
+41
-41
@@ -24,54 +24,54 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("processUserInput fires PromotionRequiredEvent when pawn reaches back rank") {
|
||||
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val engine = engineWith(promotionBoard)
|
||||
val events = captureEvents(engine)
|
||||
val engine = engineWith(promotionBoard)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e7e8")
|
||||
|
||||
events.exists(_.isInstanceOf[PromotionRequiredEvent]) should be (true)
|
||||
events.collect { case e: PromotionRequiredEvent => e }.head.from should be (sq(File.E, Rank.R7))
|
||||
events.exists(_.isInstanceOf[PromotionRequiredEvent]) should be(true)
|
||||
events.collect { case e: PromotionRequiredEvent => e }.head.from should be(sq(File.E, Rank.R7))
|
||||
}
|
||||
|
||||
test("isPendingPromotion is true after PromotionRequired input") {
|
||||
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val engine = engineWith(promotionBoard)
|
||||
val engine = engineWith(promotionBoard)
|
||||
captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e7e8")
|
||||
|
||||
engine.isPendingPromotion should be (true)
|
||||
engine.isPendingPromotion should be(true)
|
||||
}
|
||||
|
||||
test("isPendingPromotion is false before any promotion input") {
|
||||
val engine = new GameEngine()
|
||||
engine.isPendingPromotion should be (false)
|
||||
engine.isPendingPromotion should be(false)
|
||||
}
|
||||
|
||||
test("completePromotion fires MoveExecutedEvent with promoted piece") {
|
||||
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val engine = engineWith(promotionBoard)
|
||||
val events = captureEvents(engine)
|
||||
val engine = engineWith(promotionBoard)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e7e8")
|
||||
engine.completePromotion(PromotionPiece.Queen)
|
||||
|
||||
engine.isPendingPromotion should be (false)
|
||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Queen)))
|
||||
engine.board.pieceAt(sq(File.E, Rank.R7)) should be (None)
|
||||
engine.isPendingPromotion should be(false)
|
||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be(Some(Piece(Color.White, PieceType.Queen)))
|
||||
engine.board.pieceAt(sq(File.E, Rank.R7)) should be(None)
|
||||
engine.context.moves.last.moveType shouldBe MoveType.Promotion(PromotionPiece.Queen)
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be (true)
|
||||
events.exists(_.isInstanceOf[MoveExecutedEvent]) should be(true)
|
||||
}
|
||||
|
||||
test("completePromotion with rook underpromotion") {
|
||||
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val engine = engineWith(promotionBoard)
|
||||
val engine = engineWith(promotionBoard)
|
||||
captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e7e8")
|
||||
engine.completePromotion(PromotionPiece.Rook)
|
||||
|
||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Rook)))
|
||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be(Some(Piece(Color.White, PieceType.Rook)))
|
||||
}
|
||||
|
||||
test("completePromotion with no pending promotion fires InvalidMoveEvent") {
|
||||
@@ -80,71 +80,71 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
||||
|
||||
engine.completePromotion(PromotionPiece.Queen)
|
||||
|
||||
events.exists(_.isInstanceOf[InvalidMoveEvent]) should be (true)
|
||||
engine.isPendingPromotion should be (false)
|
||||
events.exists(_.isInstanceOf[InvalidMoveEvent]) should be(true)
|
||||
engine.isPendingPromotion should be(false)
|
||||
}
|
||||
|
||||
test("completePromotion fires CheckDetectedEvent when promotion gives check") {
|
||||
val promotionBoard = FenParser.parseBoard("3k4/4P3/8/8/8/8/8/8").get
|
||||
val engine = engineWith(promotionBoard)
|
||||
val events = captureEvents(engine)
|
||||
val engine = engineWith(promotionBoard)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e7e8")
|
||||
engine.completePromotion(PromotionPiece.Queen)
|
||||
|
||||
events.exists(_.isInstanceOf[CheckDetectedEvent]) should be (true)
|
||||
events.exists(_.isInstanceOf[CheckDetectedEvent]) should be(true)
|
||||
}
|
||||
|
||||
test("completePromotion results in Moved when promotion doesn't give check") {
|
||||
val board = FenParser.parseBoard("8/4P3/8/8/8/8/k7/8").get
|
||||
val board = FenParser.parseBoard("8/4P3/8/8/8/8/k7/8").get
|
||||
val engine = engineWith(board)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e7e8")
|
||||
engine.completePromotion(PromotionPiece.Queen)
|
||||
|
||||
engine.isPendingPromotion should be (false)
|
||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Queen)))
|
||||
engine.isPendingPromotion should be(false)
|
||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be(Some(Piece(Color.White, PieceType.Queen)))
|
||||
events.filter(_.isInstanceOf[MoveExecutedEvent]) should not be empty
|
||||
events.exists(_.isInstanceOf[CheckDetectedEvent]) should be (false)
|
||||
events.exists(_.isInstanceOf[CheckDetectedEvent]) should be(false)
|
||||
}
|
||||
|
||||
test("completePromotion results in Checkmate when promotion delivers checkmate") {
|
||||
val board = FenParser.parseBoard("k7/7P/1K6/8/8/8/8/8").get
|
||||
val board = FenParser.parseBoard("k7/7P/1K6/8/8/8/8/8").get
|
||||
val engine = engineWith(board)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("h7h8")
|
||||
engine.completePromotion(PromotionPiece.Queen)
|
||||
|
||||
engine.isPendingPromotion should be (false)
|
||||
events.exists(_.isInstanceOf[CheckmateEvent]) should be (true)
|
||||
engine.isPendingPromotion should be(false)
|
||||
events.exists(_.isInstanceOf[CheckmateEvent]) should be(true)
|
||||
}
|
||||
|
||||
test("completePromotion results in Stalemate when promotion creates stalemate") {
|
||||
val board = FenParser.parseBoard("k7/1PB5/1K6/8/8/8/8/8").get
|
||||
val board = FenParser.parseBoard("k7/1PB5/1K6/8/8/8/8/8").get
|
||||
val engine = engineWith(board)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("b7b8")
|
||||
engine.completePromotion(PromotionPiece.Knight)
|
||||
|
||||
engine.isPendingPromotion should be (false)
|
||||
events.exists(_.isInstanceOf[StalemateEvent]) should be (true)
|
||||
engine.isPendingPromotion should be(false)
|
||||
events.exists(_.isInstanceOf[StalemateEvent]) should be(true)
|
||||
}
|
||||
|
||||
test("completePromotion with black pawn promotion results in Moved") {
|
||||
val board = FenParser.parseBoard("k7/8/8/8/8/7K/4p3/8").get
|
||||
val board = FenParser.parseBoard("k7/8/8/8/8/7K/4p3/8").get
|
||||
val engine = engineWith(board, Color.Black)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e2e1")
|
||||
engine.completePromotion(PromotionPiece.Queen)
|
||||
|
||||
engine.isPendingPromotion should be (false)
|
||||
engine.board.pieceAt(sq(File.E, Rank.R1)) should be (Some(Piece(Color.Black, PieceType.Queen)))
|
||||
engine.isPendingPromotion should be(false)
|
||||
engine.board.pieceAt(sq(File.E, Rank.R1)) should be(Some(Piece(Color.Black, PieceType.Queen)))
|
||||
events.filter(_.isInstanceOf[MoveExecutedEvent]) should not be empty
|
||||
events.exists(_.isInstanceOf[CheckDetectedEvent]) should be (false)
|
||||
events.exists(_.isInstanceOf[CheckDetectedEvent]) should be(false)
|
||||
}
|
||||
|
||||
test("completePromotion fires InvalidMoveEvent when legalMoves returns only Normal moves to back rank") {
|
||||
@@ -177,21 +177,21 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
||||
DefaultRules.applyMove(context)(move)
|
||||
|
||||
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val initialCtx = GameContext.initial.withBoard(promotionBoard).withTurn(Color.White)
|
||||
val engine = new GameEngine(initialCtx, delegatingRuleSet)
|
||||
val events = captureEvents(engine)
|
||||
val initialCtx = GameContext.initial.withBoard(promotionBoard).withTurn(Color.White)
|
||||
val engine = new GameEngine(initialCtx, delegatingRuleSet)
|
||||
val events = captureEvents(engine)
|
||||
|
||||
// isPromotionMove will fire because pawn is on rank 7 heading to rank 8,
|
||||
// and legalMoves returns Normal candidates (still non-empty) — sets pendingPromotion
|
||||
engine.processUserInput("e7e8")
|
||||
engine.isPendingPromotion should be (true)
|
||||
engine.isPendingPromotion should be(true)
|
||||
|
||||
// completePromotion looks for Move(e7, e8, Promotion(Queen)) in legalMoves,
|
||||
// but only Normal moves exist → fires InvalidMoveEvent
|
||||
engine.completePromotion(PromotionPiece.Queen)
|
||||
|
||||
engine.isPendingPromotion should be (false)
|
||||
events.exists(_.isInstanceOf[InvalidMoveEvent]) should be (true)
|
||||
engine.isPendingPromotion should be(false)
|
||||
events.exists(_.isInstanceOf[InvalidMoveEvent]) should be(true)
|
||||
val invalidEvt = events.collect { case e: InvalidMoveEvent => e }.last
|
||||
invalidEvt.reason should include ("Error completing promotion")
|
||||
invalidEvt.reason should include("Error completing promotion")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.chess.engine
|
||||
|
||||
import de.nowchess.api.board.{Color, File, Rank, Square, Piece}
|
||||
import de.nowchess.api.board.{Color, File, Piece, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.chess.observer.*
|
||||
import de.nowchess.io.fen.FenParser
|
||||
@@ -13,7 +13,7 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
|
||||
// ── Observer wiring ────────────────────────────────────────────
|
||||
|
||||
test("observer subscribe and unsubscribe behavior"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
engine.processUserInput("e2e4")
|
||||
@@ -56,28 +56,28 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
|
||||
// ── Invalid moves (minimal) ────────────────────────────────────
|
||||
|
||||
test("invalid move forms trigger InvalidMoveEvent and keep turn where relevant"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
engine.processUserInput("h3h4")
|
||||
|
||||
observer.hasEvent[InvalidMoveEvent] shouldBe true
|
||||
engine.turn shouldBe Color.White // turn unchanged
|
||||
engine.turn shouldBe Color.White // turn unchanged
|
||||
|
||||
engine.processUserInput("e7e5") // try to move black pawn on white's turn
|
||||
engine.processUserInput("e7e5") // try to move black pawn on white's turn
|
||||
|
||||
observer.hasEvent[InvalidMoveEvent] shouldBe true
|
||||
|
||||
engine.processUserInput("e2e4")
|
||||
engine.processUserInput("e5e4") // pawn backward
|
||||
engine.processUserInput("e5e4") // pawn backward
|
||||
|
||||
observer.hasEvent[InvalidMoveEvent] shouldBe true
|
||||
|
||||
// ── Undo/Redo ────────────────────────────────────────────────
|
||||
|
||||
test("undo redo success and empty-history failures"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -103,7 +103,7 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
|
||||
// ── Fifty-move rule ────────────────────────────────────────────
|
||||
|
||||
test("fifty-move event and draw claim success/failure"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
|
||||
+10
-10
@@ -11,7 +11,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
// ── Castling ────────────────────────────────────────────────────
|
||||
|
||||
test("kingside castling executes successfully"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -25,7 +25,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
engine.turn shouldBe Color.Black
|
||||
|
||||
test("queenside castling executes successfully"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -39,7 +39,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
engine.turn shouldBe Color.Black
|
||||
|
||||
test("undo castling emits PGN notation"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -57,7 +57,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
// ── En passant ──────────────────────────────────────────────────
|
||||
|
||||
test("en passant capture executes successfully"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -69,10 +69,10 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
|
||||
observer.hasEvent[MoveExecutedEvent] shouldBe true
|
||||
val moveEvt = observer.getEvent[MoveExecutedEvent]
|
||||
moveEvt.get.capturedPiece shouldBe defined // pawn was captured
|
||||
moveEvt.get.capturedPiece shouldBe defined // pawn was captured
|
||||
|
||||
test("undo en passant emits file-x-destination notation"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -90,7 +90,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
// ── Pawn promotion ─────────────────────────────────────────────
|
||||
|
||||
test("pawn reaching back rank requires promotion"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -143,7 +143,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
engine.turn shouldBe Color.Black
|
||||
|
||||
test("promotion to Queen with discovered check emits CheckDetectedEvent"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -157,7 +157,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
observer.hasEvent[CheckDetectedEvent] shouldBe true
|
||||
|
||||
test("promotion to Queen with checkmate emits CheckmateEvent"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
@@ -171,7 +171,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
|
||||
observer.hasEvent[CheckmateEvent] shouldBe true
|
||||
|
||||
test("undo promotion emits notation with piece suffix"):
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val engine = EngineTestHelpers.makeEngine()
|
||||
val observer = new EngineTestHelpers.MockObserver()
|
||||
engine.subscribe(observer)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user