refactor(core): enhance GameEngine and GUI for undo/redo functionality and integrate FEN/PGN export/import
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -15,8 +15,8 @@ import de.nowchess.rules.sets.DefaultRules
|
|||||||
* All user interactions go through Commands; state changes are broadcast via GameEvents.
|
* All user interactions go through Commands; state changes are broadcast via GameEvents.
|
||||||
*/
|
*/
|
||||||
class GameEngine(
|
class GameEngine(
|
||||||
initialContext: GameContext = GameContext.initial,
|
val initialContext: GameContext = GameContext.initial,
|
||||||
ruleSet: RuleSet = DefaultRules
|
val ruleSet: RuleSet = DefaultRules
|
||||||
) extends Observable:
|
) extends Observable:
|
||||||
private var currentContext: GameContext = initialContext
|
private var currentContext: GameContext = initialContext
|
||||||
private val invoker = new CommandInvoker()
|
private val invoker = new CommandInvoker()
|
||||||
@@ -59,7 +59,6 @@ class GameEngine(
|
|||||||
|
|
||||||
case "draw" =>
|
case "draw" =>
|
||||||
if currentContext.halfMoveClock >= 100 then
|
if currentContext.halfMoveClock >= 100 then
|
||||||
currentContext = GameContext.initial
|
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
notifyObservers(DrawClaimedEvent(currentContext))
|
notifyObservers(DrawClaimedEvent(currentContext))
|
||||||
else
|
else
|
||||||
@@ -202,10 +201,8 @@ class GameEngine(
|
|||||||
|
|
||||||
if ruleSet.isCheckmate(currentContext) then
|
if ruleSet.isCheckmate(currentContext) then
|
||||||
val winner = currentContext.turn.opposite
|
val winner = currentContext.turn.opposite
|
||||||
currentContext = GameContext.initial
|
|
||||||
notifyObservers(CheckmateEvent(currentContext, winner))
|
notifyObservers(CheckmateEvent(currentContext, winner))
|
||||||
else if ruleSet.isStalemate(currentContext) then
|
else if ruleSet.isStalemate(currentContext) then
|
||||||
currentContext = GameContext.initial
|
|
||||||
notifyObservers(StalemateEvent(currentContext))
|
notifyObservers(StalemateEvent(currentContext))
|
||||||
else if ruleSet.isCheck(currentContext) then
|
else if ruleSet.isCheck(currentContext) then
|
||||||
notifyObservers(CheckDetectedEvent(currentContext))
|
notifyObservers(CheckDetectedEvent(currentContext))
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import org.gradle.api.file.DuplicatesStrategy
|
||||||
|
import org.gradle.jvm.tasks.Jar
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("scala")
|
id("scala")
|
||||||
id("org.scoverage")
|
id("org.scoverage")
|
||||||
@@ -38,6 +41,10 @@ tasks.named<JavaExec>("run") {
|
|||||||
standardInput = System.`in`
|
standardInput = System.`in`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named<Jar>("jar") {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation("org.scala-lang:scala3-compiler_3") {
|
implementation("org.scala-lang:scala3-compiler_3") {
|
||||||
@@ -54,6 +61,7 @@ dependencies {
|
|||||||
implementation(project(":modules:core"))
|
implementation(project(":modules:core"))
|
||||||
implementation(project(":modules:rule"))
|
implementation(project(":modules:rule"))
|
||||||
implementation(project(":modules:api"))
|
implementation(project(":modules:api"))
|
||||||
|
implementation(project(":modules:io"))
|
||||||
|
|
||||||
// ScalaFX dependencies
|
// ScalaFX dependencies
|
||||||
implementation("org.scalafx:scalafx_3:${versions["SCALAFX"]!!}")
|
implementation("org.scalafx:scalafx_3:${versions["SCALAFX"]!!}")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.nowchess.ui.gui
|
package de.nowchess.ui.gui
|
||||||
|
|
||||||
|
import scala.compiletime.uninitialized
|
||||||
import scalafx.Includes.*
|
import scalafx.Includes.*
|
||||||
import scalafx.application.Platform
|
import scalafx.application.Platform
|
||||||
import scalafx.geometry.{Insets, Pos}
|
import scalafx.geometry.{Insets, Pos}
|
||||||
@@ -10,7 +11,9 @@ 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, Piece, PieceType, Square, File, Rank}
|
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square, File, Rank}
|
||||||
|
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.engine.GameEngine
|
import de.nowchess.chess.engine.GameEngine
|
||||||
|
|
||||||
/** ScalaFX chess board view that displays the game state.
|
/** ScalaFX chess board view that displays the game state.
|
||||||
@@ -33,6 +36,9 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
|||||||
private var selectedSquare: Option[Square] = None
|
private var selectedSquare: Option[Square] = None
|
||||||
private val squareViews = scala.collection.mutable.Map[(Int, Int), StackPane]()
|
private val squareViews = scala.collection.mutable.Map[(Int, Int), StackPane]()
|
||||||
|
|
||||||
|
private var undoButton: Button = uninitialized
|
||||||
|
private var redoButton: Button = uninitialized
|
||||||
|
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
initializeBoard()
|
initializeBoard()
|
||||||
|
|
||||||
@@ -66,15 +72,23 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
|||||||
spacing = 10
|
spacing = 10
|
||||||
alignment = Pos.Center
|
alignment = Pos.Center
|
||||||
children = Seq(
|
children = Seq(
|
||||||
new Button("Undo") {
|
{
|
||||||
font = Font.font(comicSansFontFamily, 12)
|
undoButton = new Button("Undo") {
|
||||||
onAction = _ => if engine.canUndo then engine.undo()
|
font = Font.font(comicSansFontFamily, 12)
|
||||||
style = "-fx-background-radius: 8; -fx-background-color: #B9DAD1;"
|
onAction = _ => if engine.canUndo then engine.undo()
|
||||||
|
style = "-fx-background-radius: 8; -fx-background-color: #B9DAD1;"
|
||||||
|
disable = !engine.canUndo
|
||||||
|
}
|
||||||
|
undoButton
|
||||||
},
|
},
|
||||||
new Button("Redo") {
|
{
|
||||||
font = Font.font(comicSansFontFamily, 12)
|
redoButton = new Button("Redo") {
|
||||||
onAction = _ => if engine.canRedo then engine.redo()
|
font = Font.font(comicSansFontFamily, 12)
|
||||||
style = "-fx-background-radius: 8; -fx-background-color: #B9C2DA;"
|
onAction = _ => if engine.canRedo then engine.redo()
|
||||||
|
style = "-fx-background-radius: 8; -fx-background-color: #B9C2DA;"
|
||||||
|
disable = !engine.canRedo
|
||||||
|
}
|
||||||
|
redoButton
|
||||||
},
|
},
|
||||||
new Button("Reset") {
|
new Button("Reset") {
|
||||||
font = Font.font(comicSansFontFamily, 12)
|
font = Font.font(comicSansFontFamily, 12)
|
||||||
@@ -161,12 +175,12 @@ 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)
|
||||||
// TODO: Update legal move highlighting to use new RuleSet interface from modules/rule
|
|
||||||
// val legalDests = GameRules.legalMoves(currentBoard, engine.history, currentTurn)
|
val legalDests = engine.ruleSet.legalMoves(engine.context, clickedSquare)
|
||||||
// .collect { case (`clickedSquare`, to) => to }
|
.collect { case move if move.from == clickedSquare => move.to }
|
||||||
// legalDests.foreach { sq =>
|
legalDests.foreach { sq =>
|
||||||
// highlightSquare(sq.rank.ordinal, sq.file.ordinal, PieceSprites.SquareColors.ValidMove)
|
highlightSquare(sq.rank.ordinal, sq.file.ordinal, PieceSprites.SquareColors.ValidMove)
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case Some(fromSquare) =>
|
case Some(fromSquare) =>
|
||||||
@@ -214,7 +228,13 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
|||||||
|
|
||||||
stackPane.children = children
|
stackPane.children = children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUndoRedoButtons()
|
||||||
|
|
||||||
|
def updateUndoRedoButtons(): Unit =
|
||||||
|
if undoButton != null then undoButton.disable = !engine.canUndo
|
||||||
|
if redoButton != null then redoButton.disable = !engine.canRedo
|
||||||
|
|
||||||
private def highlightSquare(rank: Int, file: Int, color: String): Unit =
|
private def highlightSquare(rank: Int, file: Int, color: String): Unit =
|
||||||
squareViews.get((rank, file)).foreach { stackPane =>
|
squareViews.get((rank, file)).foreach { stackPane =>
|
||||||
val bgRect = new Rectangle {
|
val bgRect = new Rectangle {
|
||||||
@@ -256,17 +276,33 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
|||||||
case _ => engine.completePromotion(PromotionPiece.Queen) // Default
|
case _ => engine.completePromotion(PromotionPiece.Queen) // Default
|
||||||
|
|
||||||
private def doFenExport(): Unit =
|
private def doFenExport(): Unit =
|
||||||
// TODO: Update FEN export to use GameContext instead of GameHistory and calculator helpers
|
val fen = de.nowchess.io.fen.FenExporter.gameContextToFen(engine.context)
|
||||||
showMessage("FEN export temporarily disabled during NCS-22 refactoring")
|
showCopyDialog("FEN Export", fen)
|
||||||
|
|
||||||
private def doFenImport(): Unit =
|
private def doFenImport(): Unit =
|
||||||
showMessage("FEN import temporarily disabled during NCS-22 refactoring")
|
showInputDialog("FEN Import", rows = 1).foreach { fen =>
|
||||||
|
de.nowchess.io.fen.FenParser.parseFen(fen) match
|
||||||
|
case Some(gameContext) =>
|
||||||
|
engine.loadPosition(gameContext)
|
||||||
|
case None =>
|
||||||
|
showMessage("⚠️ Invalid FEN string")
|
||||||
|
}
|
||||||
|
|
||||||
private def doPgnExport(): Unit =
|
private def doPgnExport(): Unit =
|
||||||
showMessage("PGN export temporarily disabled during NCS-22 refactoring")
|
val pgn = de.nowchess.io.pgn.PgnExporter.exportGame(
|
||||||
|
Map("Event" -> "NowChess Game", "Date" -> "2026.04.04"),
|
||||||
|
exportableGameHistory()
|
||||||
|
)
|
||||||
|
showCopyDialog("PGN Export", pgn)
|
||||||
|
|
||||||
private def doPgnImport(): Unit =
|
private def doPgnImport(): Unit =
|
||||||
showMessage("PGN import temporarily disabled during NCS-22 refactoring")
|
showInputDialog("PGN Import", rows = 5).foreach { pgn =>
|
||||||
|
engine.loadPgn(pgn) match
|
||||||
|
case Right(_) =>
|
||||||
|
showMessage("✓ PGN loaded successfully!")
|
||||||
|
case Left(err) =>
|
||||||
|
showMessage(s"⚠️ PGN Error: $err")
|
||||||
|
}
|
||||||
|
|
||||||
private def showCopyDialog(title: String, content: String): Unit =
|
private def showCopyDialog(title: String, content: String): Unit =
|
||||||
val area = new javafx.scene.control.TextArea(content)
|
val area = new javafx.scene.control.TextArea(content)
|
||||||
@@ -298,3 +334,34 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
|||||||
dialog.initOwner(stage.delegate)
|
dialog.initOwner(stage.delegate)
|
||||||
val result = dialog.showAndWait()
|
val result = dialog.showAndWait()
|
||||||
if result.isPresent && result.get != null && result.get.nonEmpty then Some(result.get) else None
|
if result.isPresent && result.get != null && result.get.nonEmpty then Some(result.get) else None
|
||||||
|
|
||||||
|
private def exportableGameHistory(): GameHistory =
|
||||||
|
val moveCommands = engine.commandHistory.collect { case moveCmd: MoveCommand => moveCmd }
|
||||||
|
val activeMoveCount = engine.context.moves.length
|
||||||
|
val historyMoves = moveCommands.take(activeMoveCount).flatMap: moveCmd =>
|
||||||
|
moveCmd.previousContext.flatMap: previousContext =>
|
||||||
|
moveCmd.moveResult.collect:
|
||||||
|
case MoveResult.Successful(_, captured) =>
|
||||||
|
val movingPiece = previousContext.board.pieceAt(moveCmd.from)
|
||||||
|
val pieceType = movingPiece.map(_.pieceType).getOrElse(PieceType.Pawn)
|
||||||
|
val castleSide = moveCmd.notation match
|
||||||
|
case "O-O" => Some("Kingside")
|
||||||
|
case "O-O-O" => Some("Queenside")
|
||||||
|
case _ => None
|
||||||
|
val promotionPiece = moveCmd.notation.split("=").lastOption.flatMap:
|
||||||
|
case "Q" => Some(PromotionPiece.Queen)
|
||||||
|
case "R" => Some(PromotionPiece.Rook)
|
||||||
|
case "B" => Some(PromotionPiece.Bishop)
|
||||||
|
case "N" => Some(PromotionPiece.Knight)
|
||||||
|
case _ => None
|
||||||
|
HistoryMove(
|
||||||
|
from = moveCmd.from,
|
||||||
|
to = moveCmd.to,
|
||||||
|
castleSide = castleSide,
|
||||||
|
promotionPiece = promotionPiece,
|
||||||
|
pieceType = pieceType,
|
||||||
|
isCapture = captured.isDefined
|
||||||
|
)
|
||||||
|
|
||||||
|
GameHistory(historyMoves.toList)
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,27 @@ class GUIObserver(private val boardView: ChessBoardView) extends Observer:
|
|||||||
case e: DrawClaimedEvent =>
|
case e: DrawClaimedEvent =>
|
||||||
boardView.updateBoard(e.context.board, e.context.turn)
|
boardView.updateBoard(e.context.board, e.context.turn)
|
||||||
showAlert(AlertType.Information, "Draw Claimed", "Draw claimed! The game is a draw.")
|
showAlert(AlertType.Information, "Draw Claimed", "Draw claimed! The game is a draw.")
|
||||||
case e: FiftyMoveRuleAvailableEvent =>
|
|
||||||
|
case e: FiftyMoveRuleAvailableEvent =>
|
||||||
boardView.showMessage("50-move rule available! The game is a draw.")
|
boardView.showMessage("50-move rule available! The game is a draw.")
|
||||||
|
|
||||||
|
case e: MoveUndoneEvent =>
|
||||||
|
boardView.updateBoard(e.context.board, e.context.turn)
|
||||||
|
boardView.showMessage(s"↶ Undo: ${e.pgnNotation}")
|
||||||
|
boardView.updateUndoRedoButtons()
|
||||||
|
|
||||||
|
case e: MoveRedoneEvent =>
|
||||||
|
boardView.updateBoard(e.context.board, e.context.turn)
|
||||||
|
if e.capturedPiece.isDefined then
|
||||||
|
boardView.showMessage(s"↷ Redo: ${e.pgnNotation} — Captured: ${e.capturedPiece.get}")
|
||||||
|
else
|
||||||
|
boardView.showMessage(s"↷ Redo: ${e.pgnNotation}")
|
||||||
|
boardView.updateUndoRedoButtons()
|
||||||
|
|
||||||
|
case e: PgnLoadedEvent =>
|
||||||
|
boardView.updateBoard(e.context.board, e.context.turn)
|
||||||
|
boardView.showMessage("✓ PGN loaded successfully!")
|
||||||
|
boardView.updateUndoRedoButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
private def showAlert(alertType: AlertType, titleText: String, content: String): Unit =
|
private def showAlert(alertType: AlertType, titleText: String, content: String): Unit =
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
|||||||
println(s"Captured: $cap on ${e.toSquare}")
|
println(s"Captured: $cap on ${e.toSquare}")
|
||||||
printPrompt(e.context.turn)
|
printPrompt(e.context.turn)
|
||||||
|
|
||||||
|
case e: MoveUndoneEvent =>
|
||||||
|
println(s"Undo: ${e.pgnNotation}")
|
||||||
|
println()
|
||||||
|
print(Renderer.render(e.context.board))
|
||||||
|
printPrompt(e.context.turn)
|
||||||
|
|
||||||
|
case e: MoveRedoneEvent =>
|
||||||
|
println(s"Redo: ${e.pgnNotation}")
|
||||||
|
println()
|
||||||
|
print(Renderer.render(e.context.board))
|
||||||
|
printPrompt(e.context.turn)
|
||||||
|
|
||||||
case e: CheckDetectedEvent =>
|
case e: CheckDetectedEvent =>
|
||||||
println(s"${e.context.turn.label} is in check!")
|
println(s"${e.context.turn.label} is in check!")
|
||||||
|
|
||||||
@@ -57,6 +69,12 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
|||||||
case _: FiftyMoveRuleAvailableEvent =>
|
case _: FiftyMoveRuleAvailableEvent =>
|
||||||
println("50-move rule available! The game is a draw.")
|
println("50-move rule available! The game is a draw.")
|
||||||
|
|
||||||
|
case e: PgnLoadedEvent =>
|
||||||
|
println("PGN loaded successfully.")
|
||||||
|
println()
|
||||||
|
print(Renderer.render(e.context.board))
|
||||||
|
printPrompt(e.context.turn)
|
||||||
|
|
||||||
/** Start the terminal UI game loop. */
|
/** Start the terminal UI game loop. */
|
||||||
def start(): Unit =
|
def start(): Unit =
|
||||||
// Register as observer
|
// Register as observer
|
||||||
|
|||||||
Reference in New Issue
Block a user