refactor(tests): enhance test coverage for move application and piece movement logic
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import glob,re,os
|
||||
rows=[]
|
||||
for f in glob.glob('modules/*/build/test-results/test/TEST-*.xml'):
|
||||
txt=open(f,encoding='utf-8').read(500)
|
||||
m1=re.search(r'name="([^"]+)"',txt)
|
||||
m2=re.search(r'tests="(\d+)"',txt)
|
||||
if m1 and m2:
|
||||
rows.append((int(m2.group(1)),f,m1.group(1)))
|
||||
for n,f,name in sorted(rows, reverse=True)[:20]:
|
||||
print(f'{n:3} {name} ({f})')
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.nowchess.api.board
|
||||
|
||||
import de.nowchess.api.move.Move
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
@@ -120,3 +121,13 @@ class BoardTest extends AnyFunSuite with Matchers:
|
||||
removed.pieceAt(e2) shouldBe None
|
||||
removed.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
|
||||
}
|
||||
|
||||
test("applyMove uses move.from and move.to to relocate a piece") {
|
||||
val b = Board(Map(e2 -> Piece.WhitePawn))
|
||||
|
||||
val moved = b.applyMove(Move(e2, e4))
|
||||
|
||||
moved.pieceAt(e4) shouldBe Some(Piece.WhitePawn)
|
||||
moved.pieceAt(e2) shouldBe None
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.nowchess.api.board
|
||||
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class CastlingRightsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("hasAnyRights and hasRights reflect current flags"):
|
||||
val rights = CastlingRights(
|
||||
whiteKingSide = true,
|
||||
whiteQueenSide = false,
|
||||
blackKingSide = false,
|
||||
blackQueenSide = true
|
||||
)
|
||||
|
||||
rights.hasAnyRights shouldBe true
|
||||
rights.hasRights(Color.White) shouldBe true
|
||||
rights.hasRights(Color.Black) shouldBe true
|
||||
|
||||
CastlingRights.None.hasAnyRights shouldBe false
|
||||
CastlingRights.None.hasRights(Color.White) shouldBe false
|
||||
CastlingRights.None.hasRights(Color.Black) shouldBe false
|
||||
|
||||
test("revokeColor clears both castling sides for selected color"):
|
||||
val all = CastlingRights.All
|
||||
|
||||
val whiteRevoked = all.revokeColor(Color.White)
|
||||
whiteRevoked.whiteKingSide shouldBe false
|
||||
whiteRevoked.whiteQueenSide shouldBe false
|
||||
whiteRevoked.blackKingSide shouldBe true
|
||||
whiteRevoked.blackQueenSide shouldBe true
|
||||
|
||||
val blackRevoked = all.revokeColor(Color.Black)
|
||||
blackRevoked.whiteKingSide shouldBe true
|
||||
blackRevoked.whiteQueenSide shouldBe true
|
||||
blackRevoked.blackKingSide shouldBe false
|
||||
blackRevoked.blackQueenSide shouldBe false
|
||||
|
||||
test("revokeKingSide and revokeQueenSide disable only requested side"):
|
||||
val all = CastlingRights.All
|
||||
|
||||
val whiteKingSideRevoked = all.revokeKingSide(Color.White)
|
||||
whiteKingSideRevoked.whiteKingSide shouldBe false
|
||||
whiteKingSideRevoked.whiteQueenSide shouldBe true
|
||||
|
||||
val whiteQueenSideRevoked = all.revokeQueenSide(Color.White)
|
||||
whiteQueenSideRevoked.whiteKingSide shouldBe true
|
||||
whiteQueenSideRevoked.whiteQueenSide shouldBe false
|
||||
|
||||
val blackKingSideRevoked = all.revokeKingSide(Color.Black)
|
||||
blackKingSideRevoked.blackKingSide shouldBe false
|
||||
blackKingSideRevoked.blackQueenSide shouldBe true
|
||||
|
||||
val blackQueenSideRevoked = all.revokeQueenSide(Color.Black)
|
||||
blackQueenSideRevoked.blackKingSide shouldBe true
|
||||
blackQueenSideRevoked.blackQueenSide shouldBe false
|
||||
|
||||
@@ -5,18 +5,13 @@ import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class ColorTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("White.opposite returns Black") {
|
||||
Color.White.opposite shouldBe Color.Black
|
||||
}
|
||||
test("Color values expose opposite and label consistently"):
|
||||
val cases = List(
|
||||
(Color.White, Color.Black, "White"),
|
||||
(Color.Black, Color.White, "Black")
|
||||
)
|
||||
|
||||
test("Black.opposite returns White") {
|
||||
Color.Black.opposite shouldBe Color.White
|
||||
}
|
||||
|
||||
test("White.label returns 'White'") {
|
||||
Color.White.label shouldBe "White"
|
||||
}
|
||||
|
||||
test("Black.label returns 'Black'") {
|
||||
Color.Black.label shouldBe "Black"
|
||||
}
|
||||
cases.foreach { (color, opposite, label) =>
|
||||
color.opposite shouldBe opposite
|
||||
color.label shouldBe label
|
||||
}
|
||||
|
||||
@@ -5,26 +5,16 @@ import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class PieceTypeTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("Pawn.label returns 'Pawn'") {
|
||||
PieceType.Pawn.label shouldBe "Pawn"
|
||||
}
|
||||
test("PieceType values expose the expected labels"):
|
||||
val expectedLabels = List(
|
||||
PieceType.Pawn -> "Pawn",
|
||||
PieceType.Knight -> "Knight",
|
||||
PieceType.Bishop -> "Bishop",
|
||||
PieceType.Rook -> "Rook",
|
||||
PieceType.Queen -> "Queen",
|
||||
PieceType.King -> "King"
|
||||
)
|
||||
|
||||
test("Knight.label returns 'Knight'") {
|
||||
PieceType.Knight.label shouldBe "Knight"
|
||||
}
|
||||
|
||||
test("Bishop.label returns 'Bishop'") {
|
||||
PieceType.Bishop.label shouldBe "Bishop"
|
||||
}
|
||||
|
||||
test("Rook.label returns 'Rook'") {
|
||||
PieceType.Rook.label shouldBe "Rook"
|
||||
}
|
||||
|
||||
test("Queen.label returns 'Queen'") {
|
||||
PieceType.Queen.label shouldBe "Queen"
|
||||
}
|
||||
|
||||
test("King.label returns 'King'") {
|
||||
PieceType.King.label shouldBe "King"
|
||||
}
|
||||
expectedLabels.foreach { (pieceType, expectedLabel) =>
|
||||
pieceType.label shouldBe expectedLabel
|
||||
}
|
||||
|
||||
@@ -60,3 +60,13 @@ class SquareTest extends AnyFunSuite with Matchers:
|
||||
test("fromAlgebraic returns None for rank 9") {
|
||||
Square.fromAlgebraic("e9") shouldBe None
|
||||
}
|
||||
|
||||
test("offset returns target square for in-bounds delta") {
|
||||
Square(File.E, Rank.R4).offset(1, 2) shouldBe Some(Square(File.F, Rank.R6))
|
||||
}
|
||||
|
||||
test("offset returns None for out-of-bounds delta") {
|
||||
Square(File.A, Rank.R1).offset(-1, 0) shouldBe None
|
||||
Square(File.H, Rank.R8).offset(0, 1) shouldBe None
|
||||
}
|
||||
|
||||
|
||||
@@ -9,53 +9,26 @@ class MoveTest extends AnyFunSuite with Matchers:
|
||||
private val e2 = Square(File.E, Rank.R2)
|
||||
private val e4 = Square(File.E, Rank.R4)
|
||||
|
||||
test("Move defaults moveType to Normal") {
|
||||
test("Move defaults to Normal and keeps from/to squares") {
|
||||
val m = Move(e2, e4)
|
||||
m.from shouldBe e2
|
||||
m.to shouldBe e4
|
||||
m.moveType shouldBe MoveType.Normal()
|
||||
}
|
||||
|
||||
test("MoveType.Normal supports capture flag") {
|
||||
val m = Move(e2, e4, MoveType.Normal(isCapture = true))
|
||||
m.moveType shouldBe MoveType.Normal(true)
|
||||
}
|
||||
test("Move accepts all supported move types") {
|
||||
val moveTypes = List(
|
||||
MoveType.Normal(isCapture = true),
|
||||
MoveType.CastleKingside,
|
||||
MoveType.CastleQueenside,
|
||||
MoveType.EnPassant,
|
||||
MoveType.Promotion(PromotionPiece.Queen),
|
||||
MoveType.Promotion(PromotionPiece.Rook),
|
||||
MoveType.Promotion(PromotionPiece.Bishop),
|
||||
MoveType.Promotion(PromotionPiece.Knight)
|
||||
)
|
||||
|
||||
test("Move stores from and to squares") {
|
||||
val m = Move(e2, e4)
|
||||
m.from shouldBe e2
|
||||
m.to shouldBe e4
|
||||
}
|
||||
|
||||
test("Move with CastleKingside moveType") {
|
||||
val m = Move(e2, e4, MoveType.CastleKingside)
|
||||
m.moveType shouldBe MoveType.CastleKingside
|
||||
}
|
||||
|
||||
test("Move with CastleQueenside moveType") {
|
||||
val m = Move(e2, e4, MoveType.CastleQueenside)
|
||||
m.moveType shouldBe MoveType.CastleQueenside
|
||||
}
|
||||
|
||||
test("Move with EnPassant moveType") {
|
||||
val m = Move(e2, e4, MoveType.EnPassant)
|
||||
m.moveType shouldBe MoveType.EnPassant
|
||||
}
|
||||
|
||||
test("Move with Promotion to Queen") {
|
||||
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Queen))
|
||||
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Queen)
|
||||
}
|
||||
|
||||
test("Move with Promotion to Knight") {
|
||||
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Knight))
|
||||
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Knight)
|
||||
}
|
||||
|
||||
test("Move with Promotion to Bishop") {
|
||||
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Bishop))
|
||||
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Bishop)
|
||||
}
|
||||
|
||||
test("Move with Promotion to Rook") {
|
||||
val m = Move(e2, e4, MoveType.Promotion(PromotionPiece.Rook))
|
||||
m.moveType shouldBe MoveType.Promotion(PromotionPiece.Rook)
|
||||
moveTypes.foreach { moveType =>
|
||||
Move(e2, e4, moveType).moveType shouldBe moveType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,14 @@ import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class PlayerInfoTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("PlayerId.apply wraps a string") {
|
||||
val id = PlayerId("player-123")
|
||||
id.value shouldBe "player-123"
|
||||
}
|
||||
test("PlayerId and PlayerInfo preserve constructor values") {
|
||||
val raw = "player-123"
|
||||
val id = PlayerId(raw)
|
||||
|
||||
test("PlayerId.value unwraps to original string") {
|
||||
val raw = "abc-456"
|
||||
PlayerId(raw).value shouldBe raw
|
||||
}
|
||||
id.value shouldBe raw
|
||||
|
||||
test("PlayerInfo holds id and displayName") {
|
||||
val id = PlayerId("p1")
|
||||
val info = PlayerInfo(id, "Magnus")
|
||||
val playerId = PlayerId("p1")
|
||||
val info = PlayerInfo(playerId, "Magnus")
|
||||
info.id.value shouldBe "p1"
|
||||
info.displayName shouldBe "Magnus"
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class GameEngine(
|
||||
else
|
||||
replayMoves(ctx.moves, savedContext)
|
||||
|
||||
private def replayMoves(moves: List[Move], savedContext: GameContext): Either[String, Unit] =
|
||||
private[engine] def replayMoves(moves: List[Move], savedContext: GameContext): Either[String, Unit] =
|
||||
var error: Option[String] = None
|
||||
moves.foreach: move =>
|
||||
if error.isEmpty then
|
||||
@@ -170,7 +170,6 @@ class GameEngine(
|
||||
else
|
||||
error = Some(s"Promotion required for move ${move.from}${move.to}")
|
||||
case _ => ()
|
||||
|
||||
error match
|
||||
case Some(err) =>
|
||||
currentContext = savedContext
|
||||
@@ -256,7 +255,7 @@ class GameEngine(
|
||||
case PromotionPiece.Knight => "N"
|
||||
s"${move.to}=$ppChar"
|
||||
|
||||
private def normalMoveNotation(move: Move, boardBefore: Board, isCapture: Boolean): String =
|
||||
private[engine] def normalMoveNotation(move: Move, boardBefore: Board, isCapture: Boolean): String =
|
||||
boardBefore.pieceAt(move.from).map(_.pieceType) match
|
||||
case Some(PieceType.Pawn) =>
|
||||
if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}"
|
||||
@@ -264,11 +263,9 @@ class GameEngine(
|
||||
case Some(pt) =>
|
||||
val letter = pieceNotation(pt)
|
||||
if isCapture then s"${letter}x${move.to}" else s"$letter${move.to}"
|
||||
// $COVERAGE-OFF$ unreachable: executeMove is only called after a piece existence check
|
||||
case None => move.to.toString
|
||||
// $COVERAGE-ON$
|
||||
|
||||
private def pieceNotation(pieceType: PieceType): String =
|
||||
private[engine] def pieceNotation(pieceType: PieceType): String =
|
||||
pieceType match
|
||||
case PieceType.Knight => "N"
|
||||
case PieceType.Bishop => "B"
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package de.nowchess.chess.engine
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, MoveRedoneEvent, Observer}
|
||||
import de.nowchess.io.GameContextImport
|
||||
import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class GameEngineCoverageRegressionTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def sq(alg: String): Square =
|
||||
Square.fromAlgebraic(alg).getOrElse(fail(s"Invalid square in test: $alg"))
|
||||
|
||||
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 })
|
||||
events
|
||||
|
||||
test("accessors expose redo availability and command history"):
|
||||
val engine = new GameEngine()
|
||||
|
||||
engine.canRedo shouldBe false
|
||||
engine.commandHistory shouldBe empty
|
||||
|
||||
engine.processUserInput("e2e4")
|
||||
engine.commandHistory.nonEmpty shouldBe true
|
||||
|
||||
test("processUserInput handles undo redo empty and malformed commands"):
|
||||
val engine = new GameEngine()
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("")
|
||||
engine.processUserInput("oops")
|
||||
engine.processUserInput("undo")
|
||||
engine.processUserInput("redo")
|
||||
|
||||
events.count(_.isInstanceOf[InvalidMoveEvent]) should be >= 3
|
||||
|
||||
test("processUserInput emits Illegal move for syntactically valid but illegal target"):
|
||||
val engine = new GameEngine()
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e2e5")
|
||||
|
||||
events.exists {
|
||||
case InvalidMoveEvent(_, reason) => reason.contains("Illegal move")
|
||||
case _ => false
|
||||
} shouldBe true
|
||||
|
||||
test("loadGame returns Left when importer fails"):
|
||||
val engine = new GameEngine()
|
||||
val failingImporter = new GameContextImport:
|
||||
def importGameContext(input: String): Either[String, GameContext] = Left("boom")
|
||||
|
||||
engine.loadGame(failingImporter, "ignored") shouldBe Left("boom")
|
||||
|
||||
test("loadPosition replaces context clears history and notifies reset"):
|
||||
val engine = new GameEngine()
|
||||
val events = captureEvents(engine)
|
||||
|
||||
engine.processUserInput("e2e4")
|
||||
val target = GameContext.initial.withTurn(Color.Black)
|
||||
engine.loadPosition(target)
|
||||
|
||||
engine.context shouldBe target
|
||||
engine.commandHistory shouldBe empty
|
||||
events.lastOption.exists(_.isInstanceOf[de.nowchess.chess.observer.BoardResetEvent]) shouldBe true
|
||||
|
||||
test("redo event includes captured piece description when replaying a capture"):
|
||||
val engine = new GameEngine()
|
||||
val events = captureEvents(engine)
|
||||
|
||||
EngineTestHelpers.loadFen(engine, "4k3/8/8/8/8/8/4K3/R6r w - - 0 1")
|
||||
events.clear()
|
||||
|
||||
engine.processUserInput("a1h1")
|
||||
engine.processUserInput("undo")
|
||||
engine.processUserInput("redo")
|
||||
|
||||
val redo = events.collectFirst { case e: MoveRedoneEvent => e }
|
||||
redo.flatMap(_.capturedPiece) shouldBe Some("Black Rook")
|
||||
|
||||
test("loadGame replay handles promotion moves when pending promotion exists"):
|
||||
val promotionMove = Move(sq("e2"), sq("e8"), MoveType.Promotion(PromotionPiece.Queen))
|
||||
|
||||
val permissiveRules = new RuleSet:
|
||||
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 applyMove(context: GameContext, move: Move): GameContext = DefaultRules.applyMove(context, move)
|
||||
|
||||
val engine = new GameEngine(ruleSet = permissiveRules)
|
||||
val importer = new GameContextImport:
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
Right(GameContext.initial.copy(moves = List(promotionMove)))
|
||||
|
||||
engine.loadGame(importer, "ignored") shouldBe Right(())
|
||||
engine.context.moves.lastOption shouldBe Some(promotionMove)
|
||||
|
||||
test("loadGame replay restores previous context when promotion cannot be completed"):
|
||||
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
|
||||
|
||||
val engine = new GameEngine(ruleSet = noLegalMoves)
|
||||
engine.processUserInput("e2e4")
|
||||
val saved = engine.context
|
||||
|
||||
val importer = new GameContextImport:
|
||||
def importGameContext(input: String): Either[String, GameContext] =
|
||||
Right(GameContext.initial.copy(moves = List(promotionMove)))
|
||||
|
||||
val result = engine.loadGame(importer, "ignored")
|
||||
|
||||
result.isLeft shouldBe true
|
||||
result.left.toOption.get should include("Promotion required")
|
||||
engine.context shouldBe saved
|
||||
|
||||
test("loadGame replay executes non-promotion moves through default replay branch"):
|
||||
val normalMove = Move(sq("e2"), sq("e4"), MoveType.Normal())
|
||||
val engine = new GameEngine()
|
||||
|
||||
engine.replayMoves(List(normalMove), engine.context) shouldBe Right(())
|
||||
engine.context.moves.lastOption shouldBe Some(normalMove)
|
||||
|
||||
test("loadGame replay will stop on errors"):
|
||||
val normalMove = Move(sq("e2"), sq("e4"), MoveType.Normal())
|
||||
val engine = new GameEngine()
|
||||
|
||||
engine.replayMoves(List(normalMove), engine.context) shouldBe Right(())
|
||||
engine.context.moves.lastOption shouldBe Some(normalMove)
|
||||
|
||||
test("normalMoveNotation handles missing source piece"):
|
||||
val engine = new GameEngine()
|
||||
val result = engine.normalMoveNotation(Move(sq("e3"), sq("e4")), Board.initial, isCapture = false)
|
||||
|
||||
result shouldBe "e4"
|
||||
|
||||
test("pieceNotation default branch returns empty string"):
|
||||
val engine = new GameEngine()
|
||||
val result = engine.pieceNotation(PieceType.Pawn)
|
||||
|
||||
result shouldBe ""
|
||||
|
||||
test("observerCount reflects subscribe and unsubscribe operations"):
|
||||
val engine = new GameEngine()
|
||||
val observer = new Observer:
|
||||
def onGameEvent(event: GameEvent): Unit = ()
|
||||
|
||||
engine.observerCount shouldBe 0
|
||||
engine.subscribe(observer)
|
||||
engine.observerCount shouldBe 1
|
||||
engine.unsubscribe(observer)
|
||||
engine.observerCount shouldBe 0
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package de.nowchess.io
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class IoCoverageRegressionTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def sq(alg: String): Square =
|
||||
Square.fromAlgebraic(alg).getOrElse(fail(s"Invalid square in test: $alg"))
|
||||
|
||||
test("FenParser rejects malformed board shapes and invalid piece symbols"):
|
||||
FenParser.parseBoard("8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("9/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("8p/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("7/8/8/8/8/8/8/8") shouldBe None
|
||||
FenParser.parseBoard("8/8/8/8/8/8/8/7X") shouldBe None
|
||||
|
||||
test("FenExporter exportGameContext forwards to gameContextToFen"):
|
||||
val context = GameContext.initial
|
||||
|
||||
FenExporter.exportGameContext(context) shouldBe FenExporter.gameContextToFen(context)
|
||||
|
||||
test("PgnParser rejects too-short notation and invalid piece letters"):
|
||||
val initial = GameContext.initial
|
||||
|
||||
PgnParser.parseAlgebraicMove("e", initial, Color.White) shouldBe None
|
||||
PgnParser.parseAlgebraicMove("Xe5", initial, Color.White) shouldBe None
|
||||
|
||||
test("PgnParser rejects notation with invalid promotion piece"):
|
||||
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").getOrElse(fail("valid board expected"))
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
|
||||
PgnParser.parseAlgebraicMove("e7e8=X", context, Color.White) shouldBe None
|
||||
|
||||
test("PgnExporter emits notation for all normal piece types and captures"):
|
||||
val moves = List(
|
||||
Move(sq("e2"), sq("e4")),
|
||||
Move(sq("a7"), sq("a6")),
|
||||
Move(sq("g1"), sq("f3")),
|
||||
Move(sq("b7"), sq("b6")),
|
||||
Move(sq("f1"), sq("b5"), MoveType.Normal(true)),
|
||||
Move(sq("g8"), sq("f6")),
|
||||
Move(sq("a1"), sq("a8"), MoveType.Normal(true)),
|
||||
Move(sq("c7"), sq("c6")),
|
||||
Move(sq("d1"), sq("d7"), MoveType.Normal(true)),
|
||||
Move(sq("d8"), sq("d7"), MoveType.Normal(true)),
|
||||
Move(sq("e1"), sq("e2"), MoveType.Normal(true))
|
||||
)
|
||||
|
||||
val pgn = PgnExporter.exportGame(Map("Result" -> "*"), moves)
|
||||
|
||||
pgn should include("e4")
|
||||
pgn should include("Nf3")
|
||||
pgn should include("Bxb5")
|
||||
pgn should include("Rxa8")
|
||||
pgn should include("Qxd7")
|
||||
pgn should include("Kxe2")
|
||||
|
||||
test("PgnExporter emits en-passant and promotion capture notation"):
|
||||
val enPassant = Move(sq("e2"), sq("d3"), MoveType.EnPassant)
|
||||
val promotionCapture = Move(sq("e7"), sq("f8"), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val pawnCapture = Move(sq("e2"), sq("d3"), MoveType.Normal(isCapture = true))
|
||||
val promotionQuietSetup = Move(sq("e8"), sq("e7"))
|
||||
val promotionQuiet = Move(sq("e2"), sq("e8"), MoveType.Promotion(PromotionPiece.Queen))
|
||||
|
||||
val pgn = PgnExporter.exportGame(Map.empty, List(enPassant, promotionCapture))
|
||||
val pawnCapturePgn = PgnExporter.exportGame(Map.empty, List(pawnCapture))
|
||||
val quietPromotionPgn = PgnExporter.exportGame(Map.empty, List(promotionQuietSetup, promotionQuiet))
|
||||
|
||||
pgn should include("exd3")
|
||||
pgn should include("exf8=Q")
|
||||
pawnCapturePgn should include("exd3")
|
||||
quietPromotionPgn should include("e8=Q")
|
||||
|
||||
test("PgnExporter emits all promotion suffixes"):
|
||||
val promotions = List(
|
||||
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Queen)),
|
||||
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Rook)),
|
||||
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Bishop)),
|
||||
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Knight))
|
||||
)
|
||||
|
||||
val pgn = PgnExporter.exportGame(Map.empty, promotions)
|
||||
|
||||
pgn should include("=Q")
|
||||
pgn should include("=R")
|
||||
pgn should include("=B")
|
||||
pgn should include("=N")
|
||||
|
||||
test("PgnParser parsePgn silently skips unknown tokens"):
|
||||
val parsed = PgnParser.parsePgn("1. e4 ??? e5")
|
||||
|
||||
parsed.map(_.moves.size) shouldBe Some(2)
|
||||
|
||||
|
||||
|
||||
@@ -167,3 +167,28 @@ class PgnParserTest extends AnyFunSuite with Matchers:
|
||||
val result = PgnParser.importGameContext(invalidPgn)
|
||||
// Empty PGN is still valid (no moves), so check for reasonable parsing
|
||||
result.isRight shouldBe true
|
||||
|
||||
test("parseAlgebraicMove: uppercase file token still fails when destination is unreachable"):
|
||||
val result = PgnParser.parseAlgebraicMove("E5", GameContext.initial, Color.White)
|
||||
result shouldBe None
|
||||
|
||||
test("parseAlgebraicMove: non-file/rank hint characters are ignored"):
|
||||
val result = PgnParser.parseAlgebraicMove("N?f3", GameContext.initial, Color.White)
|
||||
result.isDefined shouldBe true
|
||||
result.get.to shouldBe Square(File.F, Rank.R3)
|
||||
|
||||
test("extractPromotion returns None for unsupported promotion letter"):
|
||||
PgnParser.extractPromotion("e7e8=X") shouldBe None
|
||||
|
||||
test("parseAlgebraicMove rejects promotion target without promotion suffix"):
|
||||
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||
val result = PgnParser.parseAlgebraicMove("e8", GameContext.initial.withBoard(board), Color.White)
|
||||
result shouldBe None
|
||||
|
||||
test("parseAlgebraicMove: king notation resolves a legal king move"):
|
||||
val board = FenParser.parseBoard("4k3/8/8/8/8/8/8/4K3").get
|
||||
val result = PgnParser.parseAlgebraicMove("Ke2", GameContext.initial.withBoard(board), Color.White)
|
||||
result.isDefined shouldBe true
|
||||
result.get.from shouldBe Square(File.E, Rank.R1)
|
||||
result.get.to shouldBe Square(File.E, Rank.R2)
|
||||
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
package de.nowchess.rule
|
||||
|
||||
import de.nowchess.api.board.{CastlingRights, Color, Piece, PieceType, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.io.fen.FenParser
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def contextFromFen(fen: String): GameContext =
|
||||
FenParser.parseFen(fen).fold(err => fail(err), identity)
|
||||
|
||||
private def sq(alg: String): Square =
|
||||
Square.fromAlgebraic(alg).getOrElse(fail(s"Invalid square in test: $alg"))
|
||||
|
||||
test("isCheckmate returns true for a known mate pattern"):
|
||||
val context = contextFromFen("rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3")
|
||||
|
||||
DefaultRules.isCheck(context) shouldBe true
|
||||
DefaultRules.isCheckmate(context) shouldBe true
|
||||
DefaultRules.allLegalMoves(context) shouldBe empty
|
||||
|
||||
test("isStalemate returns true for a known stalemate pattern"):
|
||||
val context = contextFromFen("7k/5K2/6Q1/8/8/8/8/8 b - - 0 1")
|
||||
|
||||
DefaultRules.isCheck(context) shouldBe false
|
||||
DefaultRules.isStalemate(context) shouldBe true
|
||||
DefaultRules.allLegalMoves(context) shouldBe empty
|
||||
|
||||
test("isInsufficientMaterial returns true for king versus king"):
|
||||
val context = contextFromFen("8/8/8/8/8/8/4k3/4K3 w - - 0 1")
|
||||
|
||||
DefaultRules.isInsufficientMaterial(context) shouldBe true
|
||||
|
||||
test("isInsufficientMaterial returns true for king and bishop versus king"):
|
||||
val context = contextFromFen("8/8/8/8/8/8/4k3/3BK3 w - - 0 1")
|
||||
|
||||
DefaultRules.isInsufficientMaterial(context) shouldBe true
|
||||
|
||||
test("isInsufficientMaterial returns false for king and rook versus king"):
|
||||
val context = contextFromFen("8/8/8/8/8/8/4k3/3RK3 w - - 0 1")
|
||||
|
||||
DefaultRules.isInsufficientMaterial(context) shouldBe false
|
||||
|
||||
test("isFiftyMoveRule returns true when halfMoveClock is 100"):
|
||||
val context = contextFromFen("8/8/8/8/8/8/4k3/4K3 w - - 100 1")
|
||||
|
||||
DefaultRules.isFiftyMoveRule(context) shouldBe true
|
||||
|
||||
test("applyMove toggles turn and records move"):
|
||||
val move = Move(sq("e2"), sq("e4"))
|
||||
val next = DefaultRules.applyMove(GameContext.initial, move)
|
||||
|
||||
next.turn shouldBe Color.Black
|
||||
next.moves.lastOption shouldBe Some(move)
|
||||
|
||||
test("applyMove sets en passant square after double pawn push"):
|
||||
val move = Move(sq("e2"), sq("e4"))
|
||||
val next = DefaultRules.applyMove(GameContext.initial, move)
|
||||
|
||||
next.enPassantSquare shouldBe Some(sq("e3"))
|
||||
|
||||
test("applyMove clears en passant square for non double pawn push"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/4P3/4K3 w - d6 3 1")
|
||||
val move = Move(sq("e2"), sq("e3"))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.enPassantSquare shouldBe None
|
||||
|
||||
test("applyMove resets halfMoveClock on pawn move"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/4P3/4K3 w - - 12 1")
|
||||
val move = Move(sq("e2"), sq("e4"))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.halfMoveClock shouldBe 0
|
||||
|
||||
test("applyMove increments halfMoveClock on quiet non pawn move"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/8/4K1N1 w - - 7 1")
|
||||
val move = Move(sq("g1"), sq("f3"))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.halfMoveClock shouldBe 8
|
||||
|
||||
test("applyMove resets halfMoveClock on capture"):
|
||||
val context = contextFromFen("r3k3/8/8/8/8/8/8/R3K3 w Qq - 9 1")
|
||||
val move = Move(sq("a1"), sq("a8"), MoveType.Normal(isCapture = true))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.halfMoveClock shouldBe 0
|
||||
next.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Rook))
|
||||
|
||||
test("applyMove updates castling rights after king move"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")
|
||||
val move = Move(sq("e1"), sq("e2"))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.castlingRights.whiteKingSide shouldBe false
|
||||
next.castlingRights.whiteQueenSide shouldBe false
|
||||
next.castlingRights.blackKingSide shouldBe true
|
||||
next.castlingRights.blackQueenSide shouldBe true
|
||||
|
||||
test("applyMove updates castling rights after rook move from h1"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K2R w KQkq - 0 1")
|
||||
val move = Move(sq("h1"), sq("h2"))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.castlingRights.whiteKingSide shouldBe false
|
||||
next.castlingRights.whiteQueenSide shouldBe true
|
||||
|
||||
test("applyMove revokes opponent castling right when rook on starting square is captured"):
|
||||
val context = contextFromFen("r3k3/8/8/8/8/8/8/R3K3 w Qq - 2 1")
|
||||
val move = Move(sq("a1"), sq("a8"), MoveType.Normal(isCapture = true))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.castlingRights.blackQueenSide shouldBe false
|
||||
|
||||
test("applyMove executes kingside castling and repositions king and rook"):
|
||||
val context = contextFromFen("4k2r/8/8/8/8/8/8/R3K2R w KQk - 0 1")
|
||||
val move = Move(sq("e1"), sq("g1"), MoveType.CastleKingside)
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.board.pieceAt(sq("g1")) shouldBe Some(Piece(Color.White, PieceType.King))
|
||||
next.board.pieceAt(sq("f1")) shouldBe Some(Piece(Color.White, PieceType.Rook))
|
||||
next.board.pieceAt(sq("e1")) shouldBe None
|
||||
next.board.pieceAt(sq("h1")) shouldBe None
|
||||
|
||||
test("applyMove executes queenside castling and repositions king and rook"):
|
||||
val context = contextFromFen("r3k3/8/8/8/8/8/8/R3K2R w KQq - 0 1")
|
||||
val move = Move(sq("e1"), sq("c1"), MoveType.CastleQueenside)
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.board.pieceAt(sq("c1")) shouldBe Some(Piece(Color.White, PieceType.King))
|
||||
next.board.pieceAt(sq("d1")) shouldBe Some(Piece(Color.White, PieceType.Rook))
|
||||
next.board.pieceAt(sq("e1")) shouldBe None
|
||||
next.board.pieceAt(sq("a1")) shouldBe None
|
||||
|
||||
test("applyMove executes en passant and removes captured pawn"):
|
||||
val context = contextFromFen("k7/8/8/3pP3/8/8/8/7K w - d6 0 1")
|
||||
val move = Move(sq("e5"), sq("d6"), MoveType.EnPassant)
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.board.pieceAt(sq("d6")) shouldBe Some(Piece(Color.White, PieceType.Pawn))
|
||||
next.board.pieceAt(sq("d5")) shouldBe None
|
||||
next.board.pieceAt(sq("e5")) shouldBe None
|
||||
|
||||
test("applyMove executes promotion with selected piece type"):
|
||||
val context = contextFromFen("4k3/P7/8/8/8/8/8/4K3 w - - 0 1")
|
||||
val move = Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Knight))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Knight))
|
||||
next.board.pieceAt(sq("a7")) shouldBe None
|
||||
|
||||
test("candidateMoves returns empty for opponent piece on selected square"):
|
||||
val context = GameContext.initial.withTurn(Color.Black)
|
||||
|
||||
DefaultRules.candidateMoves(context, sq("e2")) shouldBe empty
|
||||
|
||||
test("legalMoves keeps king safe by filtering pinned bishop moves"):
|
||||
val context = contextFromFen("8/8/8/8/8/8/r1B1K3/8 w - - 0 1")
|
||||
|
||||
val bishopMoves = DefaultRules.legalMoves(context, sq("c2"))
|
||||
|
||||
bishopMoves shouldBe empty
|
||||
|
||||
test("applyMove preserves black castling rights after white kingside castling"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")
|
||||
val move = Move(sq("e1"), sq("g1"), MoveType.CastleKingside)
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.castlingRights.whiteKingSide shouldBe false
|
||||
next.castlingRights.whiteQueenSide shouldBe false
|
||||
next.castlingRights.blackKingSide shouldBe true
|
||||
next.castlingRights.blackQueenSide shouldBe true
|
||||
|
||||
test("applyMove can revoke both white castling rights when both rooks are captured"):
|
||||
val context = GameContext(
|
||||
board = contextFromFen("4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1").board.updated(sq("a8"), Piece(Color.Black, PieceType.Queen)),
|
||||
turn = Color.Black,
|
||||
castlingRights = CastlingRights(true, true, false, false),
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty
|
||||
)
|
||||
|
||||
val afterA1Capture = DefaultRules.applyMove(context, Move(sq("a8"), sq("a1"), MoveType.Normal(isCapture = true)))
|
||||
val afterH1Capture = DefaultRules.applyMove(afterA1Capture, Move(sq("a1"), sq("h1"), MoveType.Normal(isCapture = true)))
|
||||
|
||||
afterH1Capture.castlingRights.whiteKingSide shouldBe false
|
||||
afterH1Capture.castlingRights.whiteQueenSide shouldBe false
|
||||
|
||||
test("isInsufficientMaterial returns true for opposite color bishops only"):
|
||||
val context = contextFromFen("8/8/8/8/8/8/4k1b1/3BK3 w - - 0 1")
|
||||
|
||||
DefaultRules.isInsufficientMaterial(context) shouldBe true
|
||||
|
||||
test("candidateMoves for rook includes enemy capture move"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/4K3/R6r w - - 0 1")
|
||||
|
||||
val rookMoves = DefaultRules.candidateMoves(context, sq("a1"))
|
||||
|
||||
rookMoves.exists(m => m.to == sq("h1") && m.moveType == MoveType.Normal(isCapture = true)) shouldBe true
|
||||
|
||||
test("candidateMoves for knight includes enemy capture move"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/3p4/5N2/4K3 w - - 0 1")
|
||||
|
||||
val knightMoves = DefaultRules.candidateMoves(context, sq("f2"))
|
||||
|
||||
knightMoves.exists(m => m.to == sq("d3") && m.moveType == MoveType.Normal(isCapture = true)) shouldBe true
|
||||
|
||||
test("candidateMoves includes black kingside and queenside castling options"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K3 b kq - 0 1")
|
||||
|
||||
val kingMoves = DefaultRules.candidateMoves(context, sq("e8"))
|
||||
|
||||
kingMoves.exists(_.moveType == MoveType.CastleKingside) shouldBe true
|
||||
kingMoves.exists(_.moveType == MoveType.CastleQueenside) shouldBe true
|
||||
|
||||
test("applyMove executes black kingside castling and repositions pieces on rank 8"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K3 b kq - 0 1")
|
||||
val move = Move(sq("e8"), sq("g8"), MoveType.CastleKingside)
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.board.pieceAt(sq("g8")) shouldBe Some(Piece(Color.Black, PieceType.King))
|
||||
next.board.pieceAt(sq("f8")) shouldBe Some(Piece(Color.Black, PieceType.Rook))
|
||||
next.board.pieceAt(sq("e8")) shouldBe None
|
||||
next.board.pieceAt(sq("h8")) shouldBe None
|
||||
|
||||
test("applyMove revokes black castling rights when black rook moves from h8"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K3 b kq - 0 1")
|
||||
val move = Move(sq("h8"), sq("h7"))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.castlingRights.blackKingSide shouldBe false
|
||||
next.castlingRights.blackQueenSide shouldBe true
|
||||
|
||||
test("applyMove revokes black queenside castling right when black rook moves from a8"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K3 b kq - 0 1")
|
||||
val move = Move(sq("a8"), sq("a7"))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.castlingRights.blackKingSide shouldBe true
|
||||
next.castlingRights.blackQueenSide shouldBe false
|
||||
|
||||
test("applyMove revokes black kingside castling right when rook on h8 is captured"):
|
||||
val context = contextFromFen("4k2r/8/8/8/8/8/8/4K2R w Kk - 0 1")
|
||||
val move = Move(sq("h1"), sq("h8"), MoveType.Normal(isCapture = true))
|
||||
|
||||
val next = DefaultRules.applyMove(context, move)
|
||||
|
||||
next.castlingRights.blackKingSide shouldBe false
|
||||
|
||||
test("candidateMoves creates all promotion move variants for black pawn"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/p7/4K3 b - - 0 1")
|
||||
val to = sq("a1")
|
||||
|
||||
val pawnMoves = DefaultRules.candidateMoves(context, sq("a2"))
|
||||
val promotions = pawnMoves.collect { case Move(_, `to`, MoveType.Promotion(piece)) => piece }
|
||||
|
||||
promotions.toSet shouldBe Set(
|
||||
PromotionPiece.Queen,
|
||||
PromotionPiece.Rook,
|
||||
PromotionPiece.Bishop,
|
||||
PromotionPiece.Knight
|
||||
)
|
||||
|
||||
test("applyMove promotion supports queen rook and bishop targets"):
|
||||
val base = contextFromFen("4k3/P7/8/8/8/8/8/4K3 w - - 0 1")
|
||||
|
||||
val queen = DefaultRules.applyMove(base, Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Queen)))
|
||||
val rook = DefaultRules.applyMove(base, Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Rook)))
|
||||
val bishop = DefaultRules.applyMove(base, Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Bishop)))
|
||||
|
||||
queen.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Queen))
|
||||
rook.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Rook))
|
||||
bishop.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Bishop))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.nowchess.ui.utils
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class RendererAndUnicodeTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private val whiteKing = Piece(Color.White, PieceType.King)
|
||||
private val blackPawn = Piece(Color.Black, PieceType.Pawn)
|
||||
|
||||
test("unicode returns the correct symbol for white king"):
|
||||
whiteKing.unicode shouldBe "\u2654"
|
||||
|
||||
test("unicode returns the correct symbol for black pawn"):
|
||||
blackPawn.unicode shouldBe "\u265F"
|
||||
|
||||
test("render includes board coordinates on top and bottom"):
|
||||
val rendered = Renderer.render(Board(Map.empty))
|
||||
val lines = rendered.trim.split("\\n").toList.map(_.trim)
|
||||
|
||||
lines.head shouldBe "a b c d e f g h"
|
||||
lines.last shouldBe "a b c d e f g h"
|
||||
|
||||
test("render includes rank labels from 8 down to 1"):
|
||||
val rendered = Renderer.render(Board(Map.empty))
|
||||
|
||||
rendered should include("8")
|
||||
rendered should include("1")
|
||||
|
||||
test("render places a piece unicode glyph on occupied square"):
|
||||
val board = Board(Map(Square(File.E, Rank.R4) -> Piece(Color.White, PieceType.Queen)))
|
||||
val rendered = Renderer.render(board)
|
||||
|
||||
rendered should include("\u2655")
|
||||
rendered should include("\u001b[")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user