fix: add thread synchronization to awaitingPromotion and complete coverage tests for all promotion pieces

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 19:56:41 +02:00
parent 8adff2d527
commit c0719c1696
2 changed files with 81 additions and 19 deletions
@@ -48,7 +48,7 @@ class TerminalUI(engine: GameEngine) extends Observer:
case _: PromotionRequiredEvent =>
println("Promote to: q=Queen, r=Rook, b=Bishop, n=Knight")
awaitingPromotion = true
synchronized { awaitingPromotion = true }
/** Start the terminal UI game loop. */
def start(): Unit =
@@ -63,24 +63,26 @@ class TerminalUI(engine: GameEngine) extends Observer:
// Game loop
while running do
val input = Option(StdIn.readLine()).getOrElse("quit").trim
if awaitingPromotion then
input.toLowerCase match
case "q" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Queen)
case "r" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Rook)
case "b" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Bishop)
case "n" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Knight)
case _ =>
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)
synchronized {
if awaitingPromotion then
input.toLowerCase match
case "q" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Queen)
case "r" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Rook)
case "b" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Bishop)
case "n" => awaitingPromotion = false; engine.completePromotion(PromotionPiece.Knight)
case _ =>
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
engine.unsubscribe(this)
@@ -264,4 +264,64 @@ class TerminalUITest extends AnyFunSuite with Matchers {
capturedPiece should be(Some(PromotionPiece.Rook))
out.toString should include("Invalid")
}
test("TerminalUI routes Bishop 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\nb\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.Bishop))
}
test("TerminalUI routes Knight 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\nn\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.Knight))
}
}