From 62e180c6d9ef7401540bd33945ebf4b679fad2d3 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 24 Mar 2026 13:04:57 +0100 Subject: [PATCH] refactor: migrate GameController to GameContext (signatures only) Co-Authored-By: Claude Sonnet 4.6 --- .../main/scala/de/nowchess/chess/Main.scala | 5 +- .../chess/controller/GameController.scala | 61 +++++++++--------- .../chess/controller/GameControllerTest.scala | 62 ++++++++++--------- 3 files changed, 66 insertions(+), 62 deletions(-) diff --git a/modules/core/src/main/scala/de/nowchess/chess/Main.scala b/modules/core/src/main/scala/de/nowchess/chess/Main.scala index eee7624..234e025 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/Main.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/Main.scala @@ -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) } diff --git a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala index d08f0fd..dd10f68 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala @@ -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) diff --git a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala index 0f8a544..b5b3055 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala @@ -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!")