refactor(core): replace GameHistory with HistoryMove in PGN export logic
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package de.nowchess.chess.view
|
||||
|
||||
import de.nowchess.api.board.{Color, Piece, PieceType}
|
||||
|
||||
extension (p: Piece)
|
||||
def unicode: String = (p.color, p.pieceType) match
|
||||
case (Color.White, PieceType.King) => "\u2654"
|
||||
case (Color.White, PieceType.Queen) => "\u2655"
|
||||
case (Color.White, PieceType.Rook) => "\u2656"
|
||||
case (Color.White, PieceType.Bishop) => "\u2657"
|
||||
case (Color.White, PieceType.Knight) => "\u2658"
|
||||
case (Color.White, PieceType.Pawn) => "\u2659"
|
||||
case (Color.Black, PieceType.King) => "\u265A"
|
||||
case (Color.Black, PieceType.Queen) => "\u265B"
|
||||
case (Color.Black, PieceType.Rook) => "\u265C"
|
||||
case (Color.Black, PieceType.Bishop) => "\u265D"
|
||||
case (Color.Black, PieceType.Knight) => "\u265E"
|
||||
case (Color.Black, PieceType.Pawn) => "\u265F"
|
||||
@@ -1,28 +0,0 @@
|
||||
package de.nowchess.chess.view
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, Rank, Square}
|
||||
|
||||
object Renderer:
|
||||
|
||||
private val AnsiReset = "\u001b[0m"
|
||||
private val AnsiLightSquare = "\u001b[48;5;223m" // warm beige
|
||||
private val AnsiDarkSquare = "\u001b[48;5;130m" // brown
|
||||
private val AnsiWhitePiece = "\u001b[97m" // bright white text
|
||||
private val AnsiBlackPiece = "\u001b[30m" // black text
|
||||
|
||||
def render(board: Board): String =
|
||||
val rows = (0 until 8).reverse.map { rank =>
|
||||
val cells = (0 until 8).map { file =>
|
||||
val sq = Square(File.values(file), Rank.values(rank))
|
||||
val isLightSq = (file + rank) % 2 != 0
|
||||
val bgColor = if isLightSq then AnsiLightSquare else AnsiDarkSquare
|
||||
board.pieceAt(sq) match
|
||||
case Some(piece) =>
|
||||
val fgColor = if piece.color == Color.White then AnsiWhitePiece else AnsiBlackPiece
|
||||
s"$bgColor$fgColor ${piece.unicode} $AnsiReset"
|
||||
case None =>
|
||||
s"$bgColor $AnsiReset"
|
||||
}.mkString
|
||||
s"${rank + 1} $cells ${rank + 1}"
|
||||
}.mkString("\n")
|
||||
s" a b c d e f g h\n$rows\n a b c d e f g h\n"
|
||||
@@ -1,105 +0,0 @@
|
||||
package de.nowchess.chess.logic
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.move.PromotionPiece
|
||||
import de.nowchess.api.game.{GameHistory, HistoryMove}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class GameHistoryTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def sq(f: File, r: Rank): Square = Square(f, r)
|
||||
|
||||
test("GameHistory starts empty"):
|
||||
val history = GameHistory.empty
|
||||
history.moves shouldBe empty
|
||||
|
||||
test("GameHistory can add a move"):
|
||||
val history = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
history.moves should have length 1
|
||||
history.moves.head.from shouldBe sq(File.E, Rank.R2)
|
||||
history.moves.head.to shouldBe sq(File.E, Rank.R4)
|
||||
history.moves.head.castleSide shouldBe None
|
||||
|
||||
test("GameHistory can add multiple moves in order"):
|
||||
val h1 = GameHistory.empty
|
||||
val h2 = h1.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val h3 = h2.addMove(sq(File.C, Rank.R7), sq(File.C, Rank.R5))
|
||||
h3.moves should have length 2
|
||||
h3.moves(0).from shouldBe sq(File.E, Rank.R2)
|
||||
h3.moves(1).from shouldBe sq(File.C, Rank.R7)
|
||||
|
||||
test("GameHistory can add a castle move"):
|
||||
val history = GameHistory.empty.addMove(
|
||||
sq(File.E, Rank.R1),
|
||||
sq(File.G, Rank.R1),
|
||||
Some("Kingside")
|
||||
)
|
||||
history.moves.head.castleSide shouldBe Some("Kingside")
|
||||
|
||||
test("GameHistory.addMove with two arguments uses None for castleSide default"):
|
||||
val history = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
history.moves should have length 1
|
||||
history.moves.head.castleSide shouldBe None
|
||||
|
||||
test("Move with promotion records the promotion piece"):
|
||||
val move = HistoryMove(sq(File.E, Rank.R7), sq(File.E, Rank.R8), None, Some(PromotionPiece.Queen))
|
||||
move.promotionPiece should be (Some(PromotionPiece.Queen))
|
||||
|
||||
test("Normal move has no promotion piece"):
|
||||
val move = HistoryMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4), None, None)
|
||||
move.promotionPiece should be (None)
|
||||
|
||||
test("addMove with promotion stores promotionPiece"):
|
||||
val history = GameHistory.empty
|
||||
val newHistory = history.addMove(sq(File.E, Rank.R7), sq(File.E, Rank.R8), None, Some(PromotionPiece.Rook))
|
||||
newHistory.moves should have length 1
|
||||
newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Rook))
|
||||
|
||||
test("addMove with castleSide only uses promotionPiece default (None)"):
|
||||
val history = GameHistory.empty
|
||||
// With overload 3 removed, this uses the 4-param version and triggers addMove$default$4
|
||||
val newHistory = history.addMove(sq(File.E, Rank.R1), sq(File.G, Rank.R1), Some("Kingside"))
|
||||
newHistory.moves should have length 1
|
||||
newHistory.moves.head.castleSide should be (Some("Kingside"))
|
||||
newHistory.moves.head.promotionPiece should be (None)
|
||||
|
||||
test("addMove using named parameters with only promotion, using castleSide default"):
|
||||
val history = GameHistory.empty
|
||||
val newHistory = history.addMove(from = sq(File.E, Rank.R7), to = sq(File.E, Rank.R8), promotionPiece = Some(PromotionPiece.Queen))
|
||||
newHistory.moves should have length 1
|
||||
newHistory.moves.head.castleSide should be (None)
|
||||
newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Queen))
|
||||
|
||||
// ──── half-move clock ────────────────────────────────────────────────
|
||||
|
||||
test("halfMoveClock starts at 0"):
|
||||
GameHistory.empty.halfMoveClock shouldBe 0
|
||||
|
||||
test("halfMoveClock increments on a non-pawn non-capture move"):
|
||||
val h = GameHistory.empty.addMove(sq(File.G, Rank.R1), sq(File.F, Rank.R3))
|
||||
h.halfMoveClock shouldBe 1
|
||||
|
||||
test("halfMoveClock resets to 0 on a pawn move"):
|
||||
val h = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4), wasPawnMove = true)
|
||||
h.halfMoveClock shouldBe 0
|
||||
|
||||
test("halfMoveClock resets to 0 on a capture"):
|
||||
val h = GameHistory.empty.addMove(sq(File.E, Rank.R5), sq(File.D, Rank.R6), wasCapture = true)
|
||||
h.halfMoveClock shouldBe 0
|
||||
|
||||
test("halfMoveClock resets to 0 when both wasPawnMove and wasCapture are true"):
|
||||
val h = GameHistory.empty.addMove(sq(File.E, Rank.R5), sq(File.D, Rank.R6), wasPawnMove = true, wasCapture = true)
|
||||
h.halfMoveClock shouldBe 0
|
||||
|
||||
test("halfMoveClock carries across multiple moves"):
|
||||
val h = GameHistory.empty
|
||||
.addMove(sq(File.G, Rank.R1), sq(File.F, Rank.R3)) // +1 → 1
|
||||
.addMove(sq(File.G, Rank.R8), sq(File.F, Rank.R6)) // +1 → 2
|
||||
.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4), wasPawnMove = true) // reset → 0
|
||||
.addMove(sq(File.B, Rank.R1), sq(File.C, Rank.R3)) // +1 → 1
|
||||
h.halfMoveClock shouldBe 1
|
||||
|
||||
test("GameHistory can be initialised with a non-zero halfMoveClock"):
|
||||
val h = GameHistory(halfMoveClock = 42)
|
||||
h.halfMoveClock shouldBe 42
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.nowchess.chess.logic
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.move.PromotionPiece
|
||||
import de.nowchess.api.game.HistoryMove
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class HistoryMoveTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def sq(f: File, r: Rank): Square = Square(f, r)
|
||||
|
||||
test("Move with promotion records the promotion piece"):
|
||||
val move = HistoryMove(sq(File.E, Rank.R7), sq(File.E, Rank.R8), None, Some(PromotionPiece.Queen))
|
||||
move.promotionPiece should be (Some(PromotionPiece.Queen))
|
||||
|
||||
test("Normal move has no promotion piece"):
|
||||
val move = HistoryMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4), None, None)
|
||||
move.promotionPiece should be (None)
|
||||
|
||||
test("HistoryMove default values are compatible with normal pawn move"):
|
||||
val move = HistoryMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
move.castleSide shouldBe None
|
||||
move.promotionPiece shouldBe None
|
||||
move.pieceType shouldBe PieceType.Pawn
|
||||
move.isCapture shouldBe false
|
||||
|
||||
test("HistoryMove stores castle side and capture marker"):
|
||||
val move = HistoryMove(
|
||||
from = sq(File.E, Rank.R1),
|
||||
to = sq(File.G, Rank.R1),
|
||||
castleSide = Some("Kingside"),
|
||||
pieceType = PieceType.King
|
||||
)
|
||||
move.castleSide shouldBe Some("Kingside")
|
||||
move.pieceType shouldBe PieceType.King
|
||||
|
||||
test("HistoryMove stores explicit piece and capture flags"):
|
||||
val move = HistoryMove(
|
||||
from = sq(File.G, Rank.R1),
|
||||
to = sq(File.F, Rank.R3),
|
||||
pieceType = PieceType.Knight,
|
||||
isCapture = true
|
||||
)
|
||||
move.pieceType shouldBe PieceType.Knight
|
||||
move.isCapture shouldBe true
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package de.nowchess.chess.view
|
||||
|
||||
import de.nowchess.api.board.Piece
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class PieceUnicodeTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("White King maps to ♔"):
|
||||
Piece.WhiteKing.unicode shouldBe "\u2654"
|
||||
|
||||
test("White Queen maps to ♕"):
|
||||
Piece.WhiteQueen.unicode shouldBe "\u2655"
|
||||
|
||||
test("White Rook maps to ♖"):
|
||||
Piece.WhiteRook.unicode shouldBe "\u2656"
|
||||
|
||||
test("White Bishop maps to ♗"):
|
||||
Piece.WhiteBishop.unicode shouldBe "\u2657"
|
||||
|
||||
test("White Knight maps to ♘"):
|
||||
Piece.WhiteKnight.unicode shouldBe "\u2658"
|
||||
|
||||
test("White Pawn maps to ♙"):
|
||||
Piece.WhitePawn.unicode shouldBe "\u2659"
|
||||
|
||||
test("Black King maps to ♚"):
|
||||
Piece.BlackKing.unicode shouldBe "\u265A"
|
||||
|
||||
test("Black Queen maps to ♛"):
|
||||
Piece.BlackQueen.unicode shouldBe "\u265B"
|
||||
|
||||
test("Black Rook maps to ♜"):
|
||||
Piece.BlackRook.unicode shouldBe "\u265C"
|
||||
|
||||
test("Black Bishop maps to ♝"):
|
||||
Piece.BlackBishop.unicode shouldBe "\u265D"
|
||||
|
||||
test("Black Knight maps to ♞"):
|
||||
Piece.BlackKnight.unicode shouldBe "\u265E"
|
||||
|
||||
test("Black Pawn maps to ♟"):
|
||||
Piece.BlackPawn.unicode shouldBe "\u265F"
|
||||
@@ -1,41 +0,0 @@
|
||||
package de.nowchess.chess.view
|
||||
|
||||
import de.nowchess.api.board.{Board, File, Piece, Rank, Square}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class RendererTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("render contains column header with all file labels"):
|
||||
Renderer.render(Board.initial) should include("a b c d e f g h")
|
||||
|
||||
test("render output begins with the column header"):
|
||||
Renderer.render(Board.initial) should startWith(" a b c d e f g h")
|
||||
|
||||
test("render contains rank labels 1 through 8"):
|
||||
val output = Renderer.render(Board.initial)
|
||||
for rank <- 1 to 8 do output should include(s"$rank ")
|
||||
|
||||
test("render shows white king unicode symbol for initial board"):
|
||||
Renderer.render(Board.initial) should include("\u2654")
|
||||
|
||||
test("render shows black king unicode symbol for initial board"):
|
||||
Renderer.render(Board.initial) should include("\u265A")
|
||||
|
||||
test("render contains ANSI light-square background code"):
|
||||
Renderer.render(Board.initial) should include("\u001b[48;5;223m")
|
||||
|
||||
test("render contains ANSI dark-square background code"):
|
||||
Renderer.render(Board.initial) should include("\u001b[48;5;130m")
|
||||
|
||||
test("render uses white-piece foreground color for white pieces"):
|
||||
Renderer.render(Board.initial) should include("\u001b[97m")
|
||||
|
||||
test("render uses black-piece foreground color for black pieces"):
|
||||
Renderer.render(Board.initial) should include("\u001b[30m")
|
||||
|
||||
test("render of empty board contains no piece unicode"):
|
||||
val output = Renderer.render(Board(Map.empty))
|
||||
output should include("a b c d e f g h")
|
||||
output should not include "\u2654"
|
||||
output should not include "\u265A"
|
||||
Reference in New Issue
Block a user