feat: update TerminalUI to handle pawn promotion I/O via awaitingPromotion flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package de.nowchess.ui.terminal
|
package de.nowchess.ui.terminal
|
||||||
|
|
||||||
import scala.io.StdIn
|
import scala.io.StdIn
|
||||||
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.chess.engine.GameEngine
|
import de.nowchess.chess.engine.GameEngine
|
||||||
import de.nowchess.chess.observer.{Observer, GameEvent, *}
|
import de.nowchess.chess.observer.{Observer, GameEvent, *}
|
||||||
import de.nowchess.chess.view.Renderer
|
import de.nowchess.chess.view.Renderer
|
||||||
@@ -11,6 +12,7 @@ import de.nowchess.chess.view.Renderer
|
|||||||
*/
|
*/
|
||||||
class TerminalUI(engine: GameEngine) extends Observer:
|
class TerminalUI(engine: GameEngine) extends Observer:
|
||||||
private var running = true
|
private var running = true
|
||||||
|
private var awaitingPromotion = false
|
||||||
|
|
||||||
/** Called by GameEngine whenever a game event occurs. */
|
/** Called by GameEngine whenever a game event occurs. */
|
||||||
override def onGameEvent(event: GameEvent): Unit =
|
override def onGameEvent(event: GameEvent): Unit =
|
||||||
@@ -44,6 +46,10 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
|||||||
print(Renderer.render(e.board))
|
print(Renderer.render(e.board))
|
||||||
printPrompt(e.turn)
|
printPrompt(e.turn)
|
||||||
|
|
||||||
|
case _: PromotionRequiredEvent =>
|
||||||
|
println("Promote to: q=Queen, r=Rook, b=Bishop, n=Knight")
|
||||||
|
awaitingPromotion = true
|
||||||
|
|
||||||
/** Start the terminal UI game loop. */
|
/** Start the terminal UI game loop. */
|
||||||
def start(): Unit =
|
def start(): Unit =
|
||||||
// Register as observer
|
// Register as observer
|
||||||
@@ -57,14 +63,24 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
|||||||
// Game loop
|
// Game loop
|
||||||
while running do
|
while running do
|
||||||
val input = Option(StdIn.readLine()).getOrElse("quit").trim
|
val input = Option(StdIn.readLine()).getOrElse("quit").trim
|
||||||
input.toLowerCase match
|
if awaitingPromotion then
|
||||||
case "quit" | "q" =>
|
input.toLowerCase match
|
||||||
running = false
|
case "q" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Queen)
|
||||||
println("Game over. Goodbye!")
|
case "r" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Rook)
|
||||||
case "" =>
|
case "b" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Bishop)
|
||||||
printPrompt(engine.turn)
|
case "n" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Knight)
|
||||||
case _ =>
|
case _ =>
|
||||||
engine.processUserInput(input)
|
println("Invalid choice. Enter q, r, b, or n.")
|
||||||
|
println("Promote to: q=Queen, r=Rook, b=Bishop, n=Knight")
|
||||||
|
else
|
||||||
|
input.toLowerCase match
|
||||||
|
case "quit" | "q" =>
|
||||||
|
running = false
|
||||||
|
println("Game over. Goodbye!")
|
||||||
|
case "" =>
|
||||||
|
printPrompt(engine.turn)
|
||||||
|
case _ =>
|
||||||
|
engine.processUserInput(input)
|
||||||
|
|
||||||
// Unsubscribe when done
|
// Unsubscribe when done
|
||||||
engine.unsubscribe(this)
|
engine.unsubscribe(this)
|
||||||
@@ -73,4 +89,3 @@ class TerminalUI(engine: GameEngine) extends Observer:
|
|||||||
val undoHint = if engine.canUndo then " [undo]" else ""
|
val undoHint = if engine.canUndo then " [undo]" else ""
|
||||||
val redoHint = if engine.canRedo then " [redo]" else ""
|
val redoHint = if engine.canRedo then " [redo]" else ""
|
||||||
print(s"${turn.label}'s turn. Enter move (or 'quit'/'q' to exit)$undoHint$redoHint: ")
|
print(s"${turn.label}'s turn. Enter move (or 'quit'/'q' to exit)$undoHint$redoHint: ")
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.scalatest.matchers.should.Matchers
|
|||||||
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
|
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
|
||||||
import de.nowchess.chess.engine.GameEngine
|
import de.nowchess.chess.engine.GameEngine
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.api.board.{Board, Color}
|
import de.nowchess.api.board.{Board, Color, File, Rank, Square}
|
||||||
import de.nowchess.chess.logic.GameHistory
|
import de.nowchess.chess.logic.GameHistory
|
||||||
|
|
||||||
class TerminalUITest extends AnyFunSuite with Matchers {
|
class TerminalUITest extends AnyFunSuite with Matchers {
|
||||||
@@ -186,4 +186,82 @@ class TerminalUITest extends AnyFunSuite with Matchers {
|
|||||||
// The move should have been processed and the board displayed
|
// The move should have been processed and the board displayed
|
||||||
engine.turn shouldBe Color.Black
|
engine.turn shouldBe Color.Black
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("TerminalUI shows promotion prompt on PromotionRequiredEvent") {
|
||||||
|
val out = new ByteArrayOutputStream()
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val ui = new TerminalUI(engine)
|
||||||
|
|
||||||
|
Console.withOut(out) {
|
||||||
|
ui.onGameEvent(PromotionRequiredEvent(
|
||||||
|
Board(Map.empty), GameHistory(), Color.White,
|
||||||
|
Square(File.E, Rank.R7), Square(File.E, Rank.R8)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
out.toString should include("Promote to")
|
||||||
|
}
|
||||||
|
|
||||||
|
test("TerminalUI routes promotion choice to engine.completePromotion") {
|
||||||
|
import de.nowchess.api.move.PromotionPiece
|
||||||
|
|
||||||
|
var capturedPiece: Option[PromotionPiece] = None
|
||||||
|
|
||||||
|
val engine = new GameEngine() {
|
||||||
|
override def processUserInput(rawInput: String): Unit =
|
||||||
|
if rawInput.trim == "e7e8" then
|
||||||
|
notifyObservers(PromotionRequiredEvent(
|
||||||
|
Board(Map.empty), GameHistory.empty, Color.White,
|
||||||
|
Square(File.E, Rank.R7), Square(File.E, Rank.R8)
|
||||||
|
))
|
||||||
|
override def completePromotion(piece: PromotionPiece): Unit =
|
||||||
|
capturedPiece = Some(piece)
|
||||||
|
notifyObservers(MoveExecutedEvent(Board(Map.empty), GameHistory.empty, Color.Black, "e7", "e8", None))
|
||||||
|
}
|
||||||
|
|
||||||
|
val in = new ByteArrayInputStream("e7e8\nq\nquit\n".getBytes)
|
||||||
|
val out = new ByteArrayOutputStream()
|
||||||
|
val ui = new TerminalUI(engine)
|
||||||
|
|
||||||
|
Console.withIn(in) {
|
||||||
|
Console.withOut(out) {
|
||||||
|
ui.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capturedPiece should be(Some(PromotionPiece.Queen))
|
||||||
|
out.toString should include("Promote to")
|
||||||
|
}
|
||||||
|
|
||||||
|
test("TerminalUI re-prompts on invalid promotion choice") {
|
||||||
|
import de.nowchess.api.move.PromotionPiece
|
||||||
|
|
||||||
|
var capturedPiece: Option[PromotionPiece] = None
|
||||||
|
|
||||||
|
val engine = new GameEngine() {
|
||||||
|
override def processUserInput(rawInput: String): Unit =
|
||||||
|
if rawInput.trim == "e7e8" then
|
||||||
|
notifyObservers(PromotionRequiredEvent(
|
||||||
|
Board(Map.empty), GameHistory.empty, Color.White,
|
||||||
|
Square(File.E, Rank.R7), Square(File.E, Rank.R8)
|
||||||
|
))
|
||||||
|
override def completePromotion(piece: PromotionPiece): Unit =
|
||||||
|
capturedPiece = Some(piece)
|
||||||
|
notifyObservers(MoveExecutedEvent(Board(Map.empty), GameHistory.empty, Color.Black, "e7", "e8", None))
|
||||||
|
}
|
||||||
|
|
||||||
|
// "x" is invalid, then "r" for rook
|
||||||
|
val in = new ByteArrayInputStream("e7e8\nx\nr\nquit\n".getBytes)
|
||||||
|
val out = new ByteArrayOutputStream()
|
||||||
|
val ui = new TerminalUI(engine)
|
||||||
|
|
||||||
|
Console.withIn(in) {
|
||||||
|
Console.withOut(out) {
|
||||||
|
ui.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capturedPiece should be(Some(PromotionPiece.Rook))
|
||||||
|
out.toString should include("Invalid")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user