all repos — emchess @ 582b0387fa0b9b6f7b51a8a13c4fee17764ffb39

src/game/analysis.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
import type { Game, Square, Color } from "./state";

const VALUE: Record<string, number> = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 100 };

export function pieceValue(type: string): number {
  return VALUE[type] ?? 0;
}

export const PIECE_NAME: Record<string, string> = {
  p: "pawn",
  n: "knight",
  b: "bishop",
  r: "rook",
  q: "queen",
  k: "king",
};

// Static exchange evaluation on an occupied square. Returns the material the owner
// stands to lose if the enemy starts capturing there (positive = a real loss).
export function exchangeLoss(game: Game, square: Square): number {
  const victim = game.get(square);
  if (!victim) return 0;
  const owner = victim.color;
  const enemy: Color = owner === "w" ? "b" : "w";
  const attackers = valuesOf(game, game.attackers(square, enemy));
  const defenders = valuesOf(game, game.attackers(square, owner));
  if (attackers.length === 0) return 0;
  return swap(pieceValue(victim.type), attackers, defenders);
}

function valuesOf(game: Game, squares: Square[]): number[] {
  return squares
    .map((sq) => pieceValue(game.get(sq)!.type))
    .sort((a, b) => a - b);
}

// Classic swap-off: least valuable piece captures first, each side may stop when
// continuing would lose material. Returns net material for the attacking side.
function swap(target: number, attackers: number[], defenders: number[]): number {
  const gains: number[] = [target];
  let onSquare = attackers[0];
  let a = 1;
  let d = 0;
  let depth = 1;
  let defenderToMove = true;
  while (true) {
    if (defenderToMove) {
      if (d >= defenders.length) break;
      gains[depth] = onSquare - gains[depth - 1];
      onSquare = defenders[d++];
    } else {
      if (a >= attackers.length) break;
      gains[depth] = onSquare - gains[depth - 1];
      onSquare = attackers[a++];
    }
    depth++;
    defenderToMove = !defenderToMove;
  }
  for (let i = depth - 1; i > 0; i--) {
    gains[i - 1] = -Math.max(-gains[i - 1], gains[i]);
  }
  return gains[0];
}

export interface Threat {
  square: Square;
  type: string;
  loss: number;
}

// Every piece of `color` the opponent can win material from, worst first.
export function threats(game: Game, color: Color): Threat[] {
  const out: Threat[] = [];
  for (const { square, type } of game.pieces(color)) {
    if (type === "k") continue;
    const loss = exchangeLoss(game, square);
    if (loss > 0) out.push({ square, type, loss });
  }
  return out.sort((a, b) => b.loss - a.loss);
}