feat: Gui added (not done, not reviewed)

This commit is contained in:
shahdlala66
2026-04-01 00:34:00 +02:00
committed by Janis
parent 14e88470de
commit 23a5c763c5
24 changed files with 488 additions and 3 deletions
+76
View File
@@ -0,0 +1,76 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
application
}
group = "de.nowchess"
version = "1.0-SNAPSHOT"
@Suppress("UNCHECKED_CAST")
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
repositories {
mavenCentral()
}
scala {
scalaVersion = versions["SCALA3"]!!
}
scoverage {
scoverageVersion.set(versions["SCOVERAGE"]!!)
}
dependencies {
implementation("org.scala-lang:scala3-compiler_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
implementation("org.scala-lang:scala3-library_3") {
version {
strictly(versions["SCALA3"]!!)
}
}
implementation(project(":modules:core"))
implementation(project(":modules:api"))
// ScalaFX dependencies
implementation("org.scalafx:scalafx_3:21.0.0-R32")
// JavaFX dependencies for the current platform
val javaFXVersion = "21.0.1"
val osName = System.getProperty("os.name").lowercase()
val platform = when {
osName.contains("win") -> "win"
osName.contains("mac") -> "mac"
osName.contains("linux") -> "linux"
else -> "linux"
}
listOf("base", "controls", "graphics", "media").forEach { module ->
implementation("org.openjfx:javafx-$module:$javaFXVersion:$platform")
}
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform {
includeEngines("scalatest")
testLogging {
events("passed", "skipped", "failed")
}
}
finalizedBy(tasks.reportScoverage)
}
tasks.reportScoverage {
dependsOn(tasks.test)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

+30
View File
@@ -0,0 +1,30 @@
/* Arabian Chess GUI Styles */
.root {
-fx-font-family: "Comic Sans MS", "Comic Sans", cursive;
-fx-background-color: #F3C8A0;
}
.button {
-fx-background-radius: 8;
-fx-padding: 8 16 8 16;
-fx-font-family: "Comic Sans MS", cursive;
-fx-font-size: 12px;
-fx-cursor: hand;
}
.button:hover {
-fx-opacity: 0.8;
}
.label {
-fx-font-family: "Comic Sans MS", cursive;
}
.dialog-pane {
-fx-background-color: #F3C8A0;
}
.dialog-pane .content {
-fx-font-family: "Comic Sans MS", cursive;
}
@@ -0,0 +1,217 @@
package de.nowchess.gui
import scalafx.Includes.*
import scalafx.application.Platform
import scalafx.geometry.{Insets, Pos}
import scalafx.scene.control.{Button, ButtonType, ChoiceDialog, Label}
import scalafx.scene.layout.{BorderPane, GridPane, HBox, VBox, StackPane}
import scalafx.scene.paint.Color as FXColor
import scalafx.scene.shape.Rectangle
import scalafx.scene.text.{Font, Text}
import scalafx.stage.Stage
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square, File, Rank}
import de.nowchess.api.move.PromotionPiece
import de.nowchess.chess.engine.GameEngine
/** ScalaFX chess board view that displays the game state.
* Uses Arabian chess sprites and color palette.
* Handles user interactions (clicks) and sends moves to GameEngine.
*/
class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends BorderPane:
private val squareSize = 70.0
private val boardGrid = new GridPane()
private val messageLabel = new Label {
text = "Welcome to Arabian Chess!"
font = Font.font("Comic Sans MS", 16)
padding = Insets(10)
}
private var currentBoard: Board = engine.board
private var currentTurn: Color = engine.turn
private var selectedSquare: Option[Square] = None
private val squareViews = scala.collection.mutable.Map[(Int, Int), StackPane]()
// Initialize UI
initializeBoard()
top = new VBox {
padding = Insets(10)
spacing = 5
alignment = Pos.Center
children = Seq(
new Label {
text = "Arabian Chess"
font = Font.font("Comic Sans MS", 24)
style = "-fx-font-weight: bold;"
},
messageLabel
)
}
center = new VBox {
padding = Insets(20)
alignment = Pos.Center
style = s"-fx-background-color: ${PieceSprites.SquareColors.Border};"
children = boardGrid
}
bottom = new HBox {
padding = Insets(10)
spacing = 10
alignment = Pos.Center
children = Seq(
new Button("Undo") {
font = Font.font("Comic Sans MS", 12)
onAction = _ => if engine.canUndo then engine.undo()
style = "-fx-background-radius: 8; -fx-background-color: #B9DAD1;"
},
new Button("Redo") {
font = Font.font("Comic Sans MS", 12)
onAction = _ => if engine.canRedo then engine.redo()
style = "-fx-background-radius: 8; -fx-background-color: #B9C2DA;"
},
new Button("Reset") {
font = Font.font("Comic Sans MS", 12)
onAction = _ => engine.reset()
style = "-fx-background-radius: 8; -fx-background-color: #E1EAA9;"
}
)
}
private def initializeBoard(): Unit =
boardGrid.padding = Insets(5)
boardGrid.hgap = 0
boardGrid.vgap = 0
// Create 8x8 board with rank/file labels
for
rank <- 0 until 8
file <- 0 until 8
do
val square = createSquare(rank, file)
squareViews((rank, file)) = square
boardGrid.add(square, file, 7 - rank) // Flip rank for proper display
updateBoard(currentBoard, currentTurn)
private def createSquare(rank: Int, file: Int): StackPane =
val isWhite = (rank + file) % 2 == 0
val baseColor = if isWhite then PieceSprites.SquareColors.White else PieceSprites.SquareColors.Black
val bgRect = new Rectangle {
width = squareSize
height = squareSize
fill = FXColor.web(baseColor)
arcWidth = 8
arcHeight = 8
}
val square = new StackPane {
children = Seq(bgRect)
onMouseClicked = _ => handleSquareClick(rank, file)
style = "-fx-cursor: hand;"
}
square
private def handleSquareClick(rank: Int, file: Int): Unit =
if engine.isPendingPromotion then
return // Don't allow moves during promotion
val clickedSquare = Square(File.values(file), Rank.values(rank))
selectedSquare match
case None =>
// First click - select piece if it belongs to current player
currentBoard.pieceAt(clickedSquare).foreach { piece =>
if piece.color == currentTurn then
selectedSquare = Some(clickedSquare)
highlightSquare(rank, file, PieceSprites.SquareColors.Selected)
}
case Some(fromSquare) =>
// Second click - attempt move
if clickedSquare == fromSquare then
// Deselect
selectedSquare = None
updateBoard(currentBoard, currentTurn)
else
// Try to move
val moveStr = s"${fromSquare}$clickedSquare"
engine.processUserInput(moveStr)
selectedSquare = None
def updateBoard(board: Board, turn: Color): Unit =
currentBoard = board
currentTurn = turn
selectedSquare = None
// Update all squares
for
rank <- 0 until 8
file <- 0 until 8
do
squareViews.get((rank, file)).foreach { stackPane =>
val isWhite = (rank + file) % 2 == 0
val baseColor = if isWhite then PieceSprites.SquareColors.White else PieceSprites.SquareColors.Black
val bgRect = new Rectangle {
width = squareSize
height = squareSize
fill = FXColor.web(baseColor)
arcWidth = 8
arcHeight = 8
}
val square = Square(File.values(file), Rank.values(rank))
val pieceOption = board.pieceAt(square)
val children = pieceOption match
case Some(piece) =>
Seq(bgRect, PieceSprites.loadPieceImage(piece, squareSize * 0.8))
case None =>
Seq(bgRect)
stackPane.children = children
}
private def highlightSquare(rank: Int, file: Int, color: String): Unit =
squareViews.get((rank, file)).foreach { stackPane =>
val bgRect = new Rectangle {
width = squareSize
height = squareSize
fill = FXColor.web(color)
arcWidth = 8
arcHeight = 8
}
val square = Square(File.values(file), Rank.values(rank))
val pieceOption = currentBoard.pieceAt(square)
stackPane.children = pieceOption match
case Some(piece) =>
Seq(bgRect, PieceSprites.loadPieceImage(piece, squareSize * 0.8))
case None =>
Seq(bgRect)
}
def showMessage(msg: String): Unit =
messageLabel.text = msg
def showPromotionDialog(from: Square, to: Square): Unit =
val choices = Seq("Queen", "Rook", "Bishop", "Knight")
val dialog = new ChoiceDialog(defaultChoice = "Queen", choices = choices) {
initOwner(stage)
title = "Pawn Promotion"
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
@@ -0,0 +1,63 @@
package de.nowchess.gui
import javafx.application.{Application => JFXApplication, Platform => JFXPlatform}
import javafx.stage.Stage as JFXStage
import scalafx.application.Platform
import scalafx.scene.Scene
import scalafx.stage.Stage
import de.nowchess.chess.engine.GameEngine
/** ScalaFX GUI Application for Arabian Chess.
* This is launched from Main alongside the TUI.
* Both subscribe to the same GameEngine via Observer pattern.
*/
class ChessGUIApp extends JFXApplication:
override def start(primaryStage: JFXStage): Unit =
val engine = ChessGUILauncher.getEngine
val stage = new Stage(primaryStage)
stage.title = "Arabian Chess"
stage.width = 700
stage.height = 800
stage.resizable = false
val boardView = new ChessBoardView(stage, engine)
val guiObserver = new GUIObserver(boardView)
// Subscribe GUI observer to engine
engine.subscribe(guiObserver)
stage.scene = new Scene {
root = boardView
// Load CSS if available
try {
val cssUrl = getClass.getResource("/styles.css")
if cssUrl != null then
stylesheets.add(cssUrl.toExternalForm)
} catch {
case _: Exception => // CSS is optional
}
}
stage.onCloseRequest = _ => {
// Unsubscribe when window closes
engine.unsubscribe(guiObserver)
}
stage.show()
/** Launcher object that holds the engine reference and launches GUI in separate thread. */
object ChessGUILauncher:
@volatile private var engine: GameEngine = scala.compiletime.uninitialized
def getEngine: GameEngine = engine
def launch(eng: GameEngine): Unit =
engine = eng
val guiThread = new Thread(() => {
JFXApplication.launch(classOf[ChessGUIApp])
})
guiThread.setDaemon(false)
guiThread.setName("ScalaFX-GUI-Thread")
guiThread.start()
@@ -0,0 +1,54 @@
package de.nowchess.gui
import scalafx.application.Platform
import scalafx.scene.control.Alert
import scalafx.scene.control.Alert.AlertType
import de.nowchess.chess.observer.{Observer, GameEvent, *}
import de.nowchess.api.board.Board
/** GUI Observer that implements the Observer pattern.
* Receives game events from GameEngine and updates the ScalaFX UI.
* All UI updates must be done on the JavaFX Application Thread.
*/
class GUIObserver(private val boardView: ChessBoardView) extends Observer:
override def onGameEvent(event: GameEvent): Unit =
// Ensure UI updates happen on JavaFX thread
Platform.runLater {
event match
case e: MoveExecutedEvent =>
boardView.updateBoard(e.board, e.turn)
e.capturedPiece.foreach { piece =>
boardView.showMessage(s"Captured: $piece on ${e.toSquare}")
}
case e: CheckDetectedEvent =>
boardView.updateBoard(e.board, e.turn)
boardView.showMessage(s"${e.turn.label} is in check!")
case e: CheckmateEvent =>
boardView.updateBoard(e.board, e.turn)
showAlert(AlertType.Information, "Game Over", s"Checkmate! ${e.winner.label} wins.")
case e: StalemateEvent =>
boardView.updateBoard(e.board, e.turn)
showAlert(AlertType.Information, "Game Over", "Stalemate! The game is a draw.")
case e: InvalidMoveEvent =>
boardView.showMessage(s"⚠️ ${e.reason}")
case e: BoardResetEvent =>
boardView.updateBoard(e.board, e.turn)
boardView.showMessage("Board has been reset to initial position.")
case e: PromotionRequiredEvent =>
boardView.showPromotionDialog(e.from, e.to)
}
private def showAlert(alertType: AlertType, titleText: String, content: String): Unit =
new Alert(alertType) {
initOwner(boardView.stage)
title = titleText
headerText = None
contentText = content
}.showAndWait()
@@ -0,0 +1,38 @@
package de.nowchess.gui
import scalafx.scene.image.{Image, ImageView}
import de.nowchess.api.board.{Piece, PieceType, Color}
/** Utility object for loading Arabian chess piece sprites. */
object PieceSprites:
private val spriteCache = scala.collection.mutable.Map[String, 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
}
private def loadImage(key: String): Image =
val path = s"/sprites/pieces/$key.png"
val stream = getClass.getResourceAsStream(path)
if stream == null then
throw new RuntimeException(s"Could not load sprite: $path")
new Image(stream)
/** Get square colors for the board using Arabian theme. */
object SquareColors:
val White = "#F3C8A0" // Warm light beige
val Black = "#BA6D4B" // Warm terracotta
val Selected = "#C19EF5" // Purple highlight
val ValidMove = "#E1EAA9" // Light yellow-green
val Border = "#5A2C28" // Dark brown border
+1
View File
@@ -50,6 +50,7 @@ dependencies {
implementation(project(":modules:core"))
implementation(project(":modules:api"))
implementation(project(":modules:gui"))
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
@@ -2,14 +2,20 @@ package de.nowchess.ui
import de.nowchess.chess.engine.GameEngine
import de.nowchess.ui.terminal.TerminalUI
import de.nowchess.gui.ChessGUILauncher
/** Application entry point - starts the Terminal UI for the chess game. */
/** Application entry point - starts both GUI and Terminal UI for the chess game.
* Both views subscribe to the same GameEngine via Observer pattern.
*/
object Main:
def main(args: Array[String]): Unit =
// Create the core game engine (single source of truth)
val engine = new GameEngine()
// Create and start the terminal UI
// Launch ScalaFX GUI in separate thread
ChessGUILauncher.launch(engine)
// Create and start the terminal UI (blocks on main thread)
val tui = new TerminalUI(engine)
tui.start()
+1 -1
View File
@@ -1,2 +1,2 @@
rootProject.name = "NowChessSystems"
include("modules:core", "modules:api", "modules:ui")
include("modules:core", "modules:api", "modules:ui", "modules:gui")