From 8e393959921cde902723dfcf0958a413eca4515c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 22:40:00 +0200 Subject: [PATCH] feat(eval): add static exchange evaluation --- See.scala | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 See.scala diff --git a/See.scala b/See.scala new file mode 100644 index 0000000..ce80e7e --- /dev/null +++ b/See.scala @@ -0,0 +1,115 @@ +package gambit + +object See: + private val value = Array(0, 100, 320, 330, 500, 900, 20000) + + private val pawnCaptureWhite = Array(-17, -15) + private val pawnCaptureBlack = Array(17, 15) + + def gain(pos: Position, move: Int): Int = + val board = pos.board + val to = Move.to(move) + val from = Move.from(move) + val us = pos.sideToMove + val removed = new Array[Boolean](128) + + if Move.isEnPassant(move) then + val capSq = if us == Color.White then to - 16 else to + 16 + removed(capSq) = true + + val victim = if Move.isEnPassant(move) then Piece.Pawn else Piece.typeOf(board(to)) + val gains = new Array[Int](40) + gains(0) = value(victim) + removed(from) = true + var occupant = value(Piece.typeOf(board(from))) + var side = us ^ 1 + var d = 0 + var running = true + + while running do + val attacker = leastAttacker(pos, to, side, removed) + if attacker < 0 then running = false + else if Piece.typeOf(board(attacker)) == Piece.King && leastAttacker(pos, to, side ^ 1, removed) >= 0 then + running = false + else + d += 1 + gains(d) = occupant - gains(d - 1) + occupant = value(Piece.typeOf(board(attacker))) + removed(attacker) = true + side ^= 1 + + while d > 0 do + gains(d - 1) = -math.max(-gains(d - 1), gains(d)) + d -= 1 + gains(0) + + def isWinning(pos: Position, move: Int): Boolean = gain(pos, move) >= 0 + + def isLosing(pos: Position, move: Int): Boolean = gain(pos, move) < 0 + + private def leastAttacker(pos: Position, target: Int, side: Int, removed: Array[Boolean]): Int = + val board = pos.board + var bestSquare = -1 + var bestValue = Int.MaxValue + + val pawn = Piece.make(side, Piece.Pawn) + val pawnOffsets = if side == Color.White then pawnCaptureWhite else pawnCaptureBlack + var i = 0 + while i < 2 do + val s = target + pawnOffsets(i) + if Square.onBoard(s) && !removed(s) && board(s) == pawn && value(Piece.Pawn) < bestValue then + bestValue = value(Piece.Pawn) + bestSquare = s + i += 1 + + val knight = Piece.make(side, Piece.Knight) + i = 0 + while i < Directions.knight.length do + val s = target + Directions.knight(i) + if Square.onBoard(s) && !removed(s) && board(s) == knight && value(Piece.Knight) < bestValue then + bestValue = value(Piece.Knight) + bestSquare = s + i += 1 + + val bishop = Piece.make(side, Piece.Bishop) + val queen = Piece.make(side, Piece.Queen) + i = 0 + while i < Directions.bishop.length do + val s = raySlider(board, target, Directions.bishop(i), removed) + if s >= 0 then + val p = board(s) + if p == bishop && value(Piece.Bishop) < bestValue then + bestValue = value(Piece.Bishop); bestSquare = s + else if p == queen && value(Piece.Queen) < bestValue then + bestValue = value(Piece.Queen); bestSquare = s + i += 1 + + val rook = Piece.make(side, Piece.Rook) + i = 0 + while i < Directions.rook.length do + val s = raySlider(board, target, Directions.rook(i), removed) + if s >= 0 then + val p = board(s) + if p == rook && value(Piece.Rook) < bestValue then + bestValue = value(Piece.Rook); bestSquare = s + else if p == queen && value(Piece.Queen) < bestValue then + bestValue = value(Piece.Queen); bestSquare = s + i += 1 + + val king = Piece.make(side, Piece.King) + i = 0 + while i < Directions.king.length do + val s = target + Directions.king(i) + if Square.onBoard(s) && !removed(s) && board(s) == king && value(Piece.King) < bestValue then + bestValue = value(Piece.King) + bestSquare = s + i += 1 + + bestSquare + + private def raySlider(board: Array[Int], target: Int, offset: Int, removed: Array[Boolean]): Int = + var c = target + offset + while Square.onBoard(c) do + if !removed(c) && board(c) != Piece.Empty then return c + c += offset + -1