/*
  SAN Kit Minimal Engine
  ======================
  
  Minimal CECP engine built on Steven Edwards' SAN Kit library */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

#include "sandef.h"
#include "sanvar.h"

#include "sanint.h"
#include "sangen.h"
#include "sanmer.h"
#include "sanmpu.h"
#include "sanfen.h"
#include "sanpce.h"
#include "sanrst.h"
#include "sanatk.h"
#include "sanmlm.h"

/*--> GenLegalMoves: generate a fully legal move list (mf_chec
      already set on each move), without the extra mate/stalemate
      scan and SAN disambiguation SANGenMoves() adds on top -- see
      the comment on its use in Negamax() below for why that matters
      here. This is exactly what the library's own (but file-static,
      and so unusable outside sangen.c) SANGenClean() does:
      pseudo-legal generation, then execute-and-test legality
      filtering (SANMLPolice(), which sets mf_chec along the way). */
static void
GenLegalMoves (void)
{
  SANGenFullPL ();
  SANMLPolice ();

  return;
}

/* controls whether the engine auto-replies after the opponent's
   move; toggled by force/go for entirely ordinary reasons, so it
   does NOT by itself mean the game is unrecoverably over */
static siT playing;

/* set once, permanently, when the move-history capacity safety net
   in ReportStatus() fires (see there): unlike "playing", this must
   never be turned back on short of a fresh "new"/"setboard", since
   it exists specifically to stop hn from ever growing again -- see
   the usermove handler in main(), which refuses to apply *any*
   further move once this is set, even if the GUI or the opponent's
   engine keeps sending them after our "resign" */
static siT capacity_resigned;

/*--> MoveToCoord: format a move as plain coordinate notation
      ("e2e4", or "e7e8q" for a promotion) rather than SAN, since
      that is what the CECP protocol expects back */
static void
MoveToCoord (mptrT mptr, char *buf)
{
  fileT ffile, tfile;
  rankT frank, trank;
  pT promo;

  ffile = map_file_sq (mptr->m_frsq);
  frank = map_rank_sq (mptr->m_frsq);
  tfile = map_file_sq (mptr->m_tosq);
  trank = map_rank_sq (mptr->m_tosq);

  buf[0] = acfv[ffile];
  buf[1] = acrv[frank];
  buf[2] = acfv[tfile];
  buf[3] = acrv[trank];
  buf[4] = '\0';

  promo = cv_p_scmvv[mptr->m_scmv];
  if (promo != p_x)
    {
      buf[4] = (char) tolower ((unsigned char) acpv[promo]);
      buf[5] = '\0';
    };

  return;
}

/*--> LooksLikeCoordMove: strict syntax check for "e2e4"-style text,
      so protocol keywords (e.g. "easy", "draw", "hint") are never
      mistaken for a badly-typed move just because they start with a
      letter that happens to also be a file letter */
static siT
LooksLikeCoordMove (const char *s)
{
  size_t len;

  len = strlen (s);
  if ((len != 4) && (len != 5))
    return (0);
  if (!SANCheckFile (s[0]) || !SANCheckRank (s[1]))
    return (0);
  if (!SANCheckFile (s[2]) || !SANCheckRank (s[3]))
    return (0);
  if (len == 5)
    {
      char pc = (char) tolower ((unsigned char) s[4]);
      if ((pc != 'q') && (pc != 'r') && (pc != 'b') && (pc != 'n'))
        return (0);
    };

  return (1);
}

/*--> FindMoveByCoord: locate the legal move matching a coordinate
      string, by generating the legal move list and comparing
      from-square/to-square/promotion piece against each entry */
