This commit is contained in:
@@ -5,4 +5,5 @@ enum DrawReason:
|
|||||||
case Stalemate
|
case Stalemate
|
||||||
case InsufficientMaterial
|
case InsufficientMaterial
|
||||||
case FiftyMoveRule
|
case FiftyMoveRule
|
||||||
|
case ThreefoldRepetition
|
||||||
case Agreement
|
case Agreement
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ case class GameContext(
|
|||||||
halfMoveClock: Int,
|
halfMoveClock: Int,
|
||||||
moves: List[Move],
|
moves: List[Move],
|
||||||
result: Option[GameResult] = None,
|
result: Option[GameResult] = None,
|
||||||
|
initialBoard: Board = Board.initial,
|
||||||
):
|
):
|
||||||
/** Create new context with updated board. */
|
/** Create new context with updated board. */
|
||||||
def withBoard(newBoard: Board): GameContext = copy(board = newBoard)
|
def withBoard(newBoard: Board): GameContext = copy(board = newBoard)
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ class GameEngine(
|
|||||||
val initialContext: GameContext = GameContext.initial,
|
val initialContext: GameContext = GameContext.initial,
|
||||||
val ruleSet: RuleSet = DefaultRules,
|
val ruleSet: RuleSet = DefaultRules,
|
||||||
) extends Observable:
|
) 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"))
|
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||||
private var currentContext: GameContext = initialContext
|
private var currentContext: GameContext = contextWithInitialBoard
|
||||||
private val invoker = new CommandInvoker()
|
private val invoker = new CommandInvoker()
|
||||||
|
|
||||||
/** Pending promotion: the Move that triggered it (from/to only, moveType filled in later). */
|
/** 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)))
|
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.FiftyMoveRule)))
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule))
|
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
|
else
|
||||||
notifyObservers(
|
notifyObservers(
|
||||||
InvalidMoveEvent(
|
InvalidMoveEvent(
|
||||||
currentContext,
|
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()
|
invoker.clear()
|
||||||
|
|
||||||
if ctx.moves.isEmpty then
|
if ctx.moves.isEmpty then
|
||||||
currentContext = ctx
|
currentContext = ctx.copy(initialBoard = ctx.board)
|
||||||
Right(())
|
Right(())
|
||||||
else replayMoves(ctx.moves, savedContext)
|
else replayMoves(ctx.moves, savedContext)
|
||||||
|
|
||||||
@@ -182,7 +191,11 @@ class GameEngine(
|
|||||||
|
|
||||||
/** Load an arbitrary board position, clearing all history and undo/redo state. */
|
/** Load an arbitrary board position, clearing all history and undo/redo state. */
|
||||||
def loadPosition(newContext: GameContext): Unit = synchronized {
|
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
|
pendingPromotion = None
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
notifyObservers(BoardResetEvent(currentContext))
|
notifyObservers(BoardResetEvent(currentContext))
|
||||||
@@ -237,6 +250,7 @@ class GameEngine(
|
|||||||
else if ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext))
|
else if ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext))
|
||||||
|
|
||||||
if currentContext.halfMoveClock >= 100 then notifyObservers(FiftyMoveRuleAvailableEvent(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 =
|
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
|
||||||
move.moveType match
|
move.moveType match
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ case class FiftyMoveRuleAvailableEvent(
|
|||||||
context: GameContext,
|
context: GameContext,
|
||||||
) extends GameEvent
|
) 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. */
|
/** Fired when a move is undone, carrying PGN notation of the reversed move. */
|
||||||
case class MoveUndoneEvent(
|
case class MoveUndoneEvent(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
|||||||
def isStalemate(context: GameContext): Boolean = false
|
def isStalemate(context: GameContext): Boolean = false
|
||||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||||
def isFiftyMoveRule(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)
|
def applyMove(context: GameContext)(move: Move): GameContext = DefaultRules.applyMove(context)(move)
|
||||||
|
|
||||||
val engine = new GameEngine(ruleSet = permissiveRules)
|
val engine = new GameEngine(ruleSet = permissiveRules)
|
||||||
@@ -119,6 +120,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
|||||||
def isStalemate(context: GameContext): Boolean = false
|
def isStalemate(context: GameContext): Boolean = false
|
||||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||||
|
def isThreefoldRepetition(context: GameContext): Boolean = false
|
||||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||||
|
|
||||||
val engine = new GameEngine(ruleSet = noLegalMoves)
|
val engine = new GameEngine(ruleSet = noLegalMoves)
|
||||||
|
|||||||
@@ -221,3 +221,75 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
|
|||||||
evt.isDefined shouldBe true
|
evt.isDefined shouldBe true
|
||||||
evt.get.reason shouldBe DrawReason.InsufficientMaterial
|
evt.get.reason shouldBe DrawReason.InsufficientMaterial
|
||||||
engine.context.result shouldBe Some(GameResult.Draw(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)
|
DefaultRules.isInsufficientMaterial(context)
|
||||||
def isFiftyMoveRule(context: GameContext): Boolean =
|
def isFiftyMoveRule(context: GameContext): Boolean =
|
||||||
DefaultRules.isFiftyMoveRule(context)
|
DefaultRules.isFiftyMoveRule(context)
|
||||||
|
def isThreefoldRepetition(context: GameContext): Boolean =
|
||||||
|
DefaultRules.isThreefoldRepetition(context)
|
||||||
def applyMove(context: GameContext)(move: Move): GameContext =
|
def applyMove(context: GameContext)(move: Move): GameContext =
|
||||||
DefaultRules.applyMove(context)(move)
|
DefaultRules.applyMove(context)(move)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ trait RuleSet:
|
|||||||
/** True if halfMoveClock >= 100 (50-move rule). */
|
/** True if halfMoveClock >= 100 (50-move rule). */
|
||||||
def isFiftyMoveRule(context: GameContext): Boolean
|
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,
|
/** 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.
|
* 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:
|
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 ──────────────────────────────────────────────
|
// ── Direction vectors ──────────────────────────────────────────────
|
||||||
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
|
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))
|
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 =
|
override def isFiftyMoveRule(context: GameContext): Boolean =
|
||||||
context.halfMoveClock >= 100
|
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) ───────────────────────────
|
// ── Sliding pieces (Bishop, Rook, Queen) ───────────────────────────
|
||||||
|
|
||||||
private def slidingMoves(
|
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
|
// White is in check; only moves that block or move the king are legal
|
||||||
moves.nonEmpty shouldBe true
|
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
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class GUIObserver(private val boardView: ChessBoardView) extends Observer:
|
|||||||
case DrawReason.Stalemate => "Stalemate! The game is a draw."
|
case DrawReason.Stalemate => "Stalemate! The game is a draw."
|
||||||
case DrawReason.InsufficientMaterial => "Draw by insufficient material."
|
case DrawReason.InsufficientMaterial => "Draw by insufficient material."
|
||||||
case DrawReason.FiftyMoveRule => "Draw claimed under the 50-move rule."
|
case DrawReason.FiftyMoveRule => "Draw claimed under the 50-move rule."
|
||||||
|
case DrawReason.ThreefoldRepetition => "Draw by threefold repetition."
|
||||||
case DrawReason.Agreement => "Draw by agreement."
|
case DrawReason.Agreement => "Draw by agreement."
|
||||||
showAlert(AlertType.Information, "Game Over", msg)
|
showAlert(AlertType.Information, "Game Over", msg)
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ class GUIObserver(private val boardView: ChessBoardView) extends Observer:
|
|||||||
case e: FiftyMoveRuleAvailableEvent =>
|
case e: FiftyMoveRuleAvailableEvent =>
|
||||||
boardView.showMessage("50-move rule is now available — type 'draw' to claim.")
|
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 =>
|
case e: MoveUndoneEvent =>
|
||||||
boardView.updateBoard(e.context.board, e.context.turn)
|
boardView.updateBoard(e.context.board, e.context.turn)
|
||||||
boardView.showMessage(s"↶ Undo: ${e.pgnNotation}")
|
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.Stalemate => "Stalemate! The game is a draw."
|
||||||
case DrawReason.InsufficientMaterial => "Draw by insufficient material."
|
case DrawReason.InsufficientMaterial => "Draw by insufficient material."
|
||||||
case DrawReason.FiftyMoveRule => "Draw claimed under the 50-move rule."
|
case DrawReason.FiftyMoveRule => "Draw claimed under the 50-move rule."
|
||||||
|
case DrawReason.ThreefoldRepetition => "Draw by threefold repetition."
|
||||||
case DrawReason.Agreement => "Draw by agreement."
|
case DrawReason.Agreement => "Draw by agreement."
|
||||||
println(msg)
|
println(msg)
|
||||||
println()
|
println()
|
||||||
@@ -70,6 +71,9 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
|||||||
case _: FiftyMoveRuleAvailableEvent =>
|
case _: FiftyMoveRuleAvailableEvent =>
|
||||||
println("50-move rule is now available — type 'draw' to claim.")
|
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 =>
|
case e: PgnLoadedEvent =>
|
||||||
println("PGN loaded successfully.")
|
println("PGN loaded successfully.")
|
||||||
println()
|
println()
|
||||||
|
|||||||
Reference in New Issue
Block a user