- Removed unused CastleSide import from PgnExporter - Updated TerminalUI and GUIObserver to use GameContext - Temporarily disabled GUI FEN/PGN import-export (requires full rework with GameContext) - Deleted unused logic files and GameController per spec Note: GameEngine still needs final refactoring to use RuleSet + GameContext. Core architecture (api -> rule -> core) is structurally complete.
8.1 KiB
Module Refactor: Interface Abstraction Layer — NCS-22
Date: 2026-04-03
Epic: NCS-20 (Reduce Token Usage)
Task: NCS-22 (Split module into smaller modules)
Author: Claude Code
Status: Design Approved
Objective
Refactor NowChessSystems from a monolithic modules/core into a three-layer architecture with clean interface boundaries:
- Reduce complexity and token usage
- Extract rules logic into dedicated
modules/rule - Establish RuleSet as the single source of truth for all chess rule decisions
- Enable future rule variants (Chess960, etc.) via interface implementations
Current State
modules/core conflates multiple concerns:
GameEngine(state management, observer pattern)GameController(move validation orchestration)GameRules,MoveValidator,CastlingRightsCalculator,EnPassantCalculator(rule logic)- Notation (PGN/FEN parsing and export)
- Command/undo system
Problem: GameEngine depends directly on validation logic; no abstraction boundary; rules tightly coupled to engine implementation.
modules/rule (stubbed):
RuleSettrait: defines interface for rule queriesStandardRules(partial): scaffolded but uses different package/type names
Proposed Architecture
Three-Layer Model
┌─ modules/api ─────────────────────────────────┐
│ Shared types: GameContext, Board, Move, etc. │
└───────────────────────────────────────────────┘
↑
┌─ modules/rule ────────────────────────────────┐
│ RuleSet trait (interface) │
│ StandardRules (implementation) │
│ All move generation & validation logic │
└───────────────────────────────────────────────┘
↑
┌─ modules/core ────────────────────────────────┐
│ GameEngine (state + observer pattern) │
│ Command/undo system │
│ Notation parsers (PGN/FEN) │
└───────────────────────────────────────────────┘
Dependencies:
modules/ruledepends onmodules/apimodules/coredepends onmodules/ruleandmodules/api- No circular dependencies
modules/apidepends only on std library
Core Types
GameContext (new, in modules/api)
Immutable value type bundling complete game state:
case class GameContext(
board: Board,
turn: Color,
castlingRights: CastlingRights,
enPassantSquare: Option[Square],
halfMoveClock: Int,
moves: List[Move] // game history
):
def withBoard(newBoard: Board): GameContext = copy(board = newBoard)
def withTurn(newTurn: Color): GameContext = copy(turn = newTurn)
def withMove(move: Move): GameContext = copy(moves = moves :+ move)
// ... other immutable updates
Replaces both Situation (from StandardRules) and GameHistory (from GameEngine).
RuleSet (in modules/rule)
Single source of truth for all rule decisions:
trait RuleSet:
def candidateMoves(context: GameContext, square: Square): List[Move]
def legalMoves(context: GameContext, square: Square): List[Move]
def allLegalMoves(context: GameContext): List[Move]
def isCheck(context: GameContext): Boolean
def isCheckmate(context: GameContext): Boolean
def isStalemate(context: GameContext): Boolean
def isInsufficientMaterial(context: GameContext): Boolean
def isFiftyMoveRule(context: GameContext): Boolean
StandardRules (in modules/rule)
Concrete implementation of RuleSet for standard chess:
- Move generation (pawns, knights, bishops, rooks, queens, kings, castling, en passant)
- Check/checkmate/stalemate detection
- Insufficient material detection
- 50-move rule tracking
Refactored from existing StandardRules scaffold to use NowChess types and naming conventions. No manual logic duplication from modules/core/logic/*.
GameEngine Refactoring
Before: GameEngine → GameController → GameRules/MoveValidator
After: GameEngine → RuleSet directly
Move from:
GameController.processMove(board, history, turn, moveInput) match
case MoveResult.Moved(...) => ...
To:
val moves = ruleSet.legalMoves(context, from)
if moves.contains(move) then
val newBoard = board.applyMove(move)
val newContext = context.withBoard(newBoard).withMove(move)
// emit event
Removed:
modules/core/controller/GameController.scala(logic → RuleSet, orchestration → GameEngine)- All rule logic from
modules/core/logic/*(→ modules/rule)
Retained:
- Command/undo system (depends on GameContext instead of GameHistory)
- Observer pattern (event notifications)
- PGN/FEN parsing and export
Design Decisions
Why Immutable GameContext?
- Enables replay: Undo/redo regenerate state from commands
- Thread-safe: No synchronization needed for reads
- Testable: Each state change is explicit
- Composable: Easier to build derived contexts
Why Remove GameController?
- Not an abstraction: It's implementation detail orchestration
- Duplicates logic: Validates moves, applies moves, checks outcomes — all in RuleSet now
- Single Responsibility: GameEngine handles I/O and state, RuleSet handles rules
Why RuleSet as interface?
- Extensibility: Chess960, variants inherit from RuleSet
- Testability: Mock RuleSet for engine tests
- Clear contract: Engine doesn't need to know how moves are generated, only that RuleSet provides them
Test Strategy
- No manual board construction: Use FEN for position setup
- Use PGN for move validation: Assert sequences of moves are legal
- RuleSet tests: Direct unit tests of move generation, check detection, etc. (all via FEN/PGN)
- GameEngine tests: Verify event emission and state transitions with RuleSet mocks or real RuleSet
Files to Create/Modify
| Action | File | Purpose |
|---|---|---|
| Create | modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala |
Immutable game state |
| Refactor | modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala |
Interface definition |
| Rewrite | modules/rule/src/main/scala/de/nowchess/rules/StandardRules.scala |
Implementation (adapted from scaffold) |
| Create | modules/rule/build.gradle.kts |
Gradle config with api dependency |
| Refactor | modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala |
Call RuleSet directly |
| Delete | modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala |
No longer needed |
| Delete | modules/core/src/main/scala/de/nowchess/chess/logic/*.scala |
Move to modules/rule |
| Update | modules/core/build.gradle.kts |
Add rule dependency |
| Update | settings.gradle.kts |
Already includes rule; no changes needed |
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| GameEngine refactor breaks observer/undo | Keep observer and command patterns intact; only change what RuleSet returns |
| GameContext replaces two types (Situation/GameHistory) | Design GameContext upfront; validate it works with undo/redo before full migration |
| Move logic extraction from core is fragile | Extract incrementally: extract one type at a time, validate with existing tests first |
| PGN/FEN still depend on core classes | Create wrapper types in api if needed; avoid circular deps |
Done Criteria
- GameContext type created and used in RuleSet
- RuleSet interface and StandardRules implementation complete
- GameEngine refactored to call RuleSet (no GameController)
- All rule logic extracted from modules/core to modules/rule
- No circular dependencies
- Build succeeds
- Regression tests written using FEN/PGN (not manual boards)
- Code freeze can be lifted