refactor: migrate GameController to GameContext (signatures only)
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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!")
|
||||
|
||||
Reference in New Issue
Block a user