refactor: migrate GameController to GameContext (signatures only)
Build & Test (NowChessSystems) TeamCity build finished

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LQ63
2026-03-24 13:04:57 +01:00
parent c9a59d3ad1
commit 62e180c6d9
3 changed files with 66 additions and 62 deletions
@@ -1,10 +1,11 @@
package de.nowchess.chess
import de.nowchess.api.board.{Board, Color}
import de.nowchess.api.board.Color
import de.nowchess.chess.controller.GameController
import de.nowchess.chess.logic.GameContext
object Main {
def main(args: Array[String]): Unit =
println("NowChess TUI — type moves in coordinate notation (e.g. e2e4). Type 'quit' to exit.")
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
}
@@ -11,15 +11,15 @@ import de.nowchess.chess.view.Renderer
sealed trait MoveResult
object MoveResult:
case object Quit extends MoveResult
case class InvalidFormat(raw: String) extends MoveResult
case object NoPiece extends MoveResult
case object WrongColor extends MoveResult
case object IllegalMove extends MoveResult
case class Moved(newBoard: Board, captured: Option[Piece], newTurn: Color) extends MoveResult
case class MovedInCheck(newBoard: Board, captured: Option[Piece], newTurn: Color) extends MoveResult
case class Checkmate(winner: Color) extends MoveResult
case object Stalemate extends MoveResult
case object Quit extends MoveResult
case class InvalidFormat(raw: String) extends MoveResult
case object NoPiece extends MoveResult
case object WrongColor extends MoveResult
case object IllegalMove extends MoveResult
case class Moved(newCtx: GameContext, captured: Option[Piece], newTurn: Color) extends MoveResult
case class MovedInCheck(newCtx: GameContext, captured: Option[Piece], newTurn: Color) extends MoveResult
case class Checkmate(winner: Color) extends MoveResult
case object Stalemate extends MoveResult
// ---------------------------------------------------------------------------
// Controller
@@ -27,10 +27,10 @@ object MoveResult:
object GameController:
/** Pure function: interprets one raw input line against the current board state.
/** Pure function: interprets one raw input line against the current game context.
* Has no I/O side effects all output must be handled by the caller.
*/
def processMove(board: Board, turn: Color, raw: String): MoveResult =
def processMove(ctx: GameContext, turn: Color, raw: String): MoveResult =
raw.trim match
case "quit" | "q" =>
MoveResult.Quit
@@ -39,61 +39,62 @@ object GameController:
case None =>
MoveResult.InvalidFormat(trimmed)
case Some((from, to)) =>
board.pieceAt(from) match
ctx.board.pieceAt(from) match
case None =>
MoveResult.NoPiece
case Some(piece) if piece.color != turn =>
MoveResult.WrongColor
case Some(_) =>
if !MoveValidator.isLegal(board, from, to) then
if !MoveValidator.isLegal(ctx, from, to) then
MoveResult.IllegalMove
else
val (newBoard, captured) = board.withMove(from, to)
GameRules.gameStatus(GameContext(newBoard), turn.opposite) match
case PositionStatus.Normal => MoveResult.Moved(newBoard, captured, turn.opposite)
case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, captured, turn.opposite)
val (newBoard, captured) = ctx.board.withMove(from, to)
val newCtx = ctx.copy(board = newBoard)
GameRules.gameStatus(newCtx, turn.opposite) match
case PositionStatus.Normal => MoveResult.Moved(newCtx, captured, turn.opposite)
case PositionStatus.InCheck => MoveResult.MovedInCheck(newCtx, captured, turn.opposite)
case PositionStatus.Mated => MoveResult.Checkmate(turn)
case PositionStatus.Drawn => MoveResult.Stalemate
/** Thin I/O shell: renders the board, reads a line, delegates to processMove,
* prints the outcome, and recurses until the game ends.
*/
def gameLoop(board: Board, turn: Color): Unit =
def gameLoop(ctx: GameContext, turn: Color): Unit =
println()
print(Renderer.render(board))
print(Renderer.render(ctx.board))
println(s"${turn.label}'s turn. Enter move: ")
val input = Option(StdIn.readLine()).getOrElse("quit").trim
processMove(board, turn, input) match
processMove(ctx, turn, input) match
case MoveResult.Quit =>
println("Game over. Goodbye!")
case MoveResult.InvalidFormat(raw) =>
println(s"Invalid move format '$raw'. Use coordinate notation, e.g. e2e4.")
gameLoop(board, turn)
gameLoop(ctx, turn)
case MoveResult.NoPiece =>
println(s"No piece on ${Parser.parseMove(input).map(_._1).fold("?")(_.toString)}.")
gameLoop(board, turn)
gameLoop(ctx, turn)
case MoveResult.WrongColor =>
println(s"That is not your piece.")
gameLoop(board, turn)
gameLoop(ctx, turn)
case MoveResult.IllegalMove =>
println(s"Illegal move.")
gameLoop(board, turn)
case MoveResult.Moved(newBoard, captured, newTurn) =>
gameLoop(ctx, turn)
case MoveResult.Moved(newCtx, captured, newTurn) =>
val prevTurn = newTurn.opposite
captured.foreach: cap =>
val toSq = Parser.parseMove(input).map(_._2).fold("?")(_.toString)
println(s"${prevTurn.label} captures ${cap.color.label} ${cap.pieceType.label} on $toSq")
gameLoop(newBoard, newTurn)
case MoveResult.MovedInCheck(newBoard, captured, newTurn) =>
gameLoop(newCtx, newTurn)
case MoveResult.MovedInCheck(newCtx, captured, newTurn) =>
val prevTurn = newTurn.opposite
captured.foreach: cap =>
val toSq = Parser.parseMove(input).map(_._2).fold("?")(_.toString)
println(s"${prevTurn.label} captures ${cap.color.label} ${cap.pieceType.label} on $toSq")
println(s"${newTurn.label} is in check!")
gameLoop(newBoard, newTurn)
gameLoop(newCtx, newTurn)
case MoveResult.Checkmate(winner) =>
println(s"Checkmate! ${winner.label} wins.")
gameLoop(Board.initial, Color.White)
gameLoop(GameContext.initial, Color.White)
case MoveResult.Stalemate =>
println("Stalemate! The game is a draw.")
gameLoop(Board.initial, Color.White)
gameLoop(GameContext.initial, Color.White)
@@ -1,6 +1,8 @@
package de.nowchess.chess.controller
import de.nowchess.api.board.*
import de.nowchess.api.game.CastlingRights
import de.nowchess.chess.logic.{GameContext, CastleSide}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -9,7 +11,7 @@ import java.io.ByteArrayInputStream
class GameControllerTest extends AnyFunSuite with Matchers:
private def sq(f: File, r: Rank): Square = Square(f, r)
private val initial = Board.initial
private val initial = GameContext.initial
// ──── processMove ────────────────────────────────────────────────────
@@ -39,24 +41,24 @@ class GameControllerTest extends AnyFunSuite with Matchers:
test("processMove: legal pawn move returns Moved with updated board and flipped turn"):
GameController.processMove(initial, Color.White, "e2e4") match
case MoveResult.Moved(newBoard, captured, newTurn) =>
newBoard.pieceAt(sq(File.E, Rank.R4)) shouldBe Some(Piece.WhitePawn)
newBoard.pieceAt(sq(File.E, Rank.R2)) shouldBe None
case MoveResult.Moved(newCtx, captured, newTurn) =>
newCtx.board.pieceAt(sq(File.E, Rank.R4)) shouldBe Some(Piece.WhitePawn)
newCtx.board.pieceAt(sq(File.E, Rank.R2)) shouldBe None
captured shouldBe None
newTurn shouldBe Color.Black
case other => fail(s"Expected Moved, got $other")
test("processMove: legal capture returns Moved with the captured piece"):
val captureBoard = Board(Map(
val captureCtx = GameContext(Board(Map(
sq(File.E, Rank.R5) -> Piece.WhitePawn,
sq(File.D, Rank.R6) -> Piece.BlackPawn,
sq(File.H, Rank.R1) -> Piece.BlackKing,
sq(File.H, Rank.R8) -> Piece.WhiteKing
))
GameController.processMove(captureBoard, Color.White, "e5d6") match
case MoveResult.Moved(newBoard, captured, newTurn) =>
)))
GameController.processMove(captureCtx, Color.White, "e5d6") match
case MoveResult.Moved(newCtx, captured, newTurn) =>
captured shouldBe Some(Piece.BlackPawn)
newBoard.pieceAt(sq(File.D, Rank.R6)) shouldBe Some(Piece.WhitePawn)
newCtx.board.pieceAt(sq(File.D, Rank.R6)) shouldBe Some(Piece.WhitePawn)
newTurn shouldBe Color.Black
case other => fail(s"Expected Moved, got $other")
@@ -68,33 +70,33 @@ class GameControllerTest extends AnyFunSuite with Matchers:
test("gameLoop: 'quit' exits cleanly without exception"):
withInput("quit\n"):
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
test("gameLoop: EOF (null readLine) exits via quit fallback"):
withInput(""):
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
test("gameLoop: invalid format prints message and recurses until quit"):
withInput("badmove\nquit\n"):
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
test("gameLoop: NoPiece prints message and recurses until quit"):
// E3 is empty in the initial position
withInput("e3e4\nquit\n"):
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
test("gameLoop: WrongColor prints message and recurses until quit"):
// E7 has a Black pawn; it is White's turn
withInput("e7e6\nquit\n"):
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
test("gameLoop: IllegalMove prints message and recurses until quit"):
withInput("e2e5\nquit\n"):
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
test("gameLoop: legal non-capture move recurses with new board then quits"):
withInput("e2e4\nquit\n"):
GameController.gameLoop(Board.initial, Color.White)
GameController.gameLoop(GameContext.initial, Color.White)
test("gameLoop: capture move prints capture message then recurses and quits"):
val captureBoard = Board(Map(
@@ -104,7 +106,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
sq(File.H, Rank.R8) -> Piece.WhiteKing
))
withInput("e5d6\nquit\n"):
GameController.gameLoop(captureBoard, Color.White)
GameController.gameLoop(GameContext(captureBoard), Color.White)
// ──── helpers ────────────────────────────────────────────────────────
@@ -118,12 +120,12 @@ class GameControllerTest extends AnyFunSuite with Matchers:
test("processMove: legal move that delivers check returns MovedInCheck"):
// White Ra1, Ka3; Black Kh8 — White plays Ra1-Ra8, Ra8 attacks rank 8 putting Kh8 in check
// Kh8 can escape to g7/g8/h7 so this is InCheck, not Mated
val b = Board(Map(
val ctx = GameContext(Board(Map(
sq(File.A, Rank.R1) -> Piece.WhiteRook,
sq(File.C, Rank.R3) -> Piece.WhiteKing,
sq(File.H, Rank.R8) -> Piece.BlackKing
))
GameController.processMove(b, Color.White, "a1a8") match
)))
GameController.processMove(ctx, Color.White, "a1a8") match
case MoveResult.MovedInCheck(_, _, newTurn) => newTurn shouldBe Color.Black
case other => fail(s"Expected MovedInCheck, got $other")
@@ -131,24 +133,24 @@ class GameControllerTest extends AnyFunSuite with Matchers:
// White Qa1, Ka6; Black Ka8 — White plays Qa1-Qh8 (diagonal a1→h8)
// After Qh8: White Qh8 + Ka6 vs Black Ka8 = checkmate (spec-verified position)
// Qa1 does NOT currently attack Ka8 — path along file A is blocked by Ka6
val b = Board(Map(
val ctx = GameContext(Board(Map(
sq(File.A, Rank.R1) -> Piece.WhiteQueen,
sq(File.A, Rank.R6) -> Piece.WhiteKing,
sq(File.A, Rank.R8) -> Piece.BlackKing
))
GameController.processMove(b, Color.White, "a1h8") match
)))
GameController.processMove(ctx, Color.White, "a1h8") match
case MoveResult.Checkmate(winner) => winner shouldBe Color.White
case other => fail(s"Expected Checkmate(White), got $other")
test("processMove: legal move that results in stalemate returns Stalemate"):
// White Qb1, Kc6; Black Ka8 — White plays Qb1-Qb6
// After Qb6: White Qb6 + Kc6 vs Black Ka8 = stalemate (spec-verified position)
val b = Board(Map(
val ctx = GameContext(Board(Map(
sq(File.B, Rank.R1) -> Piece.WhiteQueen,
sq(File.C, Rank.R6) -> Piece.WhiteKing,
sq(File.A, Rank.R8) -> Piece.BlackKing
))
GameController.processMove(b, Color.White, "b1b6") match
)))
GameController.processMove(ctx, Color.White, "b1b6") match
case MoveResult.Stalemate => succeed
case other => fail(s"Expected Stalemate, got $other")
@@ -163,7 +165,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
))
val output = captureOutput:
withInput("a1h8\nquit\n"):
GameController.gameLoop(b, Color.White)
GameController.gameLoop(GameContext(b), Color.White)
output should include("Checkmate! White wins.")
test("gameLoop: stalemate prints draw message and resets to new game"):
@@ -174,7 +176,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
))
val output = captureOutput:
withInput("b1b6\nquit\n"):
GameController.gameLoop(b, Color.White)
GameController.gameLoop(GameContext(b), Color.White)
output should include("Stalemate! The game is a draw.")
test("gameLoop: MovedInCheck without capture prints check message"):
@@ -185,7 +187,7 @@ class GameControllerTest extends AnyFunSuite with Matchers:
))
val output = captureOutput:
withInput("a1a8\nquit\n"):
GameController.gameLoop(b, Color.White)
GameController.gameLoop(GameContext(b), Color.White)
output should include("Black is in check!")
test("gameLoop: MovedInCheck with capture prints both capture and check message"):
@@ -198,6 +200,6 @@ class GameControllerTest extends AnyFunSuite with Matchers:
))
val output = captureOutput:
withInput("a1a8\nquit\n"):
GameController.gameLoop(b, Color.White)
GameController.gameLoop(GameContext(b), Color.White)
output should include("captures")
output should include("Black is in check!")