feat: Refactor promotion handling to use UCI notation and improve move validation
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-16 18:48:46 +02:00
parent 95537bc709
commit 96b8249e7e
17 changed files with 214 additions and 236 deletions
@@ -11,7 +11,7 @@ import scalafx.scene.shape.Rectangle
import scalafx.scene.text.{Font, Text}
import scalafx.stage.Stage
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.move.PromotionPiece
import de.nowchess.api.move.MoveType
import de.nowchess.chess.command.{MoveCommand, MoveResult}
import de.nowchess.chess.engine.GameEngine
import de.nowchess.io.fen.{FenExporter, FenParser}
@@ -178,36 +178,37 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
square
private def handleSquareClick(rank: Int, file: Int): Unit =
if !engine.isPendingPromotion then
val clickedSquare = Square(File.values(file), Rank.values(rank))
val clickedSquare = Square(File.values(file), Rank.values(rank))
selectedSquare.get() match
case None =>
// First click - select piece if it belongs to current player
currentBoard.get().pieceAt(clickedSquare).foreach { piece =>
if piece.color == currentTurn.get() then
selectedSquare.set(Some(clickedSquare))
highlightSquare(rank, file, PieceSprites.SquareColors.Selected)
selectedSquare.get() match
case None =>
// First click - select piece if it belongs to current player
currentBoard.get().pieceAt(clickedSquare).foreach { piece =>
if piece.color == currentTurn.get() then
selectedSquare.set(Some(clickedSquare))
highlightSquare(rank, file, PieceSprites.SquareColors.Selected)
val legalDests = engine.ruleSet
.legalMoves(engine.context)(clickedSquare)
.collect { case move if move.from == clickedSquare => move.to }
legalDests.foreach { sq =>
highlightSquare(sq.rank.ordinal, sq.file.ordinal, PieceSprites.SquareColors.ValidMove)
}
}
val legalDests = engine.ruleSet
.legalMoves(engine.context)(clickedSquare)
.collect { case move if move.from == clickedSquare => move.to }
legalDests.foreach { sq =>
highlightSquare(sq.rank.ordinal, sq.file.ordinal, PieceSprites.SquareColors.ValidMove)
}
}
case Some(fromSquare) =>
// Second click - attempt move
if clickedSquare == fromSquare then
// Deselect
selectedSquare.set(None)
updateBoard(currentBoard.get(), currentTurn.get())
else
// Try to move
val moveStr = s"${fromSquare}$clickedSquare"
engine.processUserInput(moveStr)
selectedSquare.set(None)
case Some(fromSquare) =>
// Second click - attempt move
if clickedSquare == fromSquare then
// Deselect
selectedSquare.set(None)
updateBoard(currentBoard.get(), currentTurn.get())
else
val isPromo = engine.ruleSet
.legalMoves(engine.context)(fromSquare)
.exists(m => m.to == clickedSquare && m.moveType.isInstanceOf[MoveType.Promotion])
if isPromo then showPromotionDialog(fromSquare, clickedSquare)
else engine.processUserInput(s"${fromSquare}$clickedSquare")
selectedSquare.set(None)
def updateBoard(board: Board, turn: Color): Unit =
currentBoard.set(board)
@@ -280,14 +281,12 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
headerText = "Choose promotion piece"
contentText = "Promote to:"
}
val result = dialog.showAndWait()
result match
case Some("Queen") => engine.completePromotion(PromotionPiece.Queen)
case Some("Rook") => engine.completePromotion(PromotionPiece.Rook)
case Some("Bishop") => engine.completePromotion(PromotionPiece.Bishop)
case Some("Knight") => engine.completePromotion(PromotionPiece.Knight)
case _ => engine.completePromotion(PromotionPiece.Queen) // Default
val uciSuffix = dialog.showAndWait() match
case Some("Rook") => "r"
case Some("Bishop") => "b"
case Some("Knight") => "n"
case _ => "q"
engine.processUserInput(s"${from}${to}$uciSuffix")
private def doFenExport(): Unit =
doExport(FenExporter, "FEN")
@@ -47,9 +47,6 @@ class GUIObserver(private val boardView: ChessBoardView) extends Observer:
boardView.updateBoard(e.context.board, e.context.turn)
boardView.showMessage("Board has been reset to initial position.")
case e: PromotionRequiredEvent =>
boardView.showPromotionDialog(e.from, e.to)
case e: FiftyMoveRuleAvailableEvent =>
boardView.showMessage("50-move rule is now available — type 'draw' to claim.")
@@ -6,26 +6,24 @@ import de.nowchess.api.board.{Color, Piece, PieceType}
/** Utility object for loading chess piece sprites. */
object PieceSprites:
private val spriteCache = scala.collection.mutable.Map[String, Image]()
private val spriteCache = scala.collection.mutable.Map[String, Option[Image]]()
/** Load a piece sprite image from resources. Sprites are cached for performance.
*/
def loadPieceImage(piece: Piece, size: Double = 60.0): ImageView =
val key = s"${piece.color.label.toLowerCase}_${piece.pieceType.label.toLowerCase}"
val image = spriteCache.getOrElseUpdate(key, loadImage(key))
new ImageView(image) {
fitWidth = size
fitHeight = size
preserveRatio = true
smooth = true
def loadPieceImage(piece: Piece, size: Double = 60.0): Option[ImageView] =
val key = s"${piece.color.label.toLowerCase}_${piece.pieceType.label.toLowerCase}"
spriteCache.getOrElseUpdate(key, loadImage(key)).map { image =>
new ImageView(image) {
fitWidth = size
fitHeight = size
preserveRatio = true
smooth = true
}
}
private def loadImage(key: String): Image =
private def loadImage(key: String): Option[Image] =
val path = s"/sprites/pieces/$key.png"
Option(getClass.getResourceAsStream(path)) match
case Some(stream) => new Image(stream)
case None => sys.error(s"Could not load sprite: $path")
Option(getClass.getResourceAsStream(path)).map(new Image(_))
/** Get square colors for the board using theme. */
object SquareColors:
@@ -1,9 +1,7 @@
package de.nowchess.ui.terminal
import java.util.concurrent.atomic.AtomicBoolean
import scala.io.StdIn
import de.nowchess.api.move.PromotionPiece
import de.nowchess.api.game.DrawReason
import de.nowchess.chess.engine.GameEngine
import de.nowchess.chess.observer.*
import de.nowchess.ui.utils.Renderer
@@ -90,7 +88,6 @@ class TerminalUI(engine: GameEngine) extends Observer:
print(Renderer.render(engine.board))
printPrompt(engine.turn)
// Game loop
while running.get() do
val input = Option(StdIn.readLine()).getOrElse("quit").trim
synchronized {