static mptrT
FindMoveByCoord (const char *s)
{
  siT i;
  mptrT mptr, found;
  fileT ffile, tfile;
  rankT frank, trank;
  sqT frsq, tosq;
  pT promo;

  if (!LooksLikeCoordMove (s))
    return (NULL);

  ffile = SANEvalFile (s[0]);
  frank = SANEvalRank (s[1]);
  tfile = SANEvalFile (s[2]);
  trank = SANEvalRank (s[3]);
  frsq = map_sq_rank_file (frank, ffile);
  tosq = map_sq_rank_file (trank, tfile);

  promo = p_x;
  if (strlen (s) == 5)
    {
      char pc = (char) tolower ((unsigned char) s[4]);
      if (pc == 'q') promo = p_q;
      else if (pc == 'r') promo = p_r;
      else if (pc == 'b') promo = p_b;
      else if (pc == 'n') promo = p_n;
    };

  GenLegalMoves ();

  found = NULL;
  for (i = 0; (found == NULL) && (i < curr_g.g_gmvc); i++)
    {
      mptr = &curr_g.g_base[i];
      if ((mptr->m_frsq == frsq) && (mptr->m_tosq == tosq))
        {
          if (promo == p_x)
            found = mptr;
          else if (cv_p_scmvv[mptr->m_scmv] == promo)
            found = mptr;
        };
    };

  return (found);
}

/*--> ReportStatus: inspect SANStatus() and react to it.
 *
 * Every one of the thirteen gsT values SANStatus() can return is
 * handled explicitly below, and every branch writes at least one
 * '#' comment line to stderr explaining what that code means and
 * why this engine reacts the way it does -- CECP GUIs ignore lines
 * starting with '#' on stdout, but since these are diagnostic
 * rather than protocol output they go to stderr instead, so they
 * never risk being misread as part of the protocol stream.
 *
 * Two things are deliberately NOT taken from the SAN Kit's own
 * cv_gtc_gsv[]/gtcstrv[] result tables (sanint.c), and are computed
 * here instead:
 *
 *  - gs_wcmt/gs_bcmt (checkmate): cv_gtc_gsv[] maps these backwards
 *    (gs_wcmt, "White is checkmated" i.e. Black wins, is mapped to
 *    gtc_wwin, "White win" -- and gs_bcmt is mapped the same wrong
 *    way round). This is a genuine, reproducible bug in the
 *    original 1994 sanint.c, not a misunderstanding on our part:
 *    feed this engine the moves for the "fool's mate" (Black mates)
 *    and it will report "1-0" if it trusts that table, and "0-1"
 *    for the "scholar's mate" (White mates). Since SANPGNEventReport
 *    (sanpgn.c) uses the very same table, this also affects PGN
 *    export from the stock "san" tool. We simply don't use the
 *    table for these two codes.
 *
 *  - gs_over: see below -- not a chess result at all.
 *
 * Returns the gsT value it examined, so callers can decide whether
 * to keep playing without querying SANStatus() a second time.
 */
