feat: Refactor promotion handling to use UCI notation and improve move validation
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user