refactor(core): replace GameHistory with HistoryMove in PGN export logic
This commit is contained in:
@@ -1,51 +0,0 @@
|
|||||||
package de.nowchess.api.game
|
|
||||||
|
|
||||||
import de.nowchess.api.board.{PieceType, Square}
|
|
||||||
import de.nowchess.api.move.{Move, PromotionPiece}
|
|
||||||
|
|
||||||
/** A single move recorded in the game history. Distinct from api.move.Move which represents user intent. */
|
|
||||||
case class HistoryMove(
|
|
||||||
from: Square,
|
|
||||||
to: Square,
|
|
||||||
castleSide: Option[String] = None,
|
|
||||||
promotionPiece: Option[PromotionPiece] = None,
|
|
||||||
pieceType: PieceType = PieceType.Pawn,
|
|
||||||
isCapture: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Complete game history: ordered list of moves plus the half-move clock for the 50-move rule.
|
|
||||||
*
|
|
||||||
* @param moves moves played so far, oldest first
|
|
||||||
* @param halfMoveClock plies since the last pawn move or capture (FIDE 50-move rule counter)
|
|
||||||
*
|
|
||||||
* Deprecated: Use GameContext instead. This exists for compatibility during migration.
|
|
||||||
*/
|
|
||||||
case class GameHistory(moves: List[HistoryMove] = List.empty, halfMoveClock: Int = 0):
|
|
||||||
|
|
||||||
/** Add a raw HistoryMove record. Clock increments by 1.
|
|
||||||
* Use the coordinate overload when you know whether the move is a pawn move or capture.
|
|
||||||
*/
|
|
||||||
def addMove(move: HistoryMove): GameHistory =
|
|
||||||
GameHistory(moves :+ move, halfMoveClock + 1)
|
|
||||||
|
|
||||||
/** Add a move by coordinates.
|
|
||||||
*
|
|
||||||
* @param wasPawnMove true when the moving piece is a pawn — resets the clock to 0
|
|
||||||
* @param wasCapture true when a piece was captured (including en passant) — resets the clock to 0
|
|
||||||
*
|
|
||||||
* If neither flag is set the clock increments by 1.
|
|
||||||
*/
|
|
||||||
def addMove(
|
|
||||||
from: Square,
|
|
||||||
to: Square,
|
|
||||||
castleSide: Option[String] = None,
|
|
||||||
promotionPiece: Option[PromotionPiece] = None,
|
|
||||||
wasPawnMove: Boolean = false,
|
|
||||||
wasCapture: Boolean = false,
|
|
||||||
pieceType: PieceType = PieceType.Pawn
|
|
||||||
): GameHistory =
|
|
||||||
val newClock = if wasPawnMove || wasCapture then 0 else halfMoveClock + 1
|
|
||||||
GameHistory(moves :+ HistoryMove(from, to, castleSide, promotionPiece, pieceType, wasCapture), newClock)
|
|
||||||
|
|
||||||
object GameHistory:
|
|
||||||
val empty: GameHistory = GameHistory()
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.nowchess.api.game
|
||||||
|
|
||||||
|
import de.nowchess.api.board.{PieceType, Square}
|
||||||
|
import de.nowchess.api.move.PromotionPiece
|
||||||
|
|
||||||
|
/** A single move recorded in game history and PGN workflows. */
|
||||||
|
case class HistoryMove(
|
||||||
|
from: Square,
|
||||||
|
to: Square,
|
||||||
|
castleSide: Option[String] = None,
|
||||||
|
promotionPiece: Option[PromotionPiece] = None,
|
||||||
|
pieceType: PieceType = PieceType.Pawn,
|
||||||
|
isCapture: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ package de.nowchess.io.pgn
|
|||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.move.{PromotionPiece, MoveType}
|
import de.nowchess.api.move.{PromotionPiece, MoveType}
|
||||||
import de.nowchess.api.game.{GameHistory, HistoryMove, GameContext}
|
import de.nowchess.api.game.{GameContext, HistoryMove}
|
||||||
import de.nowchess.io.GameContextExport
|
import de.nowchess.io.GameContextExport
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
|
|
||||||
@@ -42,18 +42,17 @@ object PgnExporter extends GameContextExport:
|
|||||||
historyMoves += HistoryMove(move.from, move.to, castleSide, promotionPiece, pieceType, isCapture)
|
historyMoves += HistoryMove(move.from, move.to, castleSide, promotionPiece, pieceType, isCapture)
|
||||||
ctx = DefaultRules.applyMove(ctx, move)
|
ctx = DefaultRules.applyMove(ctx, move)
|
||||||
|
|
||||||
val history = GameHistory(historyMoves.toList, context.halfMoveClock)
|
exportGame(headers, historyMoves.toList)
|
||||||
exportGame(headers, history)
|
|
||||||
|
|
||||||
/** Export a game with headers and history to PGN format. */
|
/** Export a game with headers and history moves to PGN format. */
|
||||||
def exportGame(headers: Map[String, String], history: GameHistory): String =
|
def exportGame(headers: Map[String, String], moves: List[HistoryMove]): String =
|
||||||
val headerLines = headers.map { case (key, value) =>
|
val headerLines = headers.map { case (key, value) =>
|
||||||
s"""[$key "$value"]"""
|
s"""[$key "$value"]"""
|
||||||
}.mkString("\n")
|
}.mkString("\n")
|
||||||
|
|
||||||
val moveText = if history.moves.isEmpty then ""
|
val moveText = if moves.isEmpty then ""
|
||||||
else
|
else
|
||||||
val groupedMoves = history.moves.zipWithIndex.groupBy(_._2 / 2)
|
val groupedMoves = moves.zipWithIndex.groupBy(_._2 / 2)
|
||||||
val moveLines = for (moveNumber, movePairs) <- groupedMoves.toList.sortBy(_._1) yield
|
val moveLines = for (moveNumber, movePairs) <- groupedMoves.toList.sortBy(_._1) yield
|
||||||
val moveNum = moveNumber + 1
|
val moveNum = moveNumber + 1
|
||||||
val whiteMoveStr = movePairs.find(_._2 % 2 == 0).map(p => moveToAlgebraic(p._1)).getOrElse("")
|
val whiteMoveStr = movePairs.find(_._2 % 2 == 0).map(p => moveToAlgebraic(p._1)).getOrElse("")
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ package de.nowchess.io.pgn
|
|||||||
|
|
||||||
import de.nowchess.api.board.{PieceType, *}
|
import de.nowchess.api.board.{PieceType, *}
|
||||||
import de.nowchess.api.move.{PromotionPiece, Move, MoveType}
|
import de.nowchess.api.move.{PromotionPiece, Move, MoveType}
|
||||||
import de.nowchess.api.game.{GameHistory, HistoryMove, GameContext}
|
import de.nowchess.api.game.{GameContext, HistoryMove}
|
||||||
import de.nowchess.io.pgn.PgnParser
|
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
@@ -11,8 +10,7 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
test("export empty game") {
|
test("export empty game") {
|
||||||
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B")
|
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B")
|
||||||
val history = GameHistory.empty
|
val pgn = PgnExporter.exportGame(headers, List.empty)
|
||||||
val pgn = PgnExporter.exportGame(headers, history)
|
|
||||||
|
|
||||||
pgn.contains("[Event \"Test\"]") shouldBe true
|
pgn.contains("[Event \"Test\"]") shouldBe true
|
||||||
pgn.contains("[White \"A\"]") shouldBe true
|
pgn.contains("[White \"A\"]") shouldBe true
|
||||||
@@ -21,97 +19,87 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
test("export single move") {
|
test("export single move") {
|
||||||
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B")
|
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B")
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
val pgn = PgnExporter.exportGame(headers, moves)
|
||||||
val pgn = PgnExporter.exportGame(headers, history)
|
|
||||||
|
|
||||||
pgn.contains("1. e4") shouldBe true
|
pgn.contains("1. e4") shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
test("export castling") {
|
test("export castling") {
|
||||||
val headers = Map("Event" -> "Test")
|
val headers = Map("Event" -> "Test")
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R1), Square(File.G, Rank.R1), Some("Kingside")))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R1), Square(File.G, Rank.R1), Some("Kingside")))
|
val pgn = PgnExporter.exportGame(headers, moves)
|
||||||
val pgn = PgnExporter.exportGame(headers, history)
|
|
||||||
|
|
||||||
pgn.contains("O-O") shouldBe true
|
pgn.contains("O-O") shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
test("export game sequence") {
|
test("export game sequence") {
|
||||||
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B", "Result" -> "1-0")
|
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B", "Result" -> "1-0")
|
||||||
val history = GameHistory()
|
val moves = List(
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None),
|
||||||
.addMove(HistoryMove(Square(File.C, Rank.R7), Square(File.C, Rank.R5), None))
|
HistoryMove(Square(File.C, Rank.R7), Square(File.C, Rank.R5), None),
|
||||||
.addMove(HistoryMove(Square(File.G, Rank.R1), Square(File.F, Rank.R3), None, pieceType = PieceType.Knight))
|
HistoryMove(Square(File.G, Rank.R1), Square(File.F, Rank.R3), None, pieceType = PieceType.Knight)
|
||||||
val pgn = PgnExporter.exportGame(headers, history)
|
)
|
||||||
|
val pgn = PgnExporter.exportGame(headers, moves)
|
||||||
|
|
||||||
pgn.contains("1. e4 c5") shouldBe true
|
pgn.contains("1. e4 c5") shouldBe true
|
||||||
pgn.contains("2. Nf3") shouldBe true
|
pgn.contains("2. Nf3") shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
test("export game with no headers returns only move text") {
|
test("export game with no headers returns only move text") {
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
val pgn = PgnExporter.exportGame(Map.empty, moves)
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
|
||||||
|
|
||||||
pgn shouldBe "1. e4 *"
|
pgn shouldBe "1. e4 *"
|
||||||
}
|
}
|
||||||
|
|
||||||
test("export queenside castling") {
|
test("export queenside castling") {
|
||||||
val headers = Map("Event" -> "Test")
|
val headers = Map("Event" -> "Test")
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R1), Square(File.C, Rank.R1), Some("Queenside")))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R1), Square(File.C, Rank.R1), Some("Queenside")))
|
val pgn = PgnExporter.exportGame(headers, moves)
|
||||||
val pgn = PgnExporter.exportGame(headers, history)
|
|
||||||
|
|
||||||
pgn.contains("O-O-O") shouldBe true
|
pgn.contains("O-O-O") shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
test("exportGame encodes promotion to Queen as =Q suffix") {
|
test("exportGame encodes promotion to Queen as =Q suffix") {
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Queen)))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Queen)))
|
val pgn = PgnExporter.exportGame(Map.empty, moves)
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
|
||||||
pgn should include ("e8=Q")
|
pgn should include ("e8=Q")
|
||||||
}
|
}
|
||||||
|
|
||||||
test("exportGame encodes promotion to Rook as =R suffix") {
|
test("exportGame encodes promotion to Rook as =R suffix") {
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Rook)))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Rook)))
|
val pgn = PgnExporter.exportGame(Map.empty, moves)
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
|
||||||
pgn should include ("e8=R")
|
pgn should include ("e8=R")
|
||||||
}
|
}
|
||||||
|
|
||||||
test("exportGame encodes promotion to Bishop as =B suffix") {
|
test("exportGame encodes promotion to Bishop as =B suffix") {
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Bishop)))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Bishop)))
|
val pgn = PgnExporter.exportGame(Map.empty, moves)
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
|
||||||
pgn should include ("e8=B")
|
pgn should include ("e8=B")
|
||||||
}
|
}
|
||||||
|
|
||||||
test("exportGame encodes promotion to Knight as =N suffix") {
|
test("exportGame encodes promotion to Knight as =N suffix") {
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Knight)))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Knight)))
|
val pgn = PgnExporter.exportGame(Map.empty, moves)
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
|
||||||
pgn should include ("e8=N")
|
pgn should include ("e8=N")
|
||||||
}
|
}
|
||||||
|
|
||||||
test("exportGame does not add suffix for normal moves") {
|
test("exportGame does not add suffix for normal moves") {
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None, None))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None, None))
|
val pgn = PgnExporter.exportGame(Map.empty, moves)
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
|
||||||
pgn should include ("e4")
|
pgn should include ("e4")
|
||||||
pgn should not include ("=")
|
pgn should not include "="
|
||||||
}
|
}
|
||||||
|
|
||||||
test("exportGame uses Result header as termination marker"):
|
test("exportGame uses Result header as termination marker"):
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
val pgn = PgnExporter.exportGame(Map("Result" -> "1/2-1/2"), moves)
|
||||||
val pgn = PgnExporter.exportGame(Map("Result" -> "1/2-1/2"), history)
|
|
||||||
pgn should endWith("1/2-1/2")
|
pgn should endWith("1/2-1/2")
|
||||||
|
|
||||||
test("exportGame with no Result header still uses * as default"):
|
test("exportGame with no Result header still uses * as default"):
|
||||||
val history = GameHistory()
|
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
||||||
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
val pgn = PgnExporter.exportGame(Map.empty, moves)
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
|
||||||
pgn shouldBe "1. e4 *"
|
pgn shouldBe "1. e4 *"
|
||||||
|
|
||||||
test("exportGameContext: moves are preserved in output") {
|
test("exportGameContext: moves are preserved in output") {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package de.nowchess.io.pgn
|
|||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.api.game.{GameHistory, HistoryMove}
|
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import scalafx.scene.shape.Rectangle
|
|||||||
import scalafx.scene.text.{Font, Text}
|
import scalafx.scene.text.{Font, Text}
|
||||||
import scalafx.stage.Stage
|
import scalafx.stage.Stage
|
||||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||||
import de.nowchess.api.game.{GameHistory, HistoryMove}
|
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.chess.command.{MoveCommand, MoveResult}
|
import de.nowchess.chess.command.{MoveCommand, MoveResult}
|
||||||
import de.nowchess.chess.engine.GameEngine
|
import de.nowchess.chess.engine.GameEngine
|
||||||
@@ -178,7 +177,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
|||||||
if piece.color == currentTurn then
|
if piece.color == currentTurn then
|
||||||
selectedSquare = Some(clickedSquare)
|
selectedSquare = Some(clickedSquare)
|
||||||
highlightSquare(rank, file, PieceSprites.SquareColors.Selected)
|
highlightSquare(rank, file, PieceSprites.SquareColors.Selected)
|
||||||
|
|
||||||
val legalDests = engine.ruleSet.legalMoves(engine.context, clickedSquare)
|
val legalDests = engine.ruleSet.legalMoves(engine.context, clickedSquare)
|
||||||
.collect { case move if move.from == clickedSquare => move.to }
|
.collect { case move if move.from == clickedSquare => move.to }
|
||||||
legalDests.foreach { sq =>
|
legalDests.foreach { sq =>
|
||||||
@@ -289,7 +288,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
|||||||
|
|
||||||
private def doPgnImport(): Unit =
|
private def doPgnImport(): Unit =
|
||||||
doImport(PgnParser, "PGN")
|
doImport(PgnParser, "PGN")
|
||||||
|
|
||||||
private def doExport(exporter: GameContextExport, formatName: String): Unit = {
|
private def doExport(exporter: GameContextExport, formatName: String): Unit = {
|
||||||
val exported = exporter.exportGameContext(engine.context)
|
val exported = exporter.exportGameContext(engine.context)
|
||||||
showCopyDialog(s"$formatName Export", exported)
|
showCopyDialog(s"$formatName Export", exported)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package de.nowchess.ui.terminal
|
|||||||
import scala.io.StdIn
|
import scala.io.StdIn
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.chess.engine.GameEngine
|
import de.nowchess.chess.engine.GameEngine
|
||||||
import de.nowchess.chess.observer.{Observer, GameEvent, *}
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.chess.view.Renderer
|
import de.nowchess.ui.utils.Renderer
|
||||||
|
|
||||||
/** Terminal UI that implements Observer pattern.
|
/** Terminal UI that implements Observer pattern.
|
||||||
* Subscribes to GameEngine and receives state change events.
|
* Subscribes to GameEngine and receives state change events.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.nowchess.chess.view
|
package de.nowchess.ui.utils
|
||||||
|
|
||||||
import de.nowchess.api.board.{Color, Piece, PieceType}
|
import de.nowchess.api.board.{Color, Piece, PieceType}
|
||||||
|
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package de.nowchess.chess.view
|
package de.nowchess.ui.utils
|
||||||
|
|
||||||
import de.nowchess.api.board.{Board, Color, File, Rank, Square}
|
import de.nowchess.api.board.*
|
||||||
|
|
||||||
object Renderer:
|
object Renderer:
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.nowchess.chess.view
|
package de.nowchess.ui.utils
|
||||||
|
|
||||||
import de.nowchess.api.board.Piece
|
import de.nowchess.api.board.Piece
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package de.nowchess.chess.view
|
package de.nowchess.ui.utils
|
||||||
|
|
||||||
import de.nowchess.api.board.{Board, File, Piece, Rank, Square}
|
import de.nowchess.api.board.*
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
Reference in New Issue
Block a user