feat: undo/redo added
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package de.nowchess.chess.command
|
||||
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.board.{Square, Board, Color, Piece}
|
||||
import de.nowchess.chess.logic.GameHistory
|
||||
|
||||
/** Marker trait for all commands that can be executed and undone.
|
||||
* Commands encapsulate user actions and game state transitions.
|
||||
@@ -15,12 +16,33 @@ trait Command:
|
||||
/** A human-readable description of this command. */
|
||||
def description: String
|
||||
|
||||
/** Command to move a piece from one square to another. */
|
||||
case class MoveCommand(from: Square, to: Square) extends Command:
|
||||
override def execute(): Boolean = true
|
||||
override def undo(): Boolean = true
|
||||
/** Command to move a piece from one square to another.
|
||||
* Stores the move result so undo can restore previous state.
|
||||
*/
|
||||
case class MoveCommand(
|
||||
from: Square,
|
||||
to: Square,
|
||||
var moveResult: Option[MoveResult] = None,
|
||||
var previousBoard: Option[Board] = None,
|
||||
var previousHistory: Option[GameHistory] = None,
|
||||
var previousTurn: Option[Color] = None
|
||||
) extends Command:
|
||||
|
||||
override def execute(): Boolean =
|
||||
moveResult.isDefined
|
||||
|
||||
override def undo(): Boolean =
|
||||
previousBoard.isDefined && previousHistory.isDefined && previousTurn.isDefined
|
||||
|
||||
override def description: String = s"Move from $from to $to"
|
||||
|
||||
// Sealed hierarchy of move outcomes (for tracking state changes)
|
||||
sealed trait MoveResult
|
||||
object MoveResult:
|
||||
case class Successful(newBoard: Board, newHistory: GameHistory, newTurn: Color, captured: Option[Piece]) extends MoveResult
|
||||
case object InvalidFormat extends MoveResult
|
||||
case object InvalidMove extends MoveResult
|
||||
|
||||
/** Command to quit the game. */
|
||||
case class QuitCommand() extends Command:
|
||||
override def execute(): Boolean = true
|
||||
@@ -28,7 +50,15 @@ case class QuitCommand() extends Command:
|
||||
override def description: String = "Quit game"
|
||||
|
||||
/** Command to reset the board to initial position. */
|
||||
case class ResetCommand() extends Command:
|
||||
case class ResetCommand(
|
||||
var previousBoard: Option[Board] = None,
|
||||
var previousHistory: Option[GameHistory] = None,
|
||||
var previousTurn: Option[Color] = None
|
||||
) extends Command:
|
||||
|
||||
override def execute(): Boolean = true
|
||||
override def undo(): Boolean = true
|
||||
|
||||
override def undo(): Boolean =
|
||||
previousBoard.isDefined && previousHistory.isDefined && previousTurn.isDefined
|
||||
|
||||
override def description: String = "Reset board"
|
||||
|
||||
@@ -1,140 +1,245 @@
|
||||
package de.nowchess.chess.engine
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, Piece}
|
||||
import de.nowchess.api.board.{Board, Color, Piece, Square}
|
||||
import de.nowchess.chess.logic.{GameHistory, GameRules, PositionStatus}
|
||||
import de.nowchess.chess.controller.{GameController, Parser, MoveResult}
|
||||
import de.nowchess.chess.observer.*
|
||||
import de.nowchess.chess.command.{CommandInvoker, MoveCommand}
|
||||
|
||||
/** Pure game engine that manages game state and notifies observers of state changes.
|
||||
* This class is the single source of truth for the game state.
|
||||
* All user interactions must go through this engine, and all state changes
|
||||
* All user interactions must go through this engine via Commands, and all state changes
|
||||
* are communicated to observers via GameEvent notifications.
|
||||
*/
|
||||
class GameEngine extends Observable:
|
||||
private var currentBoard: Board = Board.initial
|
||||
private var currentHistory: GameHistory = GameHistory.empty
|
||||
private var currentTurn: Color = Color.White
|
||||
private val invoker = new CommandInvoker()
|
||||
|
||||
// Synchronized accessors for current state
|
||||
def board: Board = synchronized { currentBoard }
|
||||
def history: GameHistory = synchronized { currentHistory }
|
||||
def turn: Color = synchronized { currentTurn }
|
||||
|
||||
/** Check if undo is available. */
|
||||
def canUndo: Boolean = synchronized { invoker.canUndo }
|
||||
|
||||
/** Check if redo is available. */
|
||||
def canRedo: Boolean = synchronized { invoker.canRedo }
|
||||
|
||||
/** Get the command history for inspection (testing/debugging). */
|
||||
def commandHistory: List[de.nowchess.chess.command.Command] = synchronized { invoker.history }
|
||||
|
||||
/** Process a raw move input string and update game state if valid.
|
||||
* Notifies all observers of the outcome via GameEvent.
|
||||
*/
|
||||
def processUserInput(rawInput: String): Unit = synchronized {
|
||||
GameController.processMove(currentBoard, currentHistory, currentTurn, rawInput) match
|
||||
case MoveResult.Quit =>
|
||||
val trimmed = rawInput.trim.toLowerCase
|
||||
trimmed match
|
||||
case "quit" | "q" =>
|
||||
// Client should handle quit logic; we just return
|
||||
()
|
||||
|
||||
case MoveResult.InvalidFormat(raw) =>
|
||||
case "undo" =>
|
||||
performUndo()
|
||||
|
||||
case "redo" =>
|
||||
performRedo()
|
||||
|
||||
case "" =>
|
||||
val event = InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
s"Invalid move format '$raw'. Use coordinate notation, e.g. e2e4."
|
||||
"Please enter a valid move or command."
|
||||
)
|
||||
notifyObservers(event)
|
||||
|
||||
case MoveResult.NoPiece =>
|
||||
val event = InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
s"No piece on that square."
|
||||
)
|
||||
notifyObservers(event)
|
||||
case moveInput =>
|
||||
// Try to parse as a move
|
||||
Parser.parseMove(moveInput) match
|
||||
case None =>
|
||||
val event = InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
s"Invalid move format '$moveInput'. Use coordinate notation, e.g. e2e4."
|
||||
)
|
||||
notifyObservers(event)
|
||||
|
||||
case MoveResult.WrongColor =>
|
||||
val event = InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
"That is not your piece."
|
||||
)
|
||||
notifyObservers(event)
|
||||
case Some((from, to)) =>
|
||||
// Create a move command with current state snapshot
|
||||
val cmd = MoveCommand(
|
||||
from = from,
|
||||
to = to,
|
||||
previousBoard = Some(currentBoard),
|
||||
previousHistory = Some(currentHistory),
|
||||
previousTurn = Some(currentTurn)
|
||||
)
|
||||
|
||||
case MoveResult.IllegalMove =>
|
||||
val event = InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
"Illegal move."
|
||||
)
|
||||
notifyObservers(event)
|
||||
// Execute the move through GameController
|
||||
GameController.processMove(currentBoard, currentHistory, currentTurn, moveInput) match
|
||||
case MoveResult.Quit =>
|
||||
// Should not happen via processUserInput, but handle it
|
||||
()
|
||||
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, newTurn) =>
|
||||
currentBoard = newBoard
|
||||
currentHistory = newHistory
|
||||
currentTurn = newTurn
|
||||
val fromSq = Parser.parseMove(rawInput.trim).map(_._1).fold("?")(_.toString)
|
||||
val toSq = Parser.parseMove(rawInput.trim).map(_._2).fold("?")(_.toString)
|
||||
val capturedDesc = captured.map(c => s"${c.color.label} ${c.pieceType.label}")
|
||||
notifyObservers(MoveExecutedEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
fromSq,
|
||||
toSq,
|
||||
capturedDesc
|
||||
))
|
||||
case MoveResult.InvalidFormat(_) | MoveResult.NoPiece | MoveResult.WrongColor | MoveResult.IllegalMove =>
|
||||
// Move failed, don't add to history
|
||||
handleFailedMove(moveInput)
|
||||
|
||||
case MoveResult.MovedInCheck(newBoard, newHistory, captured, newTurn) =>
|
||||
currentBoard = newBoard
|
||||
currentHistory = newHistory
|
||||
currentTurn = newTurn
|
||||
val fromSq = Parser.parseMove(rawInput.trim).map(_._1).fold("?")(_.toString)
|
||||
val toSq = Parser.parseMove(rawInput.trim).map(_._2).fold("?")(_.toString)
|
||||
val capturedDesc = captured.map(c => s"${c.color.label} ${c.pieceType.label}")
|
||||
notifyObservers(MoveExecutedEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
fromSq,
|
||||
toSq,
|
||||
capturedDesc
|
||||
))
|
||||
notifyObservers(CheckDetectedEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn
|
||||
))
|
||||
case MoveResult.Moved(newBoard, newHistory, captured, newTurn) =>
|
||||
// Move succeeded - store result and execute through invoker
|
||||
cmd.moveResult = Some(de.nowchess.chess.command.MoveResult.Successful(newBoard, newHistory, newTurn, captured))
|
||||
invoker.execute(cmd)
|
||||
updateGameState(newBoard, newHistory, newTurn)
|
||||
emitMoveEvent(from.toString, to.toString, captured, newTurn)
|
||||
|
||||
case MoveResult.Checkmate(winner) =>
|
||||
currentBoard = Board.initial
|
||||
currentHistory = GameHistory.empty
|
||||
currentTurn = Color.White
|
||||
notifyObservers(CheckmateEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
winner
|
||||
))
|
||||
case MoveResult.MovedInCheck(newBoard, newHistory, captured, newTurn) =>
|
||||
// Move succeeded with check
|
||||
cmd.moveResult = Some(de.nowchess.chess.command.MoveResult.Successful(newBoard, newHistory, newTurn, captured))
|
||||
invoker.execute(cmd)
|
||||
updateGameState(newBoard, newHistory, newTurn)
|
||||
emitMoveEvent(from.toString, to.toString, captured, newTurn)
|
||||
notifyObservers(CheckDetectedEvent(currentBoard, currentHistory, currentTurn))
|
||||
|
||||
case MoveResult.Stalemate =>
|
||||
currentBoard = Board.initial
|
||||
currentHistory = GameHistory.empty
|
||||
currentTurn = Color.White
|
||||
notifyObservers(StalemateEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn
|
||||
))
|
||||
case MoveResult.Checkmate(winner) =>
|
||||
// Move resulted in checkmate
|
||||
cmd.moveResult = Some(de.nowchess.chess.command.MoveResult.Successful(Board.initial, GameHistory.empty, Color.White, None))
|
||||
invoker.execute(cmd)
|
||||
currentBoard = Board.initial
|
||||
currentHistory = GameHistory.empty
|
||||
currentTurn = Color.White
|
||||
notifyObservers(CheckmateEvent(currentBoard, currentHistory, currentTurn, winner))
|
||||
|
||||
case MoveResult.Stalemate =>
|
||||
// Move resulted in stalemate
|
||||
cmd.moveResult = Some(de.nowchess.chess.command.MoveResult.Successful(Board.initial, GameHistory.empty, Color.White, None))
|
||||
invoker.execute(cmd)
|
||||
currentBoard = Board.initial
|
||||
currentHistory = GameHistory.empty
|
||||
currentTurn = Color.White
|
||||
notifyObservers(StalemateEvent(currentBoard, currentHistory, currentTurn))
|
||||
}
|
||||
|
||||
/** Reset the board to initial position.
|
||||
* Notifies all observers of the reset.
|
||||
*/
|
||||
/** Undo the last move. */
|
||||
def undo(): Unit = synchronized {
|
||||
performUndo()
|
||||
}
|
||||
|
||||
/** Redo the last undone move. */
|
||||
def redo(): Unit = synchronized {
|
||||
performRedo()
|
||||
}
|
||||
|
||||
/** Reset the board to initial position. */
|
||||
def reset(): Unit = synchronized {
|
||||
currentBoard = Board.initial
|
||||
currentHistory = GameHistory.empty
|
||||
currentTurn = Color.White
|
||||
invoker.clear()
|
||||
notifyObservers(BoardResetEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn
|
||||
))
|
||||
}
|
||||
|
||||
// ──── Private Helpers ────
|
||||
|
||||
private def performUndo(): Unit =
|
||||
if invoker.canUndo then
|
||||
val history = invoker.history
|
||||
val currentIdx = invoker.getCurrentIndex
|
||||
if currentIdx >= 0 && currentIdx < history.size then
|
||||
val cmd = history(currentIdx)
|
||||
cmd match
|
||||
case moveCmd: MoveCommand =>
|
||||
if moveCmd.undo() then
|
||||
moveCmd.previousBoard.foreach(currentBoard = _)
|
||||
moveCmd.previousHistory.foreach(currentHistory = _)
|
||||
moveCmd.previousTurn.foreach(currentTurn = _)
|
||||
invoker.undo()
|
||||
notifyObservers(BoardResetEvent(currentBoard, currentHistory, currentTurn))
|
||||
else
|
||||
notifyObservers(InvalidMoveEvent(currentBoard, currentHistory, currentTurn, "Cannot undo this move."))
|
||||
case _ =>
|
||||
// Other command types - just revert the invoker
|
||||
invoker.undo()
|
||||
notifyObservers(BoardResetEvent(currentBoard, currentHistory, currentTurn))
|
||||
else
|
||||
notifyObservers(InvalidMoveEvent(currentBoard, currentHistory, currentTurn, "Nothing to undo."))
|
||||
|
||||
private def performRedo(): Unit =
|
||||
if invoker.canRedo then
|
||||
val history = invoker.history
|
||||
val nextIdx = invoker.getCurrentIndex + 1
|
||||
if nextIdx >= 0 && nextIdx < history.size then
|
||||
val cmd = history(nextIdx)
|
||||
cmd match
|
||||
case moveCmd: MoveCommand =>
|
||||
if moveCmd.execute() then
|
||||
moveCmd.moveResult.foreach {
|
||||
case de.nowchess.chess.command.MoveResult.Successful(newBoard, newHistory, newTurn, captured) =>
|
||||
updateGameState(newBoard, newHistory, newTurn)
|
||||
invoker.redo()
|
||||
emitMoveEvent(moveCmd.from.toString, moveCmd.to.toString, captured, newTurn)
|
||||
case _ => ()
|
||||
}
|
||||
else
|
||||
notifyObservers(InvalidMoveEvent(currentBoard, currentHistory, currentTurn, "Cannot redo this move."))
|
||||
case _ =>
|
||||
invoker.redo()
|
||||
notifyObservers(BoardResetEvent(currentBoard, currentHistory, currentTurn))
|
||||
else
|
||||
notifyObservers(InvalidMoveEvent(currentBoard, currentHistory, currentTurn, "Nothing to redo."))
|
||||
|
||||
private def updateGameState(newBoard: Board, newHistory: GameHistory, newTurn: Color): Unit =
|
||||
currentBoard = newBoard
|
||||
currentHistory = newHistory
|
||||
currentTurn = newTurn
|
||||
|
||||
private def emitMoveEvent(fromSq: String, toSq: String, captured: Option[Piece], newTurn: Color): Unit =
|
||||
val capturedDesc = captured.map(c => s"${c.color.label} ${c.pieceType.label}")
|
||||
notifyObservers(MoveExecutedEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
newTurn,
|
||||
fromSq,
|
||||
toSq,
|
||||
capturedDesc
|
||||
))
|
||||
|
||||
private def handleFailedMove(moveInput: String): Unit =
|
||||
GameController.processMove(currentBoard, currentHistory, currentTurn, moveInput) match
|
||||
case MoveResult.InvalidFormat(raw) =>
|
||||
notifyObservers(InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
s"Invalid move format '$raw'. Use coordinate notation, e.g. e2e4."
|
||||
))
|
||||
case MoveResult.NoPiece =>
|
||||
notifyObservers(InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
"No piece on that square."
|
||||
))
|
||||
case MoveResult.WrongColor =>
|
||||
notifyObservers(InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
"That is not your piece."
|
||||
))
|
||||
case MoveResult.IllegalMove =>
|
||||
notifyObservers(InvalidMoveEvent(
|
||||
currentBoard,
|
||||
currentHistory,
|
||||
currentTurn,
|
||||
"Illegal move."
|
||||
))
|
||||
case _ => ()
|
||||
|
||||
end GameEngine
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
package de.nowchess.chess.command
|
||||
|
||||
import de.nowchess.api.board.{Square, File, Rank}
|
||||
import de.nowchess.api.board.{Square, File, Rank, Board, Color}
|
||||
import de.nowchess.chess.logic.GameHistory
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private def sq(f: File, r: Rank): Square = Square(f, r)
|
||||
|
||||
private def createMoveCommand(from: Square, to: Square): MoveCommand =
|
||||
MoveCommand(
|
||||
from = from,
|
||||
to = to,
|
||||
moveResult = Some(MoveResult.Successful(Board.initial, GameHistory.empty, Color.White, None)),
|
||||
previousBoard = Some(Board.initial),
|
||||
previousHistory = Some(GameHistory.empty),
|
||||
previousTurn = Some(Color.White)
|
||||
)
|
||||
|
||||
test("CommandInvoker executes a command and adds it to history"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd) shouldBe true
|
||||
invoker.history.size shouldBe 1
|
||||
invoker.getCurrentIndex shouldBe 0
|
||||
|
||||
test("CommandInvoker executes multiple commands in sequence"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd1 = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd2 = MoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
|
||||
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
|
||||
invoker.execute(cmd1) shouldBe true
|
||||
invoker.execute(cmd2) shouldBe true
|
||||
invoker.history.size shouldBe 2
|
||||
@@ -30,13 +41,13 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("CommandInvoker.canUndo returns true after execution"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd)
|
||||
invoker.canUndo shouldBe true
|
||||
|
||||
test("CommandInvoker.undo decrements current index"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd)
|
||||
invoker.getCurrentIndex shouldBe 0
|
||||
invoker.undo() shouldBe true
|
||||
@@ -44,14 +55,14 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("CommandInvoker.canRedo returns true after undo"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd)
|
||||
invoker.undo()
|
||||
invoker.canRedo shouldBe true
|
||||
|
||||
test("CommandInvoker.redo re-executes a command"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd)
|
||||
invoker.undo() shouldBe true
|
||||
invoker.redo() shouldBe true
|
||||
@@ -59,14 +70,14 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("CommandInvoker.canUndo returns false when at beginning"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd)
|
||||
invoker.undo()
|
||||
invoker.canUndo shouldBe false
|
||||
|
||||
test("CommandInvoker clear removes all history"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
invoker.execute(cmd)
|
||||
invoker.clear()
|
||||
invoker.history.size shouldBe 0
|
||||
@@ -74,9 +85,9 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("CommandInvoker discards all history when executing after undoing all"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd1 = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd2 = MoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
|
||||
val cmd3 = MoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
|
||||
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
|
||||
val cmd3 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
|
||||
invoker.execute(cmd1)
|
||||
invoker.execute(cmd2)
|
||||
invoker.undo()
|
||||
@@ -93,9 +104,9 @@ class CommandInvokerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("CommandInvoker discards redo history when executing mid-history"):
|
||||
val invoker = new CommandInvoker()
|
||||
val cmd1 = MoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd2 = MoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
|
||||
val cmd3 = MoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
|
||||
val cmd1 = createMoveCommand(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
val cmd2 = createMoveCommand(sq(File.E, Rank.R7), sq(File.E, Rank.R5))
|
||||
val cmd3 = createMoveCommand(sq(File.D, Rank.R2), sq(File.D, Rank.R4))
|
||||
invoker.execute(cmd1)
|
||||
invoker.execute(cmd2)
|
||||
invoker.undo()
|
||||
|
||||
@@ -20,7 +20,7 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
||||
print(Renderer.render(e.board))
|
||||
e.capturedPiece.foreach: cap =>
|
||||
println(s"Captured: $cap on ${e.toSquare}")
|
||||
println(s"${e.turn.label}'s turn. Enter move (or 'quit'/'q' to exit):")
|
||||
printPrompt(e.turn)
|
||||
|
||||
case e: CheckDetectedEvent =>
|
||||
println(s"${e.turn.label} is in check!")
|
||||
@@ -36,13 +36,13 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
||||
print(Renderer.render(e.board))
|
||||
|
||||
case e: InvalidMoveEvent =>
|
||||
println(s"Invalid move: ${e.reason}")
|
||||
println(s"⚠️ ${e.reason}")
|
||||
|
||||
case e: BoardResetEvent =>
|
||||
println("Board has been reset to initial position.")
|
||||
println()
|
||||
print(Renderer.render(e.board))
|
||||
println(s"${e.turn.label}'s turn. Enter move (or 'quit'/'q' to exit):")
|
||||
printPrompt(e.turn)
|
||||
|
||||
/** Start the terminal UI game loop. */
|
||||
def start(): Unit =
|
||||
@@ -52,7 +52,7 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
||||
// Show initial board
|
||||
println()
|
||||
print(Renderer.render(engine.board))
|
||||
println(s"${engine.turn.label}'s turn. Enter move (or 'quit'/'q' to exit):")
|
||||
printPrompt(engine.turn)
|
||||
|
||||
// Game loop
|
||||
while running do
|
||||
@@ -62,11 +62,16 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
||||
running = false
|
||||
println("Game over. Goodbye!")
|
||||
case "" =>
|
||||
println("Please enter a valid move.")
|
||||
printPrompt(engine.turn)
|
||||
case _ =>
|
||||
engine.processUserInput(input)
|
||||
|
||||
// Unsubscribe when done
|
||||
engine.unsubscribe(this)
|
||||
|
||||
private def printPrompt(turn: de.nowchess.api.board.Color): Unit =
|
||||
val undoHint = if engine.canUndo then " [undo]" else ""
|
||||
val redoHint = if engine.canRedo then " [redo]" else ""
|
||||
print(s"${turn.label}'s turn. Enter move (or 'quit'/'q' to exit)$undoHint$redoHint: ")
|
||||
|
||||
end TerminalUI
|
||||
|
||||
Reference in New Issue
Block a user