Compare commits
2 Commits
core-0.15.0
...
api-0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 33e785d22a | |||
| d16cec176b |
@@ -42,3 +42,12 @@
|
|||||||
* 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-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-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-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-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||||
|
* 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))
|
||||||
|
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=6
|
MINOR=7
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -286,3 +286,30 @@
|
|||||||
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
|
* 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 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))
|
* 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-19)
|
||||||
|
|
||||||
|
### 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-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||||
|
* 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-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
|
||||||
|
* 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))
|
||||||
|
|||||||
@@ -63,3 +63,8 @@ tasks.test {
|
|||||||
tasks.reportScoverage {
|
tasks.reportScoverage {
|
||||||
dependsOn(tasks.test)
|
dependsOn(tasks.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ class GameEngine(
|
|||||||
else initialContext
|
else initialContext
|
||||||
@SuppressWarnings(Array("DisableSyntax.var"))
|
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||||
private var currentContext: GameContext = contextWithInitialBoard
|
private var currentContext: GameContext = contextWithInitialBoard
|
||||||
private val invoker = new CommandInvoker()
|
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||||
|
private var pendingDrawOffer: Option[Color] = None
|
||||||
|
private val invoker = new CommandInvoker()
|
||||||
|
|
||||||
private implicit val ec: ExecutionContext = ExecutionContext.global
|
private implicit val ec: ExecutionContext = ExecutionContext.global
|
||||||
|
|
||||||
@@ -77,12 +79,12 @@ class GameEngine(
|
|||||||
notifyObservers(
|
notifyObservers(
|
||||||
InvalidMoveEvent(
|
InvalidMoveEvent(
|
||||||
currentContext,
|
currentContext,
|
||||||
"Draw cannot be claimed: neither the 50-move rule nor threefold repetition has been triggered.",
|
InvalidMoveReason.DrawCannotBeClaimed,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
case "" =>
|
case "" =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, "Please enter a valid move or command."))
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.EmptyInput))
|
||||||
|
|
||||||
case moveInput =>
|
case moveInput =>
|
||||||
Parser.parseMove(moveInput) match
|
Parser.parseMove(moveInput) match
|
||||||
@@ -90,7 +92,7 @@ class GameEngine(
|
|||||||
notifyObservers(
|
notifyObservers(
|
||||||
InvalidMoveEvent(
|
InvalidMoveEvent(
|
||||||
currentContext,
|
currentContext,
|
||||||
s"Invalid move format '$moveInput'. Use coordinate notation, e.g. e2e4.",
|
InvalidMoveReason.InvalidMoveFormat,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
case Some((from, to, promotionPiece: Option[PromotionPiece])) =>
|
case Some((from, to, promotionPiece: Option[PromotionPiece])) =>
|
||||||
@@ -100,26 +102,26 @@ class GameEngine(
|
|||||||
private def handleParsedMove(from: Square, to: Square, promotionPiece: Option[PromotionPiece]): Unit =
|
private def handleParsedMove(from: Square, to: Square, promotionPiece: Option[PromotionPiece]): Unit =
|
||||||
currentContext.board.pieceAt(from) match
|
currentContext.board.pieceAt(from) match
|
||||||
case None =>
|
case None =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, "No piece on that square."))
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.NoSourcePiece))
|
||||||
case Some(piece) if piece.color != currentContext.turn =>
|
case Some(piece) if piece.color != currentContext.turn =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, "That is not your piece."))
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.NotYourPiece))
|
||||||
case Some(piece) =>
|
case Some(piece) =>
|
||||||
val legal = ruleSet.legalMoves(currentContext)(from)
|
val legal = ruleSet.legalMoves(currentContext)(from)
|
||||||
// Find all legal moves going to `to`
|
// Find all legal moves going to `to`
|
||||||
val candidates = legal.filter(_.to == to)
|
val candidates = legal.filter(_.to == to)
|
||||||
candidates match
|
candidates match
|
||||||
case Nil =>
|
case Nil =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, "Illegal move."))
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.IllegalMove))
|
||||||
case _ if isPromotionMove(piece, to) =>
|
case _ if isPromotionMove(piece, to) =>
|
||||||
if promotionPiece.isEmpty then
|
if promotionPiece.isEmpty then
|
||||||
notifyObservers(
|
notifyObservers(
|
||||||
InvalidMoveEvent(currentContext, "Promotion piece required: append q, r, b, or n to the move."),
|
InvalidMoveEvent(currentContext, InvalidMoveReason.PromotionPieceRequired),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
candidates.find(_.moveType == MoveType.Promotion(promotionPiece.get)) match
|
candidates.find(_.moveType == MoveType.Promotion(promotionPiece.get)) match
|
||||||
case None =>
|
case None =>
|
||||||
notifyObservers(
|
notifyObservers(
|
||||||
InvalidMoveEvent(currentContext, "Error completing promotion: no matching legal move."),
|
InvalidMoveEvent(currentContext, InvalidMoveReason.PromotionPieceInvalid),
|
||||||
)
|
)
|
||||||
case Some(move) => executeMove(move)
|
case Some(move) => executeMove(move)
|
||||||
case move :: _ =>
|
case move :: _ =>
|
||||||
@@ -137,6 +139,62 @@ class GameEngine(
|
|||||||
/** Redo the last undone move. */
|
/** Redo the last undone move. */
|
||||||
def redo(): Unit = synchronized(performRedo())
|
def redo(): Unit = synchronized(performRedo())
|
||||||
|
|
||||||
|
/** Resign from the game. The opponent wins. */
|
||||||
|
def resign(color: Color): Unit = synchronized {
|
||||||
|
if currentContext.result.isDefined then
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.GameAlreadyOver))
|
||||||
|
else
|
||||||
|
currentContext = currentContext.withResult(Some(GameResult.Win(color.opposite)))
|
||||||
|
pendingDrawOffer = None
|
||||||
|
invoker.clear()
|
||||||
|
notifyObservers(ResignEvent(currentContext, color))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Offer a draw. */
|
||||||
|
def offerDraw(color: Color): Unit = synchronized {
|
||||||
|
if currentContext.result.isDefined then
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.GameAlreadyOver))
|
||||||
|
else
|
||||||
|
pendingDrawOffer match
|
||||||
|
case Some(_) =>
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.DrawOfferPending))
|
||||||
|
case None =>
|
||||||
|
pendingDrawOffer = Some(color)
|
||||||
|
notifyObservers(DrawOfferEvent(currentContext, color))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Accept a pending draw offer. */
|
||||||
|
def acceptDraw(color: Color): Unit = synchronized {
|
||||||
|
if currentContext.result.isDefined then
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.GameAlreadyOver))
|
||||||
|
else
|
||||||
|
pendingDrawOffer match
|
||||||
|
case None =>
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.NoDrawOfferToAccept))
|
||||||
|
case Some(offerer) if offerer == color =>
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.CannotAcceptOwnDrawOffer))
|
||||||
|
case Some(_) =>
|
||||||
|
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.Agreement)))
|
||||||
|
pendingDrawOffer = None
|
||||||
|
invoker.clear()
|
||||||
|
notifyObservers(DrawEvent(currentContext, DrawReason.Agreement))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decline a pending draw offer. */
|
||||||
|
def declineDraw(color: Color): Unit = synchronized {
|
||||||
|
if currentContext.result.isDefined then
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.GameAlreadyOver))
|
||||||
|
else
|
||||||
|
pendingDrawOffer match
|
||||||
|
case None =>
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.NoDrawOfferToDecline))
|
||||||
|
case Some(offerer) if offerer == color =>
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.CannotDeclineOwnDrawOffer))
|
||||||
|
case Some(_) =>
|
||||||
|
pendingDrawOffer = None
|
||||||
|
notifyObservers(DrawOfferDeclinedEvent(currentContext, color))
|
||||||
|
}
|
||||||
|
|
||||||
/** Load a game using the provided importer. If the imported context has moves, they are replayed through the command
|
/** Load a game using the provided importer. If the imported context has moves, they are replayed through the command
|
||||||
* system. Otherwise, the position is set directly. Notifies observers with PgnLoadedEvent on success.
|
* system. Otherwise, the position is set directly. Notifies observers with PgnLoadedEvent on success.
|
||||||
*/
|
*/
|
||||||
@@ -145,6 +203,7 @@ class GameEngine(
|
|||||||
case Left(err) => Left(err)
|
case Left(err) => Left(err)
|
||||||
case Right(ctx) =>
|
case Right(ctx) =>
|
||||||
replayGame(ctx).map { _ =>
|
replayGame(ctx).map { _ =>
|
||||||
|
pendingDrawOffer = None
|
||||||
notifyObservers(PgnLoadedEvent(currentContext))
|
notifyObservers(PgnLoadedEvent(currentContext))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,6 +245,7 @@ class GameEngine(
|
|||||||
if newContext.moves.isEmpty then newContext.copy(initialBoard = newContext.board)
|
if newContext.moves.isEmpty then newContext.copy(initialBoard = newContext.board)
|
||||||
else newContext
|
else newContext
|
||||||
currentContext = contextWithInitialBoard
|
currentContext = contextWithInitialBoard
|
||||||
|
pendingDrawOffer = None
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
notifyObservers(BoardResetEvent(currentContext))
|
notifyObservers(BoardResetEvent(currentContext))
|
||||||
}
|
}
|
||||||
@@ -193,6 +253,7 @@ class GameEngine(
|
|||||||
/** Reset the board to initial position. */
|
/** Reset the board to initial position. */
|
||||||
def reset(): Unit = synchronized {
|
def reset(): Unit = synchronized {
|
||||||
currentContext = GameContext.initial
|
currentContext = GameContext.initial
|
||||||
|
pendingDrawOffer = None
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
notifyObservers(BoardResetEvent(currentContext))
|
notifyObservers(BoardResetEvent(currentContext))
|
||||||
}
|
}
|
||||||
@@ -323,9 +384,9 @@ class GameEngine(
|
|||||||
legal.find(m => m.to == to && m.moveType == move.moveType) match
|
legal.find(m => m.to == to && m.moveType == move.moveType) match
|
||||||
case Some(legalMove) => executeMove(legalMove)
|
case Some(legalMove) => executeMove(legalMove)
|
||||||
case None =>
|
case None =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, s"Bot move ${from}${to} is illegal"))
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.BotMoveIllegal))
|
||||||
case _ =>
|
case _ =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, "Bot move has invalid source square"))
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.BotMoveInvalidSource))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def handleBotNoMove(): Unit =
|
private def handleBotNoMove(): Unit =
|
||||||
@@ -344,7 +405,7 @@ class GameEngine(
|
|||||||
moveCmd.previousContext.foreach(currentContext = _)
|
moveCmd.previousContext.foreach(currentContext = _)
|
||||||
invoker.undo()
|
invoker.undo()
|
||||||
notifyObservers(MoveUndoneEvent(currentContext, moveCmd.notation))
|
notifyObservers(MoveUndoneEvent(currentContext, moveCmd.notation))
|
||||||
else notifyObservers(InvalidMoveEvent(currentContext, "Nothing to undo."))
|
else notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.NothingToUndo))
|
||||||
|
|
||||||
private def performRedo(): Unit =
|
private def performRedo(): Unit =
|
||||||
if invoker.canRedo then
|
if invoker.canRedo then
|
||||||
@@ -364,4 +425,4 @@ class GameEngine(
|
|||||||
capturedDesc,
|
capturedDesc,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else notifyObservers(InvalidMoveEvent(currentContext, "Nothing to redo."))
|
else notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.NothingToRedo))
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package de.nowchess.chess.observer
|
||||||
|
|
||||||
|
enum InvalidMoveReason:
|
||||||
|
case GameAlreadyOver
|
||||||
|
case NoSourcePiece
|
||||||
|
case NotYourPiece
|
||||||
|
case IllegalMove
|
||||||
|
case PromotionPieceRequired
|
||||||
|
case PromotionPieceInvalid
|
||||||
|
case InvalidMoveFormat
|
||||||
|
case EmptyInput
|
||||||
|
case DrawCannotBeClaimed
|
||||||
|
case NothingToUndo
|
||||||
|
case NothingToRedo
|
||||||
|
case BotMoveIllegal
|
||||||
|
case BotMoveInvalidSource
|
||||||
|
case DrawOfferPending
|
||||||
|
case NoDrawOfferToAccept
|
||||||
|
case CannotAcceptOwnDrawOffer
|
||||||
|
case NoDrawOfferToDecline
|
||||||
|
case CannotDeclineOwnDrawOffer
|
||||||
@@ -36,7 +36,7 @@ case class DrawEvent(
|
|||||||
/** Fired when a move is invalid. */
|
/** Fired when a move is invalid. */
|
||||||
case class InvalidMoveEvent(
|
case class InvalidMoveEvent(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
reason: String,
|
reason: InvalidMoveReason,
|
||||||
) extends GameEvent
|
) extends GameEvent
|
||||||
|
|
||||||
/** Fired when the board is reset. */
|
/** Fired when the board is reset. */
|
||||||
@@ -74,6 +74,24 @@ case class PgnLoadedEvent(
|
|||||||
context: GameContext,
|
context: GameContext,
|
||||||
) extends GameEvent
|
) extends GameEvent
|
||||||
|
|
||||||
|
/** Fired when a player resigns. The opponent wins. */
|
||||||
|
case class ResignEvent(
|
||||||
|
context: GameContext,
|
||||||
|
resignedColor: Color,
|
||||||
|
) extends GameEvent
|
||||||
|
|
||||||
|
/** Fired when a player offers a draw. Waiting for opponent to accept or decline. */
|
||||||
|
case class DrawOfferEvent(
|
||||||
|
context: GameContext,
|
||||||
|
offeredBy: Color,
|
||||||
|
) extends GameEvent
|
||||||
|
|
||||||
|
/** Fired when the opponent declines a draw offer. */
|
||||||
|
case class DrawOfferDeclinedEvent(
|
||||||
|
context: GameContext,
|
||||||
|
declinedBy: Color,
|
||||||
|
) extends GameEvent
|
||||||
|
|
||||||
/** Observer trait: implement to receive game state updates. */
|
/** Observer trait: implement to receive game state updates. */
|
||||||
trait Observer:
|
trait Observer:
|
||||||
def onGameEvent(event: GameEvent): Unit
|
def onGameEvent(event: GameEvent): Unit
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package de.nowchess.chess.engine
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
import de.nowchess.api.board.Color
|
||||||
|
import de.nowchess.api.game.{DrawReason, GameResult}
|
||||||
|
import de.nowchess.chess.observer.{
|
||||||
|
DrawEvent,
|
||||||
|
DrawOfferDeclinedEvent,
|
||||||
|
DrawOfferEvent,
|
||||||
|
GameEvent,
|
||||||
|
InvalidMoveEvent,
|
||||||
|
InvalidMoveReason,
|
||||||
|
Observer,
|
||||||
|
}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class GameEngineDrawOfferTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("White offers draw"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: DrawOfferEvent =>
|
||||||
|
event.offeredBy shouldBe Color.White
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected DrawOfferEvent, but got $other")
|
||||||
|
|
||||||
|
test("Black accepts White's draw offer"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.acceptDraw(Color.Black)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: DrawEvent =>
|
||||||
|
event.reason shouldBe DrawReason.Agreement
|
||||||
|
event.context.result shouldBe Some(GameResult.Draw(DrawReason.Agreement))
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected DrawEvent, but got $other")
|
||||||
|
|
||||||
|
test("Black declines White's draw offer"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.declineDraw(Color.Black)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: DrawOfferDeclinedEvent =>
|
||||||
|
event.declinedBy shouldBe Color.Black
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected DrawOfferDeclinedEvent, but got $other")
|
||||||
|
|
||||||
|
test("Black offers draw"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.Black)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: DrawOfferEvent =>
|
||||||
|
event.offeredBy shouldBe Color.Black
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected DrawOfferEvent, but got $other")
|
||||||
|
|
||||||
|
test("White accepts Black's draw offer"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.Black)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.acceptDraw(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: DrawEvent =>
|
||||||
|
event.reason shouldBe DrawReason.Agreement
|
||||||
|
event.context.result shouldBe Some(GameResult.Draw(DrawReason.Agreement))
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected DrawEvent, but got $other")
|
||||||
|
|
||||||
|
test("Cannot accept draw when no offer pending"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.acceptDraw(Color.Black)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.NoDrawOfferToAccept
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
test("Cannot decline draw when no offer pending"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.declineDraw(Color.Black)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.NoDrawOfferToDecline
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
test("Cannot offer draw when game is already over"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
// End the game with checkmate
|
||||||
|
engine.processUserInput("f2f3")
|
||||||
|
engine.processUserInput("e7e5")
|
||||||
|
engine.processUserInput("g2g4")
|
||||||
|
observer.events.clear()
|
||||||
|
engine.processUserInput("d8h4")
|
||||||
|
|
||||||
|
// Try to offer draw
|
||||||
|
observer.events.clear()
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
test("Cannot accept your own draw offer"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.acceptDraw(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.CannotAcceptOwnDrawOffer
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
test("Cannot decline your own draw offer"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.declineDraw(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.CannotDeclineOwnDrawOffer
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
test("Cannot make second draw offer when one is already pending"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.offerDraw(Color.Black)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.DrawOfferPending
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
test("Draw offer is cleared when game ends by resignation (accept)"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.resign(Color.Black)
|
||||||
|
|
||||||
|
// Try to accept the now-cleared draw offer
|
||||||
|
observer.events.clear()
|
||||||
|
engine.acceptDraw(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
test("Draw offer is cleared when game ends by resignation (decline)"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new DrawOfferMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.offerDraw(Color.White)
|
||||||
|
observer.events.clear()
|
||||||
|
engine.resign(Color.Black)
|
||||||
|
|
||||||
|
// Try to accept the now-cleared draw offer
|
||||||
|
observer.events.clear()
|
||||||
|
engine.declineDraw(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
private class DrawOfferMockObserver extends Observer:
|
||||||
|
val events = mutable.ListBuffer[GameEvent]()
|
||||||
|
|
||||||
|
override def onGameEvent(event: GameEvent): Unit =
|
||||||
|
events += event
|
||||||
+3
-3
@@ -3,7 +3,7 @@ package de.nowchess.chess.engine
|
|||||||
import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square}
|
import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, MoveRedoneEvent, Observer}
|
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, MoveRedoneEvent, Observer}
|
||||||
import de.nowchess.io.GameContextImport
|
import de.nowchess.io.GameContextImport
|
||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
@@ -50,8 +50,8 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
|||||||
engine.processUserInput("e2e5")
|
engine.processUserInput("e2e5")
|
||||||
|
|
||||||
events.exists {
|
events.exists {
|
||||||
case InvalidMoveEvent(_, reason) => reason.contains("Illegal move")
|
case InvalidMoveEvent(_, InvalidMoveReason.IllegalMove) => true
|
||||||
case _ => false
|
case _ => false
|
||||||
} shouldBe true
|
} shouldBe true
|
||||||
|
|
||||||
test("loadGame returns Left when importer fails"):
|
test("loadGame returns Left when importer fails"):
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square
|
|||||||
import de.nowchess.api.game.{DrawReason, GameContext}
|
import de.nowchess.api.game.{DrawReason, GameContext}
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.io.fen.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.{
|
||||||
|
CheckDetectedEvent,
|
||||||
|
CheckmateEvent,
|
||||||
|
DrawEvent,
|
||||||
|
GameEvent,
|
||||||
|
InvalidMoveEvent,
|
||||||
|
InvalidMoveReason,
|
||||||
|
MoveExecutedEvent,
|
||||||
|
Observer,
|
||||||
|
}
|
||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
@@ -30,8 +39,8 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
engine.processUserInput("e7e8")
|
engine.processUserInput("e7e8")
|
||||||
|
|
||||||
events.exists {
|
events.exists {
|
||||||
case InvalidMoveEvent(_, reason) => reason.contains("Promotion piece required")
|
case InvalidMoveEvent(_, InvalidMoveReason.PromotionPieceRequired) => true
|
||||||
case _ => false
|
case _ => false
|
||||||
} should be(true)
|
} should be(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,5 +207,5 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
case _ => false
|
case _ => false
|
||||||
} should be(true)
|
} should be(true)
|
||||||
val invalidEvt = events.collect { case e: InvalidMoveEvent => e }.last
|
val invalidEvt = events.collect { case e: InvalidMoveEvent => e }.last
|
||||||
invalidEvt.reason should include("Error completing promotion")
|
invalidEvt.reason shouldBe InvalidMoveReason.PromotionPieceInvalid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package de.nowchess.chess.engine
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
import de.nowchess.api.board.Color
|
||||||
|
import de.nowchess.api.game.GameResult
|
||||||
|
import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, Observer, ResignEvent}
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
|
class GameEngineResignTest extends AnyFunSuite with Matchers:
|
||||||
|
|
||||||
|
test("White resigns"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new ResignMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.resign(Color.White)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: ResignEvent =>
|
||||||
|
event.resignedColor shouldBe Color.White
|
||||||
|
event.context.result shouldBe Some(GameResult.Win(Color.Black))
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected ResignEvent, but got $other")
|
||||||
|
|
||||||
|
test("Black resigns"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new ResignMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
engine.resign(Color.Black)
|
||||||
|
|
||||||
|
observer.events should have length 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: ResignEvent =>
|
||||||
|
event.resignedColor shouldBe Color.Black
|
||||||
|
event.context.result shouldBe Some(GameResult.Win(Color.White))
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected ResignEvent, but got $other")
|
||||||
|
|
||||||
|
test("Cannot resign when game is already over"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new ResignMockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
|
||||||
|
// End the game with checkmate
|
||||||
|
engine.processUserInput("f2f3")
|
||||||
|
engine.processUserInput("e7e5")
|
||||||
|
engine.processUserInput("g2g4")
|
||||||
|
observer.events.clear()
|
||||||
|
engine.processUserInput("d8h4")
|
||||||
|
|
||||||
|
// Try to resign
|
||||||
|
observer.events.clear()
|
||||||
|
engine.resign(Color.White)
|
||||||
|
|
||||||
|
// Should get InvalidMoveEvent with GameAlreadyOver reason
|
||||||
|
observer.events.length shouldBe 1
|
||||||
|
observer.events.head match
|
||||||
|
case event: InvalidMoveEvent =>
|
||||||
|
event.reason shouldBe InvalidMoveReason.GameAlreadyOver
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected InvalidMoveEvent, but got $other")
|
||||||
|
|
||||||
|
private class ResignMockObserver extends Observer:
|
||||||
|
val events = mutable.ListBuffer[GameEvent]()
|
||||||
|
|
||||||
|
override def onGameEvent(event: GameEvent): Unit =
|
||||||
|
events += event
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=14
|
MINOR=15
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -56,3 +56,13 @@
|
|||||||
* 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-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-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))
|
* 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-19)
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=8
|
MINOR=9
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -55,3 +55,15 @@
|
|||||||
### Bug Fixes
|
### 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))
|
* 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-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||||
|
* 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-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=3
|
MINOR=4
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
@@ -91,3 +91,16 @@
|
|||||||
* 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-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-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-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-19)
|
||||||
|
|
||||||
|
### 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-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
|
||||||
|
* 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))
|
||||||
|
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=11
|
MINOR=12
|
||||||
PATCH=0
|
PATCH=0
|
||||||
|
|||||||
Reference in New Issue
Block a user