static gsT
ReportStatus (void)
{
  gsT gs;

  /* SANStatus()'s own gs_over case (see below) checks fmvn *after*
     several other statuses in a strict priority chain (gs_bust,
     gs_dhmc, gs_wcmt/gs_bcmt, gs_wsmt/gs_bsmt, gs_dmat, *then*
     gs_over -- see sanrst.c), each guarded by "if (gs == gs_norm)".
     Once an earlier one of those becomes permanently true --
     gs_dmat is the practical case, since captured material never
     comes back -- gs_over can never be reached again, no matter
     how far fmvn climbs afterwards: a self-play game was observed
     reaching insufficient material at move 198 (two moves *before*
     the 200-move mark), after which SANStatus() reported gs_dmat
     continuously all the way past the intended resignation point,
     right up to the move-history array's true hard capacity
     (hn == hL), which aborted the process via SANFatal()/exit(1)
     instead of ever resigning -- reported by the GUI as a raw
     disconnect, not a clean game result.
     So this check is done directly against the live history
     counter hn, before SANStatus() runs at all, precisely so nothing
     else can ever mask it. hmpL (fmpL full moves' worth of plies)
     is used rather than the library's own hL, to resign at the
     originally-intended 200-move mark with comfortable margin
     below the true crash point (hL = hmpL + a small fixed-size
     safety cushion the original 1994 code reserved, not meant to
     be played into). */
  if (hn >= hmpL)
    {
      fprintf (stderr,
        "# hn=%d has reached the SAN Kit's intended move-history "
        "capacity (hmpL=%d, i.e. %d full moves -- see sandef.h); "
        "resigning now regardless of what SANStatus() reports, "
        "since that check can be permanently masked (see the "
        "comment above) and this must never be allowed to run into "
        "the library's true hard limit and abort the process.\n",
        (int) hn, (int) hmpL, (int) fmpL);
      printf ("resign\n");
      playing = 0;
      capacity_resigned = 1;
      return (gs_over);
    };

  gs = SANStatus ();

  switch (gs)
    {
    case gs_norm:
      fprintf (stderr, "# status: gs_norm -- normal, game continues\n");
      break;

    case gs_bust:
      fprintf (stderr,
        "# status: gs_bust -- illegal position (SANCensus(), see "
        "sanmsc.c, failed: wrong king count, castling rights "
        "inconsistent with the actual king/rook placement, a pawn "
        "on the back rank, a piece surplus, or the side not on "
        "move left in check -- in this engine, only reachable via "
        "a syntactically-valid but chess-illegal 'setboard' FEN). "
        "Move generation is skipped entirely for an illegal "
        "position (see SANUpdate() in sanval.c), so curr_g may be "
        "stale; no move can safely be selected from here.\n");
      printf ("tellusererror illegal position (see setboard)\n");
      playing = 0;
      break;

    case gs_wcmt:
      fprintf (stderr,
        "# status: gs_wcmt -- checkmate, White is mated (Black "
        "wins). Reported directly rather than via cv_gtc_gsv[] / "
        "gtcstrv[] (sanint.c), which map this the wrong way "
        "round -- see the ReportStatus() header comment above.\n");
      printf ("0-1 {Black mates}\n");
      break;

    case gs_bcmt:
      fprintf (stderr,
        "# status: gs_bcmt -- checkmate, Black is mated (White "
        "wins). Same cv_gtc_gsv[] inversion as gs_wcmt, reported "
        "directly for the same reason.\n");
      printf ("1-0 {White mates}\n");
      break;

    case gs_wsmt:
      fprintf (stderr,
        "# status: gs_wsmt -- stalemate, White has no legal move "
        "(draw)\n");
      printf ("1/2-1/2 {White stalemated}\n");
      break;

    case gs_bsmt:
      fprintf (stderr,
        "# status: gs_bsmt -- stalemate, Black has no legal move "
        "(draw)\n");
      printf ("1/2-1/2 {Black stalemated}\n");
      break;

    case gs_dhmc:
      fprintf (stderr,
        "# status: gs_dhmc -- draw, fifty-move rule (halfmove "
        "clock reached hmvcL, see sandef.h)\n");
      printf ("1/2-1/2 {Draw by fifty-move rule}\n");
      break;

    case gs_dmat:
      fprintf (stderr,
        "# status: gs_dmat -- draw, insufficient mating material. "
        "Chess-theoretically correct, but not always accepted by "
        "cutechess-cli's own stricter referee, which has been seen "
        "forfeiting the claiming engine over it ('Invalid result "
        "claim'); this engine deliberately does not volunteer this "
        "claim -- see the README -- but still has plenty of legal "
        "moves available, so the usermove handler keeps play going "
        "for this code exactly as for gs_norm; stopping here "
        "instead would leave the engine silent forever and get it "
        "killed as a stalled connection.\n");
      break;

    case gs_drep:
      fprintf (stderr,
        "# status: gs_drep -- draw, position repetition. "
        "SANStatus() never actually produces this on its own (no "
        "repetition test exists in sanrst.c); only reachable "
        "through manual result-declaration bookkeeping (sangtc.c) "
        "this engine doesn't use.\n");
      break;

    case gs_dagr:
      fprintf (stderr,
        "# status: gs_dagr -- draw, agreed. Like gs_drep, never "
        "produced by SANStatus() itself in this engine.\n");
      break;

    case gs_wres:
      fprintf (stderr,
        "# status: gs_wres -- White resigns. Never produced by "
        "SANStatus() itself in this engine.\n");
      break;

    case gs_bres:
      fprintf (stderr,
        "# status: gs_bres -- Black resigns. Never produced by "
        "SANStatus() itself in this engine.\n");
      break;

    case gs_over:
      /* kept for defensive completeness; the hn >= hmpL check at
         the top of this function fires first and resigns before
         SANStatus() itself would ever reach this case in practice
         (see that check's comment for why relying on this branch
         alone isn't reliable) */
      fprintf (stderr,
        "# status: gs_over -- SAN Kit move-history capacity (200 "
        "full moves, fmpL in sandef.h) reached. NOT a chess draw "
        "-- see the header comment in "
        "../xboard-engine/xboard-engine.c. Resigning instead of "
        "repeating the library's own false '1/2-1/2' claim.\n");
      printf ("resign\n");
      playing = 0;
      capacity_resigned = 1;
      break;

    default:
      fprintf (stderr, "# status: unrecognized gsT value %d\n", (int) gs);
      break;
    };

  return (gs);
}

