all repos — emchess @ be8e1da8b28b364ae535696c302f3e41d52448c0

src/game/coach.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
 81
 82
 83
import type { Game, Color, Move, Square } from "./state";
import { threats, exchangeLoss, pieceValue, PIECE_NAME } from "./analysis";

// Warn before a move that drops material the opponent can simply take. Returns the
// piece at risk, or null if the move is fine. Checks are skipped — a check usually
// changes the tactics.
export function blunderCheck(
  game: Game,
  from: Square,
  to: Square,
  promotion: string | undefined,
  human: Color
): { type: string; square: Square; loss: number } | null {
  const test = game.clone();
  const move = test.play(from, to, promotion as "q" | "r" | "b" | "n" | undefined);
  if (!move || test.inCheck()) return null;
  const gained = move.captured ? pieceValue(move.captured) : 0;
  const worst = threats(test, human)[0];
  if (worst && worst.loss - gained >= 2) return worst;
  return null;
}

export function assess(cp: number | null, mate: number | null): string {
  if (mate !== null) {
    return mate > 0 ? `You have a forced mate in ${mate}.` : `You are getting mated in ${Math.abs(mate)}.`;
  }
  if (cp === null) return "The position is unclear.";
  const p = cp / 100;
  if (p >= 2.5) return "You have a winning advantage.";
  if (p >= 0.9) return "You stand a little better.";
  if (p > -0.9) return "The position is about equal.";
  if (p > -2.5) return "You stand worse.";
  return "You are in serious trouble.";
}

export function explainPosition(
  game: Game,
  human: Color,
  cp: number | null,
  mate: number | null
): string {
  const enemy: Color = human === "w" ? "b" : "w";
  const mine = threats(game, human);
  const theirs = threats(game, enemy);

  let detail: string;
  if (game.inCheck() && game.turn === human) {
    detail = "Your king is in check, so answer that first.";
  } else if (mine[0] && mine[0].loss >= 2) {
    detail = `Your ${PIECE_NAME[mine[0].type]} on ${mine[0].square} is hanging.`;
  } else if (theirs[0] && theirs[0].loss >= 2) {
    detail = `You can win the ${PIECE_NAME[theirs[0].type]} on ${theirs[0].square}.`;
  } else {
    detail = developmentHint(game, human);
  }
  return assess(cp, mate) + " " + detail;
}

// Explain, in plain terms, why the last move lost ground. Returns null when there is
// nothing concrete to say.
export function explainMistake(before: Game, after: Game, move: Move, human: Color): string | null {
  const beforeHanging = new Set(threats(before, human).map((t) => t.square));
  const newHang = threats(after, human).find((t) => !beforeHanging.has(t.square) && t.loss >= 2);
  if (newHang) {
    return `That left your ${PIECE_NAME[newHang.type]} on ${newHang.square} attacked and underdefended.`;
  }
  const movedLoss = exchangeLoss(after, move.to as Square);
  if (movedLoss >= 2) {
    return `Your ${PIECE_NAME[move.piece]} walked into a loss on ${move.to}.`;
  }
  return null;
}

function developmentHint(game: Game, human: Color): string {
  const rank = human === "w" ? "1" : "8";
  const home = game
    .pieces(human)
    .filter((p) => (p.type === "n" || p.type === "b") && p.square[1] === rank);
  if (home.length >= 2) return "Bring your knights and bishops into the game.";
  const enemy: Color = human === "w" ? "b" : "w";
  if (game.inCheck() && game.turn === enemy) return "You are giving check.";
  return "Nothing is loose. Look for a plan and improve your worst piece.";
}