refactor(core): integrate Rule module and update GameContext handling
This commit is contained in:
@@ -6,9 +6,9 @@ import de.nowchess.api.game.GameContext
|
|||||||
import de.nowchess.chess.controller.Parser
|
import de.nowchess.chess.controller.Parser
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult}
|
import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult}
|
||||||
import de.nowchess.chess.notation.{PgnExporter, PgnParser}
|
import de.nowchess.io.pgn.PgnParser
|
||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.StandardRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
|
|
||||||
/** Pure game engine that manages game state and notifies observers of state changes.
|
/** Pure game engine that manages game state and notifies observers of state changes.
|
||||||
* All rule queries delegate to the injected RuleSet.
|
* All rule queries delegate to the injected RuleSet.
|
||||||
@@ -16,7 +16,7 @@ import de.nowchess.rules.sets.StandardRules
|
|||||||
*/
|
*/
|
||||||
class GameEngine(
|
class GameEngine(
|
||||||
initialContext: GameContext = GameContext.initial,
|
initialContext: GameContext = GameContext.initial,
|
||||||
ruleSet: RuleSet = StandardRules
|
ruleSet: RuleSet = DefaultRules
|
||||||
) extends Observable:
|
) extends Observable:
|
||||||
private var currentContext: GameContext = initialContext
|
private var currentContext: GameContext = initialContext
|
||||||
private val invoker = new CommandInvoker()
|
private val invoker = new CommandInvoker()
|
||||||
@@ -146,25 +146,16 @@ class GameEngine(
|
|||||||
invoker.clear()
|
invoker.clear()
|
||||||
|
|
||||||
var error: Option[String] = None
|
var error: Option[String] = None
|
||||||
import scala.util.control.Breaks._
|
game.moves.foreach: histMove =>
|
||||||
breakable {
|
|
||||||
game.moves.foreach { histMove =>
|
|
||||||
handleParsedMove(histMove.from, histMove.to)
|
handleParsedMove(histMove.from, histMove.to)
|
||||||
histMove.promotionPiece.foreach(completePromotion)
|
histMove.promotionPiece.foreach(completePromotion)
|
||||||
// $COVERAGE-OFF$ unreachable: PgnParser.validatePgn ensures promotion annotation is present
|
|
||||||
if pendingPromotion.isDefined && histMove.promotionPiece.isEmpty then
|
if pendingPromotion.isDefined && histMove.promotionPiece.isEmpty then
|
||||||
error = Some(s"Promotion required for move ${histMove.from}${histMove.to}")
|
error = Some(s"Promotion required for move ${histMove.from}${histMove.to}")
|
||||||
break()
|
|
||||||
// $COVERAGE-ON$
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error match
|
error match
|
||||||
// $COVERAGE-OFF$ unreachable: error is only set in the unreachable block above
|
|
||||||
case Some(err) =>
|
case Some(err) =>
|
||||||
currentContext = savedContext
|
currentContext = savedContext
|
||||||
Left(err)
|
Left(err)
|
||||||
// $COVERAGE-ON$
|
|
||||||
case None =>
|
case None =>
|
||||||
notifyObservers(PgnLoadedEvent(currentContext))
|
notifyObservers(PgnLoadedEvent(currentContext))
|
||||||
Right(())
|
Right(())
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class GameEngineLoadPgnTest extends AnyFunSuite with Matchers:
|
|||||||
test("loadPosition: fires BoardResetEvent and updates context"):
|
test("loadPosition: fires BoardResetEvent and updates context"):
|
||||||
import de.nowchess.api.board.{Board, Color}
|
import de.nowchess.api.board.{Board, Color}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.chess.notation.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
|
|
||||||
val engine = new GameEngine()
|
val engine = new GameEngine()
|
||||||
val cap = new EventCapture()
|
val cap = new EventCapture()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.nowchess.chess.engine
|
|||||||
import de.nowchess.api.board.{Board, Color, File, Rank, Square}
|
import de.nowchess.api.board.{Board, Color, File, Rank, Square}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.chess.notation.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|||||||
+11
-11
@@ -3,10 +3,10 @@ package de.nowchess.chess.engine
|
|||||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.chess.notation.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.StandardRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
@@ -154,27 +154,27 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
// triggering the "Error completing promotion." branch.
|
// triggering the "Error completing promotion." branch.
|
||||||
val delegatingRuleSet: RuleSet = new RuleSet:
|
val delegatingRuleSet: RuleSet = new RuleSet:
|
||||||
def candidateMoves(context: GameContext, square: Square): List[Move] =
|
def candidateMoves(context: GameContext, square: Square): List[Move] =
|
||||||
StandardRules.candidateMoves(context, square)
|
DefaultRules.candidateMoves(context, square)
|
||||||
def legalMoves(context: GameContext, square: Square): List[Move] =
|
def legalMoves(context: GameContext, square: Square): List[Move] =
|
||||||
StandardRules.legalMoves(context, square).map { m =>
|
DefaultRules.legalMoves(context, square).map { m =>
|
||||||
m.moveType match
|
m.moveType match
|
||||||
case MoveType.Promotion(_) => Move(m.from, m.to, MoveType.Normal)
|
case MoveType.Promotion(_) => Move(m.from, m.to, MoveType.Normal)
|
||||||
case _ => m
|
case _ => m
|
||||||
}
|
}
|
||||||
def allLegalMoves(context: GameContext): List[Move] =
|
def allLegalMoves(context: GameContext): List[Move] =
|
||||||
StandardRules.allLegalMoves(context)
|
DefaultRules.allLegalMoves(context)
|
||||||
def isCheck(context: GameContext): Boolean =
|
def isCheck(context: GameContext): Boolean =
|
||||||
StandardRules.isCheck(context)
|
DefaultRules.isCheck(context)
|
||||||
def isCheckmate(context: GameContext): Boolean =
|
def isCheckmate(context: GameContext): Boolean =
|
||||||
StandardRules.isCheckmate(context)
|
DefaultRules.isCheckmate(context)
|
||||||
def isStalemate(context: GameContext): Boolean =
|
def isStalemate(context: GameContext): Boolean =
|
||||||
StandardRules.isStalemate(context)
|
DefaultRules.isStalemate(context)
|
||||||
def isInsufficientMaterial(context: GameContext): Boolean =
|
def isInsufficientMaterial(context: GameContext): Boolean =
|
||||||
StandardRules.isInsufficientMaterial(context)
|
DefaultRules.isInsufficientMaterial(context)
|
||||||
def isFiftyMoveRule(context: GameContext): Boolean =
|
def isFiftyMoveRule(context: GameContext): Boolean =
|
||||||
StandardRules.isFiftyMoveRule(context)
|
DefaultRules.isFiftyMoveRule(context)
|
||||||
def applyMove(context: GameContext, move: Move): GameContext =
|
def applyMove(context: GameContext, move: Move): GameContext =
|
||||||
StandardRules.applyMove(context, move)
|
DefaultRules.applyMove(context, move)
|
||||||
|
|
||||||
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
val promotionBoard = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
|
||||||
val initialCtx = GameContext.initial.withBoard(promotionBoard).withTurn(Color.White)
|
val initialCtx = GameContext.initial.withBoard(promotionBoard).withTurn(Color.White)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
implementation(project(":modules:api"))
|
implementation(project(":modules:api"))
|
||||||
|
implementation(project(":modules:rule"))
|
||||||
|
|
||||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import de.nowchess.api.game.GameContext
|
|||||||
|
|
||||||
trait GameContextImport {
|
trait GameContextImport {
|
||||||
|
|
||||||
def importGameContext(input: String): GameContext
|
def importGameContext(input: String): Option[GameContext]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -1,9 +1,10 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.fen
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.io.GameContextExport
|
||||||
|
|
||||||
object FenExporter:
|
object FenExporter extends GameContextExport:
|
||||||
|
|
||||||
/** Convert a Board to FEN piece-placement string (rank 8 to rank 1, separated by '/'). */
|
/** Convert a Board to FEN piece-placement string (rank 8 to rank 1, separated by '/'). */
|
||||||
def boardToFen(board: Board): String =
|
def boardToFen(board: Board): String =
|
||||||
@@ -23,7 +24,7 @@ object FenExporter:
|
|||||||
if emptyCount > 0 then
|
if emptyCount > 0 then
|
||||||
rankChars += emptyCount.toString.charAt(0)
|
rankChars += emptyCount.toString.charAt(0)
|
||||||
emptyCount = 0
|
emptyCount = 0
|
||||||
rankChars += pieceToPgnChar(piece)
|
rankChars += pieceToFenChar(piece)
|
||||||
case None =>
|
case None =>
|
||||||
emptyCount += 1
|
emptyCount += 1
|
||||||
|
|
||||||
@@ -39,6 +40,8 @@ object FenExporter:
|
|||||||
val fullMoveNumber = 1 + (context.moves.length / 2)
|
val fullMoveNumber = 1 + (context.moves.length / 2)
|
||||||
s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber"
|
s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber"
|
||||||
|
|
||||||
|
def exportGameContext(context: GameContext): String = gameContextToFen(context)
|
||||||
|
|
||||||
/** Convert castling rights to FEN notation. */
|
/** Convert castling rights to FEN notation. */
|
||||||
private def castlingString(rights: CastlingRights): String =
|
private def castlingString(rights: CastlingRights): String =
|
||||||
val wk = if rights.whiteKingSide then "K" else ""
|
val wk = if rights.whiteKingSide then "K" else ""
|
||||||
@@ -49,7 +52,7 @@ object FenExporter:
|
|||||||
if result.isEmpty then "-" else result
|
if result.isEmpty then "-" else result
|
||||||
|
|
||||||
/** Convert a Piece to its FEN character (uppercase = White, lowercase = Black). */
|
/** Convert a Piece to its FEN character (uppercase = White, lowercase = Black). */
|
||||||
private def pieceToPgnChar(piece: Piece): Char =
|
private def pieceToFenChar(piece: Piece): Char =
|
||||||
val base = piece.pieceType match
|
val base = piece.pieceType match
|
||||||
case PieceType.Pawn => 'p'
|
case PieceType.Pawn => 'p'
|
||||||
case PieceType.Knight => 'n'
|
case PieceType.Knight => 'n'
|
||||||
@@ -58,3 +61,4 @@ object FenExporter:
|
|||||||
case PieceType.Queen => 'q'
|
case PieceType.Queen => 'q'
|
||||||
case PieceType.King => 'k'
|
case PieceType.King => 'k'
|
||||||
if piece.color == Color.White then base.toUpper else base
|
if piece.color == Color.White then base.toUpper else base
|
||||||
|
|
||||||
+6
-2
@@ -1,9 +1,10 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.fen
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
|
import de.nowchess.io.GameContextImport
|
||||||
|
|
||||||
object FenParser:
|
object FenParser extends GameContextImport:
|
||||||
|
|
||||||
/** Parse a complete FEN string into a GameContext.
|
/** Parse a complete FEN string into a GameContext.
|
||||||
* Returns None if the format is invalid. */
|
* Returns None if the format is invalid. */
|
||||||
@@ -27,6 +28,8 @@ object FenParser:
|
|||||||
moves = List.empty
|
moves = List.empty
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def importGameContext(input: String): Option[GameContext] = parseFen(input)
|
||||||
|
|
||||||
/** Parse active color ("w" or "b"). */
|
/** Parse active color ("w" or "b"). */
|
||||||
private def parseColor(s: String): Option[Color] =
|
private def parseColor(s: String): Option[Color] =
|
||||||
if s == "w" then Some(Color.White)
|
if s == "w" then Some(Color.White)
|
||||||
@@ -102,3 +105,4 @@ object FenParser:
|
|||||||
case 'k' => Some(PieceType.King)
|
case 'k' => Some(PieceType.King)
|
||||||
case _ => None
|
case _ => None
|
||||||
pieceTypeOpt.map(pt => Piece(color, pt))
|
pieceTypeOpt.map(pt => Piece(color, pt))
|
||||||
|
|
||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.pgn
|
||||||
|
|
||||||
import de.nowchess.api.board.{PieceType, *}
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
import de.nowchess.api.game.{GameHistory, HistoryMove}
|
import de.nowchess.api.game.{GameHistory, HistoryMove}
|
||||||
|
|
||||||
@@ -52,3 +52,5 @@ object PgnExporter:
|
|||||||
case PieceType.Rook => s"R$capStr$dest$promSuffix"
|
case PieceType.Rook => s"R$capStr$dest$promSuffix"
|
||||||
case PieceType.Queen => s"Q$capStr$dest$promSuffix"
|
case PieceType.Queen => s"Q$capStr$dest$promSuffix"
|
||||||
case PieceType.King => s"K$capStr$dest$promSuffix"
|
case PieceType.King => s"K$capStr$dest$promSuffix"
|
||||||
|
|
||||||
|
|
||||||
+11
-9
@@ -1,9 +1,9 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.pgn
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.game.{GameContext, HistoryMove}
|
import de.nowchess.api.game.{GameContext, HistoryMove}
|
||||||
import de.nowchess.rules.sets.StandardRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
|
|
||||||
/** A parsed PGN game containing headers and the resolved move list. */
|
/** A parsed PGN game containing headers and the resolved move list. */
|
||||||
case class PgnGame(
|
case class PgnGame(
|
||||||
@@ -35,7 +35,7 @@ object PgnParser:
|
|||||||
|
|
||||||
/** Parse PGN header lines of the form [Key "Value"]. */
|
/** Parse PGN header lines of the form [Key "Value"]. */
|
||||||
private def parseHeaders(lines: Array[String]): Map[String, String] =
|
private def parseHeaders(lines: Array[String]): Map[String, String] =
|
||||||
val pattern = """^\[(\w+)\s+"([^"]*)"\s*\]$""".r
|
val pattern = """^\[(\w+)\s+"([^"]*)"\s*]$""".r
|
||||||
lines.flatMap(line => pattern.findFirstMatchIn(line).map(m => m.group(1) -> m.group(2))).toMap
|
lines.flatMap(line => pattern.findFirstMatchIn(line).map(m => m.group(1) -> m.group(2))).toMap
|
||||||
|
|
||||||
/** Parse the move-text section (e.g. "1. e4 e5 2. Nf3") into resolved HistoryMoves. */
|
/** Parse the move-text section (e.g. "1. e4 e5 2. Nf3") into resolved HistoryMoves. */
|
||||||
@@ -51,7 +51,7 @@ object PgnParser:
|
|||||||
case None => state
|
case None => state
|
||||||
case Some(move) =>
|
case Some(move) =>
|
||||||
val histMove = toHistoryMove(move, ctx, color)
|
val histMove = toHistoryMove(move, ctx, color)
|
||||||
val nextCtx = StandardRules.applyMove(ctx, move)
|
val nextCtx = DefaultRules.applyMove(ctx, move)
|
||||||
(nextCtx, color.opposite, acc :+ histMove)
|
(nextCtx, color.opposite, acc :+ histMove)
|
||||||
moves
|
moves
|
||||||
|
|
||||||
@@ -82,12 +82,12 @@ object PgnParser:
|
|||||||
case "O-O" | "O-O+" | "O-O#" =>
|
case "O-O" | "O-O+" | "O-O#" =>
|
||||||
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
||||||
val move = Move(Square(File.E, rank), Square(File.G, rank), MoveType.CastleKingside)
|
val move = Move(Square(File.E, rank), Square(File.G, rank), MoveType.CastleKingside)
|
||||||
Option.when(StandardRules.legalMoves(ctx, Square(File.E, rank)).contains(move))(move)
|
Option.when(DefaultRules.legalMoves(ctx, Square(File.E, rank)).contains(move))(move)
|
||||||
|
|
||||||
case "O-O-O" | "O-O-O+" | "O-O-O#" =>
|
case "O-O-O" | "O-O-O+" | "O-O-O#" =>
|
||||||
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
||||||
val move = Move(Square(File.E, rank), Square(File.C, rank), MoveType.CastleQueenside)
|
val move = Move(Square(File.E, rank), Square(File.C, rank), MoveType.CastleQueenside)
|
||||||
Option.when(StandardRules.legalMoves(ctx, Square(File.E, rank)).contains(move))(move)
|
Option.when(DefaultRules.legalMoves(ctx, Square(File.E, rank)).contains(move))(move)
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
parseRegularMove(notation, ctx, color)
|
parseRegularMove(notation, ctx, color)
|
||||||
@@ -118,7 +118,7 @@ object PgnParser:
|
|||||||
val promotion = extractPromotion(notation)
|
val promotion = extractPromotion(notation)
|
||||||
|
|
||||||
// Get all legal moves for this color that reach toSquare
|
// Get all legal moves for this color that reach toSquare
|
||||||
val allLegal = StandardRules.allLegalMoves(ctx)
|
val allLegal = DefaultRules.allLegalMoves(ctx)
|
||||||
val candidates = allLegal.filter { move =>
|
val candidates = allLegal.filter { move =>
|
||||||
move.to == toSquare &&
|
move.to == toSquare &&
|
||||||
ctx.board.pieceAt(move.from).exists(p =>
|
ctx.board.pieceAt(move.from).exists(p =>
|
||||||
@@ -146,7 +146,7 @@ object PgnParser:
|
|||||||
case Some(pp) => move.moveType == MoveType.Promotion(pp)
|
case Some(pp) => move.moveType == MoveType.Promotion(pp)
|
||||||
|
|
||||||
/** Extract a promotion piece from a notation string containing =Q/=R/=B/=N. */
|
/** Extract a promotion piece from a notation string containing =Q/=R/=B/=N. */
|
||||||
private[notation] def extractPromotion(notation: String): Option[PromotionPiece] =
|
private[pgn] def extractPromotion(notation: String): Option[PromotionPiece] =
|
||||||
val promotionPattern = """=([A-Z])""".r
|
val promotionPattern = """=([A-Z])""".r
|
||||||
promotionPattern.findFirstMatchIn(notation).flatMap { m =>
|
promotionPattern.findFirstMatchIn(notation).flatMap { m =>
|
||||||
m.group(1) match
|
m.group(1) match
|
||||||
@@ -181,7 +181,9 @@ object PgnParser:
|
|||||||
case None => Left(s"Illegal or impossible move: '$token'")
|
case None => Left(s"Illegal or impossible move: '$token'")
|
||||||
case Some(move) =>
|
case Some(move) =>
|
||||||
val histMove = toHistoryMove(move, ctx, color)
|
val histMove = toHistoryMove(move, ctx, color)
|
||||||
val nextCtx = StandardRules.applyMove(ctx, move)
|
val nextCtx = DefaultRules.applyMove(ctx, move)
|
||||||
Right((nextCtx, color.opposite, moves :+ histMove))
|
Right((nextCtx, color.opposite, moves :+ histMove))
|
||||||
}
|
}
|
||||||
}.map(_._3)
|
}.map(_._3)
|
||||||
|
|
||||||
|
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.fen
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
@@ -99,3 +99,4 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
|||||||
FenParser.parseFen(fen) match
|
FenParser.parseFen(fen) match
|
||||||
case Some(ctx) => ctx.halfMoveClock shouldBe 42
|
case Some(ctx) => ctx.halfMoveClock shouldBe 42
|
||||||
case None => fail("FEN parsing failed")
|
case None => fail("FEN parsing failed")
|
||||||
|
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.fen
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
@@ -130,3 +130,4 @@ class FenParserTest extends AnyFunSuite with Matchers:
|
|||||||
val board = FenParser.parseBoard(fen)
|
val board = FenParser.parseBoard(fen)
|
||||||
|
|
||||||
board shouldBe empty
|
board shouldBe empty
|
||||||
|
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.pgn
|
||||||
|
|
||||||
import de.nowchess.api.board.{PieceType, *}
|
import de.nowchess.api.board.{PieceType, *}
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
@@ -112,3 +112,4 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
|
|||||||
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
|
||||||
val pgn = PgnExporter.exportGame(Map.empty, history)
|
val pgn = PgnExporter.exportGame(Map.empty, history)
|
||||||
pgn shouldBe "1. e4 *"
|
pgn shouldBe "1. e4 *"
|
||||||
|
|
||||||
+3
-2
@@ -1,9 +1,9 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.pgn
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.move.{MoveType, PromotionPiece}
|
import de.nowchess.api.move.{MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.chess.notation.FenParser
|
import de.nowchess.io.fen.FenParser
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
@@ -437,3 +437,4 @@ class PgnParserTest extends AnyFunSuite with Matchers:
|
|||||||
val result = PgnParser.extractPromotion("e7e8")
|
val result = PgnParser.extractPromotion("e7e8")
|
||||||
result shouldBe None
|
result shouldBe None
|
||||||
}
|
}
|
||||||
|
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.nowchess.chess.notation
|
package de.nowchess.io.pgn
|
||||||
|
|
||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.move.PromotionPiece
|
import de.nowchess.api.move.PromotionPiece
|
||||||
@@ -117,3 +117,4 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
|
|||||||
// This is hard to construct via PGN alone; verify via a known impossible disambiguation
|
// This is hard to construct via PGN alone; verify via a known impossible disambiguation
|
||||||
val pgn = "[Event \"T\"]\n\n1. e4"
|
val pgn = "[Event \"T\"]\n\n1. e4"
|
||||||
PgnParser.validatePgn(pgn).isRight shouldBe true
|
PgnParser.validatePgn(pgn).isRight shouldBe true
|
||||||
|
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import scala.annotation.tailrec
|
|||||||
/** Standard chess rules implementation.
|
/** Standard chess rules implementation.
|
||||||
* Handles move generation, validation, check/checkmate/stalemate detection.
|
* Handles move generation, validation, check/checkmate/stalemate detection.
|
||||||
*/
|
*/
|
||||||
object StandardRules extends RuleSet:
|
object DefaultRules extends RuleSet:
|
||||||
|
|
||||||
// ── Direction vectors ──────────────────────────────────────────────
|
// ── Direction vectors ──────────────────────────────────────────────
|
||||||
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
|
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
|
||||||
Reference in New Issue
Block a user