/*--> OrderKey: move-ordering sort key for the search below: checks
      first, then promotions, then captures (by the value of the
      piece captured), then everything else. Trying the most
      forcing/valuable moves first is what makes alpha-beta pruning
      actually cut branches early instead of degenerating into a
      full-width search. */
static siT
OrderKey (mptrT mptr)
{
  if (mptr->m_flag & mf_chec)
    return (0);
  if (cv_p_scmvv[mptr->m_scmv] != p_x)
    return (1);
  if (valcpv[mptr->m_tocp] > 0)
    return (2);
  return (3);
}

/*--> OrderMoves: sort curr_g.g_base[0..n) in place by OrderKey();
      a plain insertion sort is more than fast enough here, since n
      is at most a few dozen */
static void
OrderMoves (siT n)
{
  siT i, j;
  mT tmp;

  for (i = 1; i < n; i++)
    {
      tmp = curr_g.g_base[i];
      j = i;
      while ((j > 0) && (OrderKey (&curr_g.g_base[j - 1]) > OrderKey (&tmp)))
        {
          curr_g.g_base[j] = curr_g.g_base[j - 1];
          j--;
        };
      curr_g.g_base[j] = tmp;
    };

  return;
}

/* a comfortably large score, well above any realistic material
   total (the most either side can ever have is a shade over 4000,
   see valcpv[] in sandef.h), used to represent "checkmate" -- minus
   how many plies deep it was found, so that among several forced
   mates the search always prefers the fastest one, and among
   several ways to lose, it prefers the one that survives longest */
#define MATE_SCORE 100000

/* how many of the attacker's own moves (not plies) iterative
   deepening will try to reach at most. This is a safety cap, not a
   target: even the worst case for branching (the initial position)
   already exhausts the 0.5s time budget around depth 5, and quieter
   or more forcing positions -- fewer pieces, fewer legal replies --
   reach this cap and stop early with time to spare, so raising it
   only helps those without risking the time budget elsewhere.
   MAX_SEARCH_DEPTH plies alone stays nowhere near aL (see below),
   but Quiescence() (see its own header comment) can extend well
   past whatever depth Negamax() stops at, and that combined total
   is what actually has to stay under aL -- see ABS_MAX_PLY below,
   which is what enforces that, not this constant by itself. */
#define MAX_SEARCH_DEPTH 12

/* an absolute ceiling on how deep the *combined* Negamax()+
   Quiescence() recursion is ever allowed to go, regardless of
   depth budget or how many further captures remain on the board.
   Needed because Quiescence() has no depth limit of its own (see
   its header comment) -- it stops naturally once the board runs out
   of captures to make, on the reasoning that with at most 30 pieces
   on the board to begin with, that's a self-limiting recursion. That
   reasoning is correct in isolation, but wrong about what it's being
   compared against: it was written as though 30 was being measured
   against aL (32) directly, forgetting that Negamax() can *already*
   have spent up to MAX_SEARCH_DEPTH of that same budget by the time
   Quiescence() is even entered. A real self-play game reached
   exactly this: a queen-and-minor-piece-rich position, thirteen
   moves deep with several pieces still in play, drove the combined
   depth past aL and crashed the whole process with the SAN Kit's
   own "SANExecute: overflow" abort (sanmer.c) -- not a hang, not a
   graceful resignation, just gone, reported by the GUI as a
   disconnect. ply mirrors an (the library's own live analysis-stack
   depth) exactly at every point in this search, both starting from
   the same call and always incrementing together, so checking ply
   against a threshold well below aL, in Quiescence() alone (the
   only place depth is ever "free"), is sufficient to guarantee an
   itself never gets close, however capture-rich the position is.
   The margin below aL is deliberately generous, not tightly
   computed against MAX_SEARCH_DEPTH: safety here matters far more
   than the few extra ply of quiescence a tighter bound would buy
   back. */
