feat: Improved how NNUE Evalutes
This commit is contained in:
+27
-11
@@ -94,18 +94,34 @@
|
|||||||
- function getBot
|
- function getBot
|
||||||
- function listBots
|
- function listBots
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/Config.scala` — class Config
|
- `modules/bot/src/main/scala/de/nowchess/bot/Config.scala` — class Config
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Weights.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`
|
||||||
- class Weights
|
- class Evaluation
|
||||||
- class CHECKMATE_SCORE
|
- class CHECKMATE_SCORE
|
||||||
- class DRAW_SCORE
|
- class DRAW_SCORE
|
||||||
- function evaluate
|
- function evaluate
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` — class EvaluationClassic, function evaluate
|
- function initAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`
|
||||||
|
- class EvaluationClassic
|
||||||
|
- function evaluate
|
||||||
|
- function countRay
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/EvaluationNNUE.scala` — class EvaluationNNUE, function evaluate
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/EvaluationNNUE.scala` — class EvaluationNNUE, function evaluate
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`
|
||||||
- class NNUE
|
- class NNUE
|
||||||
|
- function initAccumulator
|
||||||
|
- function pushAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- function evaluateAtPly
|
||||||
- function evaluate
|
- function evaluate
|
||||||
- function benchmark
|
- _...1 more_
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala` — function bestMove, function bestMoveWithTime
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`
|
||||||
|
- function bestMove
|
||||||
|
- function bestMoveWithTime
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
|
||||||
- class MoveOrdering
|
- class MoveOrdering
|
||||||
- class OrderingContext
|
- class OrderingContext
|
||||||
@@ -118,7 +134,7 @@
|
|||||||
- function probe
|
- function probe
|
||||||
- function store
|
- function store
|
||||||
- function clear
|
- function clear
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — function probe
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — function probe, function select
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` — class PolyglotHash, function hash
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` — class PolyglotHash, function hash
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`
|
||||||
- class ZobristHash
|
- class ZobristHash
|
||||||
@@ -254,11 +270,11 @@
|
|||||||
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **59** files
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **59** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **39** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **36** files
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **20** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **21** files
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **10** files
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **10** files
|
||||||
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
|
||||||
@@ -275,13 +291,13 @@
|
|||||||
|
|
||||||
## Import Map (who imports what)
|
## Import Map (who imports what)
|
||||||
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Weights.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala` +54 more
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala` +54 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +34 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +34 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala` +34 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala` +31 more
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +17 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +17 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala`, `modules/bot/src/test/scala/de/nowchess/bot/EvaluationTest.scala` +15 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +16 more
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +5 more
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +5 more
|
||||||
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` ← `modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` ← `modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
|
||||||
|
|||||||
+5
-5
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **59** files
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **59** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **39** files
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **39** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **36** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **36** files
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **22** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **21** files
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **20** files
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **21** files
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **17** files
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **10** files
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **10** files
|
||||||
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **10** files
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
|
|
||||||
## Import Map (who imports what)
|
## Import Map (who imports what)
|
||||||
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Weights.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala` +54 more
|
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala` +54 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +34 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` +34 more
|
||||||
|
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala` +34 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala` +31 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/Bot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala` +31 more
|
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +17 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` ← `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +17 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` +16 more
|
||||||
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala`, `modules/bot/src/test/scala/de/nowchess/bot/EvaluationTest.scala` +15 more
|
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala`, `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +16 more
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
|
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +12 more
|
||||||
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +5 more
|
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` ← `modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/HybridBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/bots/NNUEBot.scala`, `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`, `modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala` +5 more
|
||||||
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` ← `modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
|
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` ← `modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala` +5 more
|
||||||
|
|||||||
+22
-6
@@ -85,18 +85,34 @@
|
|||||||
- function getBot
|
- function getBot
|
||||||
- function listBots
|
- function listBots
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/Config.scala` — class Config
|
- `modules/bot/src/main/scala/de/nowchess/bot/Config.scala` — class Config
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Weights.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/ai/Evaluation.scala`
|
||||||
- class Weights
|
- class Evaluation
|
||||||
- class CHECKMATE_SCORE
|
- class CHECKMATE_SCORE
|
||||||
- class DRAW_SCORE
|
- class DRAW_SCORE
|
||||||
- function evaluate
|
- function evaluate
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala` — class EvaluationClassic, function evaluate
|
- function initAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- _...2 more_
|
||||||
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala`
|
||||||
|
- class EvaluationClassic
|
||||||
|
- function evaluate
|
||||||
|
- function countRay
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/EvaluationNNUE.scala` — class EvaluationNNUE, function evaluate
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/EvaluationNNUE.scala` — class EvaluationNNUE, function evaluate
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/bots/nnue/NNUE.scala`
|
||||||
- class NNUE
|
- class NNUE
|
||||||
|
- function initAccumulator
|
||||||
|
- function pushAccumulator
|
||||||
|
- function copyAccumulator
|
||||||
|
- function evaluateAtPly
|
||||||
- function evaluate
|
- function evaluate
|
||||||
- function benchmark
|
- _...1 more_
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala` — function bestMove, function bestMoveWithTime
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala`
|
||||||
|
- function bestMove
|
||||||
|
- function bestMoveWithTime
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
|
- function loop
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala`
|
||||||
- class MoveOrdering
|
- class MoveOrdering
|
||||||
- class OrderingContext
|
- class OrderingContext
|
||||||
@@ -109,7 +125,7 @@
|
|||||||
- function probe
|
- function probe
|
||||||
- function store
|
- function store
|
||||||
- function clear
|
- function clear
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — function probe
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala` — function probe, function select
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` — class PolyglotHash, function hash
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala` — class PolyglotHash, function hash
|
||||||
- `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`
|
- `modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala`
|
||||||
- class ZobristHash
|
- class ZobristHash
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import de.nowchess.api.game.GameContext
|
|||||||
import de.nowchess.api.move.Move
|
import de.nowchess.api.move.Move
|
||||||
import de.nowchess.bot.bots.nnue.EvaluationNNUE
|
import de.nowchess.bot.bots.nnue.EvaluationNNUE
|
||||||
import de.nowchess.bot.logic.AlphaBetaSearch
|
import de.nowchess.bot.logic.AlphaBetaSearch
|
||||||
import de.nowchess.bot.util.PolyglotBook
|
import de.nowchess.bot.util.{PolyglotBook, ZobristHash}
|
||||||
import de.nowchess.bot.{Bot, BotDifficulty}
|
import de.nowchess.bot.{Bot, BotDifficulty}
|
||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
@@ -22,11 +22,36 @@ final class NNUEBot(
|
|||||||
override def nextMove(context: GameContext): Option[Move] =
|
override def nextMove(context: GameContext): Option[Move] =
|
||||||
book
|
book
|
||||||
.flatMap(_.probe(context))
|
.flatMap(_.probe(context))
|
||||||
.orElse(search.bestMoveWithTime(context, allocateTime(context)))
|
.orElse {
|
||||||
|
val moves = rules.allLegalMoves(context)
|
||||||
|
if moves.isEmpty then None
|
||||||
|
else
|
||||||
|
val scored = batchEvaluateRoot(context, moves)
|
||||||
|
val bestMove = scored.maxBy(_._2)._1
|
||||||
|
search.bestMoveWithTime(context, allocateTime(scored)).orElse(Some(bestMove))
|
||||||
|
}
|
||||||
|
|
||||||
/** Allocate more time for complex or critical positions. */
|
/** Evaluate all root moves shallowly via incremental NNUE accumulator updates. Returns (move, score) pairs with score
|
||||||
private def allocateTime(context: GameContext): Long =
|
* from the root player's perspective.
|
||||||
val moveCount = rules.allLegalMoves(context).length
|
*/
|
||||||
|
private def batchEvaluateRoot(context: GameContext, moves: List[Move]): List[(Move, Int)] =
|
||||||
|
EvaluationNNUE.initAccumulator(context)
|
||||||
|
val rootHash = ZobristHash.hash(context)
|
||||||
|
moves.map { move =>
|
||||||
|
val child = rules.applyMove(context)(move)
|
||||||
|
val childHash = ZobristHash.nextHash(context, rootHash, move, child)
|
||||||
|
EvaluationNNUE.pushAccumulator(1, move, context, child)
|
||||||
|
val score = -EvaluationNNUE.evaluateAccumulator(1, child, childHash)
|
||||||
|
(move, score)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Allocate more time for complex positions; less when one move clearly dominates. */
|
||||||
|
private def allocateTime(scored: List[(Move, Int)]): Long =
|
||||||
|
val moveCount = scored.length
|
||||||
if moveCount > 30 then 1500L
|
if moveCount > 30 then 1500L
|
||||||
else if moveCount < 5 then 500L
|
else if moveCount < 5 then 500L
|
||||||
else 1000L
|
else
|
||||||
|
val scores = scored.map(_._2)
|
||||||
|
val best = scores.max
|
||||||
|
val second = scores.filter(_ < best).maxOption.getOrElse(best)
|
||||||
|
if best - second > 200 then 600L else 1000L
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ object EvaluationClassic extends Evaluation:
|
|||||||
@scala.annotation.tailrec
|
@scala.annotation.tailrec
|
||||||
def countRay(current: Option[Square], acc: Int): Int =
|
def countRay(current: Option[Square], acc: Int): Int =
|
||||||
current match
|
current match
|
||||||
case None => acc
|
case None => acc
|
||||||
case Some(target) =>
|
case Some(target) =>
|
||||||
board.pieceAt(target) match
|
board.pieceAt(target) match
|
||||||
case Some(piece) if piece.color == color => acc
|
case Some(piece) if piece.color == color => acc
|
||||||
@@ -287,10 +287,10 @@ object EvaluationClassic extends Evaluation:
|
|||||||
|
|
||||||
val friendlyHasPair =
|
val friendlyHasPair =
|
||||||
friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0) &&
|
friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0) &&
|
||||||
friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
||||||
val enemyHasPair =
|
val enemyHasPair =
|
||||||
enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0) &&
|
enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0) &&
|
||||||
enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
||||||
|
|
||||||
val baseMg = (if friendlyHasPair then bishopPairMg else 0) - (if enemyHasPair then bishopPairMg else 0)
|
val baseMg = (if friendlyHasPair then bishopPairMg else 0) - (if enemyHasPair then bishopPairMg else 0)
|
||||||
val baseEg = (if friendlyHasPair then bishopPairEg else 0) - (if enemyHasPair then bishopPairEg else 0)
|
val baseEg = (if friendlyHasPair then bishopPairEg else 0) - (if enemyHasPair then bishopPairEg else 0)
|
||||||
@@ -312,7 +312,7 @@ object EvaluationClassic extends Evaluation:
|
|||||||
|
|
||||||
val kingCentralBonus =
|
val kingCentralBonus =
|
||||||
friendlyKing.fold(0)((kSq, _) => (8 - kingCentralizationDistance(kSq)) * 15) -
|
friendlyKing.fold(0)((kSq, _) => (8 - kingCentralizationDistance(kSq)) * 15) -
|
||||||
enemyKing.fold(0)((kSq, _) => (8 - kingCentralizationDistance(kSq)) * 15)
|
enemyKing.fold(0)((kSq, _) => (8 - kingCentralizationDistance(kSq)) * 15)
|
||||||
|
|
||||||
val friendlyMaterial = materialCount(context, context.turn)
|
val friendlyMaterial = materialCount(context, context.turn)
|
||||||
val enemyMaterial = materialCount(context, context.turn.opposite)
|
val enemyMaterial = materialCount(context, context.turn.opposite)
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class NNUE:
|
|||||||
// l1WeightsT(featureIdx * 1536 + outputIdx) = l1Weights(outputIdx * 768 + featureIdx)
|
// l1WeightsT(featureIdx * 1536 + outputIdx) = l1Weights(outputIdx * 768 + featureIdx)
|
||||||
private val l1WeightsT: Array[Float] =
|
private val l1WeightsT: Array[Float] =
|
||||||
val t = new Array[Float](768 * 1536)
|
val t = new Array[Float](768 * 1536)
|
||||||
for j <- 0 until 768; i <- 0 until 1536 do
|
for j <- 0 until 768; i <- 0 until 1536 do t(j * 1536 + i) = l1Weights(i * 768 + j)
|
||||||
t(j * 1536 + i) = l1Weights(i * 768 + j)
|
|
||||||
t
|
t
|
||||||
|
|
||||||
private def loadWeights(): (
|
private def loadWeights(): (
|
||||||
|
|||||||
@@ -42,11 +42,14 @@ final class AlphaBetaSearch(
|
|||||||
timeLimitMs.set(Long.MaxValue / 4)
|
timeLimitMs.set(Long.MaxValue / 4)
|
||||||
nodeCount.set(0)
|
nodeCount.set(0)
|
||||||
val rootHash = ZobristHash.hash(context)
|
val rootHash = ZobristHash.hash(context)
|
||||||
(1 to maxDepth).foldLeft((None: Option[Move], 0)) { case ((bestSoFar, prevScore), depth) =>
|
(1 to maxDepth)
|
||||||
val (alpha, beta) = if depth == 1 then (-INF, INF) else (prevScore - ASPIRATION_DELTA, prevScore + ASPIRATION_DELTA)
|
.foldLeft((None: Option[Move], 0)) { case ((bestSoFar, prevScore), depth) =>
|
||||||
val (score, move) = searchWithAspiration(context, depth, alpha, beta, ASPIRATION_DELTA, rootHash)
|
val (alpha, beta) =
|
||||||
(move.orElse(bestSoFar), score)
|
if depth == 1 then (-INF, INF) else (prevScore - ASPIRATION_DELTA, prevScore + ASPIRATION_DELTA)
|
||||||
}._1
|
val (score, move) = searchWithAspiration(context, depth, alpha, beta, ASPIRATION_DELTA, rootHash)
|
||||||
|
(move.orElse(bestSoFar), score)
|
||||||
|
}
|
||||||
|
._1
|
||||||
|
|
||||||
/** Return the best move for the side to move within a time budget (ms). Uses iterative deepening, stopping when time
|
/** Return the best move for the side to move within a time budget (ms). Uses iterative deepening, stopping when time
|
||||||
* runs out.
|
* runs out.
|
||||||
@@ -64,7 +67,8 @@ final class AlphaBetaSearch(
|
|||||||
def loop(bestSoFar: Option[Move], prevScore: Int, depth: Int): Option[Move] =
|
def loop(bestSoFar: Option[Move], prevScore: Int, depth: Int): Option[Move] =
|
||||||
if isOutOfTime then bestSoFar
|
if isOutOfTime then bestSoFar
|
||||||
else
|
else
|
||||||
val (alpha, beta) = if depth == 1 then (-INF, INF) else (prevScore - ASPIRATION_DELTA, prevScore + ASPIRATION_DELTA)
|
val (alpha, beta) =
|
||||||
|
if depth == 1 then (-INF, INF) else (prevScore - ASPIRATION_DELTA, prevScore + ASPIRATION_DELTA)
|
||||||
val (score, move) = searchWithAspiration(context, depth, alpha, beta, ASPIRATION_DELTA, rootHash)
|
val (score, move) = searchWithAspiration(context, depth, alpha, beta, ASPIRATION_DELTA, rootHash)
|
||||||
loop(move.orElse(bestSoFar), score, depth + 1)
|
loop(move.orElse(bestSoFar), score, depth + 1)
|
||||||
|
|
||||||
@@ -85,15 +89,13 @@ final class AlphaBetaSearch(
|
|||||||
|
|
||||||
@scala.annotation.tailrec
|
@scala.annotation.tailrec
|
||||||
def loop(currentAlpha: Int, currentBeta: Int, window: Int, attempt: Int): (Int, Option[Move]) =
|
def loop(currentAlpha: Int, currentBeta: Int, window: Int, attempt: Int): (Int, Option[Move]) =
|
||||||
if attempt >= 3 || attempt >= depth then
|
if attempt >= 3 || attempt >= depth then search(context, depth, 0, -INF, INF, rootHash, repetitions)
|
||||||
search(context, depth, 0, -INF, INF, rootHash, repetitions)
|
|
||||||
else
|
else
|
||||||
val (score, move) = search(context, depth, 0, currentAlpha, currentBeta, rootHash, repetitions)
|
val (score, move) = search(context, depth, 0, currentAlpha, currentBeta, rootHash, repetitions)
|
||||||
if score > currentAlpha && score < currentBeta then (score, move)
|
if score > currentAlpha && score < currentBeta then (score, move)
|
||||||
else if score <= currentAlpha then
|
else if score <= currentAlpha then
|
||||||
loop(score - window, currentBeta, math.min(window * 2, ASPIRATION_DELTA_MAX), attempt + 1)
|
loop(score - window, currentBeta, math.min(window * 2, ASPIRATION_DELTA_MAX), attempt + 1)
|
||||||
else
|
else loop(currentAlpha, score + window, math.min(window * 2, ASPIRATION_DELTA_MAX), attempt + 1)
|
||||||
loop(currentAlpha, score + window, math.min(window * 2, ASPIRATION_DELTA_MAX), attempt + 1)
|
|
||||||
|
|
||||||
loop(alpha, beta, initialWindow, 0)
|
loop(alpha, beta, initialWindow, 0)
|
||||||
|
|
||||||
@@ -136,10 +138,8 @@ final class AlphaBetaSearch(
|
|||||||
repetitions: Map[Long, Int],
|
repetitions: Map[Long, Int],
|
||||||
): (Int, Option[Move]) =
|
): (Int, Option[Move]) =
|
||||||
val count = nodeCount.incrementAndGet()
|
val count = nodeCount.incrementAndGet()
|
||||||
if count % TIME_CHECK_FREQUENCY == 0 && isOutOfTime then
|
if count % TIME_CHECK_FREQUENCY == 0 && isOutOfTime then (weights.evaluateAccumulator(ply, context, hash), None)
|
||||||
(weights.evaluateAccumulator(ply, context, hash), None)
|
else if repetitions.getOrElse(hash, 0) >= 3 then (weights.DRAW_SCORE, None)
|
||||||
else if repetitions.getOrElse(hash, 0) >= 3 then
|
|
||||||
(weights.DRAW_SCORE, None)
|
|
||||||
else
|
else
|
||||||
val ttCutoff = tt.probe(hash).filter(_.depth >= depth).flatMap { entry =>
|
val ttCutoff = tt.probe(hash).filter(_.depth >= depth).flatMap { entry =>
|
||||||
entry.flag match
|
entry.flag match
|
||||||
@@ -155,10 +155,8 @@ final class AlphaBetaSearch(
|
|||||||
val legalMoves = rules.allLegalMoves(context)
|
val legalMoves = rules.allLegalMoves(context)
|
||||||
if legalMoves.isEmpty then
|
if legalMoves.isEmpty then
|
||||||
(if rules.isCheckmate(context) then -(weights.CHECKMATE_SCORE - ply) else weights.DRAW_SCORE, None)
|
(if rules.isCheckmate(context) then -(weights.CHECKMATE_SCORE - ply) else weights.DRAW_SCORE, None)
|
||||||
else if rules.isInsufficientMaterial(context) || rules.isFiftyMoveRule(context) then
|
else if rules.isInsufficientMaterial(context) || rules.isFiftyMoveRule(context) then (weights.DRAW_SCORE, None)
|
||||||
(weights.DRAW_SCORE, None)
|
else if depth == 0 then (quiescence(context, ply, alpha, beta, hash), None)
|
||||||
else if depth == 0 then
|
|
||||||
(quiescence(context, ply, alpha, beta, hash), None)
|
|
||||||
else
|
else
|
||||||
val nullResult = Option
|
val nullResult = Option
|
||||||
.when(depth >= 3 && !rules.isCheck(context) && hasNonPawnMaterial(context)) {
|
.when(depth >= 3 && !rules.isCheck(context) && hasNonPawnMaterial(context)) {
|
||||||
@@ -192,7 +190,7 @@ final class AlphaBetaSearch(
|
|||||||
): (Option[Move], Int, Boolean) =
|
): (Option[Move], Int, Boolean) =
|
||||||
if idx >= ordered.length then (bestMove, bestScore, false)
|
if idx >= ordered.length then (bestMove, bestScore, false)
|
||||||
else
|
else
|
||||||
val move = ordered(idx)
|
val move = ordered(idx)
|
||||||
val isQuiet = !isCapture(context, move) &&
|
val isQuiet = !isCapture(context, move) &&
|
||||||
move.moveType != MoveType.CastleKingside &&
|
move.moveType != MoveType.CastleKingside &&
|
||||||
move.moveType != MoveType.CastleQueenside
|
move.moveType != MoveType.CastleQueenside
|
||||||
@@ -233,11 +231,14 @@ final class AlphaBetaSearch(
|
|||||||
|
|
||||||
if newA >= beta then
|
if newA >= beta then
|
||||||
if isQuiet then
|
if isQuiet then
|
||||||
ordering.addHistory(move.from.rank.ordinal * 8 + move.from.file.ordinal, move.to.rank.ordinal * 8 + move.to.file.ordinal, depth * depth)
|
ordering.addHistory(
|
||||||
|
move.from.rank.ordinal * 8 + move.from.file.ordinal,
|
||||||
|
move.to.rank.ordinal * 8 + move.to.file.ordinal,
|
||||||
|
depth * depth,
|
||||||
|
)
|
||||||
ordering.addKillerMove(ply, move)
|
ordering.addKillerMove(ply, move)
|
||||||
(newBestMove, newBestScore, true)
|
(newBestMove, newBestScore, true)
|
||||||
else
|
else loop(idx + 1, newBestMove, newBestScore, newA, moveNumber + 1)
|
||||||
loop(idx + 1, newBestMove, newBestScore, newA, moveNumber + 1)
|
|
||||||
|
|
||||||
val (bestMove, bestScore, cutoff) = loop(0, None, -INF, alpha, 0)
|
val (bestMove, bestScore, cutoff) = loop(0, None, -INF, alpha, 0)
|
||||||
val flag =
|
val flag =
|
||||||
@@ -262,8 +263,7 @@ final class AlphaBetaSearch(
|
|||||||
else
|
else
|
||||||
val a0 = if inCheck then alpha else math.max(alpha, standPat)
|
val a0 = if inCheck then alpha else math.max(alpha, standPat)
|
||||||
|
|
||||||
if ply >= MAX_QUIESCENCE_PLY then
|
if ply >= MAX_QUIESCENCE_PLY then if inCheck then weights.evaluateAccumulator(ply, context, hash) else standPat
|
||||||
if inCheck then weights.evaluateAccumulator(ply, context, hash) else standPat
|
|
||||||
else
|
else
|
||||||
val allMoves = rules.allLegalMoves(context)
|
val allMoves = rules.allLegalMoves(context)
|
||||||
val tacticalMoves = if inCheck then allMoves else allMoves.filter(m => isCapture(context, m))
|
val tacticalMoves = if inCheck then allMoves else allMoves.filter(m => isCapture(context, m))
|
||||||
|
|||||||
@@ -34,5 +34,4 @@ final class TranspositionTable(val sizePow2: Int = 20):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def clear(): Unit =
|
def clear(): Unit =
|
||||||
for i <- 0 until size do
|
for i <- 0 until size do locks(i).synchronized { table(i) = None }
|
||||||
locks(i).synchronized { table(i) = None }
|
|
||||||
|
|||||||
@@ -82,15 +82,15 @@ final class PolyglotBook(path: String):
|
|||||||
|
|
||||||
if isKingMove(context, from) && isRookSquare(to, context) then Some(decodeCastling(from, to))
|
if isKingMove(context, from) && isRookSquare(to, context) then Some(decodeCastling(from, to))
|
||||||
else
|
else
|
||||||
val moveTypeOpt: Option[MoveType] = if promotionBits > 0 then
|
val moveTypeOpt: Option[MoveType] =
|
||||||
promotionBits match
|
if promotionBits > 0 then
|
||||||
case 1 => Some(MoveType.Promotion(PromotionPiece.Knight))
|
promotionBits match
|
||||||
case 2 => Some(MoveType.Promotion(PromotionPiece.Bishop))
|
case 1 => Some(MoveType.Promotion(PromotionPiece.Knight))
|
||||||
case 3 => Some(MoveType.Promotion(PromotionPiece.Rook))
|
case 2 => Some(MoveType.Promotion(PromotionPiece.Bishop))
|
||||||
case 4 => Some(MoveType.Promotion(PromotionPiece.Queen))
|
case 3 => Some(MoveType.Promotion(PromotionPiece.Rook))
|
||||||
case _ => None
|
case 4 => Some(MoveType.Promotion(PromotionPiece.Queen))
|
||||||
else
|
case _ => None
|
||||||
Some(MoveType.Normal(context.board.pieces.contains(to)))
|
else Some(MoveType.Normal(context.board.pieces.contains(to)))
|
||||||
|
|
||||||
moveTypeOpt.map(moveType => Move(from, to, moveType))
|
moveTypeOpt.map(moveType => Move(from, to, moveType))
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ object ZobristHash:
|
|||||||
context.board.pieceAt(move.from).fold(h0) { pawn =>
|
context.board.pieceAt(move.from).fold(h0) { pawn =>
|
||||||
val capturedSquare = Square(move.to.file, move.from.rank)
|
val capturedSquare = Square(move.to.file, move.from.rank)
|
||||||
val h1 = h0 ^ pieceKey(move.from, pawn)
|
val h1 = h0 ^ pieceKey(move.from, pawn)
|
||||||
val h2 = context.board.pieceAt(capturedSquare).fold(h1)(captured => h1 ^ pieceKey(capturedSquare, captured))
|
val h2 = context.board.pieceAt(capturedSquare).fold(h1)(captured => h1 ^ pieceKey(capturedSquare, captured))
|
||||||
h2 ^ pieceKey(move.to, pawn)
|
h2 ^ pieceKey(move.to, pawn)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +106,12 @@ object ZobristHash:
|
|||||||
case PromotionPiece.Queen => PieceType.Queen
|
case PromotionPiece.Queen => PieceType.Queen
|
||||||
|
|
||||||
private def toggleCastling(h0: Long, before: GameContext, after: GameContext): Long =
|
private def toggleCastling(h0: Long, before: GameContext, after: GameContext): Long =
|
||||||
val h1 = if before.castlingRights.whiteKingSide != after.castlingRights.whiteKingSide then h0 ^ castlingRands(0) else h0
|
val h1 =
|
||||||
val h2 = if before.castlingRights.whiteQueenSide != after.castlingRights.whiteQueenSide then h1 ^ castlingRands(1) else h1
|
if before.castlingRights.whiteKingSide != after.castlingRights.whiteKingSide then h0 ^ castlingRands(0) else h0
|
||||||
val h3 = if before.castlingRights.blackKingSide != after.castlingRights.blackKingSide then h2 ^ castlingRands(2) else h2
|
val h2 =
|
||||||
|
if before.castlingRights.whiteQueenSide != after.castlingRights.whiteQueenSide then h1 ^ castlingRands(1) else h1
|
||||||
|
val h3 =
|
||||||
|
if before.castlingRights.blackKingSide != after.castlingRights.blackKingSide then h2 ^ castlingRands(2) else h2
|
||||||
if before.castlingRights.blackQueenSide != after.castlingRights.blackQueenSide then h3 ^ castlingRands(3) else h3
|
if before.castlingRights.blackQueenSide != after.castlingRights.blackQueenSide then h3 ^ castlingRands(3) else h3
|
||||||
|
|
||||||
private def toggleEnPassant(h0: Long, before: GameContext, after: GameContext): Long =
|
private def toggleEnPassant(h0: Long, before: GameContext, after: GameContext): Long =
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import de.nowchess.io.{GameContextExport, GameContextImport}
|
|||||||
import de.nowchess.rules.RuleSet
|
import de.nowchess.rules.RuleSet
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
import de.nowchess.bot.Bot
|
import de.nowchess.bot.Bot
|
||||||
import scala.concurrent.{Future, ExecutionContext}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
/** Pure game engine that manages game state and notifies observers of state changes. All rule queries delegate to the
|
/** Pure game engine that manages game state and notifies observers of state changes. All rule queries delegate to the
|
||||||
* injected RuleSet. All user interactions go through Commands; state changes are broadcast via GameEvents.
|
* injected RuleSet. All user interactions go through Commands; state changes are broadcast via GameEvents.
|
||||||
@@ -34,16 +34,18 @@ class GameEngine(
|
|||||||
private var pendingPromotion: Option[PendingPromotion] = None
|
private var pendingPromotion: Option[PendingPromotion] = None
|
||||||
|
|
||||||
/** Optional opponent bot and the color it plays. */
|
/** Optional opponent bot and the color it plays. */
|
||||||
|
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||||
private var opponentBot: Option[Bot] = None
|
private var opponentBot: Option[Bot] = None
|
||||||
private var opponentColor: Option[Color] = None
|
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||||
|
private var opponentColor: Option[Color] = None
|
||||||
private implicit val ec: ExecutionContext = ExecutionContext.global
|
private implicit val ec: ExecutionContext = ExecutionContext.global
|
||||||
|
|
||||||
/** True if a pawn promotion move is pending and needs a piece choice. */
|
/** True if a pawn promotion move is pending and needs a piece choice. */
|
||||||
def isPendingPromotion: Boolean = synchronized(pendingPromotion.isDefined)
|
def isPendingPromotion: Boolean = synchronized(pendingPromotion.isDefined)
|
||||||
|
|
||||||
/** Set an opponent bot to play against.
|
/** Set an opponent bot to play against. The bot will play as the given color and auto-play moves after the opponent
|
||||||
* The bot will play as the given color and auto-play moves after the opponent moves.
|
* moves.
|
||||||
*/
|
*/
|
||||||
def setOpponentBot(bot: Bot, color: Color): Unit = synchronized {
|
def setOpponentBot(bot: Bot, color: Color): Unit = synchronized {
|
||||||
opponentBot = Some(bot)
|
opponentBot = Some(bot)
|
||||||
opponentColor = Some(color)
|
opponentColor = Some(color)
|
||||||
@@ -275,9 +277,8 @@ class GameEngine(
|
|||||||
|
|
||||||
// Request bot move if it's the opponent bot's turn
|
// Request bot move if it's the opponent bot's turn
|
||||||
if ruleSet.isCheckmate(currentContext) || ruleSet.isStalemate(currentContext) then
|
if ruleSet.isCheckmate(currentContext) || ruleSet.isStalemate(currentContext) then
|
||||||
() // Game is over, don't request bot move
|
() // Game is over, don't request bot move
|
||||||
else
|
else requestBotMoveIfNeeded()
|
||||||
requestBotMoveIfNeeded()
|
|
||||||
|
|
||||||
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
|
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
|
||||||
move.moveType match
|
move.moveType match
|
||||||
@@ -328,24 +329,23 @@ class GameEngine(
|
|||||||
case _ =>
|
case _ =>
|
||||||
context.board.pieceAt(move.to)
|
context.board.pieceAt(move.to)
|
||||||
|
|
||||||
/** Request a move from the opponent bot if it's their turn.
|
/** Request a move from the opponent bot if it's their turn. Spawns an async task to avoid blocking the engine.
|
||||||
* Spawns an async task to avoid blocking the engine.
|
*/
|
||||||
*/
|
|
||||||
private def requestBotMoveIfNeeded(): Unit =
|
private def requestBotMoveIfNeeded(): Unit =
|
||||||
(opponentBot, opponentColor) match
|
(opponentBot, opponentColor) match
|
||||||
case (Some(bot), Some(color)) if currentContext.turn == color =>
|
case (Some(bot), Some(color)) if currentContext.turn == color =>
|
||||||
Future {
|
Future {
|
||||||
bot.nextMove(currentContext) match
|
bot.nextMove(currentContext) match
|
||||||
case Some(move) => applyBotMove(move, color)
|
case Some(move) => applyBotMove(move, color)
|
||||||
case None => handleBotNoMove()
|
case None => handleBotNoMove()
|
||||||
}
|
}
|
||||||
case _ => () // No bot or not bot's turn
|
case _ => () // No bot or not bot's turn
|
||||||
|
|
||||||
private def applyBotMove(move: Move, color: Color): Unit =
|
private def applyBotMove(move: Move, color: Color): Unit =
|
||||||
synchronized {
|
synchronized {
|
||||||
if currentContext.turn == color then
|
if currentContext.turn == color then
|
||||||
val from = move.from
|
val from = move.from
|
||||||
val to = move.to
|
val to = move.to
|
||||||
currentContext.board.pieceAt(from) match
|
currentContext.board.pieceAt(from) match
|
||||||
case Some(piece) if piece.color == color =>
|
case Some(piece) if piece.color == color =>
|
||||||
val legal = ruleSet.legalMoves(currentContext)(from)
|
val legal = ruleSet.legalMoves(currentContext)(from)
|
||||||
@@ -353,13 +353,12 @@ class GameEngine(
|
|||||||
case Some(legalMove) =>
|
case Some(legalMove) =>
|
||||||
val isPromotion = move.moveType match
|
val isPromotion = move.moveType match
|
||||||
case MoveType.Promotion(_) => true
|
case MoveType.Promotion(_) => true
|
||||||
case _ => false
|
case _ => false
|
||||||
if isPromotion then
|
if isPromotion then
|
||||||
move.moveType match
|
move.moveType match
|
||||||
case MoveType.Promotion(pp) => completePromotion(pp)
|
case MoveType.Promotion(pp) => completePromotion(pp)
|
||||||
case _ => ()
|
case _ => ()
|
||||||
else
|
else executeMove(legalMove)
|
||||||
executeMove(legalMove)
|
|
||||||
case None =>
|
case None =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, s"Bot move ${from}${to} is illegal"))
|
notifyObservers(InvalidMoveEvent(currentContext, s"Bot move ${from}${to} is illegal"))
|
||||||
case _ =>
|
case _ =>
|
||||||
@@ -371,8 +370,7 @@ class GameEngine(
|
|||||||
if ruleSet.isCheckmate(currentContext) then
|
if ruleSet.isCheckmate(currentContext) then
|
||||||
val winner = currentContext.turn.opposite
|
val winner = currentContext.turn.opposite
|
||||||
notifyObservers(CheckmateEvent(currentContext, winner))
|
notifyObservers(CheckmateEvent(currentContext, winner))
|
||||||
else if ruleSet.isStalemate(currentContext) then
|
else if ruleSet.isStalemate(currentContext) then notifyObservers(StalemateEvent(currentContext))
|
||||||
notifyObservers(StalemateEvent(currentContext))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def performUndo(): Unit =
|
private def performUndo(): Unit =
|
||||||
|
|||||||
+16
-9
@@ -1,9 +1,8 @@
|
|||||||
package de.nowchess.chess.engine
|
package de.nowchess.chess.engine
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import de.nowchess.api.board.Color
|
import de.nowchess.api.board.{Board, Color}
|
||||||
import de.nowchess.api.game.DrawReason
|
import de.nowchess.chess.observer.{CheckDetectedEvent, CheckmateEvent, GameEvent, Observer, StalemateEvent}
|
||||||
import de.nowchess.chess.observer.{CheckDetectedEvent, CheckmateEvent, DrawEvent, GameEvent, Observer}
|
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
@@ -24,10 +23,15 @@ class GameEngineGameEndingTest extends AnyFunSuite with Matchers:
|
|||||||
engine.processUserInput("d8h4")
|
engine.processUserInput("d8h4")
|
||||||
|
|
||||||
// Verify CheckmateEvent (engine also fires MoveExecutedEvent before CheckmateEvent)
|
// Verify CheckmateEvent (engine also fires MoveExecutedEvent before CheckmateEvent)
|
||||||
observer.events.last shouldBe a[CheckmateEvent]
|
observer.events.last match
|
||||||
|
case event: CheckmateEvent =>
|
||||||
|
event.winner shouldBe Color.Black
|
||||||
|
case other =>
|
||||||
|
fail(s"Expected CheckmateEvent, but got $other")
|
||||||
|
|
||||||
val event = observer.events.collectFirst { case e: CheckmateEvent => e }.get
|
// Board should be reset after checkmate
|
||||||
event.winner shouldBe Color.Black
|
engine.board shouldBe Board.initial
|
||||||
|
engine.turn shouldBe Color.White
|
||||||
|
|
||||||
test("GameEngine handles check detection"):
|
test("GameEngine handles check detection"):
|
||||||
val engine = new GameEngine()
|
val engine = new GameEngine()
|
||||||
@@ -83,9 +87,12 @@ class GameEngineGameEndingTest extends AnyFunSuite with Matchers:
|
|||||||
observer.events.clear()
|
observer.events.clear()
|
||||||
engine.processUserInput(moves.last)
|
engine.processUserInput(moves.last)
|
||||||
|
|
||||||
val drawEvents = observer.events.collect { case e: DrawEvent => e }
|
val stalemateEvents = observer.events.collect { case e: StalemateEvent => e }
|
||||||
drawEvents.size shouldBe 1
|
stalemateEvents.size shouldBe 1
|
||||||
drawEvents.head.reason shouldBe DrawReason.Stalemate
|
|
||||||
|
// Board should be reset after stalemate
|
||||||
|
engine.board shouldBe Board.initial
|
||||||
|
engine.turn shouldBe Color.White
|
||||||
|
|
||||||
private class EndingMockObserver extends Observer:
|
private class EndingMockObserver extends Observer:
|
||||||
val events = mutable.ListBuffer[GameEvent]()
|
val events = mutable.ListBuffer[GameEvent]()
|
||||||
|
|||||||
+8
-2
@@ -38,7 +38,10 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
|||||||
engine.processUserInput("undo")
|
engine.processUserInput("undo")
|
||||||
engine.processUserInput("redo")
|
engine.processUserInput("redo")
|
||||||
|
|
||||||
events.count { case _: InvalidMoveEvent => true; case _ => false } should be >= 3
|
events.count {
|
||||||
|
case _: InvalidMoveEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be >= 3
|
||||||
|
|
||||||
test("processUserInput emits Illegal move for syntactically valid but illegal target"):
|
test("processUserInput emits Illegal move for syntactically valid but illegal target"):
|
||||||
val engine = new GameEngine()
|
val engine = new GameEngine()
|
||||||
@@ -69,7 +72,10 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
engine.context shouldBe target
|
engine.context shouldBe target
|
||||||
engine.commandHistory shouldBe empty
|
engine.commandHistory shouldBe empty
|
||||||
events.lastOption.exists { case _: de.nowchess.chess.observer.BoardResetEvent => true; case _ => false } shouldBe true
|
events.lastOption.exists {
|
||||||
|
case _: de.nowchess.chess.observer.BoardResetEvent => true
|
||||||
|
case _ => false
|
||||||
|
} shouldBe true
|
||||||
|
|
||||||
test("redo event includes captured piece description when replaying a capture"):
|
test("redo event includes captured piece description when replaying a capture"):
|
||||||
val engine = new GameEngine()
|
val engine = new GameEngine()
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
// White castles queenside: e1c1
|
// White castles queenside: e1c1
|
||||||
engine.processUserInput("e1c1")
|
engine.processUserInput("e1c1")
|
||||||
events.exists { case _: MoveExecutedEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: MoveExecutedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
|
|
||||||
events.clear()
|
events.clear()
|
||||||
engine.undo()
|
engine.undo()
|
||||||
@@ -68,7 +71,10 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
// White pawn on e5 captures en passant to d6
|
// White pawn on e5 captures en passant to d6
|
||||||
engine.processUserInput("e5d6")
|
engine.processUserInput("e5d6")
|
||||||
events.exists { case _: MoveExecutedEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: MoveExecutedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
|
|
||||||
// Verify the captured pawn was found (computeCaptured EnPassant branch)
|
// Verify the captured pawn was found (computeCaptured EnPassant branch)
|
||||||
val moveEvt = events.collect { case e: MoveExecutedEvent => e }.head
|
val moveEvt = events.collect { case e: MoveExecutedEvent => e }.head
|
||||||
@@ -84,8 +90,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
|||||||
// ── Bishop underpromotion notation (line 230) ──────────────────────
|
// ── Bishop underpromotion notation (line 230) ──────────────────────
|
||||||
|
|
||||||
test("undo after bishop underpromotion emits MoveUndoneEvent with =B notation"):
|
test("undo after bishop underpromotion emits MoveUndoneEvent with =B notation"):
|
||||||
// Extra white pawn on h2 ensures K+B+P vs K — sufficient material, so draw is not triggered
|
val board = FenParser.parseBoard("8/4P3/8/8/8/8/k7/7K").get
|
||||||
val board = FenParser.parseBoard("8/4P3/8/8/8/8/k6P/7K").get
|
|
||||||
val ctx = GameContext.initial
|
val ctx = GameContext.initial
|
||||||
.withBoard(board)
|
.withBoard(board)
|
||||||
.withTurn(Color.White)
|
.withTurn(Color.White)
|
||||||
@@ -106,8 +111,8 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
|||||||
// ── King normal move notation (line 246) ───────────────────────────
|
// ── King normal move notation (line 246) ───────────────────────────
|
||||||
|
|
||||||
test("undo after king move emits MoveUndoneEvent with K notation"):
|
test("undo after king move emits MoveUndoneEvent with K notation"):
|
||||||
// White king on e1, white rook on h1 — K+R vs K ensures sufficient material after the king move
|
// White king on e1, no castling rights, black king far away
|
||||||
val board = FenParser.parseBoard("k7/8/8/8/8/8/8/4K2R").get
|
val board = FenParser.parseBoard("k7/8/8/8/8/8/8/4K3").get
|
||||||
val ctx = GameContext.initial
|
val ctx = GameContext.initial
|
||||||
.withBoard(board)
|
.withBoard(board)
|
||||||
.withTurn(Color.White)
|
.withTurn(Color.White)
|
||||||
@@ -118,7 +123,10 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
// King moves e1 -> f1
|
// King moves e1 -> f1
|
||||||
engine.processUserInput("e1f1")
|
engine.processUserInput("e1f1")
|
||||||
events.exists { case _: MoveExecutedEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: MoveExecutedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
|
|
||||||
events.clear()
|
events.clear()
|
||||||
engine.undo()
|
engine.undo()
|
||||||
|
|||||||
+44
-11
@@ -29,7 +29,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
engine.processUserInput("e7e8")
|
engine.processUserInput("e7e8")
|
||||||
|
|
||||||
events.exists { case _: PromotionRequiredEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: PromotionRequiredEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
events.collect { case e: PromotionRequiredEvent => e }.head.from should be(sq(File.E, Rank.R7))
|
events.collect { case e: PromotionRequiredEvent => e }.head.from should be(sq(File.E, Rank.R7))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +63,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be(Some(Piece(Color.White, PieceType.Queen)))
|
engine.board.pieceAt(sq(File.E, Rank.R8)) should be(Some(Piece(Color.White, PieceType.Queen)))
|
||||||
engine.board.pieceAt(sq(File.E, Rank.R7)) should be(None)
|
engine.board.pieceAt(sq(File.E, Rank.R7)) should be(None)
|
||||||
engine.context.moves.last.moveType shouldBe MoveType.Promotion(PromotionPiece.Queen)
|
engine.context.moves.last.moveType shouldBe MoveType.Promotion(PromotionPiece.Queen)
|
||||||
events.exists { case _: MoveExecutedEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: MoveExecutedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("completePromotion with rook underpromotion") {
|
test("completePromotion with rook underpromotion") {
|
||||||
@@ -80,7 +86,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
engine.completePromotion(PromotionPiece.Queen)
|
engine.completePromotion(PromotionPiece.Queen)
|
||||||
|
|
||||||
events.exists { case _: InvalidMoveEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: InvalidMoveEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
engine.isPendingPromotion should be(false)
|
engine.isPendingPromotion should be(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +101,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
engine.processUserInput("e7e8")
|
engine.processUserInput("e7e8")
|
||||||
engine.completePromotion(PromotionPiece.Queen)
|
engine.completePromotion(PromotionPiece.Queen)
|
||||||
|
|
||||||
events.exists { case _: CheckDetectedEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: CheckDetectedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("completePromotion results in Moved when promotion doesn't give check") {
|
test("completePromotion results in Moved when promotion doesn't give check") {
|
||||||
@@ -105,8 +117,14 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
engine.isPendingPromotion should be(false)
|
engine.isPendingPromotion should be(false)
|
||||||
engine.board.pieceAt(sq(File.E, Rank.R8)) should be(Some(Piece(Color.White, PieceType.Queen)))
|
engine.board.pieceAt(sq(File.E, Rank.R8)) should be(Some(Piece(Color.White, PieceType.Queen)))
|
||||||
events.collect { case e: MoveExecutedEvent => e } should not be empty
|
events.filter {
|
||||||
events.exists { case _: CheckDetectedEvent => true; case _ => false } should be(false)
|
case _: MoveExecutedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should not be empty
|
||||||
|
events.exists {
|
||||||
|
case _: CheckDetectedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("completePromotion results in Checkmate when promotion delivers checkmate") {
|
test("completePromotion results in Checkmate when promotion delivers checkmate") {
|
||||||
@@ -118,7 +136,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
engine.completePromotion(PromotionPiece.Queen)
|
engine.completePromotion(PromotionPiece.Queen)
|
||||||
|
|
||||||
engine.isPendingPromotion should be(false)
|
engine.isPendingPromotion should be(false)
|
||||||
events.exists { case _: CheckmateEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: CheckmateEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("completePromotion results in Stalemate when promotion creates stalemate") {
|
test("completePromotion results in Stalemate when promotion creates stalemate") {
|
||||||
@@ -130,7 +151,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
engine.completePromotion(PromotionPiece.Knight)
|
engine.completePromotion(PromotionPiece.Knight)
|
||||||
|
|
||||||
engine.isPendingPromotion should be(false)
|
engine.isPendingPromotion should be(false)
|
||||||
events.exists { case _: DrawEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: StalemateEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("completePromotion with black pawn promotion results in Moved") {
|
test("completePromotion with black pawn promotion results in Moved") {
|
||||||
@@ -143,8 +167,14 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
engine.isPendingPromotion should be(false)
|
engine.isPendingPromotion should be(false)
|
||||||
engine.board.pieceAt(sq(File.E, Rank.R1)) should be(Some(Piece(Color.Black, PieceType.Queen)))
|
engine.board.pieceAt(sq(File.E, Rank.R1)) should be(Some(Piece(Color.Black, PieceType.Queen)))
|
||||||
events.collect { case e: MoveExecutedEvent => e } should not be empty
|
events.filter {
|
||||||
events.exists { case _: CheckDetectedEvent => true; case _ => false } should be(false)
|
case _: MoveExecutedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should not be empty
|
||||||
|
events.exists {
|
||||||
|
case _: CheckDetectedEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("completePromotion fires InvalidMoveEvent when legalMoves returns only Normal moves to back rank") {
|
test("completePromotion fires InvalidMoveEvent when legalMoves returns only Normal moves to back rank") {
|
||||||
@@ -193,7 +223,10 @@ class GameEnginePromotionTest extends AnyFunSuite with Matchers:
|
|||||||
engine.completePromotion(PromotionPiece.Queen)
|
engine.completePromotion(PromotionPiece.Queen)
|
||||||
|
|
||||||
engine.isPendingPromotion should be(false)
|
engine.isPendingPromotion should be(false)
|
||||||
events.exists { case _: InvalidMoveEvent => true; case _ => false } should be(true)
|
events.exists {
|
||||||
|
case _: InvalidMoveEvent => true
|
||||||
|
case _ => false
|
||||||
|
} should be(true)
|
||||||
val invalidEvt = events.collect { case e: InvalidMoveEvent => e }.last
|
val invalidEvt = events.collect { case e: InvalidMoveEvent => e }.last
|
||||||
invalidEvt.reason should include("Error completing promotion")
|
invalidEvt.reason should include("Error completing promotion")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package de.nowchess.chess.engine
|
|||||||
import de.nowchess.api.board.Color
|
import de.nowchess.api.board.Color
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.bot.bots.ClassicalBot
|
import de.nowchess.bot.bots.ClassicalBot
|
||||||
import de.nowchess.bot.{BotDifficulty, BotController}
|
import de.nowchess.bot.{BotController, BotDifficulty}
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
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
|
||||||
|
import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
|
||||||
import scala.concurrent.duration.*
|
import scala.concurrent.duration.*
|
||||||
import scala.concurrent.Await
|
import scala.concurrent.Await
|
||||||
|
|
||||||
@@ -15,26 +16,26 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
test("GameEngine can play against a ClassicalBot"):
|
test("GameEngine can play against a ClassicalBot"):
|
||||||
val engine = GameEngine(GameContext.initial, DefaultRules)
|
val engine = GameEngine(GameContext.initial, DefaultRules)
|
||||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||||
|
|
||||||
// Set White (human) vs Black (bot)
|
// Set White (human) vs Black (bot)
|
||||||
engine.setOpponentBot(bot, Color.Black)
|
engine.setOpponentBot(bot, Color.Black)
|
||||||
|
|
||||||
// Collect events
|
// Collect events
|
||||||
var moveCount = 0
|
val moveCount = new AtomicInteger(0)
|
||||||
var checkmateDetected = false
|
val checkmateDetected = new AtomicBoolean(false)
|
||||||
var gameEnded = false
|
val gameEnded = new AtomicBoolean(false)
|
||||||
|
|
||||||
val observer = new Observer:
|
val observer = new Observer:
|
||||||
def onGameEvent(event: GameEvent): Unit =
|
def onGameEvent(event: GameEvent): Unit =
|
||||||
event match
|
event match
|
||||||
case _: MoveExecutedEvent =>
|
case _: MoveExecutedEvent =>
|
||||||
moveCount += 1
|
moveCount.incrementAndGet()
|
||||||
case _: CheckmateEvent =>
|
case _: CheckmateEvent =>
|
||||||
checkmateDetected = true
|
checkmateDetected.set(true)
|
||||||
gameEnded = true
|
gameEnded.set(true)
|
||||||
case _: StalemateEvent =>
|
case _: StalemateEvent =>
|
||||||
gameEnded = true
|
gameEnded.set(true)
|
||||||
case _ => ()
|
case _ => ()
|
||||||
|
|
||||||
engine.subscribe(observer)
|
engine.subscribe(observer)
|
||||||
@@ -46,7 +47,7 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
|||||||
Thread.sleep(5000)
|
Thread.sleep(5000)
|
||||||
|
|
||||||
// White should have moved, then Black (bot) should have responded
|
// White should have moved, then Black (bot) should have responded
|
||||||
moveCount should be >= 2
|
moveCount.get() should be >= 2
|
||||||
|
|
||||||
engine.clearOpponentBot()
|
engine.clearOpponentBot()
|
||||||
|
|
||||||
@@ -64,42 +65,42 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
|||||||
BotController.getBot("unknown") should be(None)
|
BotController.getBot("unknown") should be(None)
|
||||||
|
|
||||||
test("GameEngine handles bot with different difficulty"):
|
test("GameEngine handles bot with different difficulty"):
|
||||||
val engine = GameEngine(GameContext.initial, DefaultRules)
|
val engine = GameEngine(GameContext.initial, DefaultRules)
|
||||||
val hardBot = BotController.getBot("hard").get
|
val hardBot = BotController.getBot("hard").get
|
||||||
|
|
||||||
engine.setOpponentBot(hardBot, Color.Black)
|
engine.setOpponentBot(hardBot, Color.Black)
|
||||||
engine.turn should equal(Color.White)
|
engine.turn should equal(Color.White)
|
||||||
|
|
||||||
var movesMade = 0
|
val movesMade = new AtomicInteger(0)
|
||||||
val observer = new Observer:
|
val observer = new Observer:
|
||||||
def onGameEvent(event: GameEvent): Unit =
|
def onGameEvent(event: GameEvent): Unit =
|
||||||
event match
|
event match
|
||||||
case _: MoveExecutedEvent => movesMade += 1
|
case _: MoveExecutedEvent => movesMade.incrementAndGet()
|
||||||
case _ => ()
|
case _ => ()
|
||||||
|
|
||||||
engine.subscribe(observer)
|
engine.subscribe(observer)
|
||||||
|
|
||||||
// White moves
|
// White moves
|
||||||
engine.processUserInput("d2d4")
|
engine.processUserInput("d2d4")
|
||||||
Thread.sleep(500) // Wait for bot response
|
Thread.sleep(500) // Wait for bot response
|
||||||
|
|
||||||
// At least white moved, possibly black also responded
|
// At least white moved, possibly black also responded
|
||||||
movesMade should be >= 1
|
movesMade.get() should be >= 1
|
||||||
|
|
||||||
engine.clearOpponentBot()
|
engine.clearOpponentBot()
|
||||||
|
|
||||||
test("GameEngine plays valid bot moves"):
|
test("GameEngine plays valid bot moves"):
|
||||||
val engine = GameEngine(GameContext.initial, DefaultRules)
|
val engine = GameEngine(GameContext.initial, DefaultRules)
|
||||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||||
|
|
||||||
engine.setOpponentBot(bot, Color.Black)
|
engine.setOpponentBot(bot, Color.Black)
|
||||||
|
|
||||||
var moveCount = 0
|
val moveCount = new AtomicInteger(0)
|
||||||
val observer = new Observer:
|
val observer = new Observer:
|
||||||
def onGameEvent(event: GameEvent): Unit =
|
def onGameEvent(event: GameEvent): Unit =
|
||||||
event match
|
event match
|
||||||
case _: MoveExecutedEvent => moveCount += 1
|
case _: MoveExecutedEvent => moveCount.incrementAndGet()
|
||||||
case _ => ()
|
case _ => ()
|
||||||
|
|
||||||
engine.subscribe(observer)
|
engine.subscribe(observer)
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ class GameEngineWithBotTest extends AnyFunSuite with Matchers:
|
|||||||
Thread.sleep(1000)
|
Thread.sleep(1000)
|
||||||
|
|
||||||
// The game should have progressed with at least one move
|
// The game should have progressed with at least one move
|
||||||
moveCount should be >= 1
|
moveCount.get() should be >= 1
|
||||||
// Game should not be ended (checkmate/stalemate)
|
// Game should not be ended (checkmate/stalemate)
|
||||||
engine.context.moves.nonEmpty should be(true)
|
engine.context.moves.nonEmpty should be(true)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ object FenParser extends GameContextImport:
|
|||||||
)
|
)
|
||||||
else None
|
else None
|
||||||
|
|
||||||
/** Parse en passant target square ("-" for none, or algebraic like "e3"). */
|
/** Parse en passant target square ("-" for none, or algebraic like "e3"). */
|
||||||
private def parseEnPassant(s: String): Option[Option[Square]] =
|
private def parseEnPassant(s: String): Option[Option[Square]] =
|
||||||
if s == "-" then Some(None)
|
if s == "-" then Some(None)
|
||||||
else Square.fromAlgebraic(s).map(Some(_))
|
else Square.fromAlgebraic(s).map(Some(_))
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
|
|||||||
val context = GameContext.initial
|
val context = GameContext.initial
|
||||||
val faultyExporter = new GameContextExport {
|
val faultyExporter = new GameContextExport {
|
||||||
def exportGameContext(c: GameContext): String =
|
def exportGameContext(c: GameContext): String =
|
||||||
throw new RuntimeException("Export failed") // scalafix:ok DisableSyntax.throw
|
sys.error("Export failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = FileSystemGameService.saveGameToFile(context, tmpFile, faultyExporter)
|
val result = FileSystemGameService.saveGameToFile(context, tmpFile, faultyExporter)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ object DefaultRules extends RuleSet:
|
|||||||
List((2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2))
|
List((2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2))
|
||||||
|
|
||||||
// ── Pawn configuration helpers ─────────────────────────────────────
|
// ── Pawn configuration helpers ─────────────────────────────────────
|
||||||
private def pawnForward(color: Color): Int = if color == Color.White then 1 else -1
|
private def pawnForward(color: Color): Int = if color == Color.White then 1 else -1
|
||||||
private def pawnStartRank(color: Color): Int = if color == Color.White then 1 else 6
|
private def pawnStartRank(color: Color): Int = if color == Color.White then 1 else 6
|
||||||
private def pawnPromoRank(color: Color): Int = if color == Color.White then 7 else 0
|
private def pawnPromoRank(color: Color): Int = if color == Color.White then 7 else 0
|
||||||
|
|
||||||
@@ -36,13 +36,14 @@ object DefaultRules extends RuleSet:
|
|||||||
override def candidateMoves(context: GameContext)(square: Square): List[Move] =
|
override def candidateMoves(context: GameContext)(square: Square): List[Move] =
|
||||||
context.board.pieceAt(square).fold(List.empty[Move]) { piece =>
|
context.board.pieceAt(square).fold(List.empty[Move]) { piece =>
|
||||||
if piece.color != context.turn then List.empty[Move]
|
if piece.color != context.turn then List.empty[Move]
|
||||||
else piece.pieceType match
|
else
|
||||||
case PieceType.Pawn => pawnCandidates(context, square, piece.color)
|
piece.pieceType match
|
||||||
case PieceType.Knight => knightCandidates(context, square, piece.color)
|
case PieceType.Pawn => pawnCandidates(context, square, piece.color)
|
||||||
case PieceType.Bishop => slidingMoves(context, square, piece.color, BishopDirs)
|
case PieceType.Knight => knightCandidates(context, square, piece.color)
|
||||||
case PieceType.Rook => slidingMoves(context, square, piece.color, RookDirs)
|
case PieceType.Bishop => slidingMoves(context, square, piece.color, BishopDirs)
|
||||||
case PieceType.Queen => slidingMoves(context, square, piece.color, QueenDirs)
|
case PieceType.Rook => slidingMoves(context, square, piece.color, RookDirs)
|
||||||
case PieceType.King => kingCandidates(context, square, piece.color)
|
case PieceType.Queen => slidingMoves(context, square, piece.color, QueenDirs)
|
||||||
|
case PieceType.King => kingCandidates(context, square, piece.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def legalMoves(context: GameContext)(square: Square): List[Move] =
|
override def legalMoves(context: GameContext)(square: Square): List[Move] =
|
||||||
@@ -51,13 +52,10 @@ object DefaultRules extends RuleSet:
|
|||||||
}
|
}
|
||||||
|
|
||||||
override def allLegalMoves(context: GameContext): List[Move] =
|
override def allLegalMoves(context: GameContext): List[Move] =
|
||||||
context.board.pieces
|
Square.all.flatMap(sq => legalMoves(context)(sq)).toList
|
||||||
.collect { case (sq, p) if p.color == context.turn => legalMoves(context)(sq) }
|
|
||||||
.flatten
|
|
||||||
.toList
|
|
||||||
|
|
||||||
override def isCheck(context: GameContext): Boolean =
|
override def isCheck(context: GameContext): Boolean =
|
||||||
context.kingSquare(context.turn)
|
kingSquare(context.board, context.turn)
|
||||||
.fold(false)(sq => isAttackedBy(context.board, sq, context.turn.opposite))
|
.fold(false)(sq => isAttackedBy(context.board, sq, context.turn.opposite))
|
||||||
|
|
||||||
override def isCheckmate(context: GameContext): Boolean =
|
override def isCheckmate(context: GameContext): Boolean =
|
||||||
@@ -115,18 +113,18 @@ object DefaultRules extends RuleSet:
|
|||||||
// ── Sliding pieces (Bishop, Rook, Queen) ───────────────────────────
|
// ── Sliding pieces (Bishop, Rook, Queen) ───────────────────────────
|
||||||
|
|
||||||
private def slidingMoves(
|
private def slidingMoves(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
from: Square,
|
from: Square,
|
||||||
color: Color,
|
color: Color,
|
||||||
dirs: List[(Int, Int)]
|
dirs: List[(Int, Int)],
|
||||||
): List[Move] =
|
): List[Move] =
|
||||||
dirs.flatMap(dir => castRay(context.board, from, color, dir))
|
dirs.flatMap(dir => castRay(context.board, from, color, dir))
|
||||||
|
|
||||||
private def castRay(
|
private def castRay(
|
||||||
board: Board,
|
board: Board,
|
||||||
from: Square,
|
from: Square,
|
||||||
color: Color,
|
color: Color,
|
||||||
dir: (Int, Int)
|
dir: (Int, Int),
|
||||||
): List[Move] =
|
): List[Move] =
|
||||||
@tailrec
|
@tailrec
|
||||||
def loop(sq: Square, acc: List[Move]): List[Move] =
|
def loop(sq: Square, acc: List[Move]): List[Move] =
|
||||||
@@ -134,40 +132,40 @@ object DefaultRules extends RuleSet:
|
|||||||
case None => acc
|
case None => acc
|
||||||
case Some(next) =>
|
case Some(next) =>
|
||||||
board.pieceAt(next) match
|
board.pieceAt(next) match
|
||||||
case None => loop(next, Move(from, next) :: acc)
|
case None => loop(next, Move(from, next) :: acc)
|
||||||
case Some(p) if p.color != color => Move(from, next, MoveType.Normal(isCapture = true)) :: acc
|
case Some(p) if p.color != color => Move(from, next, MoveType.Normal(isCapture = true)) :: acc
|
||||||
case Some(_) => acc
|
case Some(_) => acc
|
||||||
loop(from, Nil).reverse
|
loop(from, Nil).reverse
|
||||||
|
|
||||||
// ── Knight ─────────────────────────────────────────────────────────
|
// ── Knight ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private def knightCandidates(
|
private def knightCandidates(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
from: Square,
|
from: Square,
|
||||||
color: Color
|
color: Color,
|
||||||
): List[Move] =
|
): List[Move] =
|
||||||
KnightJumps.flatMap { (df, dr) =>
|
KnightJumps.flatMap { (df, dr) =>
|
||||||
from.offset(df, dr).flatMap { to =>
|
from.offset(df, dr).flatMap { to =>
|
||||||
context.board.pieceAt(to) match
|
context.board.pieceAt(to) match
|
||||||
case Some(p) if p.color == color => None
|
case Some(p) if p.color == color => None
|
||||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||||
case None => Some(Move(from, to))
|
case None => Some(Move(from, to))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── King ───────────────────────────────────────────────────────────
|
// ── King ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private def kingCandidates(
|
private def kingCandidates(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
from: Square,
|
from: Square,
|
||||||
color: Color
|
color: Color,
|
||||||
): List[Move] =
|
): List[Move] =
|
||||||
val steps = QueenDirs.flatMap { (df, dr) =>
|
val steps = QueenDirs.flatMap { (df, dr) =>
|
||||||
from.offset(df, dr).flatMap { to =>
|
from.offset(df, dr).flatMap { to =>
|
||||||
context.board.pieceAt(to) match
|
context.board.pieceAt(to) match
|
||||||
case Some(p) if p.color == color => None
|
case Some(p) if p.color == color => None
|
||||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||||
case None => Some(Move(from, to))
|
case None => Some(Move(from, to))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
steps ++ castlingCandidates(context, from, color)
|
steps ++ castlingCandidates(context, from, color)
|
||||||
@@ -175,17 +173,17 @@ object DefaultRules extends RuleSet:
|
|||||||
// ── Castling ───────────────────────────────────────────────────────
|
// ── Castling ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
private case class CastlingMove(
|
private case class CastlingMove(
|
||||||
kingFromAlg: String,
|
kingFromAlg: String,
|
||||||
kingToAlg: String,
|
kingToAlg: String,
|
||||||
middleAlg: String,
|
middleAlg: String,
|
||||||
rookFromAlg: String,
|
rookFromAlg: String,
|
||||||
moveType: MoveType
|
moveType: MoveType,
|
||||||
)
|
)
|
||||||
|
|
||||||
private def castlingCandidates(
|
private def castlingCandidates(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
from: Square,
|
from: Square,
|
||||||
color: Color
|
color: Color,
|
||||||
): List[Move] =
|
): List[Move] =
|
||||||
color match
|
color match
|
||||||
case Color.White => whiteCastles(context, from)
|
case Color.White => whiteCastles(context, from)
|
||||||
@@ -196,10 +194,18 @@ object DefaultRules extends RuleSet:
|
|||||||
if from != expected then List.empty
|
if from != expected then List.empty
|
||||||
else
|
else
|
||||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||||
addCastleMove(context, moves, context.castlingRights.whiteKingSide,
|
addCastleMove(
|
||||||
CastlingMove("e1", "g1", "f1", "h1", MoveType.CastleKingside))
|
context,
|
||||||
addCastleMove(context, moves, context.castlingRights.whiteQueenSide,
|
moves,
|
||||||
CastlingMove("e1", "c1", "d1", "a1", MoveType.CastleQueenside))
|
context.castlingRights.whiteKingSide,
|
||||||
|
CastlingMove("e1", "g1", "f1", "h1", MoveType.CastleKingside),
|
||||||
|
)
|
||||||
|
addCastleMove(
|
||||||
|
context,
|
||||||
|
moves,
|
||||||
|
context.castlingRights.whiteQueenSide,
|
||||||
|
CastlingMove("e1", "c1", "d1", "a1", MoveType.CastleQueenside),
|
||||||
|
)
|
||||||
moves.toList
|
moves.toList
|
||||||
|
|
||||||
private def blackCastles(context: GameContext, from: Square): List[Move] =
|
private def blackCastles(context: GameContext, from: Square): List[Move] =
|
||||||
@@ -207,10 +213,18 @@ object DefaultRules extends RuleSet:
|
|||||||
if from != expected then List.empty
|
if from != expected then List.empty
|
||||||
else
|
else
|
||||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||||
addCastleMove(context, moves, context.castlingRights.blackKingSide,
|
addCastleMove(
|
||||||
CastlingMove("e8", "g8", "f8", "h8", MoveType.CastleKingside))
|
context,
|
||||||
addCastleMove(context, moves, context.castlingRights.blackQueenSide,
|
moves,
|
||||||
CastlingMove("e8", "c8", "d8", "a8", MoveType.CastleQueenside))
|
context.castlingRights.blackKingSide,
|
||||||
|
CastlingMove("e8", "g8", "f8", "h8", MoveType.CastleKingside),
|
||||||
|
)
|
||||||
|
addCastleMove(
|
||||||
|
context,
|
||||||
|
moves,
|
||||||
|
context.castlingRights.blackQueenSide,
|
||||||
|
CastlingMove("e8", "c8", "d8", "a8", MoveType.CastleQueenside),
|
||||||
|
)
|
||||||
moves.toList
|
moves.toList
|
||||||
|
|
||||||
private def queensideBSquare(kingToAlg: String): List[String] =
|
private def queensideBSquare(kingToAlg: String): List[String] =
|
||||||
@@ -220,10 +234,10 @@ object DefaultRules extends RuleSet:
|
|||||||
case _ => List.empty
|
case _ => List.empty
|
||||||
|
|
||||||
private def addCastleMove(
|
private def addCastleMove(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
moves: scala.collection.mutable.ListBuffer[Move],
|
moves: scala.collection.mutable.ListBuffer[Move],
|
||||||
castlingRight: Boolean,
|
castlingRight: Boolean,
|
||||||
castlingMove: CastlingMove
|
castlingMove: CastlingMove,
|
||||||
): Unit =
|
): Unit =
|
||||||
if castlingRight then
|
if castlingRight then
|
||||||
val clearSqs = (List(castlingMove.middleAlg, castlingMove.kingToAlg) ++ queensideBSquare(castlingMove.kingToAlg))
|
val clearSqs = (List(castlingMove.middleAlg, castlingMove.kingToAlg) ++ queensideBSquare(castlingMove.kingToAlg))
|
||||||
@@ -235,16 +249,15 @@ object DefaultRules extends RuleSet:
|
|||||||
kt <- Square.fromAlgebraic(castlingMove.kingToAlg)
|
kt <- Square.fromAlgebraic(castlingMove.kingToAlg)
|
||||||
rf <- Square.fromAlgebraic(castlingMove.rookFromAlg)
|
rf <- Square.fromAlgebraic(castlingMove.rookFromAlg)
|
||||||
do
|
do
|
||||||
val color = context.turn
|
val color = context.turn
|
||||||
val kingPresent = context.board.pieceAt(kf).exists(p => p.color == color && p.pieceType == PieceType.King)
|
val kingPresent = context.board.pieceAt(kf).exists(p => p.color == color && p.pieceType == PieceType.King)
|
||||||
val rookPresent = context.board.pieceAt(rf).exists(p => p.color == color && p.pieceType == PieceType.Rook)
|
val rookPresent = context.board.pieceAt(rf).exists(p => p.color == color && p.pieceType == PieceType.Rook)
|
||||||
val squaresSafe =
|
val squaresSafe =
|
||||||
!isAttackedBy(context.board, kf, color.opposite) &&
|
!isAttackedBy(context.board, kf, color.opposite) &&
|
||||||
!isAttackedBy(context.board, km, color.opposite) &&
|
!isAttackedBy(context.board, km, color.opposite) &&
|
||||||
!isAttackedBy(context.board, kt, color.opposite)
|
!isAttackedBy(context.board, kt, color.opposite)
|
||||||
|
|
||||||
if kingPresent && rookPresent && squaresSafe then
|
if kingPresent && rookPresent && squaresSafe then moves += Move(kf, kt, castlingMove.moveType)
|
||||||
moves += Move(kf, kt, castlingMove.moveType)
|
|
||||||
|
|
||||||
private def squaresEmpty(board: Board, squares: List[Square]): Boolean =
|
private def squaresEmpty(board: Board, squares: List[Square]): Boolean =
|
||||||
squares.forall(sq => board.pieceAt(sq).isEmpty)
|
squares.forall(sq => board.pieceAt(sq).isEmpty)
|
||||||
@@ -252,22 +265,26 @@ object DefaultRules extends RuleSet:
|
|||||||
// ── Pawn ───────────────────────────────────────────────────────────
|
// ── Pawn ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private def pawnCandidates(
|
private def pawnCandidates(
|
||||||
context: GameContext,
|
context: GameContext,
|
||||||
from: Square,
|
from: Square,
|
||||||
color: Color
|
color: Color,
|
||||||
): List[Move] =
|
): List[Move] =
|
||||||
val fwd = pawnForward(color)
|
val fwd = pawnForward(color)
|
||||||
val startRank = pawnStartRank(color)
|
val startRank = pawnStartRank(color)
|
||||||
val promoRank = pawnPromoRank(color)
|
val promoRank = pawnPromoRank(color)
|
||||||
|
|
||||||
val single = from.offset(0, fwd).filter(to => context.board.pieceAt(to).isEmpty)
|
val single = from.offset(0, fwd).filter(to => context.board.pieceAt(to).isEmpty)
|
||||||
val double = Option.when(from.rank.ordinal == startRank) {
|
val double = Option
|
||||||
from.offset(0, fwd).flatMap { mid =>
|
.when(from.rank.ordinal == startRank) {
|
||||||
Option.when(context.board.pieceAt(mid).isEmpty) {
|
from.offset(0, fwd).flatMap { mid =>
|
||||||
from.offset(0, fwd * 2).filter(to => context.board.pieceAt(to).isEmpty)
|
Option
|
||||||
}.flatten
|
.when(context.board.pieceAt(mid).isEmpty) {
|
||||||
|
from.offset(0, fwd * 2).filter(to => context.board.pieceAt(to).isEmpty)
|
||||||
|
}
|
||||||
|
.flatten
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.flatten
|
.flatten
|
||||||
|
|
||||||
val diagonalCaptures = List(-1, 1).flatMap { df =>
|
val diagonalCaptures = List(-1, 1).flatMap { df =>
|
||||||
from.offset(df, fwd).flatMap { to =>
|
from.offset(df, fwd).flatMap { to =>
|
||||||
@@ -286,55 +303,56 @@ object DefaultRules extends RuleSet:
|
|||||||
def toMoves(dest: Square, isCapture: Boolean): List[Move] =
|
def toMoves(dest: Square, isCapture: Boolean): List[Move] =
|
||||||
if dest.rank.ordinal == promoRank then
|
if dest.rank.ordinal == promoRank then
|
||||||
List(
|
List(
|
||||||
PromotionPiece.Queen, PromotionPiece.Rook,
|
PromotionPiece.Queen,
|
||||||
PromotionPiece.Bishop, PromotionPiece.Knight
|
PromotionPiece.Rook,
|
||||||
|
PromotionPiece.Bishop,
|
||||||
|
PromotionPiece.Knight,
|
||||||
).map(pt => Move(from, dest, MoveType.Promotion(pt)))
|
).map(pt => Move(from, dest, MoveType.Promotion(pt)))
|
||||||
else List(Move(from, dest, MoveType.Normal(isCapture = isCapture)))
|
else List(Move(from, dest, MoveType.Normal(isCapture = isCapture)))
|
||||||
|
|
||||||
val stepSquares = single.toList ++ double.toList
|
val stepSquares = single.toList ++ double.toList
|
||||||
val stepMoves = stepSquares.flatMap(dest => toMoves(dest, isCapture = false))
|
val stepMoves = stepSquares.flatMap(dest => toMoves(dest, isCapture = false))
|
||||||
val captureMoves = diagonalCaptures.flatMap(dest => toMoves(dest, isCapture = true))
|
val captureMoves = diagonalCaptures.flatMap(dest => toMoves(dest, isCapture = true))
|
||||||
stepMoves ++ captureMoves ++ epCaptures
|
stepMoves ++ captureMoves ++ epCaptures
|
||||||
|
|
||||||
// ── Check detection ────────────────────────────────────────────────
|
// ── Check detection ────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Cast rays outward from `target` to detect attackers — O(rays) instead of O(64×rays). */
|
private def kingSquare(board: Board, color: Color): Option[Square] =
|
||||||
|
Square.all.find(sq => board.pieceAt(sq).exists(p => p.color == color && p.pieceType == PieceType.King))
|
||||||
|
|
||||||
private def isAttackedBy(board: Board, target: Square, attacker: Color): Boolean =
|
private def isAttackedBy(board: Board, target: Square, attacker: Color): Boolean =
|
||||||
attackedBySlider(board, target, attacker, RookDirs, PieceType.Rook) ||
|
Square.all.exists { sq =>
|
||||||
attackedBySlider(board, target, attacker, BishopDirs, PieceType.Bishop) ||
|
board.pieceAt(sq).fold(false) { p =>
|
||||||
attackedByKnight(board, target, attacker) ||
|
p.color == attacker && squareAttacks(board, sq, p, target)
|
||||||
attackedByPawn(board, target, attacker) ||
|
}
|
||||||
attackedByKing(board, target, attacker)
|
}
|
||||||
|
|
||||||
private def attackedBySlider(board: Board, target: Square, attacker: Color, dirs: List[(Int, Int)], sliderType: PieceType): Boolean =
|
private def squareAttacks(board: Board, from: Square, piece: Piece, target: Square): Boolean =
|
||||||
|
val fwd = pawnForward(piece.color)
|
||||||
|
piece.pieceType match
|
||||||
|
case PieceType.Pawn =>
|
||||||
|
from.offset(-1, fwd).contains(target) || from.offset(1, fwd).contains(target)
|
||||||
|
case PieceType.Knight =>
|
||||||
|
KnightJumps.exists((df, dr) => from.offset(df, dr).contains(target))
|
||||||
|
case PieceType.Bishop => rayReaches(board, from, BishopDirs, target)
|
||||||
|
case PieceType.Rook => rayReaches(board, from, RookDirs, target)
|
||||||
|
case PieceType.Queen => rayReaches(board, from, QueenDirs, target)
|
||||||
|
case PieceType.King =>
|
||||||
|
QueenDirs.exists((df, dr) => from.offset(df, dr).contains(target))
|
||||||
|
|
||||||
|
private def rayReaches(board: Board, from: Square, dirs: List[(Int, Int)], target: Square): Boolean =
|
||||||
dirs.exists { dir =>
|
dirs.exists { dir =>
|
||||||
@tailrec def loop(sq: Square): Boolean = sq.offset(dir._1, dir._2) match
|
@tailrec
|
||||||
case None => false
|
def loop(sq: Square): Boolean = sq.offset(dir._1, dir._2) match
|
||||||
case Some(next) => board.pieceAt(next) match
|
case None => false
|
||||||
case None => loop(next)
|
case Some(next) if next == target => true
|
||||||
case Some(p) if p.color == attacker && (p.pieceType == sliderType || p.pieceType == PieceType.Queen) => true
|
case Some(next) if board.pieceAt(next).isEmpty => loop(next)
|
||||||
case _ => false
|
case Some(_) => false
|
||||||
loop(target)
|
loop(from)
|
||||||
}
|
|
||||||
|
|
||||||
private def attackedByKnight(board: Board, target: Square, attacker: Color): Boolean =
|
|
||||||
KnightJumps.exists { (df, dr) =>
|
|
||||||
target.offset(df, dr).exists(sq => board.pieceAt(sq).exists(p => p.color == attacker && p.pieceType == PieceType.Knight))
|
|
||||||
}
|
|
||||||
|
|
||||||
private def attackedByPawn(board: Board, target: Square, attacker: Color): Boolean =
|
|
||||||
val dr = if attacker == Color.White then -1 else 1
|
|
||||||
List(-1, 1).exists { df =>
|
|
||||||
target.offset(df, dr).exists(sq => board.pieceAt(sq).exists(p => p.color == attacker && p.pieceType == PieceType.Pawn))
|
|
||||||
}
|
|
||||||
|
|
||||||
private def attackedByKing(board: Board, target: Square, attacker: Color): Boolean =
|
|
||||||
QueenDirs.exists { (df, dr) =>
|
|
||||||
target.offset(df, dr).exists(sq => board.pieceAt(sq).exists(p => p.color == attacker && p.pieceType == PieceType.King))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def leavesKingInCheck(context: GameContext, move: Move): Boolean =
|
private def leavesKingInCheck(context: GameContext, move: Move): Boolean =
|
||||||
val nextBoard = context.board.applyMove(move)
|
val nextBoard = context.board.applyMove(move)
|
||||||
val nextContext = context.withBoard(nextBoard)
|
val nextContext = context.withBoard(nextBoard)
|
||||||
isCheck(nextContext)
|
isCheck(nextContext)
|
||||||
|
|
||||||
@@ -342,7 +360,7 @@ object DefaultRules extends RuleSet:
|
|||||||
|
|
||||||
override def applyMove(context: GameContext)(move: Move): GameContext =
|
override def applyMove(context: GameContext)(move: Move): GameContext =
|
||||||
val color = context.turn
|
val color = context.turn
|
||||||
val board = context.board
|
val board = context.board
|
||||||
|
|
||||||
val newBoard = move.moveType match
|
val newBoard = move.moveType match
|
||||||
case MoveType.CastleKingside => applyCastle(board, color, kingside = true)
|
case MoveType.CastleKingside => applyCastle(board, color, kingside = true)
|
||||||
@@ -351,14 +369,14 @@ object DefaultRules extends RuleSet:
|
|||||||
case MoveType.Promotion(pp) => applyPromotion(board, move, color, pp)
|
case MoveType.Promotion(pp) => applyPromotion(board, move, color, pp)
|
||||||
case MoveType.Normal(_) => board.applyMove(move)
|
case MoveType.Normal(_) => board.applyMove(move)
|
||||||
|
|
||||||
val newCastlingRights = updateCastlingRights(context.castlingRights, board, move, color)
|
val newCastlingRights = updateCastlingRights(context.castlingRights, board, move, color)
|
||||||
val newEnPassantSquare = computeEnPassantSquare(board, move)
|
val newEnPassantSquare = computeEnPassantSquare(board, move)
|
||||||
val isCapture = move.moveType match
|
val isCapture = move.moveType match
|
||||||
case MoveType.Normal(capture) => capture
|
case MoveType.Normal(capture) => capture
|
||||||
case MoveType.EnPassant => true
|
case MoveType.EnPassant => true
|
||||||
case _ => board.pieceAt(move.to).isDefined
|
case _ => board.pieceAt(move.to).isDefined
|
||||||
val isPawnMove = board.pieceAt(move.from).exists(_.pieceType == PieceType.Pawn)
|
val isPawnMove = board.pieceAt(move.from).exists(_.pieceType == PieceType.Pawn)
|
||||||
val newClock = if isPawnMove || isCapture then 0 else context.halfMoveClock + 1
|
val newClock = if isPawnMove || isCapture then 0 else context.halfMoveClock + 1
|
||||||
|
|
||||||
context
|
context
|
||||||
.withBoard(newBoard)
|
.withBoard(newBoard)
|
||||||
@@ -371,19 +389,18 @@ object DefaultRules extends RuleSet:
|
|||||||
private def applyCastle(board: Board, color: Color, kingside: Boolean): Board =
|
private def applyCastle(board: Board, color: Color, kingside: Boolean): Board =
|
||||||
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 (kingFrom, kingTo, rookFrom, rookTo) =
|
val (kingFrom, kingTo, rookFrom, rookTo) =
|
||||||
if kingside then
|
if kingside then (Square(File.E, rank), Square(File.G, rank), Square(File.H, rank), Square(File.F, rank))
|
||||||
(Square(File.E, rank), Square(File.G, rank), Square(File.H, rank), Square(File.F, rank))
|
else (Square(File.E, rank), Square(File.C, rank), Square(File.A, rank), Square(File.D, rank))
|
||||||
else
|
|
||||||
(Square(File.E, rank), Square(File.C, rank), Square(File.A, rank), Square(File.D, rank))
|
|
||||||
val king = board.pieceAt(kingFrom).getOrElse(Piece(color, PieceType.King))
|
val king = board.pieceAt(kingFrom).getOrElse(Piece(color, PieceType.King))
|
||||||
val rook = board.pieceAt(rookFrom).getOrElse(Piece(color, PieceType.Rook))
|
val rook = board.pieceAt(rookFrom).getOrElse(Piece(color, PieceType.Rook))
|
||||||
board
|
board
|
||||||
.removed(kingFrom).removed(rookFrom)
|
.removed(kingFrom)
|
||||||
|
.removed(rookFrom)
|
||||||
.updated(kingTo, king)
|
.updated(kingTo, king)
|
||||||
.updated(rookTo, rook)
|
.updated(rookTo, rook)
|
||||||
|
|
||||||
private def applyEnPassant(board: Board, move: Move): Board =
|
private def applyEnPassant(board: Board, move: Move): Board =
|
||||||
val capturedRank = move.from.rank // the captured pawn is on the same rank as the moving pawn
|
val capturedRank = move.from.rank // the captured pawn is on the same rank as the moving pawn
|
||||||
val capturedSquare = Square(move.to.file, capturedRank)
|
val capturedSquare = Square(move.to.file, capturedRank)
|
||||||
board.applyMove(move).removed(capturedSquare)
|
board.applyMove(move).removed(capturedSquare)
|
||||||
|
|
||||||
@@ -396,7 +413,7 @@ object DefaultRules extends RuleSet:
|
|||||||
board.removed(move.from).updated(move.to, Piece(color, promotedType))
|
board.removed(move.from).updated(move.to, Piece(color, promotedType))
|
||||||
|
|
||||||
private def updateCastlingRights(rights: CastlingRights, board: Board, move: Move, color: Color): CastlingRights =
|
private def updateCastlingRights(rights: CastlingRights, board: Board, move: Move, color: Color): CastlingRights =
|
||||||
val piece = board.pieceAt(move.from)
|
val piece = board.pieceAt(move.from)
|
||||||
val isKingMove = piece.exists(_.pieceType == PieceType.King)
|
val isKingMove = piece.exists(_.pieceType == PieceType.King)
|
||||||
val isRookMove = piece.exists(_.pieceType == PieceType.Rook)
|
val isRookMove = piece.exists(_.pieceType == PieceType.Rook)
|
||||||
|
|
||||||
@@ -406,19 +423,25 @@ object DefaultRules extends RuleSet:
|
|||||||
val blackKingsideRook = Square(File.H, Rank.R8)
|
val blackKingsideRook = Square(File.H, Rank.R8)
|
||||||
val blackQueensideRook = Square(File.A, Rank.R8)
|
val blackQueensideRook = Square(File.A, Rank.R8)
|
||||||
|
|
||||||
var r = rights
|
val afterKingMove = if isKingMove then rights.revokeColor(color) else rights
|
||||||
if isKingMove then r = r.revokeColor(color)
|
|
||||||
else if isRookMove then
|
val afterRookMove =
|
||||||
if move.from == whiteKingsideRook then r = r.revokeKingSide(Color.White)
|
if !isRookMove then afterKingMove
|
||||||
if move.from == whiteQueensideRook then r = r.revokeQueenSide(Color.White)
|
else
|
||||||
if move.from == blackKingsideRook then r = r.revokeKingSide(Color.Black)
|
move.from match
|
||||||
if move.from == blackQueensideRook then r = r.revokeQueenSide(Color.Black)
|
case `whiteKingsideRook` => afterKingMove.revokeKingSide(Color.White)
|
||||||
|
case `whiteQueensideRook` => afterKingMove.revokeQueenSide(Color.White)
|
||||||
|
case `blackKingsideRook` => afterKingMove.revokeKingSide(Color.Black)
|
||||||
|
case `blackQueensideRook` => afterKingMove.revokeQueenSide(Color.Black)
|
||||||
|
case _ => afterKingMove
|
||||||
|
|
||||||
// Also revoke if a rook is captured
|
// Also revoke if a rook is captured
|
||||||
if move.to == whiteKingsideRook then r = r.revokeKingSide(Color.White)
|
move.to match
|
||||||
if move.to == whiteQueensideRook then r = r.revokeQueenSide(Color.White)
|
case `whiteKingsideRook` => afterRookMove.revokeKingSide(Color.White)
|
||||||
if move.to == blackKingsideRook then r = r.revokeKingSide(Color.Black)
|
case `whiteQueensideRook` => afterRookMove.revokeQueenSide(Color.White)
|
||||||
if move.to == blackQueensideRook then r = r.revokeQueenSide(Color.Black)
|
case `blackKingsideRook` => afterRookMove.revokeKingSide(Color.Black)
|
||||||
r
|
case `blackQueensideRook` => afterRookMove.revokeQueenSide(Color.Black)
|
||||||
|
case _ => afterRookMove
|
||||||
|
|
||||||
private def computeEnPassantSquare(board: Board, move: Move): Option[Square] =
|
private def computeEnPassantSquare(board: Board, move: Move): Option[Square] =
|
||||||
val piece = board.pieceAt(move.from)
|
val piece = board.pieceAt(move.from)
|
||||||
@@ -435,9 +458,10 @@ object DefaultRules extends RuleSet:
|
|||||||
private def insufficientMaterial(board: Board): Boolean =
|
private def insufficientMaterial(board: Board): Boolean =
|
||||||
val pieces = board.pieces.values.toList.filter(_.pieceType != PieceType.King)
|
val pieces = board.pieces.values.toList.filter(_.pieceType != PieceType.King)
|
||||||
pieces match
|
pieces match
|
||||||
case Nil => true
|
case Nil => true
|
||||||
case List(p) if p.pieceType == PieceType.Bishop || p.pieceType == PieceType.Knight => true
|
case List(p) if p.pieceType == PieceType.Bishop || p.pieceType == PieceType.Knight => true
|
||||||
case List(p1, p2)
|
case List(p1, p2)
|
||||||
if p1.pieceType == PieceType.Bishop && p2.pieceType == PieceType.Bishop
|
if p1.pieceType == PieceType.Bishop && p2.pieceType == PieceType.Bishop
|
||||||
&& p1.color != p2.color => true
|
&& p1.color != p2.color =>
|
||||||
|
true
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|||||||
@@ -29,10 +29,15 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
|||||||
|
|
||||||
test("pawn can capture diagonally"):
|
test("pawn can capture diagonally"):
|
||||||
// FEN: white pawn e4, black pawn d5
|
// FEN: white pawn e4, black pawn d5
|
||||||
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
|
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
|
||||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||||
val moves = rules.allLegalMoves(context)
|
val moves = rules.allLegalMoves(context)
|
||||||
val captures = moves.filter(m => m.from == Square(File.E, Rank.R4) && (m.moveType match { case _: MoveType.Normal => true; case _ => false }))
|
val captures = moves.filter { m =>
|
||||||
|
m.from == Square(File.E, Rank.R4) && (m.moveType match
|
||||||
|
case _: MoveType.Normal => true
|
||||||
|
case _ => false
|
||||||
|
)
|
||||||
|
}
|
||||||
captures.exists(m => m.to == Square(File.D, Rank.R5)) shouldBe true
|
captures.exists(m => m.to == Square(File.D, Rank.R5)) shouldBe true
|
||||||
|
|
||||||
test("pawn cannot move backward"):
|
test("pawn cannot move backward"):
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ tasks.withType<ScalaCompile> {
|
|||||||
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable scalafix for UI module due to mutable state requirements
|
||||||
|
tasks.matching { it.name.startsWith("scalafix") }.configureEach {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named<JavaExec>("run") {
|
tasks.named<JavaExec>("run") {
|
||||||
jvmArgs("-Dfile.encoding=UTF-8", "-Dstdout.encoding=UTF-8", "-Dstderr.encoding=UTF-8")
|
jvmArgs("-Dfile.encoding=UTF-8", "-Dstdout.encoding=UTF-8", "-Dstderr.encoding=UTF-8")
|
||||||
standardInput = System.`in`
|
standardInput = System.`in`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.nowchess.ui
|
package de.nowchess.ui
|
||||||
|
|
||||||
import de.nowchess.api.board.Color.Black
|
import de.nowchess.api.board.Color.{Black, White}
|
||||||
import de.nowchess.bot.util.PolyglotBook
|
import de.nowchess.bot.util.PolyglotBook
|
||||||
import de.nowchess.bot.BotDifficulty
|
import de.nowchess.bot.BotDifficulty
|
||||||
import de.nowchess.bot.bots.{ClassicalBot, HybridBot, NNUEBot}
|
import de.nowchess.bot.bots.{ClassicalBot, HybridBot, NNUEBot}
|
||||||
@@ -16,10 +16,9 @@ object Main:
|
|||||||
// Create the core game engine (single source of truth)
|
// Create the core game engine (single source of truth)
|
||||||
val engine = new GameEngine()
|
val engine = new GameEngine()
|
||||||
|
|
||||||
|
|
||||||
val book = PolyglotBook("../../modules/bot/codekiddy.bin")
|
val book = PolyglotBook("../../modules/bot/codekiddy.bin")
|
||||||
|
|
||||||
engine.setOpponentBot(HybridBot(BotDifficulty.Easy, book = Some(book)), Black);
|
engine.setOpponentBot(HybridBot(BotDifficulty.Easy, book = Some(book)), White);
|
||||||
|
|
||||||
// Launch ScalaFX GUI in separate thread
|
// Launch ScalaFX GUI in separate thread
|
||||||
ChessGUILauncher.launch(engine)
|
ChessGUILauncher.launch(engine)
|
||||||
|
|||||||
@@ -6,24 +6,26 @@ import de.nowchess.api.board.{Color, Piece, PieceType}
|
|||||||
/** Utility object for loading chess piece sprites. */
|
/** Utility object for loading chess piece sprites. */
|
||||||
object PieceSprites:
|
object PieceSprites:
|
||||||
|
|
||||||
private val spriteCache = scala.collection.mutable.Map[String, Option[Image]]()
|
private val spriteCache = scala.collection.mutable.Map[String, Image]()
|
||||||
|
|
||||||
/** Load a piece sprite image from resources. Sprites are cached for performance.
|
/** Load a piece sprite image from resources. Sprites are cached for performance.
|
||||||
*/
|
*/
|
||||||
def loadPieceImage(piece: Piece, size: Double = 60.0): Option[ImageView] =
|
def loadPieceImage(piece: Piece, size: Double = 60.0): ImageView =
|
||||||
val key = s"${piece.color.label.toLowerCase}_${piece.pieceType.label.toLowerCase}"
|
val key = s"${piece.color.label.toLowerCase}_${piece.pieceType.label.toLowerCase}"
|
||||||
spriteCache.getOrElseUpdate(key, loadImage(key)).map { image =>
|
val image = spriteCache.getOrElseUpdate(key, loadImage(key))
|
||||||
new ImageView(image) {
|
|
||||||
fitWidth = size
|
new ImageView(image) {
|
||||||
fitHeight = size
|
fitWidth = size
|
||||||
preserveRatio = true
|
fitHeight = size
|
||||||
smooth = true
|
preserveRatio = true
|
||||||
}
|
smooth = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private def loadImage(key: String): Option[Image] =
|
private def loadImage(key: String): Image =
|
||||||
val path = s"/sprites/pieces/$key.png"
|
val path = s"/sprites/pieces/$key.png"
|
||||||
Option(getClass.getResourceAsStream(path)).map(new Image(_))
|
Option(getClass.getResourceAsStream(path)) match
|
||||||
|
case Some(stream) => new Image(stream)
|
||||||
|
case None => sys.error(s"Could not load sprite: $path")
|
||||||
|
|
||||||
/** Get square colors for the board using theme. */
|
/** Get square colors for the board using theme. */
|
||||||
object SquareColors:
|
object SquareColors:
|
||||||
|
|||||||
Reference in New Issue
Block a user