Compare commits

..

1 Commits

Author SHA1 Message Date
Janis 3ff80318b4 feat: NCS-17 Implement basic ScalaFX UI (#14)
Build & Test (NowChessSystems) TeamCity build finished
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #14
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2026-04-01 22:48:30 +02:00
5 changed files with 28 additions and 69 deletions
+5 -1
View File
@@ -1,5 +1,6 @@
plugins { plugins {
id("org.sonarqube") version "7.2.3.7755" id("org.sonarqube") version "7.2.3.7755"
id("org.scoverage") version "8.1" apply false
} }
group = "de.nowchess" group = "de.nowchess"
@@ -28,7 +29,10 @@ val versions = mapOf(
"SCALA_LIBRARY" to "2.13.18", "SCALA_LIBRARY" to "2.13.18",
"SCALATEST" to "3.2.19", "SCALATEST" to "3.2.19",
"SCALATEST_JUNIT" to "0.1.11", "SCALATEST_JUNIT" to "0.1.11",
"SCOVERAGE" to "2.1.1" "SCOVERAGE" to "2.1.1",
"SCALAFX" to "21.0.0-R32",
"JAVAFX" to "21.0.1",
"JUNIT_BOM" to "5.13.4"
) )
extra["VERSIONS"] = versions extra["VERSIONS"] = versions
@@ -1,7 +1,7 @@
package de.nowchess.chess.engine package de.nowchess.chess.engine
import scala.collection.mutable import scala.collection.mutable
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square} import de.nowchess.api.board.{Board, Color}
import de.nowchess.chess.logic.GameHistory import de.nowchess.chess.logic.GameHistory
import de.nowchess.chess.observer.* import de.nowchess.chess.observer.*
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
@@ -86,28 +86,6 @@ class GameEngineLoadPgnTest extends AnyFunSuite with Matchers:
// state is reset to initial (reset happens before replay, which fails) // state is reset to initial (reset happens before replay, which fails)
engine.history.moves shouldBe empty engine.history.moves shouldBe empty
test("loadPgn: promotion in PGN is replayed correctly"):
val engine = new GameEngine()
val pgn =
"""[Event "T"]
1. b4 h6 2. b5 h5 3. b6 h4 4. bxa7 h3 5. a8=Q
"""
engine.loadPgn(pgn) shouldBe Right(())
engine.history.moves.length shouldBe 9
test("loadPgn: en passant capture in PGN is replayed correctly"):
val engine = new GameEngine()
val pgn =
"""[Event "T"]
1. e4 e6 2. e5 d5 3. exd6
"""
engine.loadPgn(pgn) shouldBe Right(())
engine.history.moves.length shouldBe 5
engine.board.pieceAt(Square(File.D, Rank.R6)) shouldBe Some(Piece(Color.White, PieceType.Pawn))
engine.board.pieceAt(Square(File.D, Rank.R5)) shouldBe None
// ── undo/redo notation events ───────────────────────────────────────────── // ── undo/redo notation events ─────────────────────────────────────────────
test("undo emits MoveUndoneEvent with pgnNotation"): test("undo emits MoveUndoneEvent with pgnNotation"):
@@ -162,16 +140,18 @@ class GameEngineLoadPgnTest extends AnyFunSuite with Matchers:
val engine = new GameEngine() val engine = new GameEngine()
val cap = new EventCapture() val cap = new EventCapture()
engine.subscribe(cap) engine.subscribe(cap)
// e4, then Black plays d5, White pawn on e4 captures on d5 // White builds a capture on the a-file: b4, ... a6, b5, ... h6, bxa6
engine.processUserInput("e2e4") engine.processUserInput("b2b4")
engine.processUserInput("d7d5") engine.processUserInput("a7a6")
engine.processUserInput("e4d5") // white pawn captures black pawn engine.processUserInput("b4b5")
engine.processUserInput("h7h6")
engine.processUserInput("b5a6") // white pawn captures black pawn
engine.undo() engine.undo()
cap.events.clear() cap.events.clear()
engine.redo() engine.redo()
val evt = cap.events.last.asInstanceOf[MoveRedoneEvent] val evt = cap.events.last.asInstanceOf[MoveRedoneEvent]
evt.fromSquare shouldBe "e4" evt.fromSquare shouldBe "b5"
evt.toSquare shouldBe "d5" evt.toSquare shouldBe "a6"
evt.capturedPiece.isDefined shouldBe true evt.capturedPiece.isDefined shouldBe true
test("loadPgn: clears previous game state before loading"): test("loadPgn: clears previous game state before loading"):
@@ -96,32 +96,6 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
game.moves.last.castleSide shouldBe Some(CastleSide.Queenside) game.moves.last.castleSide shouldBe Some(CastleSide.Queenside)
case Left(err) => fail(s"Expected Right but got Left($err)") case Left(err) => fail(s"Expected Right but got Left($err)")
test("validatePgn: valid promotion is accepted"):
val pgn =
"""[Event "Test"]
1. b4 h6 2. b5 h5 3. b6 h4 4. bxa7 h3 5. a8=Q
"""
PgnParser.validatePgn(pgn) match
case Right(game) =>
game.moves.takeWhile(m => m.promotionPiece.isEmpty).length shouldBe 8
game.moves.last.promotionPiece shouldBe Some(PromotionPiece.Queen)
case Left(err) => fail(s"Expected Right but got Left($err)")
test("validatePgn: en passant capture is parsed correctly"):
val pgn =
"""[Event "Test"]
1. e4 e6 2. e5 d5 3. exd6
"""
PgnParser.validatePgn(pgn) match
case Right(game) =>
game.moves.length shouldBe 5
val epMove = game.moves.last
epMove.isCapture shouldBe true
epMove.pieceType shouldBe PieceType.Pawn
case Left(err) => fail(s"Expected Right but got Left($err)")
test("validatePgn: disambiguation with two rooks is accepted"): test("validatePgn: disambiguation with two rooks is accepted"):
val pieces: Map[Square, Piece] = Map( val pieces: Map[Square, Piece] = Map(
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook), Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
+4 -4
View File
@@ -1,6 +1,6 @@
plugins { plugins {
id("scala") id("scala")
id("org.scoverage") version "8.1" id("org.scoverage")
application application
} }
@@ -55,10 +55,10 @@ dependencies {
implementation(project(":modules:api")) implementation(project(":modules:api"))
// ScalaFX dependencies // ScalaFX dependencies
implementation("org.scalafx:scalafx_3:21.0.0-R32") implementation("org.scalafx:scalafx_3:${versions["SCALAFX"]!!}")
// JavaFX dependencies for the current platform // JavaFX dependencies for the current platform
val javaFXVersion = "21.0.1" val javaFXVersion = versions["JAVAFX"]!!
val osName = System.getProperty("os.name").lowercase() val osName = System.getProperty("os.name").lowercase()
val platform = when { val platform = when {
osName.contains("win") -> "win" osName.contains("win") -> "win"
@@ -71,7 +71,7 @@ dependencies {
implementation("org.openjfx:javafx-$module:$javaFXVersion:$platform") implementation("org.openjfx:javafx-$module:$javaFXVersion:$platform")
} }
testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}") testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}") testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
@@ -23,10 +23,11 @@ import de.nowchess.chess.notation.{FenExporter, FenParser, PgnExporter, PgnParse
class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends BorderPane: class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends BorderPane:
private val squareSize = 70.0 private val squareSize = 70.0
private val comicSansFontFamily = "Comic Sans MS"
private val boardGrid = new GridPane() private val boardGrid = new GridPane()
private val messageLabel = new Label { private val messageLabel = new Label {
text = "Welcome!" text = "Welcome!"
font = Font.font("Comic Sans MS", 16) font = Font.font(comicSansFontFamily, 16)
padding = Insets(10) padding = Insets(10)
} }
@@ -45,7 +46,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
children = Seq( children = Seq(
new Label { new Label {
text = "Chess" text = "Chess"
font = Font.font("Comic Sans MS", 24) font = Font.font(comicSansFontFamily, 24)
style = "-fx-font-weight: bold;" style = "-fx-font-weight: bold;"
}, },
messageLabel messageLabel
@@ -69,17 +70,17 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
alignment = Pos.Center alignment = Pos.Center
children = Seq( children = Seq(
new Button("Undo") { new Button("Undo") {
font = Font.font("Comic Sans MS", 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => if engine.canUndo then engine.undo() onAction = _ => if engine.canUndo then engine.undo()
style = "-fx-background-radius: 8; -fx-background-color: #B9DAD1;" style = "-fx-background-radius: 8; -fx-background-color: #B9DAD1;"
}, },
new Button("Redo") { new Button("Redo") {
font = Font.font("Comic Sans MS", 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => if engine.canRedo then engine.redo() onAction = _ => if engine.canRedo then engine.redo()
style = "-fx-background-radius: 8; -fx-background-color: #B9C2DA;" style = "-fx-background-radius: 8; -fx-background-color: #B9C2DA;"
}, },
new Button("Reset") { new Button("Reset") {
font = Font.font("Comic Sans MS", 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => engine.reset() onAction = _ => engine.reset()
style = "-fx-background-radius: 8; -fx-background-color: #E1EAA9;" style = "-fx-background-radius: 8; -fx-background-color: #E1EAA9;"
} }
@@ -90,22 +91,22 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
alignment = Pos.Center alignment = Pos.Center
children = Seq( children = Seq(
new Button("FEN Export") { new Button("FEN Export") {
font = Font.font("Comic Sans MS", 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => doFenExport() onAction = _ => doFenExport()
style = "-fx-background-radius: 8; -fx-background-color: #DAC4B9;" style = "-fx-background-radius: 8; -fx-background-color: #DAC4B9;"
}, },
new Button("FEN Import") { new Button("FEN Import") {
font = Font.font("Comic Sans MS", 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => doFenImport() onAction = _ => doFenImport()
style = "-fx-background-radius: 8; -fx-background-color: #DAD4B9;" style = "-fx-background-radius: 8; -fx-background-color: #DAD4B9;"
}, },
new Button("PGN Export") { new Button("PGN Export") {
font = Font.font("Comic Sans MS", 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => doPgnExport() onAction = _ => doPgnExport()
style = "-fx-background-radius: 8; -fx-background-color: #C4DAB9;" style = "-fx-background-radius: 8; -fx-background-color: #C4DAB9;"
}, },
new Button("PGN Import") { new Button("PGN Import") {
font = Font.font("Comic Sans MS", 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => doPgnImport() onAction = _ => doPgnImport()
style = "-fx-background-radius: 8; -fx-background-color: #B9DAC4;" style = "-fx-background-radius: 8; -fx-background-color: #B9DAC4;"
} }