#define ABS_MAX_PLY (aL - 6)

static clock_t search_deadline;
static siT search_timed_out;
static unsigned long search_nodes;

/*--> InCheck: is the side currently to move in check? */
static siT
InCheck (void)
{
  return ((curr_e.e_actc == c_w) ? SANWhiteInCheck () : SANBlackInCheck ());
}

/*--> Evaluate: a purely material static evaluation, from the point
      of view of the side currently to move (so it can be negated
      straight into the negamax recursion below) -- no positional
      terms, no king safety, nothing beyond the SAN Kit's own
      valcpv[] piece values. This is deliberately the simplest
      possible evaluation function; see the README for what a
      quiescence search (extending capture sequences past the
      normal depth limit, to avoid misjudging a position that
      happens to be mid-exchange right at the search horizon) would
      add on top of it.
 * cpmv[] is read directly rather than recomputed with
 * SANSetMaterial() (a full board rescan): SANExecute()/SANRetract()
 * already keep it correct incrementally on every capture and
 * promotion (see sanmer.c), so by the time any node in the search
 * is evaluated it's already up to date at no extra cost -- this
 * matters here, since Evaluate() runs at every leaf. */
static siT
Evaluate (void)
{
  return (cpmv[curr_e.e_actc] - cpmv[inv_cv[curr_e.e_actc]]);
}

/*--> Quiescence: like Negamax() with no depth budget left, except
      that instead of returning Evaluate() outright, it keeps
      searching -- but only captures and promotions, never quiet
      moves -- until the position is "quiet" (no more of either
      available) or a cutoff occurs. This is what stops Evaluate()'s
      plain material count from ever being trusted in the middle of
      an exchange: without it, a search that stops right after
      losing a queen to an "obviously" winning capture, one ply
      before recapturing it, would misjudge the whole line as a
      disaster instead of an even trade (the classic "horizon
      effect" the rest of this file's comments refer to).
 *
 * In principle this recursion terminates on its own -- a capture
 * always removes a piece from the board, of which there are only
 * ever 30 to begin with, and each pawn can promote at most once, so
 * the number of available noisy moves strictly shrinks as the
 * position empties out -- but "eventually" isn't good enough on its
 * own: see ABS_MAX_PLY above for why this still needs, and has, a
 * hard depth cap of its own regardless.
 *
 * "Stand pat" -- the option to simply stop and accept the current
 * static score rather than capture -- is scored first, exactly like
 * a move of its own: if it already meets or beats beta, or raises
 * alpha, that's recorded before any capture is even tried, since a
 * side is never *forced* to keep trading just because a capture is
 * available.
 */
static siT
Quiescence (siT alpha, siT beta, siT ply)
{
  siT i, n, score, stand_pat;
  mptrT cand;

  if (search_timed_out)
    return (0);
  if ((((++search_nodes) & 0x3ff) == 0) && (clock () >= search_deadline))
    {
      search_timed_out = 1;
      return (0);
    };

  GenLegalMoves ();
  n = curr_g.g_gmvc;

  if (n == 0)
    return (InCheck () ? -(MATE_SCORE - ply) : 0);

  stand_pat = Evaluate ();
  if (stand_pat >= beta)
    return (beta);
  if (stand_pat > alpha)
    alpha = stand_pat;

  if (ply >= ABS_MAX_PLY)
    return (alpha);            /* hard depth cap -- see ABS_MAX_PLY */

  OrderMoves (n);               /* captures still come before quiet
                                    moves; see OrderKey() */

  for (i = 0; i < n; i++)
    {
      cand = &curr_g.g_base[i];

      /* skip anything that isn't a capture or a promotion -- a
         quiet check, say, is exactly the kind of move quiescence is
         meant to stop searching */
      if ((valcpv[cand->m_tocp] == 0) &&
          (cv_p_scmvv[cand->m_scmv] == p_x))
        continue;

      curr_g.g_curr = cand;
      SANExecute ();
      score = -Quiescence (-beta, -alpha, ply + 1);
      SANRetract ();

      if (search_timed_out)
        return (0);

      if (score >= beta)
        return (beta);          /* cutoff */
      if (score > alpha)
        alpha = score;
    };

  return (alpha);
}

/*--> Negamax: alpha-beta search, "depth" attacker-moves-worth of
      plies deep (i.e. depth 1 = itself and the opponent's reply,
      depth 2 = two full moves each, and so on), returning a score
      from the point of view of the side currently to move.
 *
 * The two things that stop the recursion are handled up front,
 * before "depth" is even looked at: if the side to move has no
 * legal moves at all, the position is either checkmate (a very bad
 * score for them, adjusted by "ply" so a faster mate always beats a
 * slower one) or stalemate (an exact draw, 0); either way there is
 * nothing further to search from here regardless of how much depth
 * budget is left. Only once that's ruled out does running out of
 * depth budget hand off to Quiescence() above, rather than trusting
 * Evaluate()'s plain material count directly.
 *
 * Moves are tried in OrderMoves() order (forcing moves first) so
 * that a cutoff (alpha >= beta, meaning the opponent already has a
 * better alternative earlier in the tree and would never let play
 * reach this position) is found as early as possible, pruning the
 * rest of this node's siblings unexamined.
 */
static siT
Negamax (siT depth, siT alpha, siT beta, siT ply)
{
  siT i, n, score, best;
  mptrT cand;

  if (search_timed_out)
    return (0);
  if ((((++search_nodes) & 0x3ff) == 0) && (clock () >= search_deadline))
    {
      search_timed_out = 1;
      return (0);
    };

  /* SANGenClean() -- a fully legal move list, with mf_chec already
     set -- rather than the fuller SANGenMoves(): the latter also
     runs SANMLScanMate() and SAN disambiguation on top, neither of
     which this search needs (checkmate/stalemate are determined
     below from n and InCheck(), and moves are only ever emitted in
     coordinate notation, never SAN), and SANMLScanMate() in
     particular costs about as much again as generation itself,
     since it re-generates one ply further for every candidate just
     to test for mate. Skipping it roughly doubles the node rate. */
  GenLegalMoves ();
  n = curr_g.g_gmvc;

  if (n == 0)
    return (InCheck () ? -(MATE_SCORE - ply) : 0);

  if (depth == 0)
    return (Quiescence (alpha, beta, ply));

  OrderMoves (n);

  best = -(MATE_SCORE + 1);
  for (i = 0; i < n; i++)
    {
      cand = &curr_g.g_base[i];
      curr_g.g_curr = cand;
      SANExecute ();
      score = -Negamax (depth - 1, -beta, -alpha, ply + 1);
      SANRetract ();

      if (search_timed_out)
        return (0);

      if (score > best)
        best = score;
      if (best > alpha)
        alpha = best;
      if (alpha >= beta)
        break;                  /* beta cutoff */
    };

  return (best);
}

/*--> SearchRoot: like Negamax(), but for the root position, where
      the actual winning move (not just its score) needs to be
      remembered. Only ever returns success for a depth it finished
      examining every root move for -- if the time budget runs out
      partway through, the moves tried so far this depth are
      discarded rather than risk preferring one of them just because
      of the order they happened to be tried in (see ChooseMove()).
      *score is only meaningful when this returns success. */
static siT
SearchRoot (siT depth, mT *best_move, siT *score_out)
{
  siT i, n, score, best;
  mptrT cand;
  siT alpha, beta;
  siT have_move;

  GenLegalMoves ();          /* see the comment in Negamax() above */
  n = curr_g.g_gmvc;
  OrderMoves (n);

  alpha = -(MATE_SCORE + 1);
  beta = MATE_SCORE + 1;
  best = alpha;
  have_move = 0;

  for (i = 0; i < n; i++)
    {
      cand = &curr_g.g_base[i];

      curr_g.g_curr = cand;
      SANExecute ();
      score = -Negamax (depth - 1, -beta, -alpha, 1);
      SANRetract ();

      if (search_timed_out)
        return (0);

      if (!have_move || (score > best))
        {
          best = score;
          *best_move = *cand;
          have_move = 1;
        };
      if (best > alpha)
        alpha = best;
    };

  *score_out = best;
  return (have_move);
}

/*--> ChooseMove: pick a move for the side currently to move, by
      iterative deepening: SearchRoot() is tried at depth 1, then 2,
      and so on, up to MAX_SEARCH_DEPTH or until the time budget
      runs out, whichever comes first. Only a depth that finished
      completely is ever used to update the move actually returned
      -- depth 1 is cheap enough to always finish, so there is
      always at least one fully-searched depth to fall back on.
 *
 * Once a depth's search reports a forced mate, deepening stops:
 * thanks to iterative deepening, the first depth to find one is
 * necessarily the *fastest* forced mate available, and no amount of
 * extra depth could ever improve on that.
 */
static void
ChooseMove (mT *result)
{
  siT depth, score, have_any, last_depth;
  mT candidate;

  search_nodes = 0;
  search_deadline = clock () + ((CLOCKS_PER_SEC * 7) / 20);   /* ~0.35s */
  have_any = 0;
  last_depth = 0;

  for (depth = 1; depth <= MAX_SEARCH_DEPTH; depth++)
    {
      search_timed_out = 0;

      if (SearchRoot (depth, &candidate, &score))
        {
          *result = candidate;
          have_any = 1;
          last_depth = depth;
          if (score >= MATE_SCORE - (2 * MAX_SEARCH_DEPTH))
            break;               /* fastest forced mate already found */
        };

      if (search_timed_out)
        break;
    };

  if (!have_any)
    {
      /* extremely defensive fallback: depth 1 above should always
         complete well within budget, so this should never actually
         trigger, but if it somehow does, just play any legal move
         rather than leave the caller with nothing at all */
      fprintf (stderr,
               "# ChooseMove(): depth 1 search did not complete in "
               "time; falling back to the first legal move\n");
      GenLegalMoves ();
      *result = curr_g.g_base[0];
    }
  else
    fprintf (stderr,
             "# search: reached depth %d (%lu nodes), score %d\n",
             (int) last_depth, search_nodes,
             score);

  return;
}

/*--> EngineMove: pick a move, play it, and report it to the GUI in
      coordinate notation */
static void
EngineMove (void)
{
  char coord[8];
  mT chosen;

  ChooseMove (&chosen);

  SANPlayMove (&chosen);
  MoveToCoord (&chosen, coord);
  printf ("move %s\n", coord);
  (void) ReportStatus ();

  return;
}

static void
Usage (const char *progname)
{
  fprintf (stderr, "usage: %s [-h]\n", progname);
  fprintf (stderr, "  runs a CECP (xboard/WinBoard) engine on "
                    "stdin/stdout\n");
  fprintf (stderr, "  plays mate-in-one when available, and avoids "
                    "moves that\n");
  fprintf (stderr, "  allow the opponent to mate in one, otherwise\n");

  return;
}

int
main (int argc, char *argv[])
{
  char line[256];
  mptrT mptr;

  /* CECP is a line-oriented protocol over stdin/stdout: never let
     output buffering delay a reply */
  setvbuf (stdout, NULL, _IONBF, 0);

  if ((argc > 1) && ((strcmp (argv[1], "-h") == 0) ||
                      (strcmp (argv[1], "--help") == 0)))
    {
      Usage (argv[0]);
      return (0);
    };

  srand ((unsigned) time (NULL));

  SANInit ();

  /* the CECP protocol has its own move/board reporting; suppress the
     SAN Kit's own automatic board and move display so it doesn't get
     mixed into the protocol stream */
  optnv[optn_nabd] = 1;
  optnv[optn_namd] = 1;

  playing = 1;          /* auto-reply to the opponent's moves */

  while (fgets (line, sizeof (line), stdin) != NULL)
    {
      line[strcspn (line, "\r\n")] = '\0';

      if (strcmp (line, "xboard") == 0)
        {
          /* nothing to do */
        }
      else if (strncmp (line, "protover", 8) == 0)
        {
          printf ("feature myname=\"SME-G\" ");
          printf ("setboard=1 usermove=1 sigint=0 sigterm=0 ");
          printf ("colors=0 draw=0 analyze=0 variants=\"normal\" done=1\n");
        }
      else if (strcmp (line, "new") == 0)
        {
          SANInitGround ();
          playing = 1;
          capacity_resigned = 0;
          (void) ReportStatus ();
        }
      else if (strncmp (line, "setboard ", 9) == 0)
        {
          if (!SANFENSet (line + 9))
            fprintf (stderr, "tellusererror illegal FEN\n");
          else
            {
              capacity_resigned = 0;
              (void) ReportStatus ();
            };
        }
      else if (strcmp (line, "force") == 0)
        {
          playing = 0;
        }
      else if (strcmp (line, "go") == 0)
        {
          if (capacity_resigned)
            fprintf (stderr,
              "# ignoring 'go': already resigned for move-history "
              "capacity; send 'new' or 'setboard' to start over\n");
          else
            {
              playing = 1;
              EngineMove ();
            };
        }
      else if (strcmp (line, "undo") == 0)
        {
          if (hn > 0)
            SANUnplayMove ();
        }
      else if (strcmp (line, "remove") == 0)
        {
          if (hn > 0) SANUnplayMove ();
          if (hn > 0) SANUnplayMove ();
        }
      else if ((strncmp (line, "usermove ", 9) == 0) ||
               LooksLikeCoordMove (line))
        {
          char *mv;

          mv = (strncmp (line, "usermove ", 9) == 0) ? line + 9 : line;

          if (capacity_resigned)
            {
              /* refuse to apply *any* further move once resigned for
                 move-history capacity, no matter how many more the
                 GUI or the opponent's engine sends: SANPlayMove()
                 unconditionally pushes to that same history array
                 regardless of "playing", so continuing to accept
                 moves here (as this engine used to) can still walk
                 straight into the library's true hard capacity and
                 abort the process, exactly as if this check weren't
                 here at all */
              fprintf (stderr,
                "# ignoring usermove %s: already resigned for "
                "move-history capacity; send 'new' or 'setboard' to "
                "start over\n", mv);
            }
          else
            {
              mptr = FindMoveByCoord (mv);
              if (mptr == NULL)
                printf ("Illegal move: %s\n", mv);
              else
                {
                  gsT gs;

                  SANPlayMove (mptr);
                  gs = ReportStatus ();

                  /* gs_dmat is deliberately never *announced* above
                     (see ReportStatus()), but the game is still very
                     much ongoing -- plenty of legal moves remain in
                     a bare king+bishop vs king+bishop ending, say,
                     just no way to force mate. Treating it the same
                     as "not gs_norm, stop responding" here would
                     leave this engine replying to absolutely nothing
                     until the GUI's own timeout kills it as a
                     stalled connection -- exactly as harmful as the
                     false claim this was written to avoid in the
                     first place (this was observed to actually
                     happen, in the sibling xboard-engine, in a
                     self-play tournament game). So gs_dmat keeps
                     play going, same as gs_norm; only the truly
                     final codes (checkmate, stalemate, the
                     fifty-move rule, gs_over, gs_bust) actually stop
                     it. */
                  if (((gs == gs_norm) || (gs == gs_dmat)) && playing)
                    EngineMove ();
                };
            };
        }
      else if (strncmp (line, "ping ", 5) == 0)
        {
          printf ("pong %s\n", line + 5);
        }
      else if (strcmp (line, "quit") == 0)
        {
          break;
        }
      else
        {
          /* silently ignore anything else: level, time, otim, st,
             sd, hard, easy, post, nopost, hint, draw, result,
             black, white, computer, name, rating, ics, accepted,
             rejected, etc. */
        };
    };

  return (0);
}
