feat: Update BotController and tests for improved bot instantiation and configuration
This commit is contained in:
@@ -4,22 +4,12 @@ import de.nowchess.bot.bots.ClassicalBot
|
||||
|
||||
object BotController {
|
||||
|
||||
private var bots: Map[String, Bot] = Map.empty
|
||||
|
||||
// Register standard bots
|
||||
locally {
|
||||
val easyBot = ClassicalBot(BotDifficulty.Easy)
|
||||
val mediumBot = ClassicalBot(BotDifficulty.Medium)
|
||||
val hardBot = ClassicalBot(BotDifficulty.Hard)
|
||||
val expertBot = ClassicalBot(BotDifficulty.Expert)
|
||||
|
||||
bots = Map(
|
||||
"easy" -> easyBot,
|
||||
"medium" -> mediumBot,
|
||||
"hard" -> hardBot,
|
||||
"expert" -> expertBot
|
||||
)
|
||||
}
|
||||
private val bots: Map[String, Bot] = Map(
|
||||
"easy" -> ClassicalBot(BotDifficulty.Easy),
|
||||
"medium" -> ClassicalBot(BotDifficulty.Medium),
|
||||
"hard" -> ClassicalBot(BotDifficulty.Hard),
|
||||
"expert" -> ClassicalBot(BotDifficulty.Expert),
|
||||
)
|
||||
|
||||
/** Get a bot by name. */
|
||||
def getBot(name: String): Option[Bot] = bots.get(name.toLowerCase)
|
||||
|
||||
@@ -2,8 +2,9 @@ package de.nowchess.bot
|
||||
|
||||
object Config:
|
||||
|
||||
/** Threshold in centipawns: if classical evaluation differs from NNUE by more than this,
|
||||
* the move is vetoed (not accepted as a suggestion). */
|
||||
/** Threshold in centipawns: if classical evaluation differs from NNUE by more than this, the move is vetoed (not
|
||||
* accepted as a suggestion).
|
||||
*/
|
||||
val VETO_THRESHOLD: Int = 150
|
||||
|
||||
/** Time budget per move for iterative deepening (milliseconds). */
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.nowchess.bot.ai
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
|
||||
trait Weights {
|
||||
|
||||
def CHECKMATE_SCORE: Int
|
||||
def DRAW_SCORE: Int
|
||||
|
||||
def evaluate(context: GameContext): Int
|
||||
|
||||
}
|
||||
@@ -10,16 +10,17 @@ import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
final class ClassicalBot(
|
||||
difficulty: BotDifficulty,
|
||||
rules: RuleSet = DefaultRules,
|
||||
book: Option[PolyglotBook] = None
|
||||
difficulty: BotDifficulty,
|
||||
rules: RuleSet = DefaultRules,
|
||||
book: Option[PolyglotBook] = None,
|
||||
) extends Bot:
|
||||
|
||||
private val search: AlphaBetaSearch = AlphaBetaSearch(rules, weights = EvaluationClassic)
|
||||
private val TIME_BUDGET_MS = 1000L
|
||||
private val TIME_BUDGET_MS = 1000L
|
||||
|
||||
override val name: String = s"ClassicalBot(${difficulty.toString})"
|
||||
|
||||
override def nextMove(context: GameContext): Option[Move] =
|
||||
book.flatMap(_.probe(context))
|
||||
book
|
||||
.flatMap(_.probe(context))
|
||||
.orElse(search.bestMoveWithTime(context, TIME_BUDGET_MS))
|
||||
|
||||
@@ -11,9 +11,9 @@ import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
final class HybridBot(
|
||||
difficulty: BotDifficulty,
|
||||
rules: RuleSet = DefaultRules,
|
||||
book: Option[PolyglotBook] = None
|
||||
difficulty: BotDifficulty,
|
||||
rules: RuleSet = DefaultRules,
|
||||
book: Option[PolyglotBook] = None,
|
||||
) extends Bot:
|
||||
|
||||
private val search = AlphaBetaSearch(rules, TranspositionTable(), EvaluationClassic)
|
||||
@@ -25,11 +25,13 @@ final class HybridBot(
|
||||
|
||||
private def searchWithVeto(context: GameContext): Option[Move] =
|
||||
search.bestMoveWithTime(context, Config.TIME_LIMIT_MS).map { move =>
|
||||
val next = rules.applyMove(context)(move)
|
||||
val next = rules.applyMove(context)(move)
|
||||
val staticNnue = EvaluationNNUE.evaluate(next)
|
||||
val classical = EvaluationClassic.evaluate(next)
|
||||
val diff = (classical - staticNnue).abs
|
||||
val diff = (classical - staticNnue).abs
|
||||
if diff > Config.VETO_THRESHOLD then
|
||||
println(f"[Veto] ${move.from}->${move.to}: nnue=$staticNnue classical=$classical diff=$diff — flagged but trusted (deep search)")
|
||||
println(
|
||||
f"[Veto] ${move.from}->${move.to}: nnue=$staticNnue classical=$classical diff=$diff — flagged but trusted (deep search)",
|
||||
)
|
||||
move
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package de.nowchess.bot.bots.classic
|
||||
|
||||
import de.nowchess.api.board.{Color, PieceType, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.bot.ai.Weights
|
||||
import de.nowchess.bot.ai.Evaluation
|
||||
|
||||
object EvaluationClassic extends Weights:
|
||||
object EvaluationClassic extends Evaluation:
|
||||
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
val DRAW_SCORE: Int = 0
|
||||
|
||||
// Material values in centipawns (indexed by PieceType.ordinal: Pawn=0, Knight=1, Bishop=2, Rook=3, Queen=4, King=5)
|
||||
private val mgMaterial = Array(100, 325, 335, 500, 900, 20_000)
|
||||
@@ -21,151 +21,87 @@ object EvaluationClassic extends Weights:
|
||||
// Black is vertically mirrored
|
||||
|
||||
private val mgPawnTable: Array[Int] = Array(
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
50, 50, 50, 50, 50, 50, 50, 50,
|
||||
10, 10, 20, 30, 30, 20, 10, 10,
|
||||
5, 5, 10, 25, 25, 10, 5, 5,
|
||||
0, 0, 0, 20, 20, 0, 0, 0,
|
||||
5, -5, -10, 0, 0, -10, -5, 5,
|
||||
5, 10, 10, -20, -20, 10, 10, 5,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, 50, 50, 50, 50, 50, 10, 10, 20, 30, 30, 20, 10, 10, 5, 5, 10, 25, 25, 10, 5, 5,
|
||||
0, 0, 0, 20, 20, 0, 0, 0, 5, -5, -10, 0, 0, -10, -5, 5, 5, 10, 10, -20, -20, 10, 10, 5, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
)
|
||||
|
||||
private val egPawnTable: Array[Int] = Array(
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
70, 70, 70, 70, 70, 70, 70, 70,
|
||||
40, 40, 40, 40, 40, 40, 40, 40,
|
||||
30, 30, 30, 30, 30, 30, 30, 30,
|
||||
20, 20, 20, 20, 20, 20, 20, 20,
|
||||
10, 10, 10, 10, 10, 10, 10, 10,
|
||||
5, 5, 5, 5, 5, 5, 5, 5,
|
||||
0, 0, 0, 0, 0, 0, 0, 0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 70, 70, 70, 70, 70, 70, 70, 70, 40, 40, 40, 40, 40, 40, 40, 40, 30, 30, 30, 30, 30, 30, 30,
|
||||
30, 20, 20, 20, 20, 20, 20, 20, 20, 10, 10, 10, 10, 10, 10, 10, 10, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
)
|
||||
|
||||
private val mgKnightTable: Array[Int] = Array(
|
||||
-50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 0, 0, 0, -20, -40, -30, 0, 10, 15, 15, 10, 0, -30, -30, 5, 15,
|
||||
20, 20, 15, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, -30, 5, 10, 15, 15, 10, 5, -30, -40, -20, 0, 5, 5, 0, -20, -40,
|
||||
-50, -40, -30, -30, -30, -30, -40, -50,
|
||||
-40, -20, 0, 0, 0, 0, -20, -40,
|
||||
-30, 0, 10, 15, 15, 10, 0, -30,
|
||||
-30, 5, 15, 20, 20, 15, 5, -30,
|
||||
-30, 0, 15, 20, 20, 15, 0, -30,
|
||||
-30, 5, 10, 15, 15, 10, 5, -30,
|
||||
-40, -20, 0, 5, 5, 0, -20, -40,
|
||||
-50, -40, -30, -30, -30, -30, -40, -50
|
||||
)
|
||||
|
||||
private val egKnightTable: Array[Int] = Array(
|
||||
-30, -20, -10, -10, -10, -10, -20, -30,
|
||||
-20, 0, 5, 5, 5, 5, 0, -20,
|
||||
-10, 5, 15, 20, 20, 15, 5, -10,
|
||||
-10, 5, 20, 25, 25, 20, 5, -10,
|
||||
-10, 5, 20, 25, 25, 20, 5, -10,
|
||||
-10, 5, 15, 20, 20, 15, 5, -10,
|
||||
-20, 0, 5, 5, 5, 5, 0, -20,
|
||||
-30, -20, -10, -10, -10, -10, -20, -30
|
||||
-30, -20, -10, -10, -10, -10, -20, -30, -20, 0, 5, 5, 5, 5, 0, -20, -10, 5, 15, 20, 20, 15, 5, -10, -10, 5, 20, 25,
|
||||
25, 20, 5, -10, -10, 5, 20, 25, 25, 20, 5, -10, -10, 5, 15, 20, 20, 15, 5, -10, -20, 0, 5, 5, 5, 5, 0, -20, -30,
|
||||
-20, -10, -10, -10, -10, -20, -30,
|
||||
)
|
||||
|
||||
private val mgBishopTable: Array[Int] = Array(
|
||||
-20, -10, -10, -10, -10, -10, -10, -20,
|
||||
-10, 0, 0, 0, 0, 0, 0, -10,
|
||||
-10, 0, 5, 10, 10, 5, 0, -10,
|
||||
-10, 5, 5, 10, 10, 5, 5, -10,
|
||||
-10, 0, 10, 10, 10, 10, 0, -10,
|
||||
-10, 10, 10, 10, 10, 10, 10, -10,
|
||||
-10, 5, 0, 0, 0, 0, 5, -10,
|
||||
-20, -10, -10, -10, -10, -10, -10, -20
|
||||
-20, -10, -10, -10, -10, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 10, 10, 5, 0, -10, -10, 5, 5, 10, 10,
|
||||
5, 5, -10, -10, 0, 10, 10, 10, 10, 0, -10, -10, 10, 10, 10, 10, 10, 10, -10, -10, 5, 0, 0, 0, 0, 5, -10, -20, -10,
|
||||
-10, -10, -10, -10, -10, -20,
|
||||
)
|
||||
|
||||
private val egBishopTable: Array[Int] = Array(
|
||||
-20, -10, -5, -5, -5, -5, -10, -20,
|
||||
-10, 0, 5, 5, 5, 5, 0, -10,
|
||||
-5, 5, 10, 10, 10, 10, 5, -5,
|
||||
-5, 5, 10, 15, 15, 10, 5, -5,
|
||||
-5, 5, 10, 15, 15, 10, 5, -5,
|
||||
-5, 5, 10, 10, 10, 10, 5, -5,
|
||||
-10, 0, 5, 5, 5, 5, 0, -10,
|
||||
-20, -10, -5, -5, -5, -5, -10, -20
|
||||
-20, -10, -5, -5, -5, -5, -10, -20, -10, 0, 5, 5, 5, 5, 0, -10, -5, 5, 10, 10, 10, 10, 5, -5, -5, 5, 10, 15, 15, 10,
|
||||
5, -5, -5, 5, 10, 15, 15, 10, 5, -5, -5, 5, 10, 10, 10, 10, 5, -5, -10, 0, 5, 5, 5, 5, 0, -10, -20, -10, -5, -5, -5,
|
||||
-5, -10, -20,
|
||||
)
|
||||
|
||||
private val mgRookTable: Array[Int] = Array(
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
5, 10, 10, 10, 10, 10, 10, 5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
0, 0, 0, 5, 5, 0, 0, 0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 5, 10, 10, 10, 10, 10, 10, 5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0,
|
||||
0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 5, 5, 0, 0, 0,
|
||||
)
|
||||
|
||||
private val egRookTable: Array[Int] = Array(
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
5, 10, 10, 10, 10, 10, 10, 5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
-5, 0, 0, 0, 0, 0, 0, -5,
|
||||
0, 0, 0, 5, 5, 0, 0, 0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 5, 10, 10, 10, 10, 10, 10, 5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0,
|
||||
0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 5, 5, 0, 0, 0,
|
||||
)
|
||||
|
||||
private val mgQueenTable: Array[Int] = Array(
|
||||
-20, -10, -10, -5, -5, -10, -10, -20,
|
||||
-10, 0, 0, 0, 0, 0, 0, -10,
|
||||
-10, 0, 5, 5, 5, 5, 0, -10,
|
||||
-5, 0, 5, 5, 5, 5, 0, -5,
|
||||
0, 0, 5, 5, 5, 5, 0, -5,
|
||||
-10, 5, 5, 5, 5, 5, 0, -10,
|
||||
-10, 0, 5, 0, 0, 0, 0, -10,
|
||||
-20, -10, -10, -5, -5, -10, -10, -20
|
||||
-20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 5, 5, 5, 0, -10, -5, 0, 5, 5, 5, 5, 0,
|
||||
-5, 0, 0, 5, 5, 5, 5, 0, -5, -10, 5, 5, 5, 5, 5, 0, -10, -10, 0, 5, 0, 0, 0, 0, -10, -20, -10, -10, -5, -5, -10,
|
||||
-10, -20,
|
||||
)
|
||||
|
||||
private val egQueenTable: Array[Int] = Array(
|
||||
-15, -10, -8, -5, -5, -8, -10, -15,
|
||||
-10, 0, 3, 5, 5, 3, 0, -10,
|
||||
-8, 3, 10, 10, 10, 10, 3, -8,
|
||||
-5, 5, 10, 15, 15, 10, 5, -5,
|
||||
-5, 5, 10, 15, 15, 10, 5, -5,
|
||||
-8, 3, 10, 10, 10, 10, 3, -8,
|
||||
-10, 0, 3, 5, 5, 3, 0, -10,
|
||||
-15, -10, -8, -5, -5, -8, -10, -15
|
||||
-15, -10, -8, -5, -5, -8, -10, -15, -10, 0, 3, 5, 5, 3, 0, -10, -8, 3, 10, 10, 10, 10, 3, -8, -5, 5, 10, 15, 15, 10,
|
||||
5, -5, -5, 5, 10, 15, 15, 10, 5, -5, -8, 3, 10, 10, 10, 10, 3, -8, -10, 0, 3, 5, 5, 3, 0, -10, -15, -10, -8, -5, -5,
|
||||
-8, -10, -15,
|
||||
)
|
||||
|
||||
private val mgKingTable: Array[Int] = Array(
|
||||
-30, -40, -40, -50, -50, -40, -40, -30,
|
||||
-30, -40, -40, -50, -50, -40, -40, -30,
|
||||
-30, -40, -40, -50, -50, -40, -40, -30,
|
||||
-30, -40, -40, -50, -50, -40, -40, -30,
|
||||
-20, -30, -30, -40, -40, -30, -30, -20,
|
||||
-10, -20, -20, -20, -20, -20, -20, -10,
|
||||
20, 20, 0, 0, 0, 0, 20, 20,
|
||||
20, 30, 10, 0, 0, 10, 30, 20
|
||||
-30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40,
|
||||
-30, -30, -40, -40, -50, -50, -40, -40, -30, -20, -30, -30, -40, -40, -30, -30, -20, -10, -20, -20, -20, -20, -20,
|
||||
-20, -10, 20, 20, 0, 0, 0, 0, 20, 20, 20, 30, 10, 0, 0, 10, 30, 20,
|
||||
)
|
||||
|
||||
private val egKingTable: Array[Int] = Array(
|
||||
-50, -40, -30, -20, -20, -30, -40, -50,
|
||||
-30, -20, -10, 0, 0, -10, -20, -30,
|
||||
-30, -10, 20, 30, 30, 20, -10, -30,
|
||||
-30, -10, 30, 40, 40, 30, -10, -30,
|
||||
-30, -10, 30, 40, 40, 30, -10, -30,
|
||||
-30, -10, 20, 30, 30, 20, -10, -30,
|
||||
-30, -30, 0, 0, 0, 0, -30, -30,
|
||||
-50, -30, -30, -30, -30, -30, -30, -50
|
||||
-50, -40, -30, -20, -20, -30, -40, -50, -30, -20, -10, 0, 0, -10, -20, -30, -30, -10, 20, 30, 30, 20, -10, -30, -30,
|
||||
-10, 30, 40, 40, 30, -10, -30, -30, -10, 30, 40, 40, 30, -10, -30, -30, -10, 20, 30, 30, 20, -10, -30, -30, -30, 0,
|
||||
0, 0, 0, -30, -30, -50, -30, -30, -30, -30, -30, -30, -50,
|
||||
)
|
||||
|
||||
private val phaseWeight: Map[PieceType, Int] = Map(
|
||||
PieceType.Knight -> 1,
|
||||
PieceType.Bishop -> 1,
|
||||
PieceType.Rook -> 2,
|
||||
PieceType.Queen -> 4
|
||||
PieceType.Rook -> 2,
|
||||
PieceType.Queen -> 4,
|
||||
)
|
||||
private val maxPhase = 24 // 4*4 + 4*2 + 4*1 + 4*1
|
||||
|
||||
private val passedPawnBonus: Array[Int] = Array(0, 5, 10, 20, 35, 60, 100, 0)
|
||||
private val passedPawnBonus: Array[Int] = Array(0, 5, 10, 20, 35, 60, 100, 0)
|
||||
private val egPassedPawnBonus: Array[Int] = Array(0, 20, 40, 80, 150, 250, 400, 0)
|
||||
|
||||
// Pawn structure penalties
|
||||
private val doubledMg = -10
|
||||
private val doubledEg = -25
|
||||
private val doubledMg = -10
|
||||
private val doubledEg = -25
|
||||
private val isolatedMg = -15
|
||||
private val isolatedEg = -20
|
||||
|
||||
@@ -174,27 +110,27 @@ object EvaluationClassic extends Weights:
|
||||
private val mobilityEg = Array(0, 4, 3, 4, 2, 0, 0)
|
||||
|
||||
// Direction offsets for sliding pieces
|
||||
private val diagonals = List((-1, -1), (-1, 1), (1, -1), (1, 1))
|
||||
private val orthogonals = List((-1, 0), (1, 0), (0, -1), (0, 1))
|
||||
private val diagonals = List((-1, -1), (-1, 1), (1, -1), (1, 1))
|
||||
private val orthogonals = List((-1, 0), (1, 0), (0, -1), (0, 1))
|
||||
private val knightOffsets = List((-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1))
|
||||
|
||||
// Rook and bishop bonuses
|
||||
private val bishopPairMg = 50
|
||||
private val bishopPairEg = 70
|
||||
private val rookOn7thMg = 20
|
||||
private val rookOn7thEg = 10
|
||||
private val rookOn7thMg = 20
|
||||
private val rookOn7thEg = 10
|
||||
|
||||
/** Evaluate the position from the perspective of context.turn.
|
||||
* Positive = good for context.turn. */
|
||||
/** Evaluate the position from the perspective of context.turn. Positive = good for context.turn.
|
||||
*/
|
||||
def evaluate(context: GameContext): Int =
|
||||
val phase = gamePhase(context.board)
|
||||
val isEg = isEndgame(phase)
|
||||
val material = materialAndPositional(context, phase)
|
||||
val structure = pawnStructure(context, phase)
|
||||
val mobility = mobilityScore(context, phase)
|
||||
val phase = gamePhase(context.board)
|
||||
val isEg = isEndgame(phase)
|
||||
val material = materialAndPositional(context, phase)
|
||||
val structure = pawnStructure(context, phase)
|
||||
val mobility = mobilityScore(context, phase)
|
||||
val rookBishop = rookAndBishopBonuses(context, phase)
|
||||
val bonuses = positionalBonuses(context, phase, isEg)
|
||||
val egBonuses = if isEg then endgameBonus(context) else 0
|
||||
val bonuses = positionalBonuses(context, phase, isEg)
|
||||
val egBonuses = if isEg then endgameBonus(context) else 0
|
||||
material + structure + mobility + rookBishop + bonuses + egBonuses + TEMPO_BONUS
|
||||
|
||||
private def gamePhase(board: de.nowchess.api.board.Board): Int =
|
||||
@@ -204,104 +140,71 @@ object EvaluationClassic extends Weights:
|
||||
math.min(phase, maxPhase)
|
||||
|
||||
private def isEndgame(phase: Int): Boolean =
|
||||
phase < 8 // Significantly reduced material indicates endgame
|
||||
phase < 8 // Significantly reduced material indicates endgame
|
||||
|
||||
private def taper(mg: Int, eg: Int, phase: Int): Int =
|
||||
(mg * phase + eg * (maxPhase - phase)) / maxPhase
|
||||
|
||||
private def materialAndPositional(context: GameContext, phase: Int): Int =
|
||||
var mg = 0
|
||||
var eg = 0
|
||||
for (square, piece) <- context.board.pieces do
|
||||
val (mg, eg) = context.board.pieces.foldLeft((0, 0)) { case ((mg, eg), (square, piece)) =>
|
||||
val (psqMg, psqEg) = squareBonus(piece.pieceType, piece.color, square)
|
||||
val pieceMg = mgMaterial(piece.pieceType.ordinal) + psqMg
|
||||
val pieceEg = egMaterial(piece.pieceType.ordinal) + psqEg
|
||||
val sign = if piece.color == context.turn then 1 else -1
|
||||
mg += sign * pieceMg
|
||||
eg += sign * pieceEg
|
||||
val pieceMg = mgMaterial(piece.pieceType.ordinal) + psqMg
|
||||
val pieceEg = egMaterial(piece.pieceType.ordinal) + psqEg
|
||||
val sign = if piece.color == context.turn then 1 else -1
|
||||
(mg + sign * pieceMg, eg + sign * pieceEg)
|
||||
}
|
||||
taper(mg, eg, phase)
|
||||
|
||||
private def squareBonus(pieceType: PieceType, color: Color, sq: Square): (Int, Int) =
|
||||
val rankIdx = if color == Color.White then sq.rank.ordinal else 7 - sq.rank.ordinal
|
||||
val fileIdx = sq.file.ordinal
|
||||
val rankIdx = if color == Color.White then sq.rank.ordinal else 7 - sq.rank.ordinal
|
||||
val fileIdx = sq.file.ordinal
|
||||
val squareIdx = rankIdx * 8 + fileIdx
|
||||
|
||||
pieceType match
|
||||
case PieceType.Pawn => (mgPawnTable(squareIdx), egPawnTable(squareIdx))
|
||||
case PieceType.Pawn => (mgPawnTable(squareIdx), egPawnTable(squareIdx))
|
||||
case PieceType.Knight => (mgKnightTable(squareIdx), egKnightTable(squareIdx))
|
||||
case PieceType.Bishop => (mgBishopTable(squareIdx), egBishopTable(squareIdx))
|
||||
case PieceType.Rook => (mgRookTable(squareIdx), egRookTable(squareIdx))
|
||||
case PieceType.Queen => (mgQueenTable(squareIdx), egQueenTable(squareIdx))
|
||||
case PieceType.King => (mgKingTable(squareIdx), egKingTable(squareIdx))
|
||||
case PieceType.Rook => (mgRookTable(squareIdx), egRookTable(squareIdx))
|
||||
case PieceType.Queen => (mgQueenTable(squareIdx), egQueenTable(squareIdx))
|
||||
case PieceType.King => (mgKingTable(squareIdx), egKingTable(squareIdx))
|
||||
|
||||
private def pawnStructure(context: GameContext, phase: Int): Int =
|
||||
val friendlyPawns = context.board.pieces.filter((_, p) => p.color == context.turn && p.pieceType == PieceType.Pawn)
|
||||
val enemyPawns = context.board.pieces.filter((_, p) => p.color != context.turn && p.pieceType == PieceType.Pawn)
|
||||
val enemyPawns = context.board.pieces.filter((_, p) => p.color != context.turn && p.pieceType == PieceType.Pawn)
|
||||
|
||||
var mg = 0
|
||||
var eg = 0
|
||||
|
||||
// Group pawns by file
|
||||
val friendlyByFile = friendlyPawns.groupMap(s => s._1.file.ordinal)(s => s._1.rank.ordinal)
|
||||
val enemyByFile = enemyPawns.groupMap(s => s._1.file.ordinal)(s => s._1.rank.ordinal)
|
||||
val enemyByFile = enemyPawns.groupMap(s => s._1.file.ordinal)(s => s._1.rank.ordinal)
|
||||
|
||||
// Doubled pawn penalty for friendly pawns
|
||||
for (file, ranks) <- friendlyByFile do
|
||||
if ranks.size > 1 then
|
||||
val doubledCount = ranks.size - 1
|
||||
mg += doubledCount * doubledMg
|
||||
eg += doubledCount * doubledEg
|
||||
val (fMg, fEg) = structureScore(friendlyByFile)
|
||||
val (eMg, eEg) = structureScore(enemyByFile)
|
||||
taper(fMg - eMg, fEg - eEg, phase)
|
||||
|
||||
// Isolated pawn penalty for friendly pawns
|
||||
for (file, _) <- friendlyByFile do
|
||||
val hasAdjacentFriendly = (file - 1 to file + 1).filter(f => f >= 0 && f < 8 && f != file).exists(friendlyByFile.contains)
|
||||
if !hasAdjacentFriendly then
|
||||
val pawnsOnFile = friendlyByFile(file).size
|
||||
mg += pawnsOnFile * isolatedMg
|
||||
eg += pawnsOnFile * isolatedEg
|
||||
|
||||
// Same for enemy pawns (subtract from score)
|
||||
var enemyMg = 0
|
||||
var enemyEg = 0
|
||||
|
||||
for (file, ranks) <- enemyByFile do
|
||||
if ranks.size > 1 then
|
||||
val doubledCount = ranks.size - 1
|
||||
enemyMg += doubledCount * doubledMg
|
||||
enemyEg += doubledCount * doubledEg
|
||||
|
||||
for (file, _) <- enemyByFile do
|
||||
val hasAdjacentEnemy = (file - 1 to file + 1).filter(f => f >= 0 && f < 8 && f != file).exists(enemyByFile.contains)
|
||||
if !hasAdjacentEnemy then
|
||||
val pawnsOnFile = enemyByFile(file).size
|
||||
enemyMg += pawnsOnFile * isolatedMg
|
||||
enemyEg += pawnsOnFile * isolatedEg
|
||||
|
||||
taper(mg - enemyMg, eg - enemyEg, phase)
|
||||
private def structureScore(byFile: Map[Int, Iterable[Int]]): (Int, Int) =
|
||||
byFile.foldLeft((0, 0)) { case ((mg, eg), (file, ranks)) =>
|
||||
val doubled = (ranks.size - 1).max(0)
|
||||
val hasAdjacent = (file - 1 to file + 1).filter(f => f >= 0 && f < 8 && f != file).exists(byFile.contains)
|
||||
val isolated = if !hasAdjacent then ranks.size else 0
|
||||
(mg + doubled * doubledMg + isolated * isolatedMg, eg + doubled * doubledEg + isolated * isolatedEg)
|
||||
}
|
||||
|
||||
private def positionalBonuses(context: GameContext, phase: Int, isEg: Boolean = false): Int =
|
||||
var score = 0
|
||||
for (sq, piece) <- context.board.pieces do
|
||||
context.board.pieces.foldLeft(0) { case (score, (sq, piece)) =>
|
||||
val bonus = piece.pieceType match
|
||||
case PieceType.Pawn =>
|
||||
if isPassedPawn(context.board, sq, piece.color) then
|
||||
if isEg then egPassedPawnBonus(sq.rank.ordinal)
|
||||
else passedPawnBonus(sq.rank.ordinal)
|
||||
if isEg then egPassedPawnBonus(sq.rank.ordinal) else passedPawnBonus(sq.rank.ordinal)
|
||||
else 0
|
||||
case PieceType.Rook =>
|
||||
rookOpenFileBonus(context.board, sq, piece.color)
|
||||
case PieceType.King =>
|
||||
kingShieldBonus(context.board, sq, piece.color, phase)
|
||||
case _ => 0
|
||||
if piece.color == context.turn then score += bonus
|
||||
else score -= bonus
|
||||
score
|
||||
case PieceType.Rook => rookOpenFileBonus(context.board, sq, piece.color)
|
||||
case PieceType.King => kingShieldBonus(context.board, sq, piece.color, phase)
|
||||
case _ => 0
|
||||
if piece.color == context.turn then score + bonus else score - bonus
|
||||
}
|
||||
|
||||
private def isPassedPawn(board: de.nowchess.api.board.Board, sq: Square, color: Color): Boolean =
|
||||
val enemyColor = color.opposite
|
||||
val pawnRank = sq.rank.ordinal
|
||||
val fileRange = (sq.file.ordinal - 1 to sq.file.ordinal + 1).filter(f => f >= 0 && f < 8)
|
||||
val rankCheck = if color == Color.White then (r: Int) => r > pawnRank else (r: Int) => r < pawnRank
|
||||
val pawnRank = sq.rank.ordinal
|
||||
val fileRange = (sq.file.ordinal - 1 to sq.file.ordinal + 1).filter(f => f >= 0 && f < 8)
|
||||
val rankCheck = if color == Color.White then (r: Int) => r > pawnRank else (r: Int) => r < pawnRank
|
||||
|
||||
board.pieces.forall { (enemySq, enemyPiece) =>
|
||||
!(enemyPiece.color == enemyColor &&
|
||||
@@ -317,14 +220,14 @@ object EvaluationClassic extends Weights:
|
||||
val hasEnemyPawn = board.pieces.exists { (sq, piece) =>
|
||||
piece.color != color && piece.pieceType == PieceType.Pawn && sq.file == rookSq.file
|
||||
}
|
||||
if !hasFriendlyPawn && !hasEnemyPawn then 20 // open file
|
||||
else if !hasFriendlyPawn then 10 // semi-open file
|
||||
if !hasFriendlyPawn && !hasEnemyPawn then 20 // open file
|
||||
else if !hasFriendlyPawn then 10 // semi-open file
|
||||
else 0
|
||||
|
||||
private def kingShieldBonus(board: de.nowchess.api.board.Board, kingSq: Square, color: Color, phase: Int): Int =
|
||||
val shieldRankDelta = if color == Color.White then 1 else -1
|
||||
val shieldFiles = (kingSq.file.ordinal - 1 to kingSq.file.ordinal + 1).filter(f => f >= 0 && f < 8)
|
||||
val shieldRank = kingSq.rank.ordinal + shieldRankDelta
|
||||
val shieldFiles = (kingSq.file.ordinal - 1 to kingSq.file.ordinal + 1).filter(f => f >= 0 && f < 8)
|
||||
val shieldRank = kingSq.rank.ordinal + shieldRankDelta
|
||||
|
||||
if shieldRank < 0 || shieldRank > 7 then 0
|
||||
else
|
||||
@@ -336,17 +239,23 @@ object EvaluationClassic extends Weights:
|
||||
} * 10
|
||||
(rawBonus * phase) / maxPhase
|
||||
|
||||
private def slidingCount(sq: Square, board: de.nowchess.api.board.Board, color: Color, directions: List[(Int, Int)]): Int =
|
||||
private def slidingCount(
|
||||
sq: Square,
|
||||
board: de.nowchess.api.board.Board,
|
||||
color: Color,
|
||||
directions: List[(Int, Int)],
|
||||
): Int =
|
||||
directions.foldLeft(0) { case (total, (fileDelta, rankDelta)) =>
|
||||
var count = 0
|
||||
var current = sq.offset(fileDelta, rankDelta)
|
||||
while current.isDefined do
|
||||
val target = current.get
|
||||
board.pieceAt(target) match
|
||||
case Some(piece) if piece.color == color => current = None
|
||||
case Some(_) => count += 1; current = None
|
||||
case None => count += 1; current = target.offset(fileDelta, rankDelta)
|
||||
total + count
|
||||
@scala.annotation.tailrec
|
||||
def countRay(current: Option[Square], acc: Int): Int =
|
||||
current match
|
||||
case None => acc
|
||||
case Some(target) =>
|
||||
board.pieceAt(target) match
|
||||
case Some(piece) if piece.color == color => acc
|
||||
case Some(_) => acc + 1
|
||||
case None => countRay(target.offset(fileDelta, rankDelta), acc + 1)
|
||||
total + countRay(sq.offset(fileDelta, rankDelta), 0)
|
||||
}
|
||||
|
||||
private def knightCount(sq: Square, board: de.nowchess.api.board.Board, color: Color): Int =
|
||||
@@ -357,83 +266,61 @@ object EvaluationClassic extends Weights:
|
||||
}
|
||||
|
||||
private def mobilityScore(context: GameContext, phase: Int): Int =
|
||||
var mg = 0
|
||||
var eg = 0
|
||||
for (sq, piece) <- context.board.pieces do
|
||||
val (mg, eg) = context.board.pieces.foldLeft((0, 0)) { case ((mg, eg), (sq, piece)) =>
|
||||
val count = piece.pieceType match
|
||||
case PieceType.Knight => knightCount(sq, context.board, piece.color)
|
||||
case PieceType.Bishop => slidingCount(sq, context.board, piece.color, diagonals)
|
||||
case PieceType.Rook => slidingCount(sq, context.board, piece.color, orthogonals)
|
||||
case PieceType.Queen => slidingCount(sq, context.board, piece.color, diagonals ++ orthogonals)
|
||||
case _ => 0
|
||||
case PieceType.Rook => slidingCount(sq, context.board, piece.color, orthogonals)
|
||||
case PieceType.Queen => slidingCount(sq, context.board, piece.color, diagonals ++ orthogonals)
|
||||
case _ => 0
|
||||
val pieceMg = count * mobilityMg(piece.pieceType.ordinal)
|
||||
val pieceEg = count * mobilityEg(piece.pieceType.ordinal)
|
||||
val sign = if piece.color == context.turn then 1 else -1
|
||||
mg += sign * pieceMg
|
||||
eg += sign * pieceEg
|
||||
val sign = if piece.color == context.turn then 1 else -1
|
||||
(mg + sign * pieceMg, eg + sign * pieceEg)
|
||||
}
|
||||
taper(mg, eg, phase)
|
||||
|
||||
private def rookAndBishopBonuses(context: GameContext, phase: Int): Int =
|
||||
var mg = 0
|
||||
var eg = 0
|
||||
|
||||
// Bishop pair bonus
|
||||
val friendlyBishops = context.board.pieces.filter((_, p) => p.color == context.turn && p.pieceType == PieceType.Bishop)
|
||||
val friendlyBishops =
|
||||
context.board.pieces.filter((_, p) => p.color == context.turn && p.pieceType == PieceType.Bishop)
|
||||
val enemyBishops = context.board.pieces.filter((_, p) => p.color != context.turn && p.pieceType == PieceType.Bishop)
|
||||
|
||||
val friendlyLightSquare = friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0)
|
||||
val friendlyDarkSquare = friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
||||
if friendlyLightSquare && friendlyDarkSquare then
|
||||
mg += bishopPairMg
|
||||
eg += bishopPairEg
|
||||
val friendlyHasPair =
|
||||
friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0) &&
|
||||
friendlyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
||||
val enemyHasPair =
|
||||
enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0) &&
|
||||
enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
||||
|
||||
val enemyLightSquare = enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 0)
|
||||
val enemyDarkSquare = enemyBishops.exists((sq, _) => (sq.file.ordinal + sq.rank.ordinal) % 2 == 1)
|
||||
if enemyLightSquare && enemyDarkSquare then
|
||||
mg -= bishopPairMg
|
||||
eg -= bishopPairEg
|
||||
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)
|
||||
|
||||
// Rook on 7th rank
|
||||
for (sq, piece) <- context.board.pieces do
|
||||
val (mg, eg) = context.board.pieces.foldLeft((baseMg, baseEg)) { case ((mg, eg), (sq, piece)) =>
|
||||
if piece.pieceType == PieceType.Rook then
|
||||
val is7th = if piece.color == Color.White then sq.rank.ordinal == 6 else sq.rank.ordinal == 1
|
||||
if is7th then
|
||||
if piece.color == context.turn then
|
||||
mg += rookOn7thMg
|
||||
eg += rookOn7thEg
|
||||
else
|
||||
mg -= rookOn7thMg
|
||||
eg -= rookOn7thEg
|
||||
|
||||
if piece.color == context.turn then (mg + rookOn7thMg, eg + rookOn7thEg)
|
||||
else (mg - rookOn7thMg, eg - rookOn7thEg)
|
||||
else (mg, eg)
|
||||
else (mg, eg)
|
||||
}
|
||||
taper(mg, eg, phase)
|
||||
|
||||
private def endgameBonus(context: GameContext): Int =
|
||||
val friendlyKing = context.board.pieces.find((_, p) => p.color == context.turn && p.pieceType == PieceType.King)
|
||||
val enemyKing = context.board.pieces.find((_, p) => p.color != context.turn && p.pieceType == PieceType.King)
|
||||
val enemyKing = context.board.pieces.find((_, p) => p.color != context.turn && p.pieceType == PieceType.King)
|
||||
|
||||
var bonus = 0
|
||||
val kingCentralBonus =
|
||||
friendlyKing.fold(0)((kSq, _) => (8 - kingCentralizationDistance(kSq)) * 15) -
|
||||
enemyKing.fold(0)((kSq, _) => (8 - kingCentralizationDistance(kSq)) * 15)
|
||||
|
||||
// King centralization: closer to center is better in endgame
|
||||
friendlyKing.foreach { (friendlyKSq, _) =>
|
||||
val centerDist = kingCentralizationDistance(friendlyKSq)
|
||||
bonus += (8 - centerDist) * 15
|
||||
}
|
||||
|
||||
enemyKing.foreach { (enemyKSq, _) =>
|
||||
val centerDist = kingCentralizationDistance(enemyKSq)
|
||||
bonus -= (8 - centerDist) * 15
|
||||
}
|
||||
|
||||
// Push enemy king to edge when materially ahead
|
||||
val friendlyMaterial = materialCount(context, context.turn)
|
||||
val enemyMaterial = materialCount(context, context.turn.opposite)
|
||||
if friendlyMaterial > enemyMaterial then
|
||||
enemyKing.foreach { (enemyKSq, _) =>
|
||||
val edgeDist = kingEdgeDistance(enemyKSq)
|
||||
bonus += (7 - edgeDist) * 10
|
||||
}
|
||||
val enemyMaterial = materialCount(context, context.turn.opposite)
|
||||
val edgeBonus =
|
||||
if friendlyMaterial > enemyMaterial then enemyKing.fold(0)((kSq, _) => (7 - kingEdgeDistance(kSq)) * 10)
|
||||
else 0
|
||||
|
||||
bonus
|
||||
kingCentralBonus + edgeBonus
|
||||
|
||||
private def kingCentralizationDistance(sq: Square): Int =
|
||||
val fileFromCenter = (sq.file.ordinal - 3.5).abs.toInt
|
||||
@@ -451,8 +338,9 @@ object EvaluationClassic extends Weights:
|
||||
sum + (piece.pieceType match
|
||||
case PieceType.Knight => 300
|
||||
case PieceType.Bishop => 300
|
||||
case PieceType.Rook => 500
|
||||
case PieceType.Queen => 900
|
||||
case _ => 0)
|
||||
case PieceType.Rook => 500
|
||||
case PieceType.Queen => 900
|
||||
case _ => 0
|
||||
)
|
||||
else sum
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import scala.collection.mutable
|
||||
object MoveOrdering:
|
||||
|
||||
class OrderingContext:
|
||||
private val killerMoves = mutable.Map[Int, List[Move]]()
|
||||
private val killerMoves = mutable.Map[Int, List[Move]]()
|
||||
private val historyTable = mutable.Map[(Int, Int), Int]()
|
||||
|
||||
def addKillerMove(ply: Int, move: Move): Unit =
|
||||
@@ -33,43 +33,44 @@ object MoveOrdering:
|
||||
historyTable.clear()
|
||||
|
||||
def score(
|
||||
context: GameContext,
|
||||
move: Move,
|
||||
ttBestMove: Option[Move],
|
||||
ply: Int = 0,
|
||||
ordering: OrderingContext = new OrderingContext()
|
||||
context: GameContext,
|
||||
move: Move,
|
||||
ttBestMove: Option[Move],
|
||||
ply: Int = 0,
|
||||
ordering: OrderingContext = new OrderingContext(),
|
||||
): Int =
|
||||
if ttBestMove.exists(m => m.from == move.from && m.to == move.to) then Int.MaxValue
|
||||
else move.moveType match
|
||||
case MoveType.Promotion(PromotionPiece.Queen) =>
|
||||
1_000_000 + promotionCaptureBonus(context, move)
|
||||
case MoveType.Normal(true) | MoveType.EnPassant =>
|
||||
captureScore(context, move)
|
||||
case MoveType.Promotion(_) =>
|
||||
50_000 + promotionCaptureBonus(context, move)
|
||||
case _ => scoreQuietMove(move, ply, ordering)
|
||||
else
|
||||
move.moveType match
|
||||
case MoveType.Promotion(PromotionPiece.Queen) =>
|
||||
1_000_000 + promotionCaptureBonus(context, move)
|
||||
case MoveType.Normal(true) | MoveType.EnPassant =>
|
||||
captureScore(context, move)
|
||||
case MoveType.Promotion(_) =>
|
||||
50_000 + promotionCaptureBonus(context, move)
|
||||
case _ => scoreQuietMove(move, ply, ordering)
|
||||
|
||||
def sort(
|
||||
context: GameContext,
|
||||
moves: List[Move],
|
||||
ttBestMove: Option[Move],
|
||||
ply: Int = 0,
|
||||
ordering: OrderingContext = new OrderingContext()
|
||||
context: GameContext,
|
||||
moves: List[Move],
|
||||
ttBestMove: Option[Move],
|
||||
ply: Int = 0,
|
||||
ordering: OrderingContext = new OrderingContext(),
|
||||
): List[Move] =
|
||||
moves.sortBy(m => -score(context, m, ttBestMove, ply, ordering))
|
||||
|
||||
private def scoreQuietMove(move: Move, ply: Int, ordering: OrderingContext): Int =
|
||||
val isKiller = ordering.getKillerMoves(ply).exists(k => k.from == move.from && k.to == move.to)
|
||||
val fromIdx = move.from.rank.ordinal * 8 + move.from.file.ordinal
|
||||
val toIdx = move.to.rank.ordinal * 8 + move.to.file.ordinal
|
||||
val history = ordering.getHistory(fromIdx, toIdx)
|
||||
val fromIdx = move.from.rank.ordinal * 8 + move.from.file.ordinal
|
||||
val toIdx = move.to.rank.ordinal * 8 + move.to.file.ordinal
|
||||
val history = ordering.getHistory(fromIdx, toIdx)
|
||||
if isKiller then 10_000 + (history / 10) else history / 10
|
||||
|
||||
private def promotionCaptureBonus(context: GameContext, move: Move): Int =
|
||||
if isCapture(context, move) then captureScore(context, move) else 0
|
||||
|
||||
private def captureScore(context: GameContext, move: Move): Int =
|
||||
val see = staticExchange(context, move)
|
||||
val see = staticExchange(context, move)
|
||||
val seeBias = if see >= 0 then 20_000 else -20_000
|
||||
100_000 + mvvLva(context, move) + seeBias + see
|
||||
|
||||
@@ -82,28 +83,28 @@ object MoveOrdering:
|
||||
private def victimValue(context: GameContext, move: Move): Int =
|
||||
move.moveType match
|
||||
case MoveType.Normal(true) => context.board.pieceAt(move.to).map(pieceValue).getOrElse(0)
|
||||
case MoveType.EnPassant => 1
|
||||
case MoveType.EnPassant => 1
|
||||
case MoveType.Promotion(_) => context.board.pieceAt(move.to).map(pieceValue).getOrElse(0)
|
||||
case _ => 0
|
||||
case _ => 0
|
||||
|
||||
private def pieceValue(piece: Piece): Int = piece.pieceType match
|
||||
case PieceType.Pawn => 1
|
||||
case PieceType.Pawn => 1
|
||||
case PieceType.Knight => 3
|
||||
case PieceType.Bishop => 3
|
||||
case PieceType.Rook => 5
|
||||
case PieceType.Queen => 9
|
||||
case PieceType.King => 200
|
||||
case PieceType.Rook => 5
|
||||
case PieceType.Queen => 9
|
||||
case PieceType.King => 200
|
||||
|
||||
private def isCapture(context: GameContext, move: Move): Boolean = move.moveType match
|
||||
case MoveType.Normal(true) => true
|
||||
case MoveType.EnPassant => true
|
||||
case MoveType.EnPassant => true
|
||||
case MoveType.Promotion(_) => context.board.pieceAt(move.to).exists(_.color != context.turn)
|
||||
case _ => false
|
||||
case _ => false
|
||||
|
||||
private def staticExchange(context: GameContext, move: Move): Int =
|
||||
if !isCapture(context, move) then 0
|
||||
else
|
||||
val target = move.to
|
||||
val target = move.to
|
||||
val initialGain = victimValue(context, move)
|
||||
movedPieceAfterMove(context, move).fold(initialGain) { moved =>
|
||||
val boardAfterMove = applySeeMove(context.board, context, move, moved)
|
||||
@@ -113,21 +114,23 @@ object MoveOrdering:
|
||||
private def movedPieceAfterMove(context: GameContext, move: Move): Option[Piece] =
|
||||
move.moveType match
|
||||
case MoveType.Promotion(pp) => Some(Piece(context.turn, promotionPieceType(pp)))
|
||||
case _ => context.board.pieceAt(move.from)
|
||||
case _ => context.board.pieceAt(move.from)
|
||||
|
||||
@tailrec
|
||||
private def exchangeGain(board: Board, target: Square, side: Color, capturedValue: Int, gains: Vector[Int]): Int =
|
||||
leastValuableAttacker(board, target, side) match
|
||||
case None => resolveGain(gains)
|
||||
case Some((from, attacker)) =>
|
||||
val nextGain = capturedValue - gains.last
|
||||
val nextGain = capturedValue - gains.last
|
||||
val nextBoard = board.removed(from).updated(target, attacker)
|
||||
exchangeGain(nextBoard, target, side.opposite, pieceValue(attacker), gains :+ nextGain)
|
||||
|
||||
private def resolveGain(gains: Vector[Int]): Int =
|
||||
(gains.length - 2 to 0 by -1).foldLeft(gains) { (acc, idx) =>
|
||||
acc.updated(idx, math.max(acc(idx), -acc(idx + 1)))
|
||||
}.head
|
||||
(gains.length - 2 to 0 by -1)
|
||||
.foldLeft(gains) { (acc, idx) =>
|
||||
acc.updated(idx, math.max(acc(idx), -acc(idx + 1)))
|
||||
}
|
||||
.head
|
||||
|
||||
private def applySeeMove(board: Board, context: GameContext, move: Move, moved: Piece): Board =
|
||||
move.moveType match
|
||||
@@ -137,9 +140,13 @@ object MoveOrdering:
|
||||
case _ => board.removed(move.from).updated(move.to, moved)
|
||||
|
||||
private def leastValuableAttacker(board: Board, target: Square, color: Color): Option[(Square, Piece)] =
|
||||
board.pieces.collect {
|
||||
case (sq, piece) if piece.color == color && attacksSquare(board, sq, target, piece) => (sq, piece)
|
||||
}.toList.sortBy { case (_, piece) => pieceValue(piece) }.headOption
|
||||
board.pieces
|
||||
.collect {
|
||||
case (sq, piece) if piece.color == color && attacksSquare(board, sq, target, piece) => (sq, piece)
|
||||
}
|
||||
.toList
|
||||
.sortBy { case (_, piece) => pieceValue(piece) }
|
||||
.headOption
|
||||
|
||||
private def attacksSquare(board: Board, from: Square, target: Square, piece: Piece): Boolean =
|
||||
val df = target.file.ordinal - from.file.ordinal
|
||||
@@ -153,26 +160,27 @@ object MoveOrdering:
|
||||
val adr = math.abs(dr)
|
||||
(adf == 1 && adr == 2) || (adf == 2 && adr == 1)
|
||||
case PieceType.Bishop => clearLine(board, from, target, df, dr, diagonal = true)
|
||||
case PieceType.Rook => clearLine(board, from, target, df, dr, diagonal = false)
|
||||
case PieceType.Rook => clearLine(board, from, target, df, dr, diagonal = false)
|
||||
case PieceType.Queen =>
|
||||
clearLine(board, from, target, df, dr, diagonal = true) ||
|
||||
clearLine(board, from, target, df, dr, diagonal = false)
|
||||
clearLine(board, from, target, df, dr, diagonal = false)
|
||||
case PieceType.King => math.abs(df) <= 1 && math.abs(dr) <= 1
|
||||
|
||||
private def clearLine(board: Board, from: Square, target: Square, df: Int, dr: Int, diagonal: Boolean): Boolean =
|
||||
val valid = if diagonal then math.abs(df) == math.abs(dr) && df != 0 else (df == 0 && dr != 0) || (dr == 0 && df != 0)
|
||||
val valid =
|
||||
if diagonal then math.abs(df) == math.abs(dr) && df != 0 else (df == 0 && dr != 0) || (dr == 0 && df != 0)
|
||||
if !valid then false
|
||||
else pathClear(board, from, target, Integer.compare(df, 0), Integer.compare(dr, 0))
|
||||
|
||||
@tailrec
|
||||
private def pathClear(board: Board, from: Square, target: Square, stepF: Int, stepR: Int): Boolean =
|
||||
from.offset(stepF, stepR) match
|
||||
case None => false
|
||||
case None => false
|
||||
case Some(next) if next == target => true
|
||||
case Some(next) => board.pieceAt(next).isEmpty && pathClear(board, next, target, stepF, stepR)
|
||||
case Some(next) => board.pieceAt(next).isEmpty && pathClear(board, next, target, stepF, stepR)
|
||||
|
||||
private def promotionPieceType(piece: PromotionPiece): PieceType = piece match
|
||||
case PromotionPiece.Knight => PieceType.Knight
|
||||
case PromotionPiece.Bishop => PieceType.Bishop
|
||||
case PromotionPiece.Rook => PieceType.Rook
|
||||
case PromotionPiece.Queen => PieceType.Queen
|
||||
case PromotionPiece.Rook => PieceType.Rook
|
||||
case PromotionPiece.Queen => PieceType.Queen
|
||||
|
||||
@@ -3,23 +3,23 @@ package de.nowchess.bot.logic
|
||||
import de.nowchess.api.move.Move
|
||||
|
||||
enum TTFlag:
|
||||
case Exact // Score is exact
|
||||
case Lower // Score is a lower bound
|
||||
case Upper // Score is an upper bound
|
||||
case Exact // Score is exact
|
||||
case Lower // Score is a lower bound
|
||||
case Upper // Score is an upper bound
|
||||
|
||||
final case class TTEntry(
|
||||
hash: Long,
|
||||
depth: Int,
|
||||
score: Int,
|
||||
flag: TTFlag,
|
||||
bestMove: Option[Move]
|
||||
hash: Long,
|
||||
depth: Int,
|
||||
score: Int,
|
||||
flag: TTFlag,
|
||||
bestMove: Option[Move],
|
||||
)
|
||||
|
||||
final class TranspositionTable(val sizePow2: Int = 20):
|
||||
private val size = 1 << sizePow2
|
||||
private val mask = size - 1L
|
||||
private val locks = Array.fill(size)(new Object())
|
||||
private var table: Array[Option[TTEntry]] = Array.fill(size)(None)
|
||||
private val size = 1 << sizePow2
|
||||
private val mask = size - 1L
|
||||
private val locks = Array.fill(size)(new Object())
|
||||
private val table: Array[Option[TTEntry]] = Array.fill(size)(None)
|
||||
|
||||
def probe(hash: Long): Option[TTEntry] =
|
||||
val index = (hash & mask).toInt
|
||||
@@ -34,8 +34,5 @@ final class TranspositionTable(val sizePow2: Int = 20):
|
||||
}
|
||||
|
||||
def clear(): Unit =
|
||||
for lock <- locks do
|
||||
lock.synchronized {
|
||||
// Clear in-place to avoid reassigning table reference during search
|
||||
}
|
||||
table = Array.fill(size)(None)
|
||||
for i <- 0 until size do
|
||||
locks(i).synchronized { table(i) = None }
|
||||
|
||||
@@ -9,13 +9,13 @@ import scala.collection.mutable
|
||||
import scala.util.Random
|
||||
|
||||
/** Reads a Polyglot opening book (.bin file) and probes it for moves.
|
||||
*
|
||||
* Polyglot books are binary files with 16-byte big-endian records:
|
||||
* - key: 8 bytes (Long) — Zobrist hash of the position
|
||||
* - move: 2 bytes (Short) — packed as (to_file | to_rank | from_file | from_rank | promotion)
|
||||
* - weight: 2 bytes (Short) — move weight (higher = preferred)
|
||||
* - learn: 4 bytes (Int) — learning data (unused)
|
||||
*/
|
||||
*
|
||||
* Polyglot books are binary files with 16-byte big-endian records:
|
||||
* - key: 8 bytes (Long) — Zobrist hash of the position
|
||||
* - move: 2 bytes (Short) — packed as (to_file | to_rank | from_file | from_rank | promotion)
|
||||
* - weight: 2 bytes (Short) — move weight (higher = preferred)
|
||||
* - learn: 4 bytes (Int) — learning data (unused)
|
||||
*/
|
||||
final class PolyglotBook(path: String):
|
||||
|
||||
private val entries: Map[Long, Vector[BookEntry]] =
|
||||
@@ -35,8 +35,7 @@ final class PolyglotBook(path: String):
|
||||
val hash = PolyglotHash.hash(context)
|
||||
println(f"0x${hash}%016X")
|
||||
entries.get(hash).flatMap { bookEntries =>
|
||||
if bookEntries.isEmpty then
|
||||
None
|
||||
if bookEntries.isEmpty then None
|
||||
else
|
||||
val entry = weightedRandom(bookEntries)
|
||||
decodeMove(entry.move, context)
|
||||
@@ -47,64 +46,53 @@ final class PolyglotBook(path: String):
|
||||
try
|
||||
val result = mutable.Map[Long, Vector[BookEntry]]()
|
||||
while input.available() > 0 do
|
||||
val key = input.readLong()
|
||||
val move = input.readShort()
|
||||
val key = input.readLong()
|
||||
val move = input.readShort()
|
||||
val weight = input.readShort()
|
||||
val learn = input.readInt()
|
||||
val learn = input.readInt()
|
||||
|
||||
val entry = BookEntry(key, move, weight)
|
||||
result.updateWith(key) {
|
||||
case Some(entries) => Some(entries :+ entry)
|
||||
case None => Some(Vector(entry))
|
||||
case None => Some(Vector(entry))
|
||||
}
|
||||
result.toMap
|
||||
finally
|
||||
input.close()
|
||||
finally input.close()
|
||||
|
||||
/** Decode a packed Polyglot move short into an Option[Move].
|
||||
*
|
||||
* Bit layout of the move Short:
|
||||
* - bits 0-2: to_file (0-7)
|
||||
* - bits 3-5: to_rank (0-7)
|
||||
* - bits 6-8: from_file (0-7)
|
||||
* - bits 9-11: from_rank (0-7)
|
||||
* - bits 12-14: promotion piece (0=none, 1=knight, 2=bishop, 3=rook, 4=queen)
|
||||
*/
|
||||
*
|
||||
* Bit layout of the move Short:
|
||||
* - bits 0-2: to_file (0-7)
|
||||
* - bits 3-5: to_rank (0-7)
|
||||
* - bits 6-8: from_file (0-7)
|
||||
* - bits 9-11: from_rank (0-7)
|
||||
* - bits 12-14: promotion piece (0=none, 1=knight, 2=bishop, 3=rook, 4=queen)
|
||||
*/
|
||||
private def decodeMove(raw: Short, context: GameContext): Option[Move] =
|
||||
val toFile = (raw & 0x07).toInt
|
||||
val toRank = ((raw >> 3) & 0x07).toInt
|
||||
val fromFile = ((raw >> 6) & 0x07).toInt
|
||||
val fromRank = ((raw >> 9) & 0x07).toInt
|
||||
val toFile = (raw & 0x07).toInt
|
||||
val toRank = ((raw >> 3) & 0x07).toInt
|
||||
val fromFile = ((raw >> 6) & 0x07).toInt
|
||||
val fromRank = ((raw >> 9) & 0x07).toInt
|
||||
val promotionBits = ((raw >> 12) & 0x07).toInt
|
||||
|
||||
// Bounds check
|
||||
if toFile > 7 || toRank > 7 || fromFile > 7 || fromRank > 7 then
|
||||
return None
|
||||
|
||||
val from = Square(File.values(fromFile), Rank.values(fromRank))
|
||||
val to = Square(File.values(toFile), Rank.values(toRank))
|
||||
|
||||
// Check for castling: in Polyglot, castling is encoded as king-to-rook-square
|
||||
// e.g. e1 (from_rank=0, from_file=4) to h1 (to_file=7, to_rank=0) is white kingside
|
||||
val isCastling = isKingMove(context, from)
|
||||
if isCastling && isRookSquare(to, context) then
|
||||
return Some(decodeCastling(from, to))
|
||||
|
||||
// Handle promotion
|
||||
val moveType = if promotionBits > 0 then
|
||||
val promotionPiece = promotionBits match
|
||||
case 1 => PromotionPiece.Knight
|
||||
case 2 => PromotionPiece.Bishop
|
||||
case 3 => PromotionPiece.Rook
|
||||
case 4 => PromotionPiece.Queen
|
||||
case _ => return None
|
||||
MoveType.Promotion(promotionPiece)
|
||||
if toFile > 7 || toRank > 7 || fromFile > 7 || fromRank > 7 then None
|
||||
else
|
||||
// Check if it's a capture
|
||||
val isCapture = context.board.pieces.contains(to)
|
||||
MoveType.Normal(isCapture)
|
||||
val from = Square(File.values(fromFile), Rank.values(fromRank))
|
||||
val to = Square(File.values(toFile), Rank.values(toRank))
|
||||
|
||||
Some(Move(from, to, moveType))
|
||||
if isKingMove(context, from) && isRookSquare(to, context) then Some(decodeCastling(from, to))
|
||||
else
|
||||
val moveTypeOpt: Option[MoveType] = if promotionBits > 0 then
|
||||
promotionBits match
|
||||
case 1 => Some(MoveType.Promotion(PromotionPiece.Knight))
|
||||
case 2 => Some(MoveType.Promotion(PromotionPiece.Bishop))
|
||||
case 3 => Some(MoveType.Promotion(PromotionPiece.Rook))
|
||||
case 4 => Some(MoveType.Promotion(PromotionPiece.Queen))
|
||||
case _ => None
|
||||
else
|
||||
Some(MoveType.Normal(context.board.pieces.contains(to)))
|
||||
|
||||
moveTypeOpt.map(moveType => Move(from, to, moveType))
|
||||
|
||||
private def isKingMove(context: GameContext, square: Square): Boolean =
|
||||
context.board.pieces.get(square).exists { piece =>
|
||||
@@ -117,33 +105,33 @@ final class PolyglotBook(path: String):
|
||||
}
|
||||
|
||||
/** Decode castling from king-to-rook square to the standard move.
|
||||
*
|
||||
* Polyglot encodes castling as:
|
||||
* - e1→h1 = White kingside (move to g1)
|
||||
* - e1→a1 = White queenside (move to c1)
|
||||
* - e8→h8 = Black kingside (move to g8)
|
||||
* - e8→a8 = Black queenside (move to c8)
|
||||
*/
|
||||
*
|
||||
* Polyglot encodes castling as:
|
||||
* - e1→h1 = White kingside (move to g1)
|
||||
* - e1→a1 = White queenside (move to c1)
|
||||
* - e8→h8 = Black kingside (move to g8)
|
||||
* - e8→a8 = Black queenside (move to c8)
|
||||
*/
|
||||
private def decodeCastling(from: Square, to: Square): Move =
|
||||
if to.file == File.H then
|
||||
Move(from, Square(File.G, to.rank), MoveType.CastleKingside)
|
||||
else if to.file == File.A then
|
||||
Move(from, Square(File.C, to.rank), MoveType.CastleQueenside)
|
||||
if to.file == File.H then Move(from, Square(File.G, to.rank), MoveType.CastleKingside)
|
||||
else if to.file == File.A then Move(from, Square(File.C, to.rank), MoveType.CastleQueenside)
|
||||
else
|
||||
// Fallback (should not happen in a valid book)
|
||||
Move(from, to, MoveType.Normal())
|
||||
|
||||
/** Select a weighted random move from the list of book entries. */
|
||||
private def weightedRandom(entries: Vector[BookEntry]): BookEntry =
|
||||
if entries.length == 1 then
|
||||
entries(0)
|
||||
if entries.length == 1 then entries.head
|
||||
else
|
||||
val totalWeight = entries.map(_.weight).sum
|
||||
var pick = Random.nextInt(totalWeight.max(1))
|
||||
var idx = 0
|
||||
while idx < entries.length && pick >= entries(idx).weight do
|
||||
pick -= entries(idx).weight
|
||||
idx += 1
|
||||
entries(idx.min(entries.length - 1))
|
||||
val pick = Random.nextInt(totalWeight.max(1))
|
||||
|
||||
@scala.annotation.tailrec
|
||||
def select(remaining: Int, idx: Int): BookEntry =
|
||||
if idx >= entries.length then entries.last
|
||||
else if remaining < entries(idx).weight then entries(idx)
|
||||
else select(remaining - entries(idx).weight, idx + 1)
|
||||
|
||||
select(pick, 0)
|
||||
|
||||
private case class BookEntry(key: Long, move: Short, weight: Int)
|
||||
|
||||
@@ -7,242 +7,186 @@ object PolyglotHash:
|
||||
|
||||
/** 781-entry Zobrist random table from the Polyglot spec. */
|
||||
private val Random: Array[Long] = Array(
|
||||
0x9D39247E33776D41L, 0x2AF7398005AAA5C7L, 0x44DB015024623547L, 0x9C15F73E62A76AE2L,
|
||||
0x75834465489C0C89L, 0x3290AC3A203001BFL, 0x0FBBAD1F61042279L, 0xE83A908FF2FB60CAL,
|
||||
0x0D7E765D58755C10L, 0x1A083822CEAFE02DL, 0x9605D5F0E25EC3B0L, 0xD021FF5CD13A2ED5L,
|
||||
0x40BDF15D4A672E32L, 0x011355146FD56395L, 0x5DB4832046F3D9E5L, 0x239F8B2D7FF719CCL,
|
||||
0x05D1A1AE85B49AA1L, 0x679F848F6E8FC971L, 0x7449BBFF801FED0BL, 0x7D11CDB1C3B7ADF0L,
|
||||
0x82C7709E781EB7CCL, 0xF3218F1C9510786CL, 0x331478F3AF51BBE6L, 0x4BB38DE5E7219443L,
|
||||
0xAA649C6EBCFD50FCL, 0x8DBD98A352AFD40BL, 0x87D2074B81D79217L, 0x19F3C751D3E92AE1L,
|
||||
0xB4AB30F062B19ABFL, 0x7B0500AC42047AC4L, 0xC9452CA81A09D85DL, 0x24AA6C514DA27500L,
|
||||
0x4C9F34427501B447L, 0x14A68FD73C910841L, 0xA71B9B83461CBD93L, 0x03488B95B0F1850FL,
|
||||
0x637B2B34FF93C040L, 0x09D1BC9A3DD90A94L, 0x3575668334A1DD3BL, 0x735E2B97A4C45A23L,
|
||||
0x18727070F1BD400BL, 0x1FCBACD259BF02E7L, 0xD310A7C2CE9B6555L, 0xBF983FE0FE5D8244L,
|
||||
0x9F74D14F7454A824L, 0x51EBDC4AB9BA3035L, 0x5C82C505DB9AB0FAL, 0xFCF7FE8A3430B241L,
|
||||
0x3253A729B9BA3DDEL, 0x8C74C368081B3075L, 0xB9BC6C87167C33E7L, 0x7EF48F2B83024E20L,
|
||||
0x11D505D4C351BD7FL, 0x6568FCA92C76A243L, 0x4DE0B0F40F32A7B8L, 0x96D693460CC37E5DL,
|
||||
0x42E240CB63689F2FL, 0x6D2BDCDAE2919661L, 0x42880B0236E4D951L, 0x5F0F4A5898171BB6L,
|
||||
0x39F890F579F92F88L, 0x93C5B5F47356388BL, 0x63DC359D8D231B78L, 0xEC16CA8AEA98AD76L,
|
||||
0x5355F900C2A82DC7L, 0x07FB9F855A997142L, 0x5093417AA8A7ED5EL, 0x7BCBC38DA25A7F3CL,
|
||||
0x19FC8A768CF4B6D4L, 0x637A7780DECFC0D9L, 0x8249A47AEE0E41F7L, 0x79AD695501E7D1E8L,
|
||||
0x14ACBAF4777D5776L, 0xF145B6BECCDEA195L, 0xDABF2AC8201752FCL, 0x24C3C94DF9C8D3F6L,
|
||||
0xBB6E2924F03912EAL, 0x0CE26C0B95C980D9L, 0xA49CD132BFBF7CC4L, 0xE99D662AF4243939L,
|
||||
0x27E6AD7891165C3FL, 0x8535F040B9744FF1L, 0x54B3F4FA5F40D873L, 0x72B12C32127FED2BL,
|
||||
0xEE954D3C7B411F47L, 0x9A85AC909A24EAA1L, 0x70AC4CD9F04F21F5L, 0xF9B89D3E99A075C2L,
|
||||
0x87B3E2B2B5C907B1L, 0xA366E5B8C54F48B8L, 0xAE4A9346CC3F7CF2L, 0x1920C04D47267BBDL,
|
||||
0x87BF02C6B49E2AE9L, 0x092237AC237F3859L, 0xFF07F64EF8ED14D0L, 0x8DE8DCA9F03CC54EL,
|
||||
0x9C1633264DB49C89L, 0xB3F22C3D0B0B38EDL, 0x390E5FB44D01144BL, 0x5BFEA5B4712768E9L,
|
||||
0x1E1032911FA78984L, 0x9A74ACB964E78CB3L, 0x4F80F7A035DAFB04L, 0x6304D09A0B3738C4L,
|
||||
0x2171E64683023A08L, 0x5B9B63EB9CEFF80CL, 0x506AACF489889342L, 0x1881AFC9A3A701D6L,
|
||||
0x6503080440750644L, 0xDFD395339CDBF4A7L, 0xEF927DBCF00C20F2L, 0x7B32F7D1E03680ECL,
|
||||
0xB9FD7620E7316243L, 0x05A7E8A57DB91B77L, 0xB5889C6E15630A75L, 0x4A750A09CE9573F7L,
|
||||
0xCF464CEC899A2F8AL, 0xF538639CE705B824L, 0x3C79A0FF5580EF7FL, 0xEDE6C87F8477609DL,
|
||||
0x799E81F05BC93F31L, 0x86536B8CF3428A8CL, 0x97D7374C60087B73L, 0xA246637CFF328532L,
|
||||
0x043FCAE60CC0EBA0L, 0x920E449535DD359EL, 0x70EB093B15B290CCL, 0x73A1921916591CBDL,
|
||||
0x56436C9FE1A1AA8DL, 0xEFAC4B70633B8F81L, 0xBB215798D45DF7AFL, 0x45F20042F24F1768L,
|
||||
0x930F80F4E8EB7462L, 0xFF6712FFCFD75EA1L, 0xAE623FD67468AA70L, 0xDD2C5BC84BC8D8FCL,
|
||||
0x7EED120D54CF2DD9L, 0x22FE545401165F1CL, 0xC91800E98FB99929L, 0x808BD68E6AC10365L,
|
||||
0xDEC468145B7605F6L, 0x1BEDE3A3AEF53302L, 0x43539603D6C55602L, 0xAA969B5C691CCB7AL,
|
||||
0xA87832D392EFEE56L, 0x65942C7B3C7E11AEL, 0xDED2D633CAD004F6L, 0x21F08570F420E565L,
|
||||
0xB415938D7DA94E3CL, 0x91B859E59ECB6350L, 0x10CFF333E0ED804AL, 0x28AED140BE0BB7DDL,
|
||||
0xC5CC1D89724FA456L, 0x5648F680F11A2741L, 0x2D255069F0B7DAB3L, 0x9BC5A38EF729ABD4L,
|
||||
0xEF2F054308F6A2BCL, 0xAF2042F5CC5C2858L, 0x480412BAB7F5BE2AL, 0xAEF3AF4A563DFE43L,
|
||||
0x19AFE59AE451497FL, 0x52593803DFF1E840L, 0xF4F076E65F2CE6F0L, 0x11379625747D5AF3L,
|
||||
0xBCE5D2248682C115L, 0x9DA4243DE836994FL, 0x066F70B33FE09017L, 0x4DC4DE189B671A1CL,
|
||||
0x51039AB7712457C3L, 0xC07A3F80C31FB4B4L, 0xB46EE9C5E64A6E7CL, 0xB3819A42ABE61C87L,
|
||||
0x21A007933A522A20L, 0x2DF16F761598AA4FL, 0x763C4A1371B368FDL, 0xF793C46702E086A0L,
|
||||
0xD7288E012AEB8D31L, 0xDE336A2A4BC1C44BL, 0x0BF692B38D079F23L, 0x2C604A7A177326B3L,
|
||||
0x4850E73E03EB6064L, 0xCFC447F1E53C8E1BL, 0xB05CA3F564268D99L, 0x9AE182C8BC9474E8L,
|
||||
0xA4FC4BD4FC5558CAL, 0xE755178D58FC4E76L, 0x69B97DB1A4C03DFEL, 0xF9B5B7C4ACC67C96L,
|
||||
0xFC6A82D64B8655FBL, 0x9C684CB6C4D24417L, 0x8EC97D2917456ED0L, 0x6703DF9D2924E97EL,
|
||||
0xC547F57E42A7444EL, 0x78E37644E7CAD29EL, 0xFE9A44E9362F05FAL, 0x08BD35CC38336615L,
|
||||
0x9315E5EB3A129ACEL, 0x94061B871E04DF75L, 0xDF1D9F9D784BA010L, 0x3BBA57B68871B59DL,
|
||||
0xD2B7ADEEDED1F73FL, 0xF7A255D83BC373F8L, 0xD7F4F2448C0CEB81L, 0xD95BE88CD210FFA7L,
|
||||
0x336F52F8FF4728E7L, 0xA74049DAC312AC71L, 0xA2F61BB6E437FDB5L, 0x4F2A5CB07F6A35B3L,
|
||||
0x87D380BDA5BF7859L, 0x16B9F7E06C453A21L, 0x7BA2484C8A0FD54EL, 0xF3A678CAD9A2E38CL,
|
||||
0x39B0BF7DDE437BA2L, 0xFCAF55C1BF8A4424L, 0x18FCF680573FA594L, 0x4C0563B89F495AC3L,
|
||||
0x40E087931A00930DL, 0x8CFFA9412EB642C1L, 0x68CA39053261169FL, 0x7A1EE967D27579E2L,
|
||||
0x9D1D60E5076F5B6FL, 0x3810E399B6F65BA2L, 0x32095B6D4AB5F9B1L, 0x35CAB62109DD038AL,
|
||||
0xA90B24499FCFAFB1L, 0x77A225A07CC2C6BDL, 0x513E5E634C70E331L, 0x4361C0CA3F692F12L,
|
||||
0xD941ACA44B20A45BL, 0x528F7C8602C5807BL, 0x52AB92BEB9613989L, 0x9D1DFA2EFC557F73L,
|
||||
0x722FF175F572C348L, 0x1D1260A51107FE97L, 0x7A249A57EC0C9BA2L, 0x04208FE9E8F7F2D6L,
|
||||
0x5A110C6058B920A0L, 0x0CD9A497658A5698L, 0x56FD23C8F9715A4CL, 0x284C847B9D887AAEL,
|
||||
0x04FEABFBBDB619CBL, 0x742E1E651C60BA83L, 0x9A9632E65904AD3CL, 0x881B82A13B51B9E2L,
|
||||
0x506E6744CD974924L, 0xB0183DB56FFC6A79L, 0x0ED9B915C66ED37EL, 0x5E11E86D5873D484L,
|
||||
0xF678647E3519AC6EL, 0x1B85D488D0F20CC5L, 0xDAB9FE6525D89021L, 0x0D151D86ADB73615L,
|
||||
0xA865A54EDCC0F019L, 0x93C42566AEF98FFBL, 0x99E7AFEABE000731L, 0x48CBFF086DDF285AL,
|
||||
0x7F9B6AF1EBF78BAFL, 0x58627E1A149BBA21L, 0x2CD16E2ABD791E33L, 0xD363EFF5F0977996L,
|
||||
0x0CE2A38C344A6EEDL, 0x1A804AADB9CFA741L, 0x907F30421D78C5DEL, 0x501F65EDB3034D07L,
|
||||
0x37624AE5A48FA6E9L, 0x957BAF61700CFF4EL, 0x3A6C27934E31188AL, 0xD49503536ABCA345L,
|
||||
0x088E049589C432E0L, 0xF943AEE7FEBF21B8L, 0x6C3B8E3E336139D3L, 0x364F6FFA464EE52EL,
|
||||
0xD60F6DCEDC314222L, 0x56963B0DCA418FC0L, 0x16F50EDF91E513AFL, 0xEF1955914B609F93L,
|
||||
0x565601C0364E3228L, 0xECB53939887E8175L, 0xBAC7A9A18531294BL, 0xB344C470397BBA52L,
|
||||
0x65D34954DAF3CEBDL, 0xB4B81B3FA97511E2L, 0xB422061193D6F6A7L, 0x071582401C38434DL,
|
||||
0x7A13F18BBEDC4FF5L, 0xBC4097B116C524D2L, 0x59B97885E2F2EA28L, 0x99170A5DC3115544L,
|
||||
0x6F423357E7C6A9F9L, 0x325928EE6E6F8794L, 0xD0E4366228B03343L, 0x565C31F7DE89EA27L,
|
||||
0x30F5611484119414L, 0xD873DB391292ED4FL, 0x7BD94E1D8E17DEBCL, 0xC7D9F16864A76E94L,
|
||||
0x947AE053EE56E63CL, 0xC8C93882F9475F5FL, 0x3A9BF55BA91F81CAL, 0xD9A11FBB3D9808E4L,
|
||||
0x0FD22063EDC29FCAL, 0xB3F256D8ACA0B0B9L, 0xB03031A8B4516E84L, 0x35DD37D5871448AFL,
|
||||
0xE9F6082B05542E4EL, 0xEBFAFA33D7254B59L, 0x9255ABB50D532280L, 0xB9AB4CE57F2D34F3L,
|
||||
0x693501D628297551L, 0xC62C58F97DD949BFL, 0xCD454F8F19C5126AL, 0xBBE83F4ECC2BDECBL,
|
||||
0xDC842B7E2819E230L, 0xBA89142E007503B8L, 0xA3BC941D0A5061CBL, 0xE9F6760E32CD8021L,
|
||||
0x09C7E552BC76492FL, 0x852F54934DA55CC9L, 0x8107FCCF064FCF56L, 0x098954D51FFF6580L,
|
||||
0x23B70EDB1955C4BFL, 0xC330DE426430F69DL, 0x4715ED43E8A45C0AL, 0xA8D7E4DAB780A08DL,
|
||||
0x0572B974F03CE0BBL, 0xB57D2E985E1419C7L, 0xE8D9ECBE2CF3D73FL, 0x2FE4B17170E59750L,
|
||||
0x11317BA87905E790L, 0x7FBF21EC8A1F45ECL, 0x1725CABFCB045B00L, 0x964E915CD5E2B207L,
|
||||
0x3E2B8BCBF016D66DL, 0xBE7444E39328A0ACL, 0xF85B2B4FBCDE44B7L, 0x49353FEA39BA63B1L,
|
||||
0x1DD01AAFCD53486AL, 0x1FCA8A92FD719F85L, 0xFC7C95D827357AFAL, 0x18A6A990C8B35EBDL,
|
||||
0xCCCB7005C6B9C28DL, 0x3BDBB92C43B17F26L, 0xAA70B5B4F89695A2L, 0xE94C39A54A98307FL,
|
||||
0xB7A0B174CFF6F36EL, 0xD4DBA84729AF48ADL, 0x2E18BC1AD9704A68L, 0x2DE0966DAF2F8B1CL,
|
||||
0xB9C11D5B1E43A07EL, 0x64972D68DEE33360L, 0x94628D38D0C20584L, 0xDBC0D2B6AB90A559L,
|
||||
0xD2733C4335C6A72FL, 0x7E75D99D94A70F4DL, 0x6CED1983376FA72BL, 0x97FCAACBF030BC24L,
|
||||
0x7B77497B32503B12L, 0x8547EDDFB81CCB94L, 0x79999CDFF70902CBL, 0xCFFE1939438E9B24L,
|
||||
0x829626E3892D95D7L, 0x92FAE24291F2B3F1L, 0x63E22C147B9C3403L, 0xC678B6D860284A1CL,
|
||||
0x5873888850659AE7L, 0x0981DCD296A8736DL, 0x9F65789A6509A440L, 0x9FF38FED72E9052FL,
|
||||
0xE479EE5B9930578CL, 0xE7F28ECD2D49EECDL, 0x56C074A581EA17FEL, 0x5544F7D774B14AEFL,
|
||||
0x7B3F0195FC6F290FL, 0x12153635B2C0CF57L, 0x7F5126DBBA5E0CA7L, 0x7A76956C3EAFB413L,
|
||||
0x3D5774A11D31AB39L, 0x8A1B083821F40CB4L, 0x7B4A38E32537DF62L, 0x950113646D1D6E03L,
|
||||
0x4DA8979A0041E8A9L, 0x3BC36E078F7515D7L, 0x5D0A12F27AD310D1L, 0x7F9D1A2E1EBE1327L,
|
||||
0xDA3A361B1C5157B1L, 0xDCDD7D20903D0C25L, 0x36833336D068F707L, 0xCE68341F79893389L,
|
||||
0xAB9090168DD05F34L, 0x43954B3252DC25E5L, 0xB438C2B67F98E5E9L, 0x10DCD78E3851A492L,
|
||||
0xDBC27AB5447822BFL, 0x9B3CDB65F82CA382L, 0xB67B7896167B4C84L, 0xBFCED1B0048EAC50L,
|
||||
0xA9119B60369FFEBDL, 0x1FFF7AC80904BF45L, 0xAC12FB171817EEE7L, 0xAF08DA9177DDA93DL,
|
||||
0x1B0CAB936E65C744L, 0xB559EB1D04E5E932L, 0xC37B45B3F8D6F2BAL, 0xC3A9DC228CAAC9E9L,
|
||||
0xF3B8B6675A6507FFL, 0x9FC477DE4ED681DAL, 0x67378D8ECCEF96CBL, 0x6DD856D94D259236L,
|
||||
0xA319CE15B0B4DB31L, 0x073973751F12DD5EL, 0x8A8E849EB32781A5L, 0xE1925C71285279F5L,
|
||||
0x74C04BF1790C0EFEL, 0x4DDA48153C94938AL, 0x9D266D6A1CC0542CL, 0x7440FB816508C4FEL,
|
||||
0x13328503DF48229FL, 0xD6BF7BAEE43CAC40L, 0x4838D65F6EF6748FL, 0x1E152328F3318DEAL,
|
||||
0x8F8419A348F296BFL, 0x72C8834A5957B511L, 0xD7A023A73260B45CL, 0x94EBC8ABCFB56DAEL,
|
||||
0x9FC10D0F989993E0L, 0xDE68A2355B93CAE6L, 0xA44CFE79AE538BBEL, 0x9D1D84FCCE371425L,
|
||||
0x51D2B1AB2DDFB636L, 0x2FD7E4B9E72CD38CL, 0x65CA5B96B7552210L, 0xDD69A0D8AB3B546DL,
|
||||
0x604D51B25FBF70E2L, 0x73AA8A564FB7AC9EL, 0x1A8C1E992B941148L, 0xAAC40A2703D9BEA0L,
|
||||
0x764DBEAE7FA4F3A6L, 0x1E99B96E70A9BE8BL, 0x2C5E9DEB57EF4743L, 0x3A938FEE32D29981L,
|
||||
0x26E6DB8FFDF5ADFEL, 0x469356C504EC9F9DL, 0xC8763C5B08D1908CL, 0x3F6C6AF859D80055L,
|
||||
0x7F7CC39420A3A545L, 0x9BFB227EBDF4C5CEL, 0x89039D79D6FC5C5CL, 0x8FE88B57305E2AB6L,
|
||||
0xA09E8C8C35AB96DEL, 0xFA7E393983325753L, 0xD6B6D0ECC617C699L, 0xDFEA21EA9E7557E3L,
|
||||
0xB67C1FA481680AF8L, 0xCA1E3785A9E724E5L, 0x1CFC8BED0D681639L, 0xD18D8549D140CAEAL,
|
||||
0x4ED0FE7E9DC91335L, 0xE4DBF0634473F5D2L, 0x1761F93A44D5AEFEL, 0x53898E4C3910DA55L,
|
||||
0x734DE8181F6EC39AL, 0x2680B122BAA28D97L, 0x298AF231C85BAFABL, 0x7983EED3740847D5L,
|
||||
0x66C1A2A1A60CD889L, 0x9E17E49642A3E4C1L, 0xEDB454E7BADC0805L, 0x50B704CAB602C329L,
|
||||
0x4CC317FB9CDDD023L, 0x66B4835D9EAFEA22L, 0x219B97E26FFC81BDL, 0x261E4E4C0A333A9DL,
|
||||
0x1FE2CCA76517DB90L, 0xD7504DFA8816EDBBL, 0xB9571FA04DC089C8L, 0x1DDC0325259B27DEL,
|
||||
0xCF3F4688801EB9AAL, 0xF4F5D05C10CAB243L, 0x38B6525C21A42B0EL, 0x36F60E2BA4FA6800L,
|
||||
0xEB3593803173E0CEL, 0x9C4CD6257C5A3603L, 0xAF0C317D32ADAA8AL, 0x258E5A80C7204C4BL,
|
||||
0x8B889D624D44885DL, 0xF4D14597E660F855L, 0xD4347F66EC8941C3L, 0xE699ED85B0DFB40DL,
|
||||
0x2472F6207C2D0484L, 0xC2A1E7B5B459AEB5L, 0xAB4F6451CC1D45ECL, 0x63767572AE3D6174L,
|
||||
0xA59E0BD101731A28L, 0x116D0016CB948F09L, 0x2CF9C8CA052F6E9FL, 0x0B090A7560A968E3L,
|
||||
0xABEEDDB2DDE06FF1L, 0x58EFC10B06A2068DL, 0xC6E57A78FBD986E0L, 0x2EAB8CA63CE802D7L,
|
||||
0x14A195640116F336L, 0x7C0828DD624EC390L, 0xD74BBE77E6116AC7L, 0x804456AF10F5FB53L,
|
||||
0xEBE9EA2ADF4321C7L, 0x03219A39EE587A30L, 0x49787FEF17AF9924L, 0xA1E9300CD8520548L,
|
||||
0x5B45E522E4B1B4EFL, 0xB49C3B3995091A36L, 0xD4490AD526F14431L, 0x12A8F216AF9418C2L,
|
||||
0x001F837CC7350524L, 0x1877B51E57A764D5L, 0xA2853B80F17F58EEL, 0x993E1DE72D36D310L,
|
||||
0xB3598080CE64A656L, 0x252F59CF0D9F04BBL, 0xD23C8E176D113600L, 0x1BDA0492E7E4586EL,
|
||||
0x21E0BD5026C619BFL, 0x3B097ADAF088F94EL, 0x8D14DEDB30BE846EL, 0xF95CFFA23AF5F6F4L,
|
||||
0x3871700761B3F743L, 0xCA672B91E9E4FA16L, 0x64C8E531BFF53B55L, 0x241260ED4AD1E87DL,
|
||||
0x106C09B972D2E822L, 0x7FBA195410E5CA30L, 0x7884D9BC6CB569D8L, 0x0647DFEDCD894A29L,
|
||||
0x63573FF03E224774L, 0x4FC8E9560F91B123L, 0x1DB956E450275779L, 0xB8D91274B9E9D4FBL,
|
||||
0xA2EBEE47E2FBFCE1L, 0xD9F1F30CCD97FB09L, 0xEFED53D75FD64E6BL, 0x2E6D02C36017F67FL,
|
||||
0xA9AA4D20DB084E9BL, 0xB64BE8D8B25396C1L, 0x70CB6AF7C2D5BCF0L, 0x98F076A4F7A2322EL,
|
||||
0xBF84470805E69B5FL, 0x94C3251F06F90CF3L, 0x3E003E616A6591E9L, 0xB925A6CD0421AFF3L,
|
||||
0x61BDD1307C66E300L, 0xBF8D5108E27E0D48L, 0x240AB57A8B888B20L, 0xFC87614BAF287E07L,
|
||||
0xEF02CDD06FFDB432L, 0xA1082C0466DF6C0AL, 0x8215E577001332C8L, 0xD39BB9C3A48DB6CFL,
|
||||
0x2738259634305C14L, 0x61CF4F94C97DF93DL, 0x1B6BACA2AE4E125BL, 0x758F450C88572E0BL,
|
||||
0x959F587D507A8359L, 0xB063E962E045F54DL, 0x60E8ED72C0DFF5D1L, 0x7B64978555326F9FL,
|
||||
0xFD080D236DA814BAL, 0x8C90FD9B083F4558L, 0x106F72FE81E2C590L, 0x7976033A39F7D952L,
|
||||
0xA4EC0132764CA04BL, 0x733EA705FAE4FA77L, 0xB4D8F77BC3E56167L, 0x9E21F4F903B33FD9L,
|
||||
0x9D765E419FB69F6DL, 0xD30C088BA61EA5EFL, 0x5D94337FBFAF7F5BL, 0x1A4E4822EB4D7A59L,
|
||||
0x6FFE73E81B637FB3L, 0xDDF957BC36D8B9CAL, 0x64D0E29EEA8838B3L, 0x08DD9BDFD96B9F63L,
|
||||
0x087E79E5A57D1D13L, 0xE328E230E3E2B3FBL, 0x1C2559E30F0946BEL, 0x720BF5F26F4D2EAAL,
|
||||
0xB0774D261CC609DBL, 0x443F64EC5A371195L, 0x4112CF68649A260EL, 0xD813F2FAB7F5C5CAL,
|
||||
0x660D3257380841EEL, 0x59AC2C7873F910A3L, 0xE846963877671A17L, 0x93B633ABFA3469F8L,
|
||||
0xC0C0F5A60EF4CDCFL, 0xCAF21ECD4377B28CL, 0x57277707199B8175L, 0x506C11B9D90E8B1DL,
|
||||
0xD83CC2687A19255FL, 0x4A29C6465A314CD1L, 0xED2DF21216235097L, 0xB5635C95FF7296E2L,
|
||||
0x22AF003AB672E811L, 0x52E762596BF68235L, 0x9AEBA33AC6ECC6B0L, 0x944F6DE09134DFB6L,
|
||||
0x6C47BEC883A7DE39L, 0x6AD047C430A12104L, 0xA5B1CFDBA0AB4067L, 0x7C45D833AFF07862L,
|
||||
0x5092EF950A16DA0BL, 0x9338E69C052B8E7BL, 0x455A4B4CFE30E3F5L, 0x6B02E63195AD0CF8L,
|
||||
0x6B17B224BAD6BF27L, 0xD1E0CCD25BB9C169L, 0xDE0C89A556B9AE70L, 0x50065E535A213CF6L,
|
||||
0x9C1169FA2777B874L, 0x78EDEFD694AF1EEDL, 0x6DC93D9526A50E68L, 0xEE97F453F06791EDL,
|
||||
0x32AB0EDB696703D3L, 0x3A6853C7E70757A7L, 0x31865CED6120F37DL, 0x67FEF95D92607890L,
|
||||
0x1F2B1D1F15F6DC9CL, 0xB69E38A8965C6B65L, 0xAA9119FF184CCCF4L, 0xF43C732873F24C13L,
|
||||
0xFB4A3D794A9A80D2L, 0x3550C2321FD6109CL, 0x371F77E76BB8417EL, 0x6BFA9AAE5EC05779L,
|
||||
0xCD04F3FF001A4778L, 0xE3273522064480CAL, 0x9F91508BFFCFC14AL, 0x049A7F41061A9E60L,
|
||||
0xFCB6BE43A9F2FE9BL, 0x08DE8A1C7797DA9BL, 0x8F9887E6078735A1L, 0xB5B4071DBFC73A66L,
|
||||
0x230E343DFBA08D33L, 0x43ED7F5A0FAE657DL, 0x3A88A0FBBCB05C63L, 0x21874B8B4D2DBC4FL,
|
||||
0x1BDEA12E35F6A8C9L, 0x53C065C6C8E63528L, 0xE34A1D250E7A8D6BL, 0xD6B04D3B7651DD7EL,
|
||||
0x5E90277E7CB39E2DL, 0x2C046F22062DC67DL, 0xB10BB459132D0A26L, 0x3FA9DDFB67E2F199L,
|
||||
0x0E09B88E1914F7AFL, 0x10E8B35AF3EEAB37L, 0x9EEDECA8E272B933L, 0xD4C718BC4AE8AE5FL,
|
||||
0x81536D601170FC20L, 0x91B534F885818A06L, 0xEC8177F83F900978L, 0x190E714FADA5156EL,
|
||||
0xB592BF39B0364963L, 0x89C350C893AE7DC1L, 0xAC042E70F8B383F2L, 0xB49B52E587A1EE60L,
|
||||
0xFB152FE3FF26DA89L, 0x3E666E6F69AE2C15L, 0x3B544EBE544C19F9L, 0xE805A1E290CF2456L,
|
||||
0x24B33C9D7ED25117L, 0xE74733427B72F0C1L, 0x0A804D18B7097475L, 0x57E3306D881EDB4FL,
|
||||
0x4AE7D6A36EB5DBCBL, 0x2D8D5432157064C8L, 0xD1E649DE1E7F268BL, 0x8A328A1CEDFE552CL,
|
||||
0x07A3AEC79624C7DAL, 0x84547DDC3E203C94L, 0x990A98FD5071D263L, 0x1A4FF12616EEFC89L,
|
||||
0xF6F7FD1431714200L, 0x30C05B1BA332F41CL, 0x8D2636B81555A786L, 0x46C9FEB55D120902L,
|
||||
0xCCEC0A73B49C9921L, 0x4E9D2827355FC492L, 0x19EBB029435DCB0FL, 0x4659D2B743848A2CL,
|
||||
0x963EF2C96B33BE31L, 0x74F85198B05A2E7DL, 0x5A0F544DD2B1FB18L, 0x03727073C2E134B1L,
|
||||
0xC7F6AA2DE59AEA61L, 0x352787BAA0D7C22FL, 0x9853EAB63B5E0B35L, 0xABBDCDD7ED5C0860L,
|
||||
0xCF05DAF5AC8D77B0L, 0x49CAD48CEBF4A71EL, 0x7A4C10EC2158C4A6L, 0xD9E92AA246BF719EL,
|
||||
0x13AE978D09FE5557L, 0x730499AF921549FFL, 0x4E4B705B92903BA4L, 0xFF577222C14F0A3AL,
|
||||
0x55B6344CF97AAFAEL, 0xB862225B055B6960L, 0xCAC09AFBDDD2CDB4L, 0xDAF8E9829FE96B5FL,
|
||||
0xB5FDFC5D3132C498L, 0x310CB380DB6F7503L, 0xE87FBB46217A360EL, 0x2102AE466EBB1148L,
|
||||
0xF8549E1A3AA5E00DL, 0x07A69AFDCC42261AL, 0xC4C118BFE78FEAAEL, 0xF9F4892ED96BD438L,
|
||||
0x1AF3DBE25D8F45DAL, 0xF5B4B0B0D2DEEEB4L, 0x962ACEEFA82E1C84L, 0x046E3ECAAF453CE9L,
|
||||
0xF05D129681949A4CL, 0x964781CE734B3C84L, 0x9C2ED44081CE5FBDL, 0x522E23F3925E319EL,
|
||||
0x177E00F9FC32F791L, 0x2BC60A63A6F3B3F2L, 0x222BBFAE61725606L, 0x486289DDCC3D6780L,
|
||||
0x7DC7785B8EFDFC80L, 0x8AF38731C02BA980L, 0x1FAB64EA29A2DDF7L, 0xE4D9429322CD065AL,
|
||||
0x9DA058C67844F20CL, 0x24C0E332B70019B0L, 0x233003B5A6CFE6ADL, 0xD586BD01C5C217F6L,
|
||||
0x5E5637885F29BC2BL, 0x7EBA726D8C94094BL, 0x0A56A5F0BFE39272L, 0xD79476A84EE20D06L,
|
||||
0x9E4C1269BAA4BF37L, 0x17EFEE45B0DEE640L, 0x1D95B0A5FCF90BC6L, 0x93CBE0B699C2585DL,
|
||||
0x65FA4F227A2B6D79L, 0xD5F9E858292504D5L, 0xC2B5A03F71471A6FL, 0x59300222B4561E00L,
|
||||
0xCE2F8642CA0712DCL, 0x7CA9723FBB2E8988L, 0x2785338347F2BA08L, 0xC61BB3A141E50E8CL,
|
||||
0x150F361DAB9DEC26L, 0x9F6A419D382595F4L, 0x64A53DC924FE7AC9L, 0x142DE49FFF7A7C3DL,
|
||||
0x0C335248857FA9E7L, 0x0A9C32D5EAE45305L, 0xE6C42178C4BBB92EL, 0x71F1CE2490D20B07L,
|
||||
0xF1BCC3D275AFE51AL, 0xE728E8C83C334074L, 0x96FBF83A12884624L, 0x81A1549FD6573DA5L,
|
||||
0x5FA7867CAF35E149L, 0x56986E2EF3ED091BL, 0x917F1DD5F8886C61L, 0xD20D8C88C8FFE65FL,
|
||||
0x31D71DCE64B2C310L, 0xF165B587DF898190L, 0xA57E6339DD2CF3A0L, 0x1EF6E6DBB1961EC9L,
|
||||
0x70CC73D90BC26E24L, 0xE21A6B35DF0C3AD7L, 0x003A93D8B2806962L, 0x1C99DED33CB890A1L,
|
||||
0xCF3145DE0ADD4289L, 0xD0E4427A5514FB72L, 0x77C621CC9FB3A483L, 0x67A34DAC4356550BL,
|
||||
0xF8D626AAAF278509L
|
||||
0x9d39247e33776d41L, 0x2af7398005aaa5c7L, 0x44db015024623547L, 0x9c15f73e62a76ae2L, 0x75834465489c0c89L,
|
||||
0x3290ac3a203001bfL, 0x0fbbad1f61042279L, 0xe83a908ff2fb60caL, 0x0d7e765d58755c10L, 0x1a083822ceafe02dL,
|
||||
0x9605d5f0e25ec3b0L, 0xd021ff5cd13a2ed5L, 0x40bdf15d4a672e32L, 0x011355146fd56395L, 0x5db4832046f3d9e5L,
|
||||
0x239f8b2d7ff719ccL, 0x05d1a1ae85b49aa1L, 0x679f848f6e8fc971L, 0x7449bbff801fed0bL, 0x7d11cdb1c3b7adf0L,
|
||||
0x82c7709e781eb7ccL, 0xf3218f1c9510786cL, 0x331478f3af51bbe6L, 0x4bb38de5e7219443L, 0xaa649c6ebcfd50fcL,
|
||||
0x8dbd98a352afd40bL, 0x87d2074b81d79217L, 0x19f3c751d3e92ae1L, 0xb4ab30f062b19abfL, 0x7b0500ac42047ac4L,
|
||||
0xc9452ca81a09d85dL, 0x24aa6c514da27500L, 0x4c9f34427501b447L, 0x14a68fd73c910841L, 0xa71b9b83461cbd93L,
|
||||
0x03488b95b0f1850fL, 0x637b2b34ff93c040L, 0x09d1bc9a3dd90a94L, 0x3575668334a1dd3bL, 0x735e2b97a4c45a23L,
|
||||
0x18727070f1bd400bL, 0x1fcbacd259bf02e7L, 0xd310a7c2ce9b6555L, 0xbf983fe0fe5d8244L, 0x9f74d14f7454a824L,
|
||||
0x51ebdc4ab9ba3035L, 0x5c82c505db9ab0faL, 0xfcf7fe8a3430b241L, 0x3253a729b9ba3ddeL, 0x8c74c368081b3075L,
|
||||
0xb9bc6c87167c33e7L, 0x7ef48f2b83024e20L, 0x11d505d4c351bd7fL, 0x6568fca92c76a243L, 0x4de0b0f40f32a7b8L,
|
||||
0x96d693460cc37e5dL, 0x42e240cb63689f2fL, 0x6d2bdcdae2919661L, 0x42880b0236e4d951L, 0x5f0f4a5898171bb6L,
|
||||
0x39f890f579f92f88L, 0x93c5b5f47356388bL, 0x63dc359d8d231b78L, 0xec16ca8aea98ad76L, 0x5355f900c2a82dc7L,
|
||||
0x07fb9f855a997142L, 0x5093417aa8a7ed5eL, 0x7bcbc38da25a7f3cL, 0x19fc8a768cf4b6d4L, 0x637a7780decfc0d9L,
|
||||
0x8249a47aee0e41f7L, 0x79ad695501e7d1e8L, 0x14acbaf4777d5776L, 0xf145b6beccdea195L, 0xdabf2ac8201752fcL,
|
||||
0x24c3c94df9c8d3f6L, 0xbb6e2924f03912eaL, 0x0ce26c0b95c980d9L, 0xa49cd132bfbf7cc4L, 0xe99d662af4243939L,
|
||||
0x27e6ad7891165c3fL, 0x8535f040b9744ff1L, 0x54b3f4fa5f40d873L, 0x72b12c32127fed2bL, 0xee954d3c7b411f47L,
|
||||
0x9a85ac909a24eaa1L, 0x70ac4cd9f04f21f5L, 0xf9b89d3e99a075c2L, 0x87b3e2b2b5c907b1L, 0xa366e5b8c54f48b8L,
|
||||
0xae4a9346cc3f7cf2L, 0x1920c04d47267bbdL, 0x87bf02c6b49e2ae9L, 0x092237ac237f3859L, 0xff07f64ef8ed14d0L,
|
||||
0x8de8dca9f03cc54eL, 0x9c1633264db49c89L, 0xb3f22c3d0b0b38edL, 0x390e5fb44d01144bL, 0x5bfea5b4712768e9L,
|
||||
0x1e1032911fa78984L, 0x9a74acb964e78cb3L, 0x4f80f7a035dafb04L, 0x6304d09a0b3738c4L, 0x2171e64683023a08L,
|
||||
0x5b9b63eb9ceff80cL, 0x506aacf489889342L, 0x1881afc9a3a701d6L, 0x6503080440750644L, 0xdfd395339cdbf4a7L,
|
||||
0xef927dbcf00c20f2L, 0x7b32f7d1e03680ecL, 0xb9fd7620e7316243L, 0x05a7e8a57db91b77L, 0xb5889c6e15630a75L,
|
||||
0x4a750a09ce9573f7L, 0xcf464cec899a2f8aL, 0xf538639ce705b824L, 0x3c79a0ff5580ef7fL, 0xede6c87f8477609dL,
|
||||
0x799e81f05bc93f31L, 0x86536b8cf3428a8cL, 0x97d7374c60087b73L, 0xa246637cff328532L, 0x043fcae60cc0eba0L,
|
||||
0x920e449535dd359eL, 0x70eb093b15b290ccL, 0x73a1921916591cbdL, 0x56436c9fe1a1aa8dL, 0xefac4b70633b8f81L,
|
||||
0xbb215798d45df7afL, 0x45f20042f24f1768L, 0x930f80f4e8eb7462L, 0xff6712ffcfd75ea1L, 0xae623fd67468aa70L,
|
||||
0xdd2c5bc84bc8d8fcL, 0x7eed120d54cf2dd9L, 0x22fe545401165f1cL, 0xc91800e98fb99929L, 0x808bd68e6ac10365L,
|
||||
0xdec468145b7605f6L, 0x1bede3a3aef53302L, 0x43539603d6c55602L, 0xaa969b5c691ccb7aL, 0xa87832d392efee56L,
|
||||
0x65942c7b3c7e11aeL, 0xded2d633cad004f6L, 0x21f08570f420e565L, 0xb415938d7da94e3cL, 0x91b859e59ecb6350L,
|
||||
0x10cff333e0ed804aL, 0x28aed140be0bb7ddL, 0xc5cc1d89724fa456L, 0x5648f680f11a2741L, 0x2d255069f0b7dab3L,
|
||||
0x9bc5a38ef729abd4L, 0xef2f054308f6a2bcL, 0xaf2042f5cc5c2858L, 0x480412bab7f5be2aL, 0xaef3af4a563dfe43L,
|
||||
0x19afe59ae451497fL, 0x52593803dff1e840L, 0xf4f076e65f2ce6f0L, 0x11379625747d5af3L, 0xbce5d2248682c115L,
|
||||
0x9da4243de836994fL, 0x066f70b33fe09017L, 0x4dc4de189b671a1cL, 0x51039ab7712457c3L, 0xc07a3f80c31fb4b4L,
|
||||
0xb46ee9c5e64a6e7cL, 0xb3819a42abe61c87L, 0x21a007933a522a20L, 0x2df16f761598aa4fL, 0x763c4a1371b368fdL,
|
||||
0xf793c46702e086a0L, 0xd7288e012aeb8d31L, 0xde336a2a4bc1c44bL, 0x0bf692b38d079f23L, 0x2c604a7a177326b3L,
|
||||
0x4850e73e03eb6064L, 0xcfc447f1e53c8e1bL, 0xb05ca3f564268d99L, 0x9ae182c8bc9474e8L, 0xa4fc4bd4fc5558caL,
|
||||
0xe755178d58fc4e76L, 0x69b97db1a4c03dfeL, 0xf9b5b7c4acc67c96L, 0xfc6a82d64b8655fbL, 0x9c684cb6c4d24417L,
|
||||
0x8ec97d2917456ed0L, 0x6703df9d2924e97eL, 0xc547f57e42a7444eL, 0x78e37644e7cad29eL, 0xfe9a44e9362f05faL,
|
||||
0x08bd35cc38336615L, 0x9315e5eb3a129aceL, 0x94061b871e04df75L, 0xdf1d9f9d784ba010L, 0x3bba57b68871b59dL,
|
||||
0xd2b7adeeded1f73fL, 0xf7a255d83bc373f8L, 0xd7f4f2448c0ceb81L, 0xd95be88cd210ffa7L, 0x336f52f8ff4728e7L,
|
||||
0xa74049dac312ac71L, 0xa2f61bb6e437fdb5L, 0x4f2a5cb07f6a35b3L, 0x87d380bda5bf7859L, 0x16b9f7e06c453a21L,
|
||||
0x7ba2484c8a0fd54eL, 0xf3a678cad9a2e38cL, 0x39b0bf7dde437ba2L, 0xfcaf55c1bf8a4424L, 0x18fcf680573fa594L,
|
||||
0x4c0563b89f495ac3L, 0x40e087931a00930dL, 0x8cffa9412eb642c1L, 0x68ca39053261169fL, 0x7a1ee967d27579e2L,
|
||||
0x9d1d60e5076f5b6fL, 0x3810e399b6f65ba2L, 0x32095b6d4ab5f9b1L, 0x35cab62109dd038aL, 0xa90b24499fcfafb1L,
|
||||
0x77a225a07cc2c6bdL, 0x513e5e634c70e331L, 0x4361c0ca3f692f12L, 0xd941aca44b20a45bL, 0x528f7c8602c5807bL,
|
||||
0x52ab92beb9613989L, 0x9d1dfa2efc557f73L, 0x722ff175f572c348L, 0x1d1260a51107fe97L, 0x7a249a57ec0c9ba2L,
|
||||
0x04208fe9e8f7f2d6L, 0x5a110c6058b920a0L, 0x0cd9a497658a5698L, 0x56fd23c8f9715a4cL, 0x284c847b9d887aaeL,
|
||||
0x04feabfbbdb619cbL, 0x742e1e651c60ba83L, 0x9a9632e65904ad3cL, 0x881b82a13b51b9e2L, 0x506e6744cd974924L,
|
||||
0xb0183db56ffc6a79L, 0x0ed9b915c66ed37eL, 0x5e11e86d5873d484L, 0xf678647e3519ac6eL, 0x1b85d488d0f20cc5L,
|
||||
0xdab9fe6525d89021L, 0x0d151d86adb73615L, 0xa865a54edcc0f019L, 0x93c42566aef98ffbL, 0x99e7afeabe000731L,
|
||||
0x48cbff086ddf285aL, 0x7f9b6af1ebf78bafL, 0x58627e1a149bba21L, 0x2cd16e2abd791e33L, 0xd363eff5f0977996L,
|
||||
0x0ce2a38c344a6eedL, 0x1a804aadb9cfa741L, 0x907f30421d78c5deL, 0x501f65edb3034d07L, 0x37624ae5a48fa6e9L,
|
||||
0x957baf61700cff4eL, 0x3a6c27934e31188aL, 0xd49503536abca345L, 0x088e049589c432e0L, 0xf943aee7febf21b8L,
|
||||
0x6c3b8e3e336139d3L, 0x364f6ffa464ee52eL, 0xd60f6dcedc314222L, 0x56963b0dca418fc0L, 0x16f50edf91e513afL,
|
||||
0xef1955914b609f93L, 0x565601c0364e3228L, 0xecb53939887e8175L, 0xbac7a9a18531294bL, 0xb344c470397bba52L,
|
||||
0x65d34954daf3cebdL, 0xb4b81b3fa97511e2L, 0xb422061193d6f6a7L, 0x071582401c38434dL, 0x7a13f18bbedc4ff5L,
|
||||
0xbc4097b116c524d2L, 0x59b97885e2f2ea28L, 0x99170a5dc3115544L, 0x6f423357e7c6a9f9L, 0x325928ee6e6f8794L,
|
||||
0xd0e4366228b03343L, 0x565c31f7de89ea27L, 0x30f5611484119414L, 0xd873db391292ed4fL, 0x7bd94e1d8e17debcL,
|
||||
0xc7d9f16864a76e94L, 0x947ae053ee56e63cL, 0xc8c93882f9475f5fL, 0x3a9bf55ba91f81caL, 0xd9a11fbb3d9808e4L,
|
||||
0x0fd22063edc29fcaL, 0xb3f256d8aca0b0b9L, 0xb03031a8b4516e84L, 0x35dd37d5871448afL, 0xe9f6082b05542e4eL,
|
||||
0xebfafa33d7254b59L, 0x9255abb50d532280L, 0xb9ab4ce57f2d34f3L, 0x693501d628297551L, 0xc62c58f97dd949bfL,
|
||||
0xcd454f8f19c5126aL, 0xbbe83f4ecc2bdecbL, 0xdc842b7e2819e230L, 0xba89142e007503b8L, 0xa3bc941d0a5061cbL,
|
||||
0xe9f6760e32cd8021L, 0x09c7e552bc76492fL, 0x852f54934da55cc9L, 0x8107fccf064fcf56L, 0x098954d51fff6580L,
|
||||
0x23b70edb1955c4bfL, 0xc330de426430f69dL, 0x4715ed43e8a45c0aL, 0xa8d7e4dab780a08dL, 0x0572b974f03ce0bbL,
|
||||
0xb57d2e985e1419c7L, 0xe8d9ecbe2cf3d73fL, 0x2fe4b17170e59750L, 0x11317ba87905e790L, 0x7fbf21ec8a1f45ecL,
|
||||
0x1725cabfcb045b00L, 0x964e915cd5e2b207L, 0x3e2b8bcbf016d66dL, 0xbe7444e39328a0acL, 0xf85b2b4fbcde44b7L,
|
||||
0x49353fea39ba63b1L, 0x1dd01aafcd53486aL, 0x1fca8a92fd719f85L, 0xfc7c95d827357afaL, 0x18a6a990c8b35ebdL,
|
||||
0xcccb7005c6b9c28dL, 0x3bdbb92c43b17f26L, 0xaa70b5b4f89695a2L, 0xe94c39a54a98307fL, 0xb7a0b174cff6f36eL,
|
||||
0xd4dba84729af48adL, 0x2e18bc1ad9704a68L, 0x2de0966daf2f8b1cL, 0xb9c11d5b1e43a07eL, 0x64972d68dee33360L,
|
||||
0x94628d38d0c20584L, 0xdbc0d2b6ab90a559L, 0xd2733c4335c6a72fL, 0x7e75d99d94a70f4dL, 0x6ced1983376fa72bL,
|
||||
0x97fcaacbf030bc24L, 0x7b77497b32503b12L, 0x8547eddfb81ccb94L, 0x79999cdff70902cbL, 0xcffe1939438e9b24L,
|
||||
0x829626e3892d95d7L, 0x92fae24291f2b3f1L, 0x63e22c147b9c3403L, 0xc678b6d860284a1cL, 0x5873888850659ae7L,
|
||||
0x0981dcd296a8736dL, 0x9f65789a6509a440L, 0x9ff38fed72e9052fL, 0xe479ee5b9930578cL, 0xe7f28ecd2d49eecdL,
|
||||
0x56c074a581ea17feL, 0x5544f7d774b14aefL, 0x7b3f0195fc6f290fL, 0x12153635b2c0cf57L, 0x7f5126dbba5e0ca7L,
|
||||
0x7a76956c3eafb413L, 0x3d5774a11d31ab39L, 0x8a1b083821f40cb4L, 0x7b4a38e32537df62L, 0x950113646d1d6e03L,
|
||||
0x4da8979a0041e8a9L, 0x3bc36e078f7515d7L, 0x5d0a12f27ad310d1L, 0x7f9d1a2e1ebe1327L, 0xda3a361b1c5157b1L,
|
||||
0xdcdd7d20903d0c25L, 0x36833336d068f707L, 0xce68341f79893389L, 0xab9090168dd05f34L, 0x43954b3252dc25e5L,
|
||||
0xb438c2b67f98e5e9L, 0x10dcd78e3851a492L, 0xdbc27ab5447822bfL, 0x9b3cdb65f82ca382L, 0xb67b7896167b4c84L,
|
||||
0xbfced1b0048eac50L, 0xa9119b60369ffebdL, 0x1fff7ac80904bf45L, 0xac12fb171817eee7L, 0xaf08da9177dda93dL,
|
||||
0x1b0cab936e65c744L, 0xb559eb1d04e5e932L, 0xc37b45b3f8d6f2baL, 0xc3a9dc228caac9e9L, 0xf3b8b6675a6507ffL,
|
||||
0x9fc477de4ed681daL, 0x67378d8eccef96cbL, 0x6dd856d94d259236L, 0xa319ce15b0b4db31L, 0x073973751f12dd5eL,
|
||||
0x8a8e849eb32781a5L, 0xe1925c71285279f5L, 0x74c04bf1790c0efeL, 0x4dda48153c94938aL, 0x9d266d6a1cc0542cL,
|
||||
0x7440fb816508c4feL, 0x13328503df48229fL, 0xd6bf7baee43cac40L, 0x4838d65f6ef6748fL, 0x1e152328f3318deaL,
|
||||
0x8f8419a348f296bfL, 0x72c8834a5957b511L, 0xd7a023a73260b45cL, 0x94ebc8abcfb56daeL, 0x9fc10d0f989993e0L,
|
||||
0xde68a2355b93cae6L, 0xa44cfe79ae538bbeL, 0x9d1d84fcce371425L, 0x51d2b1ab2ddfb636L, 0x2fd7e4b9e72cd38cL,
|
||||
0x65ca5b96b7552210L, 0xdd69a0d8ab3b546dL, 0x604d51b25fbf70e2L, 0x73aa8a564fb7ac9eL, 0x1a8c1e992b941148L,
|
||||
0xaac40a2703d9bea0L, 0x764dbeae7fa4f3a6L, 0x1e99b96e70a9be8bL, 0x2c5e9deb57ef4743L, 0x3a938fee32d29981L,
|
||||
0x26e6db8ffdf5adfeL, 0x469356c504ec9f9dL, 0xc8763c5b08d1908cL, 0x3f6c6af859d80055L, 0x7f7cc39420a3a545L,
|
||||
0x9bfb227ebdf4c5ceL, 0x89039d79d6fc5c5cL, 0x8fe88b57305e2ab6L, 0xa09e8c8c35ab96deL, 0xfa7e393983325753L,
|
||||
0xd6b6d0ecc617c699L, 0xdfea21ea9e7557e3L, 0xb67c1fa481680af8L, 0xca1e3785a9e724e5L, 0x1cfc8bed0d681639L,
|
||||
0xd18d8549d140caeaL, 0x4ed0fe7e9dc91335L, 0xe4dbf0634473f5d2L, 0x1761f93a44d5aefeL, 0x53898e4c3910da55L,
|
||||
0x734de8181f6ec39aL, 0x2680b122baa28d97L, 0x298af231c85bafabL, 0x7983eed3740847d5L, 0x66c1a2a1a60cd889L,
|
||||
0x9e17e49642a3e4c1L, 0xedb454e7badc0805L, 0x50b704cab602c329L, 0x4cc317fb9cddd023L, 0x66b4835d9eafea22L,
|
||||
0x219b97e26ffc81bdL, 0x261e4e4c0a333a9dL, 0x1fe2cca76517db90L, 0xd7504dfa8816edbbL, 0xb9571fa04dc089c8L,
|
||||
0x1ddc0325259b27deL, 0xcf3f4688801eb9aaL, 0xf4f5d05c10cab243L, 0x38b6525c21a42b0eL, 0x36f60e2ba4fa6800L,
|
||||
0xeb3593803173e0ceL, 0x9c4cd6257c5a3603L, 0xaf0c317d32adaa8aL, 0x258e5a80c7204c4bL, 0x8b889d624d44885dL,
|
||||
0xf4d14597e660f855L, 0xd4347f66ec8941c3L, 0xe699ed85b0dfb40dL, 0x2472f6207c2d0484L, 0xc2a1e7b5b459aeb5L,
|
||||
0xab4f6451cc1d45ecL, 0x63767572ae3d6174L, 0xa59e0bd101731a28L, 0x116d0016cb948f09L, 0x2cf9c8ca052f6e9fL,
|
||||
0x0b090a7560a968e3L, 0xabeeddb2dde06ff1L, 0x58efc10b06a2068dL, 0xc6e57a78fbd986e0L, 0x2eab8ca63ce802d7L,
|
||||
0x14a195640116f336L, 0x7c0828dd624ec390L, 0xd74bbe77e6116ac7L, 0x804456af10f5fb53L, 0xebe9ea2adf4321c7L,
|
||||
0x03219a39ee587a30L, 0x49787fef17af9924L, 0xa1e9300cd8520548L, 0x5b45e522e4b1b4efL, 0xb49c3b3995091a36L,
|
||||
0xd4490ad526f14431L, 0x12a8f216af9418c2L, 0x001f837cc7350524L, 0x1877b51e57a764d5L, 0xa2853b80f17f58eeL,
|
||||
0x993e1de72d36d310L, 0xb3598080ce64a656L, 0x252f59cf0d9f04bbL, 0xd23c8e176d113600L, 0x1bda0492e7e4586eL,
|
||||
0x21e0bd5026c619bfL, 0x3b097adaf088f94eL, 0x8d14dedb30be846eL, 0xf95cffa23af5f6f4L, 0x3871700761b3f743L,
|
||||
0xca672b91e9e4fa16L, 0x64c8e531bff53b55L, 0x241260ed4ad1e87dL, 0x106c09b972d2e822L, 0x7fba195410e5ca30L,
|
||||
0x7884d9bc6cb569d8L, 0x0647dfedcd894a29L, 0x63573ff03e224774L, 0x4fc8e9560f91b123L, 0x1db956e450275779L,
|
||||
0xb8d91274b9e9d4fbL, 0xa2ebee47e2fbfce1L, 0xd9f1f30ccd97fb09L, 0xefed53d75fd64e6bL, 0x2e6d02c36017f67fL,
|
||||
0xa9aa4d20db084e9bL, 0xb64be8d8b25396c1L, 0x70cb6af7c2d5bcf0L, 0x98f076a4f7a2322eL, 0xbf84470805e69b5fL,
|
||||
0x94c3251f06f90cf3L, 0x3e003e616a6591e9L, 0xb925a6cd0421aff3L, 0x61bdd1307c66e300L, 0xbf8d5108e27e0d48L,
|
||||
0x240ab57a8b888b20L, 0xfc87614baf287e07L, 0xef02cdd06ffdb432L, 0xa1082c0466df6c0aL, 0x8215e577001332c8L,
|
||||
0xd39bb9c3a48db6cfL, 0x2738259634305c14L, 0x61cf4f94c97df93dL, 0x1b6baca2ae4e125bL, 0x758f450c88572e0bL,
|
||||
0x959f587d507a8359L, 0xb063e962e045f54dL, 0x60e8ed72c0dff5d1L, 0x7b64978555326f9fL, 0xfd080d236da814baL,
|
||||
0x8c90fd9b083f4558L, 0x106f72fe81e2c590L, 0x7976033a39f7d952L, 0xa4ec0132764ca04bL, 0x733ea705fae4fa77L,
|
||||
0xb4d8f77bc3e56167L, 0x9e21f4f903b33fd9L, 0x9d765e419fb69f6dL, 0xd30c088ba61ea5efL, 0x5d94337fbfaf7f5bL,
|
||||
0x1a4e4822eb4d7a59L, 0x6ffe73e81b637fb3L, 0xddf957bc36d8b9caL, 0x64d0e29eea8838b3L, 0x08dd9bdfd96b9f63L,
|
||||
0x087e79e5a57d1d13L, 0xe328e230e3e2b3fbL, 0x1c2559e30f0946beL, 0x720bf5f26f4d2eaaL, 0xb0774d261cc609dbL,
|
||||
0x443f64ec5a371195L, 0x4112cf68649a260eL, 0xd813f2fab7f5c5caL, 0x660d3257380841eeL, 0x59ac2c7873f910a3L,
|
||||
0xe846963877671a17L, 0x93b633abfa3469f8L, 0xc0c0f5a60ef4cdcfL, 0xcaf21ecd4377b28cL, 0x57277707199b8175L,
|
||||
0x506c11b9d90e8b1dL, 0xd83cc2687a19255fL, 0x4a29c6465a314cd1L, 0xed2df21216235097L, 0xb5635c95ff7296e2L,
|
||||
0x22af003ab672e811L, 0x52e762596bf68235L, 0x9aeba33ac6ecc6b0L, 0x944f6de09134dfb6L, 0x6c47bec883a7de39L,
|
||||
0x6ad047c430a12104L, 0xa5b1cfdba0ab4067L, 0x7c45d833aff07862L, 0x5092ef950a16da0bL, 0x9338e69c052b8e7bL,
|
||||
0x455a4b4cfe30e3f5L, 0x6b02e63195ad0cf8L, 0x6b17b224bad6bf27L, 0xd1e0ccd25bb9c169L, 0xde0c89a556b9ae70L,
|
||||
0x50065e535a213cf6L, 0x9c1169fa2777b874L, 0x78edefd694af1eedL, 0x6dc93d9526a50e68L, 0xee97f453f06791edL,
|
||||
0x32ab0edb696703d3L, 0x3a6853c7e70757a7L, 0x31865ced6120f37dL, 0x67fef95d92607890L, 0x1f2b1d1f15f6dc9cL,
|
||||
0xb69e38a8965c6b65L, 0xaa9119ff184cccf4L, 0xf43c732873f24c13L, 0xfb4a3d794a9a80d2L, 0x3550c2321fd6109cL,
|
||||
0x371f77e76bb8417eL, 0x6bfa9aae5ec05779L, 0xcd04f3ff001a4778L, 0xe3273522064480caL, 0x9f91508bffcfc14aL,
|
||||
0x049a7f41061a9e60L, 0xfcb6be43a9f2fe9bL, 0x08de8a1c7797da9bL, 0x8f9887e6078735a1L, 0xb5b4071dbfc73a66L,
|
||||
0x230e343dfba08d33L, 0x43ed7f5a0fae657dL, 0x3a88a0fbbcb05c63L, 0x21874b8b4d2dbc4fL, 0x1bdea12e35f6a8c9L,
|
||||
0x53c065c6c8e63528L, 0xe34a1d250e7a8d6bL, 0xd6b04d3b7651dd7eL, 0x5e90277e7cb39e2dL, 0x2c046f22062dc67dL,
|
||||
0xb10bb459132d0a26L, 0x3fa9ddfb67e2f199L, 0x0e09b88e1914f7afL, 0x10e8b35af3eeab37L, 0x9eedeca8e272b933L,
|
||||
0xd4c718bc4ae8ae5fL, 0x81536d601170fc20L, 0x91b534f885818a06L, 0xec8177f83f900978L, 0x190e714fada5156eL,
|
||||
0xb592bf39b0364963L, 0x89c350c893ae7dc1L, 0xac042e70f8b383f2L, 0xb49b52e587a1ee60L, 0xfb152fe3ff26da89L,
|
||||
0x3e666e6f69ae2c15L, 0x3b544ebe544c19f9L, 0xe805a1e290cf2456L, 0x24b33c9d7ed25117L, 0xe74733427b72f0c1L,
|
||||
0x0a804d18b7097475L, 0x57e3306d881edb4fL, 0x4ae7d6a36eb5dbcbL, 0x2d8d5432157064c8L, 0xd1e649de1e7f268bL,
|
||||
0x8a328a1cedfe552cL, 0x07a3aec79624c7daL, 0x84547ddc3e203c94L, 0x990a98fd5071d263L, 0x1a4ff12616eefc89L,
|
||||
0xf6f7fd1431714200L, 0x30c05b1ba332f41cL, 0x8d2636b81555a786L, 0x46c9feb55d120902L, 0xccec0a73b49c9921L,
|
||||
0x4e9d2827355fc492L, 0x19ebb029435dcb0fL, 0x4659d2b743848a2cL, 0x963ef2c96b33be31L, 0x74f85198b05a2e7dL,
|
||||
0x5a0f544dd2b1fb18L, 0x03727073c2e134b1L, 0xc7f6aa2de59aea61L, 0x352787baa0d7c22fL, 0x9853eab63b5e0b35L,
|
||||
0xabbdcdd7ed5c0860L, 0xcf05daf5ac8d77b0L, 0x49cad48cebf4a71eL, 0x7a4c10ec2158c4a6L, 0xd9e92aa246bf719eL,
|
||||
0x13ae978d09fe5557L, 0x730499af921549ffL, 0x4e4b705b92903ba4L, 0xff577222c14f0a3aL, 0x55b6344cf97aafaeL,
|
||||
0xb862225b055b6960L, 0xcac09afbddd2cdb4L, 0xdaf8e9829fe96b5fL, 0xb5fdfc5d3132c498L, 0x310cb380db6f7503L,
|
||||
0xe87fbb46217a360eL, 0x2102ae466ebb1148L, 0xf8549e1a3aa5e00dL, 0x07a69afdcc42261aL, 0xc4c118bfe78feaaeL,
|
||||
0xf9f4892ed96bd438L, 0x1af3dbe25d8f45daL, 0xf5b4b0b0d2deeeb4L, 0x962aceefa82e1c84L, 0x046e3ecaaf453ce9L,
|
||||
0xf05d129681949a4cL, 0x964781ce734b3c84L, 0x9c2ed44081ce5fbdL, 0x522e23f3925e319eL, 0x177e00f9fc32f791L,
|
||||
0x2bc60a63a6f3b3f2L, 0x222bbfae61725606L, 0x486289ddcc3d6780L, 0x7dc7785b8efdfc80L, 0x8af38731c02ba980L,
|
||||
0x1fab64ea29a2ddf7L, 0xe4d9429322cd065aL, 0x9da058c67844f20cL, 0x24c0e332b70019b0L, 0x233003b5a6cfe6adL,
|
||||
0xd586bd01c5c217f6L, 0x5e5637885f29bc2bL, 0x7eba726d8c94094bL, 0x0a56a5f0bfe39272L, 0xd79476a84ee20d06L,
|
||||
0x9e4c1269baa4bf37L, 0x17efee45b0dee640L, 0x1d95b0a5fcf90bc6L, 0x93cbe0b699c2585dL, 0x65fa4f227a2b6d79L,
|
||||
0xd5f9e858292504d5L, 0xc2b5a03f71471a6fL, 0x59300222b4561e00L, 0xce2f8642ca0712dcL, 0x7ca9723fbb2e8988L,
|
||||
0x2785338347f2ba08L, 0xc61bb3a141e50e8cL, 0x150f361dab9dec26L, 0x9f6a419d382595f4L, 0x64a53dc924fe7ac9L,
|
||||
0x142de49fff7a7c3dL, 0x0c335248857fa9e7L, 0x0a9c32d5eae45305L, 0xe6c42178c4bbb92eL, 0x71f1ce2490d20b07L,
|
||||
0xf1bcc3d275afe51aL, 0xe728e8c83c334074L, 0x96fbf83a12884624L, 0x81a1549fd6573da5L, 0x5fa7867caf35e149L,
|
||||
0x56986e2ef3ed091bL, 0x917f1dd5f8886c61L, 0xd20d8c88c8ffe65fL, 0x31d71dce64b2c310L, 0xf165b587df898190L,
|
||||
0xa57e6339dd2cf3a0L, 0x1ef6e6dbb1961ec9L, 0x70cc73d90bc26e24L, 0xe21a6b35df0c3ad7L, 0x003a93d8b2806962L,
|
||||
0x1c99ded33cb890a1L, 0xcf3145de0add4289L, 0xd0e4427a5514fb72L, 0x77c621cc9fb3a483L, 0x67a34dac4356550bL,
|
||||
0xf8d626aaaf278509L,
|
||||
)
|
||||
|
||||
def hash(context: GameContext): Long =
|
||||
var h = 0L
|
||||
|
||||
// Piece hashes
|
||||
for (sq, piece) <- context.board.pieces do
|
||||
val idx = pieceIndex(piece) * 64 + squareIndex(sq)
|
||||
h ^= Random(idx)
|
||||
|
||||
// Castling rights
|
||||
if context.castlingRights.whiteKingSide then
|
||||
h ^= Random(768)
|
||||
if context.castlingRights.whiteQueenSide then
|
||||
h ^= Random(769)
|
||||
if context.castlingRights.blackKingSide then
|
||||
h ^= Random(770)
|
||||
if context.castlingRights.blackQueenSide then
|
||||
h ^= Random(771)
|
||||
|
||||
// En passant (by file only, but only if side-to-move can capture en passant)
|
||||
context.enPassantSquare.foreach { sq =>
|
||||
if canCaptureEnPassant(context, sq) then
|
||||
h ^= Random(772 + sq.file.ordinal)
|
||||
val piecesHash = context.board.pieces.foldLeft(0L) { case (h, (sq, piece)) =>
|
||||
h ^ Random(pieceIndex(piece) * 64 + squareIndex(sq))
|
||||
}
|
||||
|
||||
// Side to move (XOR if White is to move)
|
||||
if context.turn == Color.White then
|
||||
h ^= Random(780)
|
||||
|
||||
h
|
||||
val h1 = if context.castlingRights.whiteKingSide then piecesHash ^ Random(768) else piecesHash
|
||||
val h2 = if context.castlingRights.whiteQueenSide then h1 ^ Random(769) else h1
|
||||
val h3 = if context.castlingRights.blackKingSide then h2 ^ Random(770) else h2
|
||||
val h4 = if context.castlingRights.blackQueenSide then h3 ^ Random(771) else h3
|
||||
val h5 = context.enPassantSquare.fold(h4) { sq =>
|
||||
if canCaptureEnPassant(context, sq) then h4 ^ Random(772 + sq.file.ordinal) else h4
|
||||
}
|
||||
if context.turn == Color.White then h5 ^ Random(780) else h5
|
||||
|
||||
private def pieceIndex(piece: Piece): Int =
|
||||
val typeIdx = piece.pieceType match
|
||||
case PieceType.Pawn => 0
|
||||
case PieceType.Pawn => 0
|
||||
case PieceType.Knight => 1
|
||||
case PieceType.Bishop => 2
|
||||
case PieceType.Rook => 3
|
||||
case PieceType.Queen => 4
|
||||
case PieceType.King => 5
|
||||
case PieceType.Rook => 3
|
||||
case PieceType.Queen => 4
|
||||
case PieceType.King => 5
|
||||
val colorOffset = if piece.color == Color.White then 1 else 0
|
||||
typeIdx * 2 + colorOffset
|
||||
|
||||
@@ -250,7 +194,7 @@ object PolyglotHash:
|
||||
sq.file.ordinal + 8 * sq.rank.ordinal
|
||||
|
||||
private def canCaptureEnPassant(context: GameContext, epSquare: Square): Boolean =
|
||||
val pawn = Piece(context.turn, PieceType.Pawn)
|
||||
val pawn = Piece(context.turn, PieceType.Pawn)
|
||||
val rankDelta = if context.turn == Color.White then -1 else 1
|
||||
List(-1, 1).exists { fileDelta =>
|
||||
epSquare
|
||||
@@ -258,4 +202,3 @@ object PolyglotHash:
|
||||
.flatMap(context.board.pieces.get)
|
||||
.contains(pawn)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ object ZobristHash:
|
||||
private val pieceRands: Array[Long] = Array.ofDim(768)
|
||||
|
||||
// Side-to-move: XOR when Black to move
|
||||
private val sideToMoveRand: Long = Random(0x1BADB002L).nextLong()
|
||||
private val sideToMoveRand: Long = Random(0x1badb002L).nextLong()
|
||||
|
||||
// 4 entries: White kingside, White queenside, Black kingside, Black queenside
|
||||
private val castlingRands: Array[Long] = Array.ofDim(4)
|
||||
@@ -22,94 +22,79 @@ object ZobristHash:
|
||||
|
||||
// Initialize all random values using a seeded RNG for reproducibility
|
||||
locally:
|
||||
val rng = Random(0x1BADB002L)
|
||||
for i <- 0 until 768 do
|
||||
pieceRands(i) = rng.nextLong()
|
||||
for i <- 0 until 4 do
|
||||
castlingRands(i) = rng.nextLong()
|
||||
for i <- 0 until 8 do
|
||||
enPassantRands(i) = rng.nextLong()
|
||||
val rng = Random(0x1badb002L)
|
||||
for i <- 0 until 768 do pieceRands(i) = rng.nextLong()
|
||||
for i <- 0 until 4 do castlingRands(i) = rng.nextLong()
|
||||
for i <- 0 until 8 do enPassantRands(i) = rng.nextLong()
|
||||
|
||||
/** Compute a 64-bit Zobrist hash for a GameContext. */
|
||||
def hash(context: GameContext): Long =
|
||||
var h = 0L
|
||||
|
||||
// Hash board pieces
|
||||
for (square, piece) <- context.board.pieces do
|
||||
val piecesHash = context.board.pieces.foldLeft(0L) { case (h, (square, piece)) =>
|
||||
val squareIndex = square.rank.ordinal * 8 + square.file.ordinal
|
||||
val colorIndex = if piece.color == Color.White then 0 else 1
|
||||
val pieceIndex = colorIndex * 6 + piece.pieceType.ordinal
|
||||
val randIndex = squareIndex * 12 + pieceIndex
|
||||
h ^= pieceRands(randIndex)
|
||||
|
||||
// Hash side to move
|
||||
if context.turn == Color.Black then
|
||||
h ^= sideToMoveRand
|
||||
|
||||
// Hash castling rights
|
||||
if context.castlingRights.whiteKingSide then
|
||||
h ^= castlingRands(0)
|
||||
if context.castlingRights.whiteQueenSide then
|
||||
h ^= castlingRands(1)
|
||||
if context.castlingRights.blackKingSide then
|
||||
h ^= castlingRands(2)
|
||||
if context.castlingRights.blackQueenSide then
|
||||
h ^= castlingRands(3)
|
||||
|
||||
// Hash en-passant square
|
||||
context.enPassantSquare.foreach(sq => h ^= enPassantRands(sq.file.ordinal))
|
||||
|
||||
h
|
||||
val colorIndex = if piece.color == Color.White then 0 else 1
|
||||
val pieceIndex = colorIndex * 6 + piece.pieceType.ordinal
|
||||
h ^ pieceRands(squareIndex * 12 + pieceIndex)
|
||||
}
|
||||
val h1 = if context.turn == Color.Black then piecesHash ^ sideToMoveRand else piecesHash
|
||||
val h2 = if context.castlingRights.whiteKingSide then h1 ^ castlingRands(0) else h1
|
||||
val h3 = if context.castlingRights.whiteQueenSide then h2 ^ castlingRands(1) else h2
|
||||
val h4 = if context.castlingRights.blackKingSide then h3 ^ castlingRands(2) else h3
|
||||
val h5 = if context.castlingRights.blackQueenSide then h4 ^ castlingRands(3) else h4
|
||||
context.enPassantSquare.fold(h5)(sq => h5 ^ enPassantRands(sq.file.ordinal))
|
||||
|
||||
def nextHash(context: GameContext, currentHash: Long, move: Move, nextContext: GameContext): Long =
|
||||
var h = currentHash
|
||||
h ^= sideToMoveRand
|
||||
h = toggleCastling(h, context, nextContext)
|
||||
h = toggleEnPassant(h, context, nextContext)
|
||||
|
||||
val h0 = currentHash ^ sideToMoveRand
|
||||
val h1 = toggleCastling(h0, context, nextContext)
|
||||
val h2 = toggleEnPassant(h1, context, nextContext)
|
||||
move.moveType match
|
||||
case MoveType.CastleKingside | MoveType.CastleQueenside =>
|
||||
h = applyCastleDelta(h, context.turn, move.moveType == MoveType.CastleKingside)
|
||||
applyCastleDelta(h2, context.turn, move.moveType == MoveType.CastleKingside)
|
||||
case MoveType.EnPassant =>
|
||||
h = applyEnPassantDelta(h, context, move)
|
||||
applyEnPassantDelta(h2, context, move)
|
||||
case MoveType.Promotion(piece) =>
|
||||
h = applyPromotionDelta(h, context, move, piece)
|
||||
applyPromotionDelta(h2, context, move, piece)
|
||||
case MoveType.Normal(_) =>
|
||||
h = applyNormalDelta(h, context, move)
|
||||
|
||||
h
|
||||
applyNormalDelta(h2, context, move)
|
||||
|
||||
private def applyNormalDelta(h0: Long, context: GameContext, move: Move): Long =
|
||||
context.board.pieceAt(move.from).fold(h0) { mover =>
|
||||
var h = h0 ^ pieceKey(move.from, mover)
|
||||
context.board.pieceAt(move.to).foreach(captured => h ^= pieceKey(move.to, captured))
|
||||
h ^ pieceKey(move.to, mover)
|
||||
val h1 = h0 ^ pieceKey(move.from, mover)
|
||||
val h2 = context.board.pieceAt(move.to).fold(h1)(captured => h1 ^ pieceKey(move.to, captured))
|
||||
h2 ^ pieceKey(move.to, mover)
|
||||
}
|
||||
|
||||
private def applyPromotionDelta(h0: Long, context: GameContext, move: Move, promoted: PromotionPiece): Long =
|
||||
context.board.pieceAt(move.from).fold(h0) { pawn =>
|
||||
var h = h0 ^ pieceKey(move.from, pawn)
|
||||
context.board.pieceAt(move.to).foreach(captured => h ^= pieceKey(move.to, captured))
|
||||
h ^ pieceKey(move.to, Piece(context.turn, promotedPieceType(promoted)))
|
||||
val h1 = h0 ^ pieceKey(move.from, pawn)
|
||||
val h2 = context.board.pieceAt(move.to).fold(h1)(captured => h1 ^ pieceKey(move.to, captured))
|
||||
h2 ^ pieceKey(move.to, Piece(context.turn, promotedPieceType(promoted)))
|
||||
}
|
||||
|
||||
private def applyEnPassantDelta(h0: Long, context: GameContext, move: Move): Long =
|
||||
context.board.pieceAt(move.from).fold(h0) { pawn =>
|
||||
val capturedSquare = Square(move.to.file, move.from.rank)
|
||||
var h = h0 ^ pieceKey(move.from, pawn)
|
||||
context.board.pieceAt(capturedSquare).foreach(captured => h ^= pieceKey(capturedSquare, captured))
|
||||
h ^ pieceKey(move.to, pawn)
|
||||
val h1 = h0 ^ pieceKey(move.from, pawn)
|
||||
val h2 = context.board.pieceAt(capturedSquare).fold(h1)(captured => h1 ^ pieceKey(capturedSquare, captured))
|
||||
h2 ^ pieceKey(move.to, pawn)
|
||||
}
|
||||
|
||||
private def applyCastleDelta(h0: Long, color: Color, kingside: Boolean): Long =
|
||||
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
||||
val (kingFrom, kingTo, rookFrom, rookTo) =
|
||||
if kingside then
|
||||
(Square(de.nowchess.api.board.File.E, rank), Square(de.nowchess.api.board.File.G, rank),
|
||||
Square(de.nowchess.api.board.File.H, rank), Square(de.nowchess.api.board.File.F, rank))
|
||||
(
|
||||
Square(de.nowchess.api.board.File.E, rank),
|
||||
Square(de.nowchess.api.board.File.G, rank),
|
||||
Square(de.nowchess.api.board.File.H, rank),
|
||||
Square(de.nowchess.api.board.File.F, rank),
|
||||
)
|
||||
else
|
||||
(Square(de.nowchess.api.board.File.E, rank), Square(de.nowchess.api.board.File.C, rank),
|
||||
Square(de.nowchess.api.board.File.A, rank), Square(de.nowchess.api.board.File.D, rank))
|
||||
(
|
||||
Square(de.nowchess.api.board.File.E, rank),
|
||||
Square(de.nowchess.api.board.File.C, rank),
|
||||
Square(de.nowchess.api.board.File.A, rank),
|
||||
Square(de.nowchess.api.board.File.D, rank),
|
||||
)
|
||||
val king = Piece(color, PieceType.King)
|
||||
val rook = Piece(color, PieceType.Rook)
|
||||
h0 ^ pieceKey(kingFrom, king) ^ pieceKey(kingTo, king) ^ pieceKey(rookFrom, rook) ^ pieceKey(rookTo, rook)
|
||||
@@ -117,26 +102,21 @@ object ZobristHash:
|
||||
private def promotedPieceType(promotion: PromotionPiece): PieceType = promotion match
|
||||
case PromotionPiece.Knight => PieceType.Knight
|
||||
case PromotionPiece.Bishop => PieceType.Bishop
|
||||
case PromotionPiece.Rook => PieceType.Rook
|
||||
case PromotionPiece.Queen => PieceType.Queen
|
||||
case PromotionPiece.Rook => PieceType.Rook
|
||||
case PromotionPiece.Queen => PieceType.Queen
|
||||
|
||||
private def toggleCastling(h0: Long, before: GameContext, after: GameContext): Long =
|
||||
var h = h0
|
||||
if before.castlingRights.whiteKingSide != after.castlingRights.whiteKingSide then h ^= castlingRands(0)
|
||||
if before.castlingRights.whiteQueenSide != after.castlingRights.whiteQueenSide then h ^= castlingRands(1)
|
||||
if before.castlingRights.blackKingSide != after.castlingRights.blackKingSide then h ^= castlingRands(2)
|
||||
if before.castlingRights.blackQueenSide != after.castlingRights.blackQueenSide then h ^= castlingRands(3)
|
||||
h
|
||||
val h1 = if before.castlingRights.whiteKingSide != after.castlingRights.whiteKingSide then h0 ^ castlingRands(0) else h0
|
||||
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
|
||||
|
||||
private def toggleEnPassant(h0: Long, before: GameContext, after: GameContext): Long =
|
||||
var h = h0
|
||||
before.enPassantSquare.foreach(sq => h ^= enPassantRands(sq.file.ordinal))
|
||||
after.enPassantSquare.foreach(sq => h ^= enPassantRands(sq.file.ordinal))
|
||||
h
|
||||
val h1 = before.enPassantSquare.fold(h0)(sq => h0 ^ enPassantRands(sq.file.ordinal))
|
||||
after.enPassantSquare.fold(h1)(sq => h1 ^ enPassantRands(sq.file.ordinal))
|
||||
|
||||
private def pieceKey(square: Square, piece: Piece): Long =
|
||||
val squareIndex = square.rank.ordinal * 8 + square.file.ordinal
|
||||
val colorIndex = if piece.color == Color.White then 0 else 1
|
||||
val pieceIndex = colorIndex * 6 + piece.pieceType.ordinal
|
||||
val colorIndex = if piece.color == Color.White then 0 else 1
|
||||
val pieceIndex = colorIndex * 6 + piece.pieceType.ordinal
|
||||
pieceRands(squareIndex * 12 + pieceIndex)
|
||||
|
||||
|
||||
@@ -14,37 +14,37 @@ class AlphaBetaSearchTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("bestMove on initial position returns a move"):
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 2)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 2)
|
||||
move should not be None
|
||||
|
||||
test("bestMove on a position with one legal move returns that move"):
|
||||
// Create a simple position: White king on h1, Black rook on a2
|
||||
// (set up so there's only one legal move available)
|
||||
// For simplicity, just test that a position with forced mate returns a move
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val context = GameContext.initial
|
||||
val move = search.bestMove(context, maxDepth = 1)
|
||||
val move = search.bestMove(context, maxDepth = 1)
|
||||
move should not be None
|
||||
|
||||
test("bestMove returns None for initial position has no legal moves"):
|
||||
// Use a stub RuleSet that returns empty legal moves
|
||||
val stubRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = true
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = true
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val search = AlphaBetaSearch(stubRules, weights = EvaluationClassic)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 2)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 2)
|
||||
move should be(None)
|
||||
|
||||
test("transposition table is cleared at start of bestMove"):
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val context = GameContext.initial
|
||||
// Call bestMove twice and verify both work independently
|
||||
val move1 = search.bestMove(context, maxDepth = 1)
|
||||
@@ -55,19 +55,19 @@ class AlphaBetaSearchTest extends AnyFunSuite with Matchers:
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
// A position with multiple captures to verify quiescence orders them
|
||||
val context = GameContext.initial
|
||||
val move = search.bestMove(context, maxDepth = 2)
|
||||
val move = search.bestMove(context, maxDepth = 2)
|
||||
// Just verify it completes without error
|
||||
move.isDefined should be(true)
|
||||
|
||||
test("search respects alpha-beta bounds"):
|
||||
// This is implicit in the structure, but we test via behavior
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val context = GameContext.initial
|
||||
val move = search.bestMove(context, maxDepth = 3)
|
||||
val move = search.bestMove(context, maxDepth = 3)
|
||||
move should not be None
|
||||
|
||||
test("iterative deepening finds a move at each depth"):
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val search = AlphaBetaSearch(DefaultRules, weights = EvaluationClassic)
|
||||
val context = GameContext.initial
|
||||
// Searching to depth 3 should use iterative deepening (depths 1, 2, 3)
|
||||
val move = search.bestMove(context, maxDepth = 3)
|
||||
@@ -77,88 +77,90 @@ class AlphaBetaSearchTest extends AnyFunSuite with Matchers:
|
||||
// Create a stalemate stub: white to move, no legal moves, not checkmate
|
||||
val stalematRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = true
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = true
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val search = AlphaBetaSearch(stalematRules, weights = EvaluationClassic)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
move should be(None)
|
||||
|
||||
test("insufficient material returns score 0"):
|
||||
val insufficientRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = true
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = true
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val search = AlphaBetaSearch(insufficientRules, weights = EvaluationClassic)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
move should be(None)
|
||||
|
||||
test("fifty move rule returns score 0"):
|
||||
val fiftyMoveRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = true
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = true
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val search = AlphaBetaSearch(fiftyMoveRules, weights = EvaluationClassic)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
move should be(None)
|
||||
|
||||
test("capture moves are recognized in quiescence search"):
|
||||
// Create a position with a capture available
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
|
||||
val captureMove = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||
val rulesWithCapture = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List(captureMove)
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List(captureMove)
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val search = AlphaBetaSearch(rulesWithCapture, weights = EvaluationClassic)
|
||||
val move = search.bestMove(context, maxDepth = 1)
|
||||
val move = search.bestMove(context, maxDepth = 1)
|
||||
move should be(Some(captureMove))
|
||||
|
||||
test("non-capture moves are not included in quiescence"):
|
||||
val quietMove = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(false))
|
||||
val rulesQuiet = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List(quietMove)
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List(quietMove)
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val search = AlphaBetaSearch(rulesQuiet, weights = EvaluationClassic)
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
move should be(Some(quietMove)) // bestMove returns the quiet move since it's the only legal move
|
||||
val move = search.bestMove(GameContext.initial, maxDepth = 1)
|
||||
move should be(Some(quietMove)) // bestMove returns the quiet move since it's the only legal move
|
||||
|
||||
@@ -6,5 +6,4 @@ import org.scalatest.matchers.should.Matchers
|
||||
class BotControllerTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("BotController can be instantiated"):
|
||||
// BotController is an object with a private map, just test instantiation
|
||||
BotController should not be null
|
||||
BotController.listBots should not be empty
|
||||
|
||||
@@ -20,29 +20,29 @@ class ClassicalBotTest extends AnyFunSuite with Matchers:
|
||||
botMedium.name should include("Medium")
|
||||
|
||||
test("nextMove on initial position returns a move"):
|
||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||
val bot = ClassicalBot(BotDifficulty.Easy)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
move should not be None
|
||||
|
||||
test("nextMove returns None for position with no legal moves"):
|
||||
val stubRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = true
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List()
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = true
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val bot = ClassicalBot(BotDifficulty.Easy, stubRules)
|
||||
val bot = ClassicalBot(BotDifficulty.Easy, stubRules)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
move should be(None)
|
||||
|
||||
test("all BotDifficulty values work"):
|
||||
BotDifficulty.values.foreach { difficulty =>
|
||||
val bot = ClassicalBot(difficulty)
|
||||
val bot = ClassicalBot(difficulty)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
// All difficulties should return a move on the initial position
|
||||
move should not be None
|
||||
@@ -52,20 +52,20 @@ class ClassicalBotTest extends AnyFunSuite with Matchers:
|
||||
val moveToReturn = Move(
|
||||
de.nowchess.api.board.Square(de.nowchess.api.board.File.E, de.nowchess.api.board.Rank.R2),
|
||||
de.nowchess.api.board.Square(de.nowchess.api.board.File.E, de.nowchess.api.board.Rank.R4),
|
||||
de.nowchess.api.move.MoveType.Normal()
|
||||
de.nowchess.api.move.MoveType.Normal(),
|
||||
)
|
||||
|
||||
val stubRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square) = List()
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List(moveToReturn)
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
def legalMoves(context: GameContext)(square: Square) = List()
|
||||
def allLegalMoves(context: GameContext) = List(moveToReturn)
|
||||
def isCheck(context: GameContext) = false
|
||||
def isCheckmate(context: GameContext) = false
|
||||
def isStalemate(context: GameContext) = false
|
||||
def isInsufficientMaterial(context: GameContext) = false
|
||||
def isFiftyMoveRule(context: GameContext) = false
|
||||
def applyMove(context: GameContext)(move: Move) = context
|
||||
|
||||
val bot = ClassicalBot(BotDifficulty.Easy, stubRules)
|
||||
val bot = ClassicalBot(BotDifficulty.Easy, stubRules)
|
||||
val move = bot.nextMove(GameContext.initial)
|
||||
move should be(Some(moveToReturn))
|
||||
|
||||
@@ -11,36 +11,36 @@ class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("initial position evaluates to tempo bonus"):
|
||||
val eval = EvaluationClassic.evaluate(GameContext.initial)
|
||||
eval should equal(10) // TEMPO_BONUS only
|
||||
eval should equal(10) // TEMPO_BONUS only
|
||||
|
||||
test("remove white queen gives negative evaluation"):
|
||||
val initial = GameContext.initial
|
||||
val board = initial.board
|
||||
val emptySquare = Square(File.D, Rank.R1)
|
||||
val initial = GameContext.initial
|
||||
val board = initial.board
|
||||
val emptySquare = Square(File.D, Rank.R1)
|
||||
val boardWithoutQueen = board.pieces.filter((sq, _) => sq != emptySquare)
|
||||
val newContext = initial.withBoard(Board(boardWithoutQueen))
|
||||
val eval = EvaluationClassic.evaluate(newContext)
|
||||
val newContext = initial.withBoard(Board(boardWithoutQueen))
|
||||
val eval = EvaluationClassic.evaluate(newContext)
|
||||
eval should be < 0
|
||||
|
||||
test("remove black queen gives positive evaluation"):
|
||||
val initial = GameContext.initial
|
||||
val board = initial.board
|
||||
val emptySquare = Square(File.D, Rank.R8)
|
||||
val initial = GameContext.initial
|
||||
val board = initial.board
|
||||
val emptySquare = Square(File.D, Rank.R8)
|
||||
val boardWithoutQueen = board.pieces.filter((sq, _) => sq != emptySquare)
|
||||
val newContext = initial.withBoard(Board(boardWithoutQueen))
|
||||
val eval = EvaluationClassic.evaluate(newContext)
|
||||
val newContext = initial.withBoard(Board(boardWithoutQueen))
|
||||
val eval = EvaluationClassic.evaluate(newContext)
|
||||
eval should be > 0
|
||||
|
||||
test("different piece-square bonuses are applied"):
|
||||
// Knight on d4 (center) vs knight on a1 (corner) - center should be better
|
||||
val knightD4Board = Board(Map(Square(File.D, Rank.R4) -> Piece.WhiteKnight))
|
||||
val knightA1Board = Board(Map(Square(File.A, Rank.R1) -> Piece.WhiteKnight))
|
||||
val knightD4 = GameContext.initial.withBoard(knightD4Board)
|
||||
val knightA1 = GameContext.initial.withBoard(knightA1Board)
|
||||
val knightD4 = GameContext.initial.withBoard(knightD4Board)
|
||||
val knightA1 = GameContext.initial.withBoard(knightA1Board)
|
||||
|
||||
val eval1 = EvaluationClassic.evaluate(knightD4)
|
||||
val eval2 = EvaluationClassic.evaluate(knightA1)
|
||||
eval1 should be > eval2 // d4 (center) is better than a1 (corner) for knight
|
||||
eval1 should be > eval2 // d4 (center) is better than a1 (corner) for knight
|
||||
|
||||
test("all piece types are in material map"):
|
||||
PieceType.values.length should be > 0
|
||||
@@ -53,34 +53,38 @@ class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
EvaluationClassic.DRAW_SCORE should equal(0)
|
||||
|
||||
test("active knight (center) scores higher than passive knight (corner)"):
|
||||
val knightD4Board = Board(Map(Square(File.D, Rank.R4) -> Piece.WhiteKnight))
|
||||
val knightA1Board = Board(Map(Square(File.A, Rank.R1) -> Piece.WhiteKnight))
|
||||
val knightD4Board = Board(Map(Square(File.D, Rank.R4) -> Piece.WhiteKnight))
|
||||
val knightA1Board = Board(Map(Square(File.A, Rank.R1) -> Piece.WhiteKnight))
|
||||
val knightD4Context = GameContext.initial.withBoard(knightD4Board)
|
||||
val knightA1Context = GameContext.initial.withBoard(knightA1Board)
|
||||
val evalD4 = EvaluationClassic.evaluate(knightD4Context)
|
||||
val evalA1 = EvaluationClassic.evaluate(knightA1Context)
|
||||
evalD4 should be > evalA1 // Knight on d4 (center, more mobility) should score higher
|
||||
val evalD4 = EvaluationClassic.evaluate(knightD4Context)
|
||||
val evalA1 = EvaluationClassic.evaluate(knightA1Context)
|
||||
evalD4 should be > evalA1 // Knight on d4 (center, more mobility) should score higher
|
||||
|
||||
test("bishop pair scores higher than bishop + knight"):
|
||||
val bishopPairBoard = Board(Map(
|
||||
Square(File.C, Rank.R1) -> Piece.WhiteBishop,
|
||||
Square(File.F, Rank.R1) -> Piece.WhiteBishop
|
||||
))
|
||||
val bishopKnightBoard = Board(Map(
|
||||
Square(File.C, Rank.R1) -> Piece.WhiteBishop,
|
||||
Square(File.B, Rank.R1) -> Piece.WhiteKnight
|
||||
))
|
||||
val pairContext = GameContext.initial.withBoard(bishopPairBoard)
|
||||
val bishopPairBoard = Board(
|
||||
Map(
|
||||
Square(File.C, Rank.R1) -> Piece.WhiteBishop,
|
||||
Square(File.F, Rank.R1) -> Piece.WhiteBishop,
|
||||
),
|
||||
)
|
||||
val bishopKnightBoard = Board(
|
||||
Map(
|
||||
Square(File.C, Rank.R1) -> Piece.WhiteBishop,
|
||||
Square(File.B, Rank.R1) -> Piece.WhiteKnight,
|
||||
),
|
||||
)
|
||||
val pairContext = GameContext.initial.withBoard(bishopPairBoard)
|
||||
val knightContext = GameContext.initial.withBoard(bishopKnightBoard)
|
||||
val evalPair = EvaluationClassic.evaluate(pairContext)
|
||||
val evalKnight = EvaluationClassic.evaluate(knightContext)
|
||||
evalPair should be > evalKnight // Bishop pair should score higher
|
||||
val evalPair = EvaluationClassic.evaluate(pairContext)
|
||||
val evalKnight = EvaluationClassic.evaluate(knightContext)
|
||||
evalPair should be > evalKnight // Bishop pair should score higher
|
||||
|
||||
test("rook on 7th rank scores higher than rook on 4th rank"):
|
||||
val rook7thBoard = Board(Map(Square(File.A, Rank.R7) -> Piece.WhiteRook))
|
||||
val rook4thBoard = Board(Map(Square(File.A, Rank.R4) -> Piece.WhiteRook))
|
||||
val rook7thBoard = Board(Map(Square(File.A, Rank.R7) -> Piece.WhiteRook))
|
||||
val rook4thBoard = Board(Map(Square(File.A, Rank.R4) -> Piece.WhiteRook))
|
||||
val rook7thContext = GameContext.initial.withBoard(rook7thBoard)
|
||||
val rook4thContext = GameContext.initial.withBoard(rook4thBoard)
|
||||
val eval7th = EvaluationClassic.evaluate(rook7thContext)
|
||||
val eval4th = EvaluationClassic.evaluate(rook4thContext)
|
||||
eval7th should be > eval4th // Rook on 7th rank should score higher
|
||||
val eval7th = EvaluationClassic.evaluate(rook7thContext)
|
||||
val eval4th = EvaluationClassic.evaluate(rook4thContext)
|
||||
eval7th should be > eval4th // Rook on 7th rank should score higher
|
||||
|
||||
@@ -10,84 +10,98 @@ import org.scalatest.matchers.should.Matchers
|
||||
class MoveOrderingTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("queen capture ranks higher than rook capture"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackQueen,
|
||||
Square(File.E, Rank.R6) -> Piece.BlackRook
|
||||
))
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackQueen,
|
||||
Square(File.E, Rank.R6) -> Piece.BlackRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val queenCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||
val rookCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6), MoveType.Normal(true))
|
||||
val rookCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6), MoveType.Normal(true))
|
||||
|
||||
val queenScore = MoveOrdering.score(context, queenCapture, None)
|
||||
val rookScore = MoveOrdering.score(context, rookCapture, None)
|
||||
val rookScore = MoveOrdering.score(context, rookCapture, None)
|
||||
queenScore should be > rookScore
|
||||
|
||||
test("quiet move ranks lower than capture"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val capture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||
val quiet = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5))
|
||||
val quiet = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5))
|
||||
|
||||
val captureScore = MoveOrdering.score(context, capture, None)
|
||||
val quietScore = MoveOrdering.score(context, quiet, None)
|
||||
val quietScore = MoveOrdering.score(context, quiet, None)
|
||||
captureScore should be > quietScore
|
||||
|
||||
test("TT best move ranks first"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackPawn
|
||||
))
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val bestMove = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val bestMove = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
|
||||
val otherCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||
|
||||
val bestScore = MoveOrdering.score(context, bestMove, Some(bestMove))
|
||||
val bestScore = MoveOrdering.score(context, bestMove, Some(bestMove))
|
||||
val otherScore = MoveOrdering.score(context, otherCapture, Some(bestMove))
|
||||
bestScore should equal(Int.MaxValue)
|
||||
otherScore should be < bestScore
|
||||
|
||||
test("promotion to queen ranks high"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val promotionQueen = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val promotionKnight = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Knight))
|
||||
val promotionQueen =
|
||||
Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val promotionKnight =
|
||||
Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Knight))
|
||||
|
||||
val queenScore = MoveOrdering.score(context, promotionQueen, None)
|
||||
val queenScore = MoveOrdering.score(context, promotionQueen, None)
|
||||
val knightScore = MoveOrdering.score(context, promotionKnight, None)
|
||||
queenScore should be > knightScore
|
||||
queenScore should be > 100_000 // Queen promotion score is > 100_000
|
||||
queenScore should be > 100_000 // Queen promotion score is > 100_000
|
||||
|
||||
test("en passant is treated as capture"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R5) -> Piece.WhitePawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackPawn
|
||||
))
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R5) -> Piece.WhitePawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val epCapture = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant)
|
||||
val quiet = Move(Square(File.E, Rank.R5), Square(File.E, Rank.R6))
|
||||
val quiet = Move(Square(File.E, Rank.R5), Square(File.E, Rank.R6))
|
||||
|
||||
val epScore = MoveOrdering.score(context, epCapture, None)
|
||||
val epScore = MoveOrdering.score(context, epCapture, None)
|
||||
val quietScore = MoveOrdering.score(context, quiet, None)
|
||||
epScore should be > quietScore
|
||||
|
||||
test("sort returns moves ordered by score"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackRook
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val moves = List(
|
||||
Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true)), // Rook capture
|
||||
Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true)), // Pawn capture
|
||||
Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6)) // Quiet
|
||||
Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true)), // Rook capture
|
||||
Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true)), // Pawn capture
|
||||
Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6)), // Quiet
|
||||
)
|
||||
val sorted = MoveOrdering.sort(context, moves, None)
|
||||
// Rook capture should be first (higher victim value)
|
||||
@@ -98,44 +112,50 @@ class MoveOrderingTest extends AnyFunSuite with Matchers:
|
||||
sorted.last.to should equal(Square(File.E, Rank.R6))
|
||||
|
||||
test("castling move is quiet (not capture)"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook
|
||||
))
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
val castleMove = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
|
||||
val score = MoveOrdering.score(context, castleMove, None)
|
||||
score should equal(0) // Quiet move
|
||||
val score = MoveOrdering.score(context, castleMove, None)
|
||||
score should equal(0) // Quiet move
|
||||
|
||||
test("all MoveType variants are handled in victimValue"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.E, Rank.R2) -> Piece.WhitePawn
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.E, Rank.R2) -> Piece.WhitePawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
// Test castling queenside - should have victim value 0
|
||||
val castleQs = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside)
|
||||
val scoreQs = MoveOrdering.score(context, castleQs, None)
|
||||
val scoreQs = MoveOrdering.score(context, castleQs, None)
|
||||
scoreQs should equal(0)
|
||||
|
||||
test("attackerValue covers all piece types"):
|
||||
val board = Board(Map(
|
||||
Square(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.B, Rank.R1) -> Piece.WhiteKnight,
|
||||
Square(File.C, Rank.R1) -> Piece.WhiteBishop,
|
||||
Square(File.D, Rank.R1) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.F, Rank.R2) -> Piece.WhitePawn
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.B, Rank.R1) -> Piece.WhiteKnight,
|
||||
Square(File.C, Rank.R1) -> Piece.WhiteBishop,
|
||||
Square(File.D, Rank.R1) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.F, Rank.R2) -> Piece.WhitePawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
// Create captures with each piece type
|
||||
val rookCapture = Move(Square(File.A, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val rookCapture = Move(Square(File.A, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val knightCapture = Move(Square(File.B, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val bishopCapture = Move(Square(File.C, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val queenCapture = Move(Square(File.D, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val kingCapture = Move(Square(File.E, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val pawnCapture = Move(Square(File.F, Rank.R2), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val queenCapture = Move(Square(File.D, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val kingCapture = Move(Square(File.E, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val pawnCapture = Move(Square(File.F, Rank.R2), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
|
||||
// Just verify all are scored without error
|
||||
MoveOrdering.score(context, rookCapture, None) should be >= 0
|
||||
@@ -146,15 +166,19 @@ class MoveOrderingTest extends AnyFunSuite with Matchers:
|
||||
MoveOrdering.score(context, pawnCapture, None) should be >= 0
|
||||
|
||||
test("promotion capture is distinct from quiet promotion"):
|
||||
val board = Board(Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
Square(File.D, Rank.R8) -> Piece.BlackPawn
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
Square(File.D, Rank.R8) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
// Promotion with capture
|
||||
val promotionWithCapture = Move(Square(File.E, Rank.R7), Square(File.D, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val promotionWithCapture =
|
||||
Move(Square(File.E, Rank.R7), Square(File.D, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
// Regular queen promotion (no capture)
|
||||
val quietPromotion = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val quietPromotion =
|
||||
Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val score1 = MoveOrdering.score(context, promotionWithCapture, None)
|
||||
val score2 = MoveOrdering.score(context, quietPromotion, None)
|
||||
score1 should be > score2
|
||||
|
||||
@@ -24,21 +24,20 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
try
|
||||
// Write a single entry with a different key
|
||||
Using(DataOutputStream(FileOutputStream(tempFile.toFile))) { dos =>
|
||||
dos.writeLong(12345L) // some random key
|
||||
dos.writeShort(0) // move
|
||||
dos.writeShort(100) // weight
|
||||
dos.writeInt(0) // learn
|
||||
dos.writeLong(12345L) // some random key
|
||||
dos.writeShort(0) // move
|
||||
dos.writeShort(100) // weight
|
||||
dos.writeInt(0) // learn
|
||||
}.get
|
||||
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
book.probe(GameContext.initial) shouldEqual None
|
||||
finally
|
||||
Files.delete(tempFile)
|
||||
finally Files.delete(tempFile)
|
||||
|
||||
test("Book returns a move when position is in book"):
|
||||
val tempFile = Files.createTempFile("test_book", ".bin")
|
||||
try
|
||||
val ctx = GameContext.initial
|
||||
val ctx = GameContext.initial
|
||||
val hash = PolyglotHash.hash(ctx)
|
||||
|
||||
// Write an entry: e2-e4 (normal move, non-capture)
|
||||
@@ -48,22 +47,21 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
Using(DataOutputStream(FileOutputStream(tempFile.toFile))) { dos =>
|
||||
dos.writeLong(hash)
|
||||
dos.writeShort(move)
|
||||
dos.writeShort(100) // weight
|
||||
dos.writeShort(100) // weight
|
||||
dos.writeInt(0)
|
||||
}.get
|
||||
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
val result = book.probe(ctx)
|
||||
result shouldNot be(None)
|
||||
result.get.from shouldEqual Square(File.E, Rank.R2)
|
||||
result.get.to shouldEqual Square(File.E, Rank.R4)
|
||||
finally
|
||||
Files.delete(tempFile)
|
||||
finally Files.delete(tempFile)
|
||||
|
||||
test("Weighted random sampling works"):
|
||||
val tempFile = Files.createTempFile("test_book", ".bin")
|
||||
try
|
||||
val ctx = GameContext.initial
|
||||
val ctx = GameContext.initial
|
||||
val hash = PolyglotHash.hash(ctx)
|
||||
|
||||
// Two moves: e2-e4 with high weight, d2-d4 with low weight
|
||||
@@ -73,12 +71,12 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
Using(DataOutputStream(FileOutputStream(tempFile.toFile))) { dos =>
|
||||
dos.writeLong(hash)
|
||||
dos.writeShort(moveE4)
|
||||
dos.writeShort(900) // high weight
|
||||
dos.writeShort(900) // high weight
|
||||
dos.writeInt(0)
|
||||
|
||||
dos.writeLong(hash)
|
||||
dos.writeShort(moveD4)
|
||||
dos.writeShort(100) // low weight
|
||||
dos.writeShort(100) // low weight
|
||||
dos.writeInt(0)
|
||||
}.get
|
||||
|
||||
@@ -93,12 +91,11 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
|
||||
// With 900:100 weight ratio, e4 should appear more frequently
|
||||
e4Count should be > d4Count
|
||||
finally
|
||||
Files.delete(tempFile)
|
||||
finally Files.delete(tempFile)
|
||||
|
||||
test("ClassicalBot without book falls back to search"):
|
||||
val ctx = GameContext.initial
|
||||
val bot = ClassicalBot(BotDifficulty.Easy) // no book
|
||||
val ctx = GameContext.initial
|
||||
val bot = ClassicalBot(BotDifficulty.Easy) // no book
|
||||
val move = bot.nextMove(ctx)
|
||||
move shouldNot be(None)
|
||||
// The move should be legal
|
||||
@@ -108,7 +105,7 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
test("ClassicalBot with book uses book move"):
|
||||
val tempFile = Files.createTempFile("test_book", ".bin")
|
||||
try
|
||||
val ctx = GameContext.initial
|
||||
val ctx = GameContext.initial
|
||||
val hash = PolyglotHash.hash(ctx)
|
||||
|
||||
// e2-e4
|
||||
@@ -121,19 +118,18 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
dos.writeInt(0)
|
||||
}.get
|
||||
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
val botWithBook = ClassicalBot(BotDifficulty.Easy, book = Some(book))
|
||||
val move = botWithBook.nextMove(ctx)
|
||||
val move = botWithBook.nextMove(ctx)
|
||||
|
||||
// Book should return e2-e4
|
||||
move shouldEqual Some(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal()))
|
||||
finally
|
||||
Files.delete(tempFile)
|
||||
finally Files.delete(tempFile)
|
||||
|
||||
test("Promotion moves are decoded correctly"):
|
||||
val tempFile = Files.createTempFile("test_book", ".bin")
|
||||
try
|
||||
val ctx = GameContext.initial
|
||||
val ctx = GameContext.initial
|
||||
val hash = PolyglotHash.hash(ctx)
|
||||
|
||||
// Pawn promotion: a7-a8=Q
|
||||
@@ -153,6 +149,5 @@ class PolyglotBookTest extends AnyFunSuite with Matchers:
|
||||
move shouldNot be(None)
|
||||
move.get.moveType match
|
||||
case MoveType.Promotion(piece) => piece shouldEqual PromotionPiece.Queen
|
||||
case _ => fail("Expected promotion move")
|
||||
finally
|
||||
Files.delete(tempFile)
|
||||
case _ => fail("Expected promotion move")
|
||||
finally Files.delete(tempFile)
|
||||
|
||||
@@ -19,40 +19,40 @@ class PolyglotHashTest extends AnyFunSuite with Matchers:
|
||||
PolyglotHash.hash(ctx) shouldEqual java.lang.Long.parseUnsignedLong("823c9b50fd114196", 16)
|
||||
|
||||
test("Hash changes when turn changes"):
|
||||
val ctx = GameContext.initial
|
||||
val hash1 = PolyglotHash.hash(ctx)
|
||||
val ctx = GameContext.initial
|
||||
val hash1 = PolyglotHash.hash(ctx)
|
||||
val ctxBlackTurn = ctx.withTurn(Color.Black)
|
||||
val hash2 = PolyglotHash.hash(ctxBlackTurn)
|
||||
val hash2 = PolyglotHash.hash(ctxBlackTurn)
|
||||
hash1 should not equal hash2
|
||||
|
||||
test("Hash changes when castling rights change"):
|
||||
val ctx = GameContext.initial
|
||||
val ctx = GameContext.initial
|
||||
val hash1 = PolyglotHash.hash(ctx)
|
||||
val noCastling = ctx.withCastlingRights(
|
||||
de.nowchess.api.board.CastlingRights(false, false, false, false)
|
||||
de.nowchess.api.board.CastlingRights(false, false, false, false),
|
||||
)
|
||||
val hash2 = PolyglotHash.hash(noCastling)
|
||||
hash1 should not equal hash2
|
||||
|
||||
test("En passant file is ignored when no side-to-move pawn can capture"):
|
||||
val fenWithEp = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq e3 0 1"
|
||||
val fenWithEp = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq e3 0 1"
|
||||
val fenWithoutEp = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1"
|
||||
val withEp = FenParser.parseFen(fenWithEp).toOption.getOrElse(fail("FEN parse failed"))
|
||||
val withoutEp = FenParser.parseFen(fenWithoutEp).toOption.getOrElse(fail("FEN parse failed"))
|
||||
val withEp = FenParser.parseFen(fenWithEp).toOption.getOrElse(fail("FEN parse failed"))
|
||||
val withoutEp = FenParser.parseFen(fenWithoutEp).toOption.getOrElse(fail("FEN parse failed"))
|
||||
PolyglotHash.hash(withEp) shouldEqual PolyglotHash.hash(withoutEp)
|
||||
|
||||
test("Different en passant files produce different hashes when capture is possible"):
|
||||
val ctx = GameContext.initial
|
||||
val ctx = GameContext.initial
|
||||
val epFileE = ctx.withEnPassantSquare(Some(Square(File.E, Rank.R3)))
|
||||
val epFileD = ctx.withEnPassantSquare(Some(Square(File.D, Rank.R3)))
|
||||
val hash1 = PolyglotHash.hash(epFileE)
|
||||
val hash2 = PolyglotHash.hash(epFileD)
|
||||
val hash1 = PolyglotHash.hash(epFileE)
|
||||
val hash2 = PolyglotHash.hash(epFileD)
|
||||
hash1 should not equal hash2
|
||||
|
||||
test("Removing en passant changes hash"):
|
||||
val ctx = GameContext.initial
|
||||
val ctx = GameContext.initial
|
||||
val withEP = ctx.withEnPassantSquare(Some(Square(File.E, Rank.R3)))
|
||||
val hash1 = PolyglotHash.hash(withEP)
|
||||
val noEP = withEP.withEnPassantSquare(None)
|
||||
val hash2 = PolyglotHash.hash(noEP)
|
||||
val hash1 = PolyglotHash.hash(withEP)
|
||||
val noEP = withEP.withEnPassantSquare(None)
|
||||
val hash2 = PolyglotHash.hash(noEP)
|
||||
hash1 should not equal hash2
|
||||
|
||||
@@ -13,23 +13,23 @@ class TranspositionTableTest extends AnyFunSuite with Matchers:
|
||||
tt.probe(12345L) should be(None)
|
||||
|
||||
test("store then probe returns the stored entry"):
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val entry = TTEntry(hash = 12345L, depth = 3, score = 50, flag = TTFlag.Exact, bestMove = Some(move))
|
||||
tt.store(entry)
|
||||
val retrieved = tt.probe(12345L)
|
||||
retrieved should be(Some(entry))
|
||||
|
||||
test("probe returns None when hash differs (collision guard)"):
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val entry = TTEntry(hash = 12345L, depth = 3, score = 50, flag = TTFlag.Exact, bestMove = Some(move))
|
||||
tt.store(entry)
|
||||
tt.probe(54321L) should be(None)
|
||||
|
||||
test("clear removes all entries"):
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val entry = TTEntry(hash = 12345L, depth = 3, score = 50, flag = TTFlag.Exact, bestMove = Some(move))
|
||||
tt.store(entry)
|
||||
tt.probe(12345L) should be(Some(entry))
|
||||
@@ -37,7 +37,7 @@ class TranspositionTableTest extends AnyFunSuite with Matchers:
|
||||
tt.probe(12345L) should be(None)
|
||||
|
||||
test("all TTFlag values store and retrieve correctly"):
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
|
||||
TTFlag.values.foreach { flag =>
|
||||
@@ -48,16 +48,16 @@ class TranspositionTableTest extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("bestMove = None roundtrips"):
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val entry = TTEntry(hash = 99999L, depth = 1, score = 0, flag = TTFlag.Upper, bestMove = None)
|
||||
tt.store(entry)
|
||||
val retrieved = tt.probe(99999L)
|
||||
retrieved.map(_.bestMove) should be(Some(None))
|
||||
|
||||
test("always-replace overwrites at same slot"):
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move1 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val move2 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
|
||||
val tt = TranspositionTable(sizePow2 = 4)
|
||||
val move1 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val move2 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
|
||||
val entry1 = TTEntry(hash = 12345L, depth = 2, score = 50, flag = TTFlag.Exact, bestMove = Some(move1))
|
||||
val entry2 = TTEntry(hash = 12345L, depth = 3, score = 100, flag = TTFlag.Lower, bestMove = Some(move2))
|
||||
|
||||
|
||||
@@ -18,68 +18,74 @@ class ZobristHashTest extends AnyFunSuite with Matchers:
|
||||
test("hash differs after a pawn move"):
|
||||
val initial = GameContext.initial
|
||||
// Move pawn from e2 to e4
|
||||
val board = initial.board.pieces
|
||||
val newBoard = board.removed(Square(File.E, Rank.R2)).updated(Square(File.E, Rank.R4), Piece.WhitePawn)
|
||||
val board = initial.board.pieces
|
||||
val newBoard = board.removed(Square(File.E, Rank.R2)).updated(Square(File.E, Rank.R4), Piece.WhitePawn)
|
||||
val afterMove = initial.withBoard(Board(newBoard)).withTurn(Color.Black)
|
||||
val hash1 = ZobristHash.hash(initial)
|
||||
val hash2 = ZobristHash.hash(afterMove)
|
||||
val hash1 = ZobristHash.hash(initial)
|
||||
val hash2 = ZobristHash.hash(afterMove)
|
||||
hash1 should not equal hash2
|
||||
|
||||
test("hash includes castling rights"):
|
||||
val ctx1 = GameContext.initial
|
||||
val ctx2 = ctx1.withCastlingRights(CastlingRights.None)
|
||||
val ctx1 = GameContext.initial
|
||||
val ctx2 = ctx1.withCastlingRights(CastlingRights.None)
|
||||
val hash1 = ZobristHash.hash(ctx1)
|
||||
val hash2 = ZobristHash.hash(ctx2)
|
||||
hash1 should not equal hash2
|
||||
|
||||
test("hash includes en-passant square"):
|
||||
val ctx1 = GameContext.initial
|
||||
val ctx2 = ctx1.withEnPassantSquare(Some(Square(File.E, Rank.R3)))
|
||||
val ctx1 = GameContext.initial
|
||||
val ctx2 = ctx1.withEnPassantSquare(Some(Square(File.E, Rank.R3)))
|
||||
val hash1 = ZobristHash.hash(ctx1)
|
||||
val hash2 = ZobristHash.hash(ctx2)
|
||||
hash1 should not equal hash2
|
||||
|
||||
test("hash includes side to move"):
|
||||
val ctx1 = GameContext.initial
|
||||
val ctx2 = ctx1.withTurn(Color.Black)
|
||||
val ctx1 = GameContext.initial
|
||||
val ctx2 = ctx1.withTurn(Color.Black)
|
||||
val hash1 = ZobristHash.hash(ctx1)
|
||||
val hash2 = ZobristHash.hash(ctx2)
|
||||
hash1 should not equal hash2
|
||||
|
||||
test("nextHash matches recomputed hash for a normal move"):
|
||||
val context = GameContext.initial
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
val context = GameContext.initial
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
val incremental = ZobristHash.nextHash(context, ZobristHash.hash(context), move, next)
|
||||
incremental should equal(ZobristHash.hash(next))
|
||||
|
||||
test("nextHash matches recomputed hash for promotion and castling"):
|
||||
val promotionBoard = Board(Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
val promotionBoard = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.E, Rank.R8) -> Piece.BlackKing,
|
||||
),
|
||||
)
|
||||
val promotionContext = GameContext.initial
|
||||
.withBoard(promotionBoard)
|
||||
.withTurn(Color.White)
|
||||
.withCastlingRights(CastlingRights.All)
|
||||
val promotionMove = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val promotionNext = DefaultRules.applyMove(promotionContext)(promotionMove)
|
||||
val promotionHash = ZobristHash.nextHash(promotionContext, ZobristHash.hash(promotionContext), promotionMove, promotionNext)
|
||||
val promotionHash =
|
||||
ZobristHash.nextHash(promotionContext, ZobristHash.hash(promotionContext), promotionMove, promotionNext)
|
||||
promotionHash should equal(ZobristHash.hash(promotionNext))
|
||||
|
||||
val castleBoard = Board(Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.E, Rank.R8) -> Piece.BlackKing
|
||||
))
|
||||
val castleBoard = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.E, Rank.R8) -> Piece.BlackKing,
|
||||
),
|
||||
)
|
||||
val castleContext = GameContext.initial
|
||||
.withBoard(castleBoard)
|
||||
.withTurn(Color.White)
|
||||
.withCastlingRights(CastlingRights(whiteKingSide = true, whiteQueenSide = false, blackKingSide = false, blackQueenSide = false))
|
||||
.withCastlingRights(
|
||||
CastlingRights(whiteKingSide = true, whiteQueenSide = false, blackKingSide = false, blackQueenSide = false),
|
||||
)
|
||||
val castleMove = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
|
||||
val castleNext = DefaultRules.applyMove(castleContext)(castleMove)
|
||||
val castleHash = ZobristHash.nextHash(castleContext, ZobristHash.hash(castleContext), castleMove, castleNext)
|
||||
castleHash should equal(ZobristHash.hash(castleNext))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user