feat: NCS-41 Bot Platform (#33)
Co-authored-by: Janis <janis@nowchess.de> Reviewed-on: #33 Co-authored-by: Janis <janis.e.20@gmx.de> Co-committed-by: Janis <janis.e.20@gmx.de>
This commit is contained in:
@@ -23,11 +23,7 @@ scala {
|
||||
|
||||
scoverage {
|
||||
scoverageVersion.set(versions["SCOVERAGE"]!!)
|
||||
excludedPackages.set(listOf(
|
||||
"de.nowchess.ui.gui",
|
||||
"de.nowchess.ui.terminal",
|
||||
"de.nowchess.ui.Main",
|
||||
))
|
||||
excludedPackages.set(listOf("de\\.nowchess\\.ui\\..*"))
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -64,6 +60,7 @@ dependencies {
|
||||
implementation(project(":modules:rule"))
|
||||
implementation(project(":modules:api"))
|
||||
implementation(project(":modules:io"))
|
||||
implementation(project(":modules:bot"))
|
||||
|
||||
// ScalaFX dependencies
|
||||
implementation("org.scalafx:scalafx_3:${versions["SCALAFX"]!!}")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package de.nowchess.ui
|
||||
|
||||
import de.nowchess.chess.engine.GameEngine
|
||||
import de.nowchess.api.game.{BotParticipant, Human}
|
||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||
import de.nowchess.bot.util.PolyglotBook
|
||||
import de.nowchess.bot.BotDifficulty
|
||||
import de.nowchess.ui.terminal.TerminalUI
|
||||
import de.nowchess.ui.gui.ChessGUILauncher
|
||||
|
||||
@@ -9,8 +12,19 @@ import de.nowchess.ui.gui.ChessGUILauncher
|
||||
*/
|
||||
object Main:
|
||||
def main(args: Array[String]): Unit =
|
||||
val book = PolyglotBook("../../modules/bot/codekiddy.bin")
|
||||
|
||||
// Create the core game engine (single source of truth)
|
||||
val engine = new GameEngine()
|
||||
val engine = new de.nowchess.chess.engine.GameEngine(
|
||||
participants = Map(
|
||||
de.nowchess.api.board.Color.White -> BotParticipant(
|
||||
de.nowchess.bot.bots.HybridBot(BotDifficulty.Easy, book = Some(book)),
|
||||
),
|
||||
de.nowchess.api.board.Color.Black -> Human(PlayerInfo(PlayerId("p1"), "Player 1")),
|
||||
),
|
||||
)
|
||||
|
||||
engine.startGame()
|
||||
|
||||
// Launch ScalaFX GUI in separate thread
|
||||
ChessGUILauncher.launch(engine)
|
||||
|
||||
@@ -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,42 @@ 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 match
|
||||
case MoveType.Promotion(_) => true
|
||||
case _ => false
|
||||
),
|
||||
)
|
||||
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)
|
||||
@@ -266,7 +272,8 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
||||
case Some(piece) =>
|
||||
Seq(bgRect) ++ PieceSprites.loadPieceImage(piece, squareSize * 0.8).toSeq
|
||||
case None =>
|
||||
Seq(bgRect)): Seq[scalafx.scene.Node]
|
||||
Seq(bgRect)
|
||||
): Seq[scalafx.scene.Node]
|
||||
}
|
||||
|
||||
def showMessage(msg: String): Unit =
|
||||
@@ -280,14 +287,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")
|
||||
|
||||
@@ -30,9 +30,9 @@ class ChessGUIApp extends JFXApplication:
|
||||
stage.scene = new Scene {
|
||||
root = boardView
|
||||
// Load CSS if available
|
||||
try {
|
||||
try
|
||||
Option(getClass.getResource("/styles.css")).foreach(url => stylesheets.add(url.toExternalForm))
|
||||
} catch {
|
||||
catch {
|
||||
case _: Exception => // CSS is optional
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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.*
|
||||
@@ -12,8 +11,7 @@ import de.nowchess.ui.utils.Renderer
|
||||
* I/O and user interaction in the terminal.
|
||||
*/
|
||||
class TerminalUI(engine: GameEngine) extends Observer:
|
||||
private val running = new AtomicBoolean(true)
|
||||
private val awaitingPromotion = new AtomicBoolean(false)
|
||||
private val running = new AtomicBoolean(true)
|
||||
|
||||
/** Called by GameEngine whenever a game event occurs. */
|
||||
override def onGameEvent(event: GameEvent): Unit =
|
||||
@@ -65,9 +63,6 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
||||
print(Renderer.render(e.context.board))
|
||||
printPrompt(e.context.turn)
|
||||
|
||||
case _: PromotionRequiredEvent =>
|
||||
println("Promote to: q=Queen, r=Rook, b=Bishop, n=Knight")
|
||||
awaitingPromotion.set(true)
|
||||
case _: FiftyMoveRuleAvailableEvent =>
|
||||
println("50-move rule is now available — type 'draw' to claim.")
|
||||
|
||||
@@ -90,28 +85,17 @@ 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 {
|
||||
if awaitingPromotion.get() then
|
||||
input.toLowerCase match
|
||||
case "q" => awaitingPromotion.set(false); engine.completePromotion(PromotionPiece.Queen)
|
||||
case "r" => awaitingPromotion.set(false); engine.completePromotion(PromotionPiece.Rook)
|
||||
case "b" => awaitingPromotion.set(false); engine.completePromotion(PromotionPiece.Bishop)
|
||||
case "n" => awaitingPromotion.set(false); engine.completePromotion(PromotionPiece.Knight)
|
||||
case _ =>
|
||||
println("Invalid choice. Enter q, r, b, or n.")
|
||||
println("Promote to: q=Queen, r=Rook, b=Bishop, n=Knight")
|
||||
else
|
||||
input.toLowerCase match
|
||||
case "quit" | "q" =>
|
||||
running.set(false)
|
||||
println("Game over. Goodbye!")
|
||||
case "" =>
|
||||
printPrompt(engine.turn)
|
||||
case _ =>
|
||||
engine.processUserInput(input)
|
||||
input.toLowerCase match
|
||||
case "quit" | "q" =>
|
||||
running.set(false)
|
||||
println("Game over. Goodbye!")
|
||||
case "" =>
|
||||
printPrompt(engine.turn)
|
||||
case _ =>
|
||||
engine.processUserInput(input)
|
||||
}
|
||||
|
||||
// Unsubscribe when done
|
||||
|
||||
Reference in New Issue
Block a user