diff --git a/docs/unresolved.md b/docs/unresolved.md new file mode 100644 index 0000000..c8f6738 --- /dev/null +++ b/docs/unresolved.md @@ -0,0 +1,23 @@ +# Unresolved Issues + +## [2026-03-24] JUnitSuiteLike mixin not available for ScalaTest 3.2.19 with Scala 3 + +**Requirement / Bug:** +CLAUDE.md prescribes that all unit tests should extend `AnyFunSuite with Matchers with JUnitSuiteLike`. However, the `JUnitSuiteLike` trait cannot be resolved in the current build configuration. + +**Root Cause (if known):** +- ScalaTest 3.2.19 for Scala 3 does not provide `JUnitSuiteLike` in any public package. +- The `co.helmethair:scalatest-junit-runner:0.1.11` dependency does not expose this trait. +- There is no `org.scalatest:scalatest-junit_3` artifact available for version 3.2.19. +- The trait may have been removed or changed in the ScalaTest 3.x → Scala 3 migration. + +**Attempted Fixes:** +1. Tried importing from `org.scalatest.junit.JUnitSuiteLike` — not found +2. Tried importing from `org.scalatestplus.junit.JUnitSuiteLike` — not found +3. Tried importing from `co.helmethair.scalatest.junit.JUnitSuiteLike` — not found +4. Attempted to add `org.scalatest:scalatest-junit_3:3.2.19` dependency — artifact does not exist in Maven Central + +**Suggested Next Step:** +1. Either find the correct ScalaTest artifact/import for Scala 3 JUnit integration, or +2. Update CLAUDE.md to reflect the actual constraint that unit tests should extend `AnyFunSuite with Matchers` (without `JUnitSuiteLike`), or +3. Investigate whether a different test runner or configuration is needed to achieve JUnit integration with ScalaTest 3 in Scala 3 diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d01d6ea..56bfb9e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -57,7 +57,9 @@ - + + + @@ -261,6 +263,14 @@ + + + + + + + + diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/GameContext.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/GameContext.scala new file mode 100644 index 0000000..7bb2e7b --- /dev/null +++ b/modules/core/src/main/scala/de/nowchess/chess/logic/GameContext.scala @@ -0,0 +1,47 @@ +package de.nowchess.chess.logic + +import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square} +import de.nowchess.api.game.CastlingRights + +enum CastleSide: + case Kingside, Queenside + +case class GameContext( + board: Board, + whiteCastling: CastlingRights, + blackCastling: CastlingRights +): + def castlingFor(color: Color): CastlingRights = + if color == Color.White then whiteCastling else blackCastling + + def withUpdatedRights(color: Color, rights: CastlingRights): GameContext = + if color == Color.White then copy(whiteCastling = rights) + else copy(blackCastling = rights) + +object GameContext: + /** Convenience constructor for test boards: no castling rights on either side. */ + def apply(board: Board): GameContext = + GameContext(board, CastlingRights.None, CastlingRights.None) + + val initial: GameContext = + GameContext(Board.initial, CastlingRights.Both, CastlingRights.Both) + +extension (b: Board) + def withCastle(color: Color, side: CastleSide): Board = + val (kingFrom, kingTo, rookFrom, rookTo) = (color, side) match + case (Color.White, CastleSide.Kingside) => + (Square(File.E, Rank.R1), Square(File.G, Rank.R1), + Square(File.H, Rank.R1), Square(File.F, Rank.R1)) + case (Color.White, CastleSide.Queenside) => + (Square(File.E, Rank.R1), Square(File.C, Rank.R1), + Square(File.A, Rank.R1), Square(File.D, Rank.R1)) + case (Color.Black, CastleSide.Kingside) => + (Square(File.E, Rank.R8), Square(File.G, Rank.R8), + Square(File.H, Rank.R8), Square(File.F, Rank.R8)) + case (Color.Black, CastleSide.Queenside) => + (Square(File.E, Rank.R8), Square(File.C, Rank.R8), + Square(File.A, Rank.R8), Square(File.D, Rank.R8)) + val king = Piece(color, PieceType.King) + val rook = Piece(color, PieceType.Rook) + Board(b.pieces.removed(kingFrom).removed(rookFrom) + .updated(kingTo, king).updated(rookTo, rook)) diff --git a/modules/core/src/test/scala/de/nowchess/chess/logic/GameContextTest.scala b/modules/core/src/test/scala/de/nowchess/chess/logic/GameContextTest.scala new file mode 100644 index 0000000..812a7c9 --- /dev/null +++ b/modules/core/src/test/scala/de/nowchess/chess/logic/GameContextTest.scala @@ -0,0 +1,81 @@ +package de.nowchess.chess.logic + +import de.nowchess.api.board.* +import de.nowchess.api.game.CastlingRights +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class GameContextTest extends AnyFunSuite with Matchers: + + private def sq(f: File, r: Rank): Square = Square(f, r) + private def board(entries: (Square, Piece)*): Board = Board(entries.toMap) + + test("GameContext.initial has Board.initial and CastlingRights.Both for both sides"): + GameContext.initial.board shouldBe Board.initial + GameContext.initial.whiteCastling shouldBe CastlingRights.Both + GameContext.initial.blackCastling shouldBe CastlingRights.Both + + test("castlingFor returns white rights for Color.White"): + GameContext.initial.castlingFor(Color.White) shouldBe CastlingRights.Both + + test("castlingFor returns black rights for Color.Black"): + GameContext.initial.castlingFor(Color.Black) shouldBe CastlingRights.Both + + test("withUpdatedRights updates white castling without touching black"): + val ctx = GameContext.initial.withUpdatedRights(Color.White, CastlingRights.None) + ctx.whiteCastling shouldBe CastlingRights.None + ctx.blackCastling shouldBe CastlingRights.Both + + test("withUpdatedRights updates black castling without touching white"): + val ctx = GameContext.initial.withUpdatedRights(Color.Black, CastlingRights.None) + ctx.blackCastling shouldBe CastlingRights.None + ctx.whiteCastling shouldBe CastlingRights.Both + + test("withCastle: white kingside — king e1→g1, rook h1→f1"): + val b = board( + sq(File.E, Rank.R1) -> Piece.WhiteKing, + sq(File.H, Rank.R1) -> Piece.WhiteRook + ) + val after = b.withCastle(Color.White, CastleSide.Kingside) + after.pieceAt(sq(File.G, Rank.R1)) shouldBe Some(Piece.WhiteKing) + after.pieceAt(sq(File.F, Rank.R1)) shouldBe Some(Piece.WhiteRook) + after.pieceAt(sq(File.E, Rank.R1)) shouldBe None + after.pieceAt(sq(File.H, Rank.R1)) shouldBe None + + test("withCastle: white queenside — king e1→c1, rook a1→d1"): + val b = board( + sq(File.E, Rank.R1) -> Piece.WhiteKing, + sq(File.A, Rank.R1) -> Piece.WhiteRook + ) + val after = b.withCastle(Color.White, CastleSide.Queenside) + after.pieceAt(sq(File.C, Rank.R1)) shouldBe Some(Piece.WhiteKing) + after.pieceAt(sq(File.D, Rank.R1)) shouldBe Some(Piece.WhiteRook) + after.pieceAt(sq(File.E, Rank.R1)) shouldBe None + after.pieceAt(sq(File.A, Rank.R1)) shouldBe None + + test("withCastle: black kingside — king e8→g8, rook h8→f8"): + val b = board( + sq(File.E, Rank.R8) -> Piece.BlackKing, + sq(File.H, Rank.R8) -> Piece.BlackRook + ) + val after = b.withCastle(Color.Black, CastleSide.Kingside) + after.pieceAt(sq(File.G, Rank.R8)) shouldBe Some(Piece.BlackKing) + after.pieceAt(sq(File.F, Rank.R8)) shouldBe Some(Piece.BlackRook) + after.pieceAt(sq(File.E, Rank.R8)) shouldBe None + after.pieceAt(sq(File.H, Rank.R8)) shouldBe None + + test("withCastle: black queenside — king e8→c8, rook a8→d8"): + val b = board( + sq(File.E, Rank.R8) -> Piece.BlackKing, + sq(File.A, Rank.R8) -> Piece.BlackRook + ) + val after = b.withCastle(Color.Black, CastleSide.Queenside) + after.pieceAt(sq(File.C, Rank.R8)) shouldBe Some(Piece.BlackKing) + after.pieceAt(sq(File.D, Rank.R8)) shouldBe Some(Piece.BlackRook) + after.pieceAt(sq(File.E, Rank.R8)) shouldBe None + after.pieceAt(sq(File.A, Rank.R8)) shouldBe None + + test("GameContext single-arg apply defaults to CastlingRights.None for both sides"): + val ctx = GameContext(Board.initial) + ctx.whiteCastling shouldBe CastlingRights.None + ctx.blackCastling shouldBe CastlingRights.None