refactor: NCS-19 Currying (#18)

Summary

  - Curried candidateMoves, legalMoves, and applyMove in the RuleSet trait to separate (context) as the world being
  operated on from the computation parameter
  - Updated DefaultRules overrides and all internal call sites
  - Updated all external call sites: GameEngine, PgnParser, PgnExporter, ChessBoardView, and all affected tests

  Test plan

  - All existing tests pass (./gradlew build)
  - No behaviour changes — pure style refactoring, existing test suite is the regression guard

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #18
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
This commit is contained in:
2026-04-06 21:03:17 +02:00
committed by shahdlala66
parent b4bf447453
commit cfa4429d49
9 changed files with 59 additions and 59 deletions
@@ -88,7 +88,7 @@ class GameEngine(
case Some(piece) if piece.color != currentContext.turn =>
notifyObservers(InvalidMoveEvent(currentContext, "That is not your piece."))
case Some(piece) =>
val legal = ruleSet.legalMoves(currentContext, from)
val legal = ruleSet.legalMoves(currentContext)(from)
// Find all legal moves going to `to`
val candidates = legal.filter(_.to == to)
candidates match
@@ -119,7 +119,7 @@ class GameEngine(
pendingPromotion = None
val move = Move(pending.from, pending.to, MoveType.Promotion(piece))
// Verify it's actually legal
val legal = ruleSet.legalMoves(currentContext, pending.from)
val legal = ruleSet.legalMoves(currentContext)(pending.from)
if legal.contains(move) then
executeMove(move)
else
@@ -203,7 +203,7 @@ class GameEngine(
private def executeMove(move: Move): Unit =
val contextBefore = currentContext
val nextContext = ruleSet.applyMove(currentContext, move)
val nextContext = ruleSet.applyMove(currentContext)(move)
val captured = computeCaptured(currentContext, move)
val cmd = MoveCommand(
@@ -89,8 +89,8 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
val promotionMove = Move(sq("e2"), sq("e8"), MoveType.Promotion(PromotionPiece.Queen))
val permissiveRules = new RuleSet:
def candidateMoves(context: GameContext, square: Square): List[Move] = legalMoves(context, square)
def legalMoves(context: GameContext, square: Square): List[Move] =
def candidateMoves(context: GameContext)(square: Square): List[Move] = legalMoves(context)(square)
def legalMoves(context: GameContext)(square: Square): List[Move] =
if square == sq("e2") then List(promotionMove) else List.empty
def allLegalMoves(context: GameContext): List[Move] = List(promotionMove)
def isCheck(context: GameContext): Boolean = false
@@ -98,7 +98,7 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
def isStalemate(context: GameContext): Boolean = false
def isInsufficientMaterial(context: GameContext): Boolean = false
def isFiftyMoveRule(context: GameContext): Boolean = false
def applyMove(context: GameContext, move: Move): GameContext = DefaultRules.applyMove(context, move)
def applyMove(context: GameContext)(move: Move): GameContext = DefaultRules.applyMove(context)(move)
val engine = new GameEngine(ruleSet = permissiveRules)
val importer = new GameContextImport:
@@ -111,15 +111,15 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
test("loadGame replay restores previous context when promotion cannot be completed"):
val promotionMove = Move(sq("e2"), sq("e8"), MoveType.Promotion(PromotionPiece.Queen))
val noLegalMoves = new RuleSet:
def candidateMoves(context: GameContext, square: Square): List[Move] = List.empty
def legalMoves(context: GameContext, square: Square): List[Move] = List.empty
def candidateMoves(context: GameContext)(square: Square): List[Move] = List.empty
def legalMoves(context: GameContext)(square: Square): List[Move] = List.empty
def allLegalMoves(context: GameContext): List[Move] = List.empty
def isCheck(context: GameContext): Boolean = false
def isCheckmate(context: GameContext): Boolean = false
def isStalemate(context: GameContext): Boolean = false
def isInsufficientMaterial(context: GameContext): Boolean = false
def isFiftyMoveRule(context: GameContext): Boolean = false
def applyMove(context: GameContext, move: Move): GameContext = context
def applyMove(context: GameContext)(move: Move): GameContext = context
val engine = new GameEngine(ruleSet = noLegalMoves)
engine.processUserInput("e2e4")
@@ -153,10 +153,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
// This makes completePromotion unable to find Move(from, to, Promotion(Queen)),
// triggering the "Error completing promotion." branch.
val delegatingRuleSet: RuleSet = new RuleSet:
def candidateMoves(context: GameContext, square: Square): List[Move] =
DefaultRules.candidateMoves(context, square)
def legalMoves(context: GameContext, square: Square): List[Move] =
DefaultRules.legalMoves(context, square).map { m =>
def candidateMoves(context: GameContext)(square: Square): List[Move] =
DefaultRules.candidateMoves(context)(square)
def legalMoves(context: GameContext)(square: Square): List[Move] =
DefaultRules.legalMoves(context)(square).map { m =>
m.moveType match
case MoveType.Promotion(_) => Move(m.from, m.to, MoveType.Normal())
case _ => m
@@ -173,8 +173,8 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
DefaultRules.isInsufficientMaterial(context)
def isFiftyMoveRule(context: GameContext): Boolean =
DefaultRules.isFiftyMoveRule(context)
def applyMove(context: GameContext, move: Move): GameContext =
DefaultRules.applyMove(context, move)
def applyMove(context: GameContext)(move: Move): GameContext =
DefaultRules.applyMove(context)(move)
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
val initialCtx = GameContext.initial.withBoard(promotionBoard).withTurn(Color.White)