Compare commits

...

2 Commits

Author SHA1 Message Date
Janis 767d3051a7 feat: NCS-13 Implement Threefold Repetition (#31)
Build & Test (NowChessSystems) TeamCity build finished
Reviewed-on: #31
2026-04-16 18:49:20 +02:00
TeamCity b2e62dc60c ci: bump version with Build-40 2026-04-14 19:23:01 +00:00
24 changed files with 272 additions and 9 deletions
Regular → Executable
View File
+7
View File
@@ -27,3 +27,10 @@
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
## (2026-04-14)
### Features
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
@@ -5,4 +5,5 @@ enum DrawReason:
case Stalemate
case InsufficientMaterial
case FiftyMoveRule
case ThreefoldRepetition
case Agreement
@@ -13,6 +13,7 @@ case class GameContext(
halfMoveClock: Int,
moves: List[Move],
result: Option[GameResult] = None,
initialBoard: Board = Board.initial,
):
/** Create new context with updated board. */
def withBoard(newBoard: Board): GameContext = copy(board = newBoard)
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=4
MINOR=5
PATCH=0
+25
View File
@@ -235,3 +235,28 @@
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
## (2026-04-14)
### Features
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
### Bug Fixes
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
@@ -17,8 +17,13 @@ class GameEngine(
val initialContext: GameContext = GameContext.initial,
val ruleSet: RuleSet = DefaultRules,
) extends Observable:
// Ensure that initialBoard is set correctly for threefold repetition detection
private val contextWithInitialBoard = if initialContext.moves.isEmpty && initialContext.board != initialContext.initialBoard then
initialContext.copy(initialBoard = initialContext.board)
else
initialContext
@SuppressWarnings(Array("DisableSyntax.var"))
private var currentContext: GameContext = initialContext
private var currentContext: GameContext = contextWithInitialBoard
private val invoker = new CommandInvoker()
/** Pending promotion: the Move that triggered it (from/to only, moveType filled in later). */
@@ -63,11 +68,15 @@ class GameEngine(
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.FiftyMoveRule)))
invoker.clear()
notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule))
else if ruleSet.isThreefoldRepetition(currentContext) then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.ThreefoldRepetition)))
invoker.clear()
notifyObservers(DrawEvent(currentContext, DrawReason.ThreefoldRepetition))
else
notifyObservers(
InvalidMoveEvent(
currentContext,
"Draw cannot be claimed: the 50-move rule has not been triggered.",
"Draw cannot be claimed: neither the 50-move rule nor threefold repetition has been triggered.",
),
)
@@ -154,7 +163,7 @@ class GameEngine(
invoker.clear()
if ctx.moves.isEmpty then
currentContext = ctx
currentContext = ctx.copy(initialBoard = ctx.board)
Right(())
else replayMoves(ctx.moves, savedContext)
@@ -182,7 +191,11 @@ class GameEngine(
/** Load an arbitrary board position, clearing all history and undo/redo state. */
def loadPosition(newContext: GameContext): Unit = synchronized {
currentContext = newContext
val contextWithInitialBoard = if newContext.moves.isEmpty then
newContext.copy(initialBoard = newContext.board)
else
newContext
currentContext = contextWithInitialBoard
pendingPromotion = None
invoker.clear()
notifyObservers(BoardResetEvent(currentContext))
@@ -237,6 +250,7 @@ class GameEngine(
else if ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext))
if currentContext.halfMoveClock >= 100 then notifyObservers(FiftyMoveRuleAvailableEvent(currentContext))
if ruleSet.isThreefoldRepetition(currentContext) then notifyObservers(ThreefoldRepetitionAvailableEvent(currentContext))
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
move.moveType match
@@ -56,6 +56,11 @@ case class FiftyMoveRuleAvailableEvent(
context: GameContext,
) extends GameEvent
/** Fired after any move where the same position occurs for the third time — threefold repetition is now claimable. */
case class ThreefoldRepetitionAvailableEvent(
context: GameContext,
) extends GameEvent
/** Fired when a move is undone, carrying PGN notation of the reversed move. */
case class MoveUndoneEvent(
context: GameContext,
@@ -98,6 +98,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
def isStalemate(context: GameContext): Boolean = false
def isInsufficientMaterial(context: GameContext): Boolean = false
def isFiftyMoveRule(context: GameContext): Boolean = false
def isThreefoldRepetition(context: GameContext): Boolean = false
def applyMove(context: GameContext)(move: Move): GameContext = DefaultRules.applyMove(context)(move)
val engine = new GameEngine(ruleSet = permissiveRules)
@@ -119,6 +120,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
def isStalemate(context: GameContext): Boolean = false
def isInsufficientMaterial(context: GameContext): Boolean = false
def isFiftyMoveRule(context: GameContext): Boolean = false
def isThreefoldRepetition(context: GameContext): Boolean = false
def applyMove(context: GameContext)(move: Move): GameContext = context
val engine = new GameEngine(ruleSet = noLegalMoves)
@@ -221,3 +221,75 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
evt.isDefined shouldBe true
evt.get.reason shouldBe DrawReason.InsufficientMaterial
engine.context.result shouldBe Some(GameResult.Draw(DrawReason.InsufficientMaterial))
// ── Threefold Repetition ──────────────────────────────────────────
test("draw command rejected when neither 50-move rule nor threefold repetition available"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
engine.processUserInput("e2e4")
observer.clear()
engine.processUserInput("draw")
observer.hasEvent[InvalidMoveEvent] shouldBe true
test("threefold repetition fires ThreefoldRepetitionAvailableEvent after 8-move shuffle"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// Both knights shuffle home: initial position occurs 3 times on move 8 (Ng8)
engine.processUserInput("g1f3")
engine.processUserInput("g8f6")
engine.processUserInput("f3g1")
engine.processUserInput("f6g8")
engine.processUserInput("g1f3")
engine.processUserInput("g8f6")
engine.processUserInput("f3g1")
observer.clear()
engine.processUserInput("f6g8") // 3rd occurrence of initial position
observer.hasEvent[ThreefoldRepetitionAvailableEvent] shouldBe true
engine.context.result shouldBe None // claimable, not automatic
test("draw claim via threefold repetition ends game with DrawEvent"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
engine.processUserInput("g1f3")
engine.processUserInput("g8f6")
engine.processUserInput("f3g1")
engine.processUserInput("f6g8")
engine.processUserInput("g1f3")
engine.processUserInput("g8f6")
engine.processUserInput("f3g1")
engine.processUserInput("f6g8") // threefold now available
observer.clear()
engine.processUserInput("draw")
val evt = observer.getEvent[DrawEvent]
evt.isDefined shouldBe true
evt.get.reason shouldBe DrawReason.ThreefoldRepetition
engine.context.result shouldBe Some(GameResult.Draw(DrawReason.ThreefoldRepetition))
test("loadPosition with non-empty moves preserves context as-is"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// Build a context that already has a move in its history
val move = de.nowchess.api.move.Move(
de.nowchess.api.board.Square(de.nowchess.api.board.File.E, de.nowchess.api.board.Rank.R2),
de.nowchess.api.board.Square(de.nowchess.api.board.File.E, de.nowchess.api.board.Rank.R4),
)
val ctxWithMove = de.nowchess.api.game.GameContext.initial.withMove(move)
engine.loadPosition(ctxWithMove)
observer.hasEvent[BoardResetEvent] shouldBe true
@@ -173,6 +173,8 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
DefaultRules.isInsufficientMaterial(context)
def isFiftyMoveRule(context: GameContext): Boolean =
DefaultRules.isFiftyMoveRule(context)
def isThreefoldRepetition(context: GameContext): Boolean =
DefaultRules.isThreefoldRepetition(context)
def applyMove(context: GameContext)(move: Move): GameContext =
DefaultRules.applyMove(context)(move)
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=12
MINOR=13
PATCH=0
+9
View File
@@ -38,3 +38,12 @@
* NCS-29 JSON - Cherry Picked ([#28](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/28)) ([dbcafd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dbcafd286993e0604a6fa286c5543581a149439e))
* NCS-30 FEN Parser using ParserCombinators ([#21](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/21)) ([b4bc72f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4bc72f7e49f94d6e1bc805c68680e5fe8ef8e36))
* NCS-31 FastParse FEN ([#22](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/22)) ([7a045d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7a045d31d757bbc5aa6f4bad2664ebe8b8519cac))
## (2026-04-14)
### Features
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-29 JSON - Cherry Picked ([#28](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/28)) ([dbcafd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dbcafd286993e0604a6fa286c5543581a149439e))
* NCS-30 FEN Parser using ParserCombinators ([#21](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/21)) ([b4bc72f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4bc72f7e49f94d6e1bc805c68680e5fe8ef8e36))
* NCS-31 FastParse FEN ([#22](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/22)) ([7a045d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7a045d31d757bbc5aa6f4bad2664ebe8b8519cac))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=6
MINOR=7
PATCH=0
+10
View File
@@ -34,3 +34,13 @@
### Bug Fixes
* NCS-32 Queenside Castle doesn't care about pieces in the way ([#23](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/23)) ([fe8e3c0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fe8e3c05397f433bfa34d1999e9738c82790adf7))
## (2026-04-14)
### Features
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
### Bug Fixes
* NCS-32 Queenside Castle doesn't care about pieces in the way ([#23](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/23)) ([fe8e3c0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fe8e3c05397f433bfa34d1999e9738c82790adf7))
@@ -32,6 +32,9 @@ trait RuleSet:
/** True if halfMoveClock >= 100 (50-move rule). */
def isFiftyMoveRule(context: GameContext): Boolean
/** True if the same position has occurred 3 times (including current position). */
def isThreefoldRepetition(context: GameContext): Boolean
/** Apply a legal move to produce the next game context. Handles all special move types: castling, en passant,
* promotion. Updates castling rights, en passant square, half-move clock, turn, and move history.
*/
@@ -11,6 +11,14 @@ import scala.annotation.tailrec
*/
object DefaultRules extends RuleSet:
/** Represents a position for threefold repetition (board state + turn + castling + en passant). */
private case class Position(
board: Board,
turn: Color,
castlingRights: CastlingRights,
enPassantSquare: Option[Square],
)
// ── Direction vectors ──────────────────────────────────────────────
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
private val BishopDirs: List[(Int, Int)] = List((1, 1), (1, -1), (-1, 1), (-1, -1))
@@ -62,6 +70,46 @@ object DefaultRules extends RuleSet:
override def isFiftyMoveRule(context: GameContext): Boolean =
context.halfMoveClock >= 100
override def isThreefoldRepetition(context: GameContext): Boolean =
val currentPosition = Position(
board = context.board,
turn = context.turn,
castlingRights = context.castlingRights,
enPassantSquare = context.enPassantSquare,
)
countPositionOccurrences(context, currentPosition) >= 3
private def countPositionOccurrences(context: GameContext, targetPosition: Position): Int =
try
var count = 0
var tempCtx = GameContext(
board = context.initialBoard,
turn = Color.White,
castlingRights = CastlingRights.Initial,
enPassantSquare = None,
halfMoveClock = 0,
moves = List.empty,
initialBoard = context.initialBoard,
)
var tempPos = Position(tempCtx.board, tempCtx.turn, tempCtx.castlingRights, tempCtx.enPassantSquare)
if tempPos == targetPosition then count += 1
for move <- context.moves do
tempCtx = applyMove(tempCtx)(move)
tempPos = Position(
board = tempCtx.board,
turn = tempCtx.turn,
castlingRights = tempCtx.castlingRights,
enPassantSquare = tempCtx.enPassantSquare,
)
if tempPos == targetPosition then count += 1
count
catch
case _: Exception =>
// If replay fails, conservatively count only the current position (never triggers a draw)
1
// ── Sliding pieces (Bishop, Rook, Queen) ───────────────────────────
private def slidingMoves(
@@ -175,3 +175,48 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// White is in check; only moves that block or move the king are legal
moves.nonEmpty shouldBe true
// ── Threefold Repetition ─────────────────────────────────────────
test("threefold repetition returns false for initial position with no moves"):
val context = GameContext.initial
rules.isThreefoldRepetition(context) shouldBe false
test("threefold repetition returns false after single move"):
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val move1 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
val ctx1 = rules.applyMove(context)(move1)
rules.isThreefoldRepetition(ctx1) shouldBe false
test("threefold repetition detects repeated position after back-and-forth moves"):
// Both knights shuffle back and forth: initial position (White to move) occurs 3 times
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val nf3 = Move(Square(File.G, Rank.R1), Square(File.F, Rank.R3))
val nf6 = Move(Square(File.G, Rank.R8), Square(File.F, Rank.R6))
val ng1 = Move(Square(File.F, Rank.R3), Square(File.G, Rank.R1))
val ng8 = Move(Square(File.F, Rank.R6), Square(File.G, Rank.R8))
// After 8 moves the starting position (White to move, both knights home) has occurred 3 times
val ctx = List(nf3, nf6, ng1, ng8, nf3, nf6, ng1, ng8)
.foldLeft(context)(rules.applyMove(_)(_))
rules.isThreefoldRepetition(ctx) shouldBe true
test("threefold repetition catch block returns false for inconsistent context"):
// A context whose moves cannot be replayed from initialBoard (forces the catch path)
val m = Move(Square(File.E, Rank.R5), Square(File.E, Rank.R6)) // e5→e6, no pawn there in initial board
val brokenCtx = GameContext(
board = Board.initial,
turn = Color.White,
castlingRights = CastlingRights.Initial,
enPassantSquare = None,
halfMoveClock = 0,
moves = List(m),
initialBoard = Board.initial,
)
// Replay will fail → catch returns 1 → 1 >= 3 is false
rules.isThreefoldRepetition(brokenCtx) shouldBe false
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=1
MINOR=2
PATCH=0
+11
View File
@@ -68,3 +68,14 @@
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-29 JSON - Cherry Picked ([#28](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/28)) ([dbcafd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dbcafd286993e0604a6fa286c5543581a149439e))
## (2026-04-14)
### Features
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-29 JSON - Cherry Picked ([#28](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/28)) ([dbcafd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dbcafd286993e0604a6fa286c5543581a149439e))
@@ -36,6 +36,7 @@ class GUIObserver(private val boardView: ChessBoardView) extends Observer:
case DrawReason.Stalemate => "Stalemate! The game is a draw."
case DrawReason.InsufficientMaterial => "Draw by insufficient material."
case DrawReason.FiftyMoveRule => "Draw claimed under the 50-move rule."
case DrawReason.ThreefoldRepetition => "Draw by threefold repetition."
case DrawReason.Agreement => "Draw by agreement."
showAlert(AlertType.Information, "Game Over", msg)
@@ -52,6 +53,9 @@ class GUIObserver(private val boardView: ChessBoardView) extends Observer:
case e: FiftyMoveRuleAvailableEvent =>
boardView.showMessage("50-move rule is now available — type 'draw' to claim.")
case e: ThreefoldRepetitionAvailableEvent =>
boardView.showMessage("Threefold repetition is now available — type 'draw' to claim.")
case e: MoveUndoneEvent =>
boardView.updateBoard(e.context.board, e.context.turn)
boardView.showMessage(s"↶ Undo: ${e.pgnNotation}")
@@ -50,6 +50,7 @@ class TerminalUI(engine: GameEngine) extends Observer:
case DrawReason.Stalemate => "Stalemate! The game is a draw."
case DrawReason.InsufficientMaterial => "Draw by insufficient material."
case DrawReason.FiftyMoveRule => "Draw claimed under the 50-move rule."
case DrawReason.ThreefoldRepetition => "Draw by threefold repetition."
case DrawReason.Agreement => "Draw by agreement."
println(msg)
println()
@@ -70,6 +71,9 @@ class TerminalUI(engine: GameEngine) extends Observer:
case _: FiftyMoveRuleAvailableEvent =>
println("50-move rule is now available — type 'draw' to claim.")
case _: ThreefoldRepetitionAvailableEvent =>
println("Threefold repetition is now available — type 'draw' to claim.")
case e: PgnLoadedEvent =>
println("PGN loaded successfully.")
println()
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=9
MINOR=10
PATCH=0
Regular → Executable
View File