From 23a5c763c5fe7442bf1ba28ac70dc3225229fb98 Mon Sep 17 00:00:00 2001 From: shahdlala66 Date: Wed, 1 Apr 2026 00:34:00 +0200 Subject: [PATCH] feat: Gui added (not done, not reviewed) --- modules/gui/build.gradle.kts | 76 ++++++ .../resources/sprites/board/board_bottom.png | Bin 0 -> 161 bytes .../sprites/board/board_square_black.png | Bin 0 -> 188 bytes .../sprites/board/board_square_white.png | Bin 0 -> 188 bytes .../resources/sprites/pieces/black_bishop.png | Bin 0 -> 286 bytes .../resources/sprites/pieces/black_king.png | Bin 0 -> 245 bytes .../resources/sprites/pieces/black_knight.png | Bin 0 -> 266 bytes .../resources/sprites/pieces/black_pawn.png | Bin 0 -> 297 bytes .../resources/sprites/pieces/black_queen.png | Bin 0 -> 258 bytes .../resources/sprites/pieces/black_rook.png | Bin 0 -> 263 bytes .../resources/sprites/pieces/white_bishop.png | Bin 0 -> 313 bytes .../resources/sprites/pieces/white_king.png | Bin 0 -> 251 bytes .../resources/sprites/pieces/white_knight.png | Bin 0 -> 275 bytes .../resources/sprites/pieces/white_pawn.png | Bin 0 -> 305 bytes .../resources/sprites/pieces/white_queen.png | Bin 0 -> 281 bytes .../resources/sprites/pieces/white_rook.png | Bin 0 -> 280 bytes modules/gui/src/main/resources/styles.css | 30 +++ .../de/nowchess/gui/ChessBoardView.scala | 217 ++++++++++++++++++ .../main/scala/de/nowchess/gui/ChessGUI.scala | 63 +++++ .../scala/de/nowchess/gui/GUIObserver.scala | 54 +++++ .../scala/de/nowchess/gui/PieceSprites.scala | 38 +++ modules/ui/build.gradle.kts | 1 + .../src/main/scala/de/nowchess/ui/Main.scala | 10 +- settings.gradle.kts | 2 +- 24 files changed, 488 insertions(+), 3 deletions(-) create mode 100644 modules/gui/build.gradle.kts create mode 100644 modules/gui/src/main/resources/sprites/board/board_bottom.png create mode 100644 modules/gui/src/main/resources/sprites/board/board_square_black.png create mode 100644 modules/gui/src/main/resources/sprites/board/board_square_white.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/black_bishop.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/black_king.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/black_knight.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/black_pawn.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/black_queen.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/black_rook.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/white_bishop.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/white_king.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/white_knight.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/white_pawn.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/white_queen.png create mode 100644 modules/gui/src/main/resources/sprites/pieces/white_rook.png create mode 100644 modules/gui/src/main/resources/styles.css create mode 100644 modules/gui/src/main/scala/de/nowchess/gui/ChessBoardView.scala create mode 100644 modules/gui/src/main/scala/de/nowchess/gui/ChessGUI.scala create mode 100644 modules/gui/src/main/scala/de/nowchess/gui/GUIObserver.scala create mode 100644 modules/gui/src/main/scala/de/nowchess/gui/PieceSprites.scala diff --git a/modules/gui/build.gradle.kts b/modules/gui/build.gradle.kts new file mode 100644 index 0000000..7a1f99f --- /dev/null +++ b/modules/gui/build.gradle.kts @@ -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 + +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) +} diff --git a/modules/gui/src/main/resources/sprites/board/board_bottom.png b/modules/gui/src/main/resources/sprites/board/board_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..884fb3c1ead98e3e4dd96e1e231773eb58e2bf35 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^4M5Dn!3HD^?|!cXQjEnx?oJHr&dIz4a-uz5977^n zlebI>|8w4e%S|UC@L{TahIvmDBa06Y5R}{fKe9R_r0&Nl99$S3j3^ HP6|0vU9ji;U&fe&i|5YRG-qxU z?*^8P?b{^|yiLg5(6#=7Wq88D%WP6SUwG3t91U#{omObQBP*d*$lzAq&ue$JBR&`$ lFfx2}xE!PC{xWt~$(698PsMX&$> literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/board/board_square_white.png b/modules/gui/src/main/resources/sprites/board/board_square_white.png new file mode 100644 index 0000000000000000000000000000000000000000..ea97b12eb920fa8a270630b87285ad03a72d7d8d GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|$~;{hLo9li zUfRujK!L~gqVyET87)TEsmpy_P718Nsjzax5%&{p;U>?2a$4Mb{*R$%#|1yXz;k|U z*&>*>+>YjXP`1G&;;3EB9PJGcGMRbVez6(fc%*fJ>+}xU7}E_-oD#X)YhJ$-)v1t3 lm^mj&HsS8Rs|)Vj<=>{MB~tY45fjh_44$rjF6*2UngFqUMY;e0 literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/black_bishop.png b/modules/gui/src/main/resources/sprites/pieces/black_bishop.png new file mode 100644 index 0000000000000000000000000000000000000000..fe2c26017b8be7f48e098b06b5a77318e0522cc6 GIT binary patch literal 286 zcmV+(0pb3MP)Px#*hxe|R5*>TQZWvLFc9-q2)cx>NOUX{kKwI(3{x13#9Fd6Ea^~Pbb)}7T5nuz zpPc~w2s%S5i`vWY*ZWCCUukwm+*UarstB{&_3pj*L`3NXawCfe>i{4aX=5?Fk+PJq zt@1EqflMF~3^?i$82|?BqVvri?1{Cpp_0VqjF{0i3jMT1vnYX#H&_?QZIz>I6tuDE z8WkBtuTU1XXM#31#OV)yEKVB8j`IE2-IEo##`@3W1pTx`+cyS# k;v{?9*jOsdn;C(Tnm{Fqth(r?Rt?698W%}{s5$oj^gx{p7P z9KNCzBo$mRA$vZX?T(3OG%Zrq`d+s&=yk1TXK0Ao{V2nX*~29I{v{qpp~A1P+&XVF zymsqcc=7D}a4x<_uNIor%znAkq$TF}2h}sYJ7#Xv;62MbZ_<@H0gQu&X%Q~loCIE`=V`~5a literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/black_knight.png b/modules/gui/src/main/resources/sprites/pieces/black_knight.png new file mode 100644 index 0000000000000000000000000000000000000000..579db13e0353b4e49553403df9017dbab0df668a GIT binary patch literal 266 zcmV+l0rmcgP)Px##7RU!R5*>TlCchhFbqWPD#BezS0p-SNMj_}nq6k21DSph43zA|lEm>bGkRa}ws9fHf^6KJL%W`m$x-0gfO?W)pyj z;Hecuk07*qoM6N<$g7!&mG5`Po literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/black_pawn.png b/modules/gui/src/main/resources/sprites/pieces/black_pawn.png new file mode 100644 index 0000000000000000000000000000000000000000..92597c9da490b2fc1f256fbff39a04001ac2dda7 GIT binary patch literal 297 zcmV+^0oMMBP)Px#<4Ht8R5*>bQo9brAP{q^3c8T4Na;u>J|^GF$7CXFB<3=aIu-< z_V8|qz%Px#yh%hsR5*>bQZW*PAQ0TkWYk7&ozXI>d`#ZzW303!Gq!{VZLr8_KrcX%TnFs# z?Xd^^vx>AfMOqslhJ56;?s?1p$Da0_#9( zLEWH&WPpj0i1uBuXD_qHaLdFll5Y==A{0aRi3DTxMfj6i0l>IhpYUv!eEujP)Px#!AV3xR5*>Lk}(rQKoEuBnT*pMtr<$G?#JY}=Eta#GG?@)3C*hZtaBvcYq0wk z@7)3?Q2;}sJp5h2wD0l>uaAi zz!!bHuiJ69z&!h5ajlGY1AdQD0GywH>4s*GGt~vzJpO60Zbx)OqX3ZsQyGo>qYn<= zu4ZLnk%0_r3ttlqagW_UIKp8Iy#A4V0QPN!dTtN+2zkS{kUBofQ%vc5S8K{&05t#r N002ovPDHLkV1fx-Y&ie` literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/white_bishop.png b/modules/gui/src/main/resources/sprites/pieces/white_bishop.png new file mode 100644 index 0000000000000000000000000000000000000000..ab456eda281df9bb152757e9775ab452154caa3c GIT binary patch literal 313 zcmV-90mlA`P)Px#^GQTOR5*>TQn3w#Fc7?tB9V%uL?Q!FrA)^P?15N;k`?HX*nt)3Tv9PWK$Ij! zlvJN0vSkcLNXb|B`S1R0AMj(*mL}KnS>NAZHzN8{_glzq9^rJkD(rYTGcywrEj!RB zO7hqU08m0+nBwv7Xum{k^GIQmpgYh@2}rAJ%>XbqVsgGS8*F4ROyPy;44(+BO$LCW z&JZM(T|hpRT+69rBQVq%yfDR3XZ8uM=4;`%Sma-h4g;g5Hx zzJHPfGjT(mVQj?g-c|uAVkS>I2$E_-UUaZTUK5$+z$Dn-!=HGHmTfYu00000 LNkvXXu0mjfkp+Vl literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/white_king.png b/modules/gui/src/main/resources/sprites/pieces/white_king.png new file mode 100644 index 0000000000000000000000000000000000000000..435d27a75c22e00e32bb95e8d11ecd2e48c4594d GIT binary patch literal 251 zcmVPx#wMj%lR5*>TQn3-kAPoGZap!aI1*lwl3>IM?7GVrZDi%o3xpU=I$_Yv4a+jz= z@c()qfFJ8|cQ8-u#rxxYnb@aA?OJQ-Oq^1K-CKs*b?;Oxt-kpcZ z9jM>MuOVGY`=$enJQ7hEj8eDGlmHK_m8|-|*Bi>pU&5)mz}El(002ovPDHLkV1lAi BW>)|J literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/white_knight.png b/modules/gui/src/main/resources/sprites/pieces/white_knight.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf6ed6d62458cd107bd0514900a2be1955cfffe GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|&U(5yhFJ8j z4caK!Y`|0VbceF&LUrbVKqIzpgMRTu9xp9^hbj+Ot>u%>Ph$1`G*#vI|9kq(_D$>f zR&U~r*>A7^`Q1u~P_u)(6~Dc;n5nc~yXfv$hK60QAKq1vG33e(b?#z#Ytfl5dS^-5 z%}wP7Y0hgJRhV_+-N+} zc+&|61}6@MMLR@gXME+~)F_&E_v^)0v7I~R)k X%uU-TPP@(w^caJutDnm{r-UW|_*QD3 literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/white_pawn.png b/modules/gui/src/main/resources/sprites/pieces/white_pawn.png new file mode 100644 index 0000000000000000000000000000000000000000..47cb262576cd65c89f55c7073c107fb5798ce03e GIT binary patch literal 305 zcmV-10nYx3P)Px#>q$gGR5*>bQo9XT&8IlC7tQ=_RwR4l|EmGiZ?2>9i z863~m!*~x?R(9!i&N1qC@JOy<1D8#f02sViGy|aw_IUyT?DOP(rz#@Y#twBmcz|zU zAQ6Eoj3ShY?0XJqO{7Lq_IZL(21~E8jh$O-GQu`?;IaX&k;}$8;j+O#Pmwc6a6H$7 zRE07KZ-Kz!Jfb@wCt;m4c#9`5p;hu00000NkvXXu0mjf DiXM2r literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/sprites/pieces/white_queen.png b/modules/gui/src/main/resources/sprites/pieces/white_queen.png new file mode 100644 index 0000000000000000000000000000000000000000..cb53ef1514e732a66fde35c3ee7efed6e69b4f66 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|E_=E-hFJ6_ zCoE8XkUah0`Qv(j<>yPe)y&HG`I?*n1Q$-+JM!V>^WZgEhAf;ne|$VH?B?dyVkVZ; zl?PNIaKYGF@OQyWk!H7VYCKzgPR#lLUvb)DllmPcTzc(!3SrC_a*r(S)L@O0fG}78<_t6|IdHC=FO%5`{&O3 z|Nr*NpZ|Z}So;6)k&BFOCPqd^hb}X1y8jcXJK5VOLRe^DNkblovf-?cl1>IvA6*qg a85n+^$Ou>4+&CBLJqAx#KbLh*2~7Z-dUnXF+>3~)JQymp$fZc$5LYixWp@KkPm@QQd1 z-$s?wj0{URuK#z5o%{CLx)7Z`a^(yKui7t*0u?;ay3iVA(|gwH0Mo5~+a8NZ|5td+ zd|x+XlFspW6=h;}`O{~%UgTMJxXAEqA>YOJ?ait>+Z68GUl2c#e^^xduginYO7d3z cVLQ9b?Sc!r-O}2h0)5Be>FVdQ&MBb@0L4FTtpET3 literal 0 HcmV?d00001 diff --git a/modules/gui/src/main/resources/styles.css b/modules/gui/src/main/resources/styles.css new file mode 100644 index 0000000..aae36d1 --- /dev/null +++ b/modules/gui/src/main/resources/styles.css @@ -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; +} diff --git a/modules/gui/src/main/scala/de/nowchess/gui/ChessBoardView.scala b/modules/gui/src/main/scala/de/nowchess/gui/ChessBoardView.scala new file mode 100644 index 0000000..40d5a7c --- /dev/null +++ b/modules/gui/src/main/scala/de/nowchess/gui/ChessBoardView.scala @@ -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 diff --git a/modules/gui/src/main/scala/de/nowchess/gui/ChessGUI.scala b/modules/gui/src/main/scala/de/nowchess/gui/ChessGUI.scala new file mode 100644 index 0000000..20a705a --- /dev/null +++ b/modules/gui/src/main/scala/de/nowchess/gui/ChessGUI.scala @@ -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() diff --git a/modules/gui/src/main/scala/de/nowchess/gui/GUIObserver.scala b/modules/gui/src/main/scala/de/nowchess/gui/GUIObserver.scala new file mode 100644 index 0000000..e96450c --- /dev/null +++ b/modules/gui/src/main/scala/de/nowchess/gui/GUIObserver.scala @@ -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() diff --git a/modules/gui/src/main/scala/de/nowchess/gui/PieceSprites.scala b/modules/gui/src/main/scala/de/nowchess/gui/PieceSprites.scala new file mode 100644 index 0000000..526cc68 --- /dev/null +++ b/modules/gui/src/main/scala/de/nowchess/gui/PieceSprites.scala @@ -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 diff --git a/modules/ui/build.gradle.kts b/modules/ui/build.gradle.kts index f70d8c1..4f64d41 100644 --- a/modules/ui/build.gradle.kts +++ b/modules/ui/build.gradle.kts @@ -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") diff --git a/modules/ui/src/main/scala/de/nowchess/ui/Main.scala b/modules/ui/src/main/scala/de/nowchess/ui/Main.scala index c8f5562..731084c 100644 --- a/modules/ui/src/main/scala/de/nowchess/ui/Main.scala +++ b/modules/ui/src/main/scala/de/nowchess/ui/Main.scala @@ -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() diff --git a/settings.gradle.kts b/settings.gradle.kts index f164a80..75b3358 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ rootProject.name = "NowChessSystems" -include("modules:core", "modules:api", "modules:ui") \ No newline at end of file +include("modules:core", "modules:api", "modules:ui", "modules:gui") \ No newline at end of file