/*
 * This file is part of the OpenKropki project
 * Viktor - strong tactical AI (Tigran-based with deeper search + hard tactics)
 *
 * Copyright (C) 2026 Mateusz Viste
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Ideas borrowed from GNU Kropki (reimplemented, not copied):
 *  1. leaf eval of groups / almost-closed pockets / bridges (not just score)
 *  2. explicit one-move threats + capture quiescence at leaves
 *  3. pattern pruning of dead / pointless intersections
 *  4. iterative deepening + Zobrist transposition table under a time budget
 *  5. avoid futile defense (don't feed an extra stone into a lost base)
 *  6. light optional cage-arming only -- never the main plan
 */

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

#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>

#include "ai.h"
#include "kropcore.h"
#include "okp.h"

#define FIELDSLOOKUP 6
#define STEPLIMIT_MIN 2
#define STEPLIMIT_MAX 6
#define QUIESCE_LIMIT 3
#define LASTMOVES 7
#define THREADSNUM 4
#define MAXFIELDS (FIELDSLOOKUP * LASTMOVES + 96)
#define SEARCHCAP 28
#define TIME_BUDGET_MS 1500UL
#define TT_BITS 17
#define TT_SIZE (1u << TT_BITS)
#define TT_MASK (TT_SIZE - 1u)

typedef uint_fast32_t field_t;

#define FIELDX(f) ((int)(f >> 16))
#define FIELDY(f) ((int)(f & 0xffffu))
#define XYTOFIELD(x, y) ((uint_fast32_t)(((uint_fast32_t)(x) << 16) | ((uint_fast32_t)(y) & 0xffffu)))

struct fieldlist {
  int x, y;
  int best, worst;
  int simcount;
  int capture;
  int threat;       /* opponent captures if they play here (after our pass) */
  int makes_threat; /* after we play here, we can capture next */
  int trap;         /* progress toward arming a pure 4-dot cage */
  int post_threat;  /* opp's best capture if WE play here (-1 = n/a) */
  int saves;        /* how much we cut opp's capture; <=0 = futile */
  int shape;
};

struct threaddata_t {
  struct kropki_game *g;
  struct fieldlist *f;
  field_t *interestingfields;
  int interestingfields_count;
  int steplimit;
};

enum { TT_EXACT = 1, TT_LOWER = 2, TT_UPPER = 3 };

struct tt_slot {
  uint64_t key;
  int32_t worst;
  int32_t best;
  int8_t depth;
  uint8_t flag;
  uint16_t gen;
  uint32_t bestmove; /* XYTOFIELD, 0 = none */
};

static field_t lastmoves[LASTMOVES];
static int lastmoveptr;

static uint64_t zob_cell[256][16]; /* index-mix byte x compact cell state */
static uint64_t zob_side[4];
static int zob_ready;

static struct tt_slot *tt_table;
static SDL_mutex *tt_lock;
static uint16_t tt_gen;
static volatile int search_abort;
static unsigned long search_deadline_ms;

static const int ORTH[4][2] = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
static const int DIAG[4][2] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}};


static unsigned long now_ms(void) {
  struct timespec ts;
  clock_gettime(CLOCK_REALTIME, &ts);
  return((unsigned long)ts.tv_sec * 1000UL
       + (unsigned long)ts.tv_nsec / 1000000UL);
}


static uint64_t splitmix64(uint64_t *x) {
  uint64_t z = (*x += 0x9E3779B97F4A7C15ULL);
  z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
  z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
  return(z ^ (z >> 31));
}


static void zob_init(void) {
  uint64_t seed = 0xC0FFEE5A11DEADULL;
  int i, v;
  if (zob_ready) return;
  for (i = 0; i < 256; i++)
    for (v = 0; v < 16; v++)
      zob_cell[i][v] = splitmix64(&seed);
  for (i = 0; i < 4; i++)
    zob_side[i] = splitmix64(&seed);
  zob_ready = 1;
}


static int tt_ensure(void) {
  zob_init();
  if (tt_table == NULL) {
    tt_table = calloc(TT_SIZE, sizeof(struct tt_slot));
    if (tt_table == NULL) return(-1);
  }
  if (tt_lock == NULL) {
    tt_lock = SDL_CreateMutex();
    if (tt_lock == NULL) return(-1);
  }
  return(0);
}


/* Compact cell for Zobrist: empty / free dots / captured ownership bits */
static unsigned cell_zstate(unsigned char raw) {
  unsigned v = (unsigned)(raw & (0xffu ^ (unsigned)KROPKI_FLGX));
  if (v == 0) return(0);
  /* map common combinations into 1..15 */
  v &= 15u;
  return(v ? v : 1u);
}


static uint64_t hash_pos(const struct kropki_game *g) {
  uint64_t h = 0;
  int w = (int)g->field_width + 2;
  int ht = (int)g->field_height + 2;
  int n = w * ht;
  int i;
  for (i = 0; i < n; i++) {
    unsigned st = cell_zstate(g->field[i]);
    if (st == 0) continue;
    h ^= zob_cell[i & 255][st];
    /* second table lane so large boards do not alias on low byte alone */
    h ^= zob_cell[(i >> 8) & 255][(st + 1) & 15];
  }
  if (g->nextplayer < 4)
    h ^= zob_side[g->nextplayer];
  return(h);
}


static int tt_probe(uint64_t key, int depth, int *worst, int *best, field_t *hint) {
  struct tt_slot *e;
  uint8_t flag;
  if (tt_table == NULL) return(0);
  e = &tt_table[key & TT_MASK];
  SDL_LockMutex(tt_lock);
  /* Ignore entries from a previous game/search generation: scores are
   * relative to the root side, so a P1 session must not feed a P2 game. */
  if ((e->key != key) || (e->flag == 0) || (e->gen != tt_gen) || (e->depth < depth)) {
    if ((e->key == key) && (e->gen == tt_gen) && (e->bestmove != 0) && (hint != NULL))
      *hint = (field_t)e->bestmove;
    SDL_UnlockMutex(tt_lock);
    return(0);
  }
  *worst = e->worst;
  *best = e->best;
  flag = e->flag;
  if ((hint != NULL) && (e->bestmove != 0))
    *hint = (field_t)e->bestmove;
  SDL_UnlockMutex(tt_lock);
  return(flag);
}


static void tt_store(uint64_t key, int depth, int worst, int best, int flag, field_t bestmove) {
  struct tt_slot *e;
  if (tt_table == NULL) return;
  e = &tt_table[key & TT_MASK];
  SDL_LockMutex(tt_lock);
  /* prefer deeper / fresher entries */
  if ((e->key == key) && (e->depth > depth) && (e->gen == tt_gen)) {
    SDL_UnlockMutex(tt_lock);
    return;
  }
  e->key = key;
  e->worst = worst;
  e->best = best;
  e->depth = (int8_t)depth;
  e->flag = (uint8_t)flag;
  e->gen = tt_gen;
  if (bestmove != 0)
    e->bestmove = (uint32_t)bestmove;
  SDL_UnlockMutex(tt_lock);
}


void ai_viktor_reset(void) {
  memset(lastmoves, 0, sizeof(lastmoves));
  lastmoveptr = 0;
  tt_gen++;
}


static int abs_i(int v) { return (v < 0) ? -v : v; }

static int in_bounds(const struct kropki_game *g, int x, int y) {
  return (x >= 0) && (y >= 0) && (x < g->field_width) && (y < g->field_height);
}

static int cell_dot(const struct kropki_game *g, int x, int y) {
  if (!in_bounds(g, x, y)) return(-1);
  return(KROPKI_GETPOS(g, x, y) & 3);
}

static int cell_empty(const struct kropki_game *g, int x, int y) {
  return in_bounds(g, x, y) && (KROPKI_GETPOS(g, x, y) == 0);
}


static int move_gain(const struct kropki_game *src, int x, int y, int player) {
  struct kropki_game *g;
  int before, after;
  if (!in_bounds(src, x, y) || (KROPKI_GETPOS(src, x, y) != 0)) return(-1);
  g = kropki_copygame(src, kropki_NO_WALLS);
  if (g == NULL) return(-1);
  before = g->score[player - 1];
  if (kropki_setpos(g, x, y, player, kropki_NO_WALLS) != 0) {
    kropki_freegame(g);
    return(-1);
  }
  after = g->score[player - 1];
  kropki_freegame(g);
  return(after - before);
}


/* max capture available for `player` near (cx,cy) -- used for threat creation */
static int best_local_capture(const struct kropki_game *src, int player, int cx, int cy) {
  int dx, dy, best = 0;
  for (dy = -3; dy <= 3; dy++) {
    for (dx = -3; dx <= 3; dx++) {
      int gain = move_gain(src, cx + dx, cy + dy, player);
      if (gain > best) best = gain;
    }
  }
  return(best);
}


/* Best capture `player` can still make on this board (local + optional hint list). */
static int best_capture_now(const struct kropki_game *src, int player,
                            const field_t *fields, int fieldscount) {
  int best = 0, i, dx, dy;
  if (fields != NULL) {
    for (i = 0; i < fieldscount; i++) {
      int gain = move_gain(src, FIELDX(fields[i]), FIELDY(fields[i]), player);
      if (gain > best) best = gain;
    }
  }
  if (src->lastplayer != 0) {
    for (dy = -4; dy <= 4; dy++) {
      for (dx = -4; dx <= 4; dx++) {
        int gain = move_gain(src, (int)src->lastmovex + dx, (int)src->lastmovey + dy, player);
        if (gain > best) best = gain;
      }
    }
  }
  return(best);
}


/*
 * After we play at (x,y), how many can opponent still capture?
 * Returns -1 on illegal / OOM.
 */
static int opp_capture_after_our_move(const struct kropki_game *src, int x, int y,
                                      int me, const field_t *fields, int fieldscount) {
  struct kropki_game *g;
  int opp = 3 - me, rem;
  if (!cell_empty(src, x, y)) return(-1);
  g = kropki_copygame(src, kropki_NO_WALLS);
  if (g == NULL) return(-1);
  if (kropki_setpos(g, x, y, me, kropki_NO_WALLS) != 0) {
    kropki_freegame(g);
    return(-1);
  }
  rem = best_capture_now(g, opp, fields, fieldscount);
  /* also scan around our new stone -- common place for a closing move */
  {
    int dx, dy;
    for (dy = -3; dy <= 3; dy++) {
      for (dx = -3; dx <= 3; dx++) {
        int gain = move_gain(g, x + dx, y + dy, opp);
        if (gain > rem) rem = gain;
      }
    }
  }
  kropki_freegame(g);
  return(rem);
}


/* =========================================================================
 * 1) Richer leaf evaluation (GNU Kropki-inspired groups / pockets / bridges)
 *    Kept well below 100-per-point so real captures still dominate.
 * ========================================================================= */
static int structure_eval(const struct kropki_game *g, int me0) {
  int tip = 0;
  int x, y;
  int mydot = me0 + 1, oppdot = 2 - me0;
  int w = g->field_width, h = g->field_height;

  for (y = 0; y < h; y++) {
    for (x = 0; x < w; x++) {
      int cell = KROPKI_GETPOS(g, x, y);
      int dot = cell & 3;
      int cap = (cell >> 2) & 3;
      int empty_orth, own_orth, opp_orth, own_diag, i;

      if (cap != 0) continue; /* already inside a base */

      empty_orth = own_orth = opp_orth = own_diag = 0;
      for (i = 0; i < 4; i++) {
        int p = cell_dot(g, x + ORTH[i][0], y + ORTH[i][1]);
        if (p == 0) empty_orth++;
        else if (p == mydot) own_orth++;
        else if (p == oppdot) opp_orth++;
      }
      for (i = 0; i < 4; i++) {
        if (cell_dot(g, x + DIAG[i][0], y + DIAG[i][1]) == mydot) own_diag++;
      }

      if (dot == 0) {
        /* fence / pocket building on empty intersections */
        if ((own_orth >= 2) && (opp_orth >= 1)) tip += 3 + own_orth;
        else if (own_orth >= 3) tip += 2 + own_orth;
        else if ((own_orth >= 2) && (own_diag >= 1)) tip += 2;
        else if ((own_orth >= 1) && (opp_orth >= 1)) tip += 1;

        /* pure cages -- almost ignore in leaf eval */
        if ((opp_orth == 0) && (own_orth == 4)) tip += 2;
      } else if (dot == oppdot) {
        /* pressure on enemy dots by liberty count -- lean offensive */
        if (empty_orth == 0) tip += 14;
        else if (empty_orth == 1) tip += 12;
        else if (empty_orth == 2) tip += 7;
        if (own_orth >= 2) tip += 5;
        if ((own_orth >= 1) && (empty_orth <= 2)) tip += 3;
      } else if (dot == mydot) {
        if ((empty_orth <= 1) && (opp_orth >= 2)) tip -= 5;
        else if (empty_orth >= 3) tip += 1;
      }
    }
  }

  /* local tip around last move */
  if (g->lastplayer != 0) {
    int x0 = g->lastmovex, y0 = g->lastmovey;
    for (y = y0 - 2; y <= y0 + 2; y++) {
      for (x = x0 - 2; x <= x0 + 2; x++) {
        int fri = 0, ene = 0, dx, dy;
        if (!cell_empty(g, x, y)) continue;
        for (dy = -1; dy <= 1; dy++) {
          for (dx = -1; dx <= 1; dx++) {
            int p;
            if ((dx == 0) && (dy == 0)) continue;
            p = cell_dot(g, x + dx, y + dy);
            if (p == mydot) fri++;
            else if (p == oppdot) ene++;
          }
        }
        if ((fri > 0) && (ene > 0)) tip += 2;
        if ((fri >= 2) && (ene >= 2)) tip += 3;
        if (fri >= 4) tip += 2;
        if ((fri >= 5) && (ene >= 1)) tip += 3;
        if ((ene >= 4) && (fri == 0)) tip -= 3;
      }
    }
  }

  if (tip > 80) tip = 80;
  if (tip < -80) tip = -80;
  return(tip);
}


static int eval_pos(const struct kropki_game *g, int self) {
  int opp = 1 - self;
  return((int)g->score[self] - (int)g->score[opp]) * 100 + structure_eval(g, self);
}


static int local_shape(const struct kropki_game *g, int x, int y, int player) {
  int me = player, opp = 3 - player;
  int fri = 0, ene = 0, dx, dy, score = 0;
  int n, e, s, w;
  int own_orth = 0, opp_orth = 0;

  n = kropki_getpos(g, x, y - 1) & 3;
  e = kropki_getpos(g, x + 1, y) & 3;
  s = kropki_getpos(g, x, y + 1) & 3;
  w = kropki_getpos(g, x - 1, y) & 3;
  if (n == me) own_orth++; else if (n == opp) opp_orth++;
  if (e == me) own_orth++; else if (e == opp) opp_orth++;
  if (s == me) own_orth++; else if (s == opp) opp_orth++;
  if (w == me) own_orth++; else if (w == opp) opp_orth++;

  for (dy = -1; dy <= 1; dy++) {
    for (dx = -1; dx <= 1; dx++) {
      int p;
      if ((dx == 0) && (dy == 0)) continue;
      p = kropki_getpos(g, x + dx, y + dy) & 3;
      if (p == me) fri++;
      else if (p == opp) ene++;
    }
  }

  if ((ene >= 3) && (fri == 0) && (opp_orth >= 3))
    score -= 50;

  /* contact / surround beats solo geometry */
  if ((fri > 0) && (ene > 0)) score += 28 + fri * 4 + ene * 5;
  if ((fri >= 2) && (ene >= 2)) score += 18;
  if (ene >= 3) score += 12;
  if (fri >= 2) score += 8 + fri * 2;
  if ((n == me) && (e == me)) score += 10;
  if ((e == me) && (s == me)) score += 10;
  if ((s == me) && (w == me)) score += 10;
  if ((w == me) && (n == me)) score += 10;
  if ((n == me) && (s == me)) score += 12;
  if ((e == me) && (w == me)) score += 12;
  if (own_orth >= 3) score += 6;

  if (g->lastplayer != 0) {
    int d = abs_i(x - (int)g->lastmovex) + abs_i(y - (int)g->lastmovey);
    if (d <= 1) score += 25;
    else if (d == 2) score += 12;
    else if (d == 3) score += 5;
    else score -= (d - 3);
  }
  return(score);
}


/* =========================================================================
 * 5) Trap building -- pure 4-orth cages (opponent dies if they step inside)
 * ========================================================================= */
static int trap_build_score(const struct kropki_game *g, int x, int y, int me) {
  int score = 0;
  int i, j;
  int opp = 3 - me;
  int own_here = 0, opp_here = 0;

  if (!cell_empty(g, x, y)) return(0);

  /* never play inside our own (almost) cage */
  for (i = 0; i < 4; i++) {
    int p = cell_dot(g, x + ORTH[i][0], y + ORTH[i][1]);
    if (p == me) own_here++;
    else if (p == opp) opp_here++;
  }
  if ((opp_here == 0) && (own_here >= 3)) return(-40);

  /* placing here arms / advances cages whose centre is an orth neighbour */
  for (i = 0; i < 4; i++) {
    int cx = x + ORTH[i][0], cy = y + ORTH[i][1];
    int own = 0, oppn = 0;
    if (!cell_empty(g, cx, cy)) continue;
    for (j = 0; j < 4; j++) {
      int p = cell_dot(g, cx + ORTH[j][0], cy + ORTH[j][1]);
      if (p == me) own++;
      else if (p == opp) oppn++;
    }
    if (oppn != 0) continue; /* contaminated -- not a quiet trap */
    /* only care about actually arming a 3->4 cage; ignore box-building */
    if (own + 1 == 4) score += 20;
  }

  return(score);
}


/* =========================================================================
 * 3) Pattern pruning -- pointless intersections (unless tactical)
 * ========================================================================= */
static int is_dead_pattern(const struct kropki_game *g, int x, int y, int me) {
  int opp = 3 - me;
  int own_orth = 0, opp_orth = 0, i;
  int fri8 = 0, ene8 = 0, dx, dy;

  if (!cell_empty(g, x, y)) return(1);

  for (i = 0; i < 4; i++) {
    int p = cell_dot(g, x + ORTH[i][0], y + ORTH[i][1]);
    if (p == 0) { /* empty */ }
    else if (p == me) own_orth++;
    else if (p == opp) opp_orth++;
  }

  if (opp_orth == 4) return(1); /* enemy dead eye */
  if (own_orth == 4) return(1); /* filling our own eye */

  /* empty corner of a 2x2 already owned by us */
  {
    int a, b, c;
    a = cell_dot(g, x - 1, y); b = cell_dot(g, x, y - 1); c = cell_dot(g, x - 1, y - 1);
    if ((a == me) && (b == me) && (c == me)) return(1);
    a = cell_dot(g, x + 1, y); b = cell_dot(g, x, y - 1); c = cell_dot(g, x + 1, y - 1);
    if ((a == me) && (b == me) && (c == me)) return(1);
    a = cell_dot(g, x - 1, y); b = cell_dot(g, x, y + 1); c = cell_dot(g, x - 1, y + 1);
    if ((a == me) && (b == me) && (c == me)) return(1);
    a = cell_dot(g, x + 1, y); b = cell_dot(g, x, y + 1); c = cell_dot(g, x + 1, y + 1);
    if ((a == me) && (b == me) && (c == me)) return(1);
  }

  for (dy = -1; dy <= 1; dy++) {
    for (dx = -1; dx <= 1; dx++) {
      int p;
      if ((dx == 0) && (dy == 0)) continue;
      p = cell_dot(g, x + dx, y + dy);
      if (p == me) fri8++;
      else if (p == opp) ene8++;
    }
  }

  if ((fri8 == 0) && (ene8 == 0) && (g->lastplayer != 0)) {
    int d = abs_i(x - (int)g->lastmovex) + abs_i(y - (int)g->lastmovey);
    if (d > 4) return(1);
  }

  if ((opp_orth >= 3) && (own_orth == 0) && (fri8 == 0)) return(1);

  return(0);
}


static int find_capture_move(const struct kropki_game *g, int player,
                             const field_t *fields, int fieldscount,
                             int *ox, int *oy) {
  int i, best = 0, bx = -1, by = -1;
  int me = player;

  if (fields != NULL) {
    for (i = 0; i < fieldscount; i++) {
      int x = FIELDX(fields[i]), y = FIELDY(fields[i]);
      int gain;
      if (KROPKI_GETPOS(g, x, y) != 0) continue;
      gain = move_gain(g, x, y, me);
      if (gain > best) { best = gain; bx = x; by = y; }
    }
  }
  if (g->lastplayer != 0) {
    int dx, dy;
    for (dy = -3; dy <= 3; dy++) {
      for (dx = -3; dx <= 3; dx++) {
        int x = (int)g->lastmovex + dx, y = (int)g->lastmovey + dy;
        int gain = move_gain(g, x, y, me);
        if (gain > best) { best = gain; bx = x; by = y; }
      }
    }
  }
  if (best <= 0) return(0);
  *ox = bx;
  *oy = by;
  return(best);
}


static void ai_think(const struct kropki_game *gamesrc, int movex, int movey,
                     const field_t *fields, int fieldscount, int steplimit,
                     int self, int *worst, int *best, int *simcount,
                     const int *parentworst, int qlimit) {
  struct kropki_game *g;
  int i, stepschecked;
  int worstchild = 0, bestchild = 0;
  const int opp = 1 - self;
  uint64_t key;
  field_t tt_hint = 0;
  field_t child_best = 0;
  int probe;

  *worst = gamesrc->score[self] - gamesrc->score[opp];
  *best = *worst;

  if (search_abort) return;
  if (now_ms() >= search_deadline_ms) {
    search_abort = 1;
    return;
  }

  if (gamesrc->playablefields == 0) return;

  g = kropki_copygame(gamesrc, kropki_NO_WALLS);
  if (g == NULL) return;

  if (kropki_setpos(g, movex, movey, g->nextplayer, kropki_NO_WALLS) != 0) {
    kropki_freegame(g);
    return;
  }

  key = hash_pos(g);
  probe = tt_probe(key, steplimit, worst, best, &tt_hint);
  if (probe == TT_EXACT) {
    kropki_freegame(g);
    return;
  }

  /* ===== 2) capture quiescence at the horizon ===== */
  if (steplimit == 0) {
    if ((qlimit > 0) && (g->nextplayer >= 1) && (g->playablefields > 0)) {
      int cx, cy;
      if (find_capture_move(g, g->nextplayer, fields, fieldscount, &cx, &cy) > 0) {
        ai_think(g, cx, cy, fields, fieldscount, 0, self, worst, best, simcount, parentworst, qlimit - 1);
        if (!search_abort)
          tt_store(key, 0, *worst, *best, TT_EXACT, XYTOFIELD(cx, cy));
        kropki_freegame(g);
        return;
      }
    }
    *worst = eval_pos(g, self);
    *best = *worst;
    if (!search_abort)
      tt_store(key, 0, *worst, *best, TT_EXACT, 0);
    kropki_freegame(g);
    return;
  }

  stepschecked = 0;

  /* try TT hint first for better cutoffs */
  if ((tt_hint != 0) && (KROPKI_GETPOS(g, FIELDX(tt_hint), FIELDY(tt_hint)) == 0)) {
    int b, w;
    ai_think(g, FIELDX(tt_hint), FIELDY(tt_hint), fields, fieldscount, steplimit - 1,
             self, &w, &b, simcount, NULL, qlimit);
    if (!search_abort) {
      stepschecked = 1;
      worstchild = w;
      bestchild = b;
      child_best = tt_hint;
    }
  }

  for (i = 0; i < fieldscount; i++) {
    int x = FIELDX(fields[i]), y = FIELDY(fields[i]);
    int b, w;
    int *pworst = NULL;
    field_t f = fields[i];

    if (search_abort) break;
    if (KROPKI_GETPOS(g, x, y) != 0) continue;
    if ((tt_hint != 0) && (f == tt_hint)) continue; /* already tried */
    if (stepschecked != 0) pworst = &worstchild;

    ai_think(g, x, y, fields, fieldscount, steplimit - 1, self, &w, &b, simcount, pworst, qlimit);
    if (search_abort) break;
    stepschecked++;

    if (stepschecked == 1) {
      worstchild = w;
      bestchild = b;
      child_best = f;
      continue;
    }
    if (b > bestchild) bestchild = b;
    if (g->lastplayer != (self + 1)) {
      if (w > worstchild) {
        worstchild = w;
        child_best = f;
      }
    } else {
      if (w < worstchild) {
        worstchild = w;
        child_best = f;
      }
      if ((parentworst != NULL) && (w < *parentworst)) break;
    }
  }

  if (search_abort) {
    kropki_freegame(g);
    return;
  }

  if (stepschecked > 0) {
    (*simcount) += stepschecked;
    *worst = worstchild;
    *best = bestchild;
    tt_store(key, steplimit, *worst, *best, TT_EXACT, child_best);
  } else {
    *worst = eval_pos(g, self);
    *best = *worst;
    tt_store(key, steplimit, *worst, *best, TT_EXACT, 0);
  }
  kropki_freegame(g);
}


static int computeinterestingfields(const struct kropki_game *g, field_t *fieldslist, field_t f, int maxfields) {
  const int inc[2] = {1, -1};
  int x = FIELDX(f), y = FIELDY(f), i, bok, count = 0;

  for (bok = 1;; bok++) {
    for (i = 0; i < bok; i++) {
      x += inc[bok & 1];
      if (kropki_getpos(g, x, y) == 0) {
        fieldslist[count++] = XYTOFIELD(x, y);
        if (count >= maxfields) return(count);
      }
    }
    for (i = 0; i < bok; i++) {
      y += inc[bok & 1];
      if (kropki_getpos(g, x, y) == 0) {
        fieldslist[count++] = XYTOFIELD(x, y);
        if (count >= maxfields) return(count);
      }
    }
    if ((bok > g->field_width * 2) && (bok > g->field_height * 2)) return(count);
  }
}


static int field_in(const field_t *list, int count, field_t f) {
  int i;
  for (i = 0; i < count; i++) if (list[i] == f) return(1);
  return(0);
}


static int start_ai_thinking(void *param) {
  struct threaddata_t *d = param;
  ai_think(d->g, d->f->x, d->f->y, d->interestingfields, d->interestingfields_count,
           d->steplimit, d->g->nextplayer - 1, &(d->f->worst), &(d->f->best), &(d->f->simcount),
           NULL, QUIESCE_LIMIT);
  return(0);
}


static int pick_root_move(struct fieldlist *flist, int flistlen, const struct kropki_game *g) {
  int i, mymove = 0;
  int mymoves[MAXFIELDS], mymoves_count;

  for (i = 1; i < flistlen; i++) {
    if (flist[i].worst > flist[mymove].worst) mymove = i;
    else if (flist[i].worst == flist[mymove].worst) {
      if (flist[i].best > flist[mymove].best) mymove = i;
      else if (flist[i].best == flist[mymove].best) {
        if (flist[i].capture > flist[mymove].capture) mymove = i;
        else if (flist[i].capture == flist[mymove].capture) {
          if (flist[i].makes_threat > flist[mymove].makes_threat) mymove = i;
          else if (flist[i].makes_threat == flist[mymove].makes_threat) {
            if (flist[i].threat > flist[mymove].threat) mymove = i;
            else if (flist[i].threat == flist[mymove].threat) {
              if (flist[i].trap > flist[mymove].trap) mymove = i;
              else if (flist[i].trap == flist[mymove].trap) {
                if (flist[i].shape > flist[mymove].shape) mymove = i;
                else if ((flist[i].shape == flist[mymove].shape)
                      && (flist[i].simcount > flist[mymove].simcount)) mymove = i;
              }
            }
          }
        }
      }
    }
  }

  mymoves_count = 0;
  for (i = 0; i < flistlen; i++) {
    if ((flist[i].worst != flist[mymove].worst) || (flist[i].best != flist[mymove].best)
     || (flist[i].capture != flist[mymove].capture)
     || (flist[i].makes_threat != flist[mymove].makes_threat)
     || (flist[i].threat != flist[mymove].threat)
     || (flist[i].trap != flist[mymove].trap)
     || (flist[i].shape != flist[mymove].shape))
      continue;
    mymoves[mymoves_count++] = i;
  }
  if (mymoves_count < 1) mymoves[mymoves_count++] = mymove;
  {
    int besttie = mymoves[0];
    int bestd = 9999;
    for (i = 0; i < mymoves_count; i++) {
      int idx = mymoves[i];
      int d = abs_i(flist[idx].x - (int)g->lastmovex) + abs_i(flist[idx].y - (int)g->lastmovey);
      if (d < bestd) { bestd = d; besttie = idx; }
    }
    mymove = besttie;
  }
  return(mymove);
}


static void search_depth(struct kropki_game *g, struct fieldlist *flist, int flistlen,
                         field_t *interestingmoves, int interestingmoves_count, int steplimit) {
  int i, nextthread = 0;
  SDL_Thread *threadid[THREADSNUM];
  struct threaddata_t threadparam[THREADSNUM];
  memset(threadid, 0, sizeof(threadid));

  for (i = 0; i < flistlen; i++) {
    flist[i].worst = 0;
    flist[i].best = 0;
    flist[i].simcount = 0;
  }

  for (i = 0; i < flistlen; i++) {
    if (search_abort) break;
    if (threadid[nextthread] != NULL) SDL_WaitThread(threadid[nextthread], NULL);
    threadparam[nextthread].g = g;
    threadparam[nextthread].f = &flist[i];
    threadparam[nextthread].interestingfields = interestingmoves;
    threadparam[nextthread].interestingfields_count = interestingmoves_count;
    threadparam[nextthread].steplimit = steplimit;
    threadid[nextthread] = SDL_CreateThread(start_ai_thinking, "ai_viktor", &threadparam[nextthread]);
    nextthread = (nextthread + 1) % THREADSNUM;
  }
  for (nextthread = 0; nextthread < THREADSNUM; nextthread++) {
    if (threadid[nextthread] != NULL) SDL_WaitThread(threadid[nextthread], NULL);
  }
}


void ai_viktor(const char *okp, int *movex, int *movey) {
  struct kropki_game *g = NULL;
  field_t interestingmoves[MAXFIELDS];
  int interestingmoves_count = 0;
  struct fieldlist flist[MAXFIELDS];
  int flistlen = 0;
  int i, n, mymove;
  int maxcap = 0, maxthreat = 0, maxmake = 0;
  int bestcap = -1, bestthreat = -1, bestmake = -1;
  int me, opp;
  int depth, completed_depth = 0;
  int best_x = -1, best_y = -1, best_worst = 0, best_best = 0;
  unsigned long t0;

  *movex = -1;
  *movey = -1;

  g = kropki_okp2struct(okp, NULL, NULL, 0);
  if (g == NULL) goto QUIT;
  if (g->playablefields == 0) goto QUIT;

  me = g->nextplayer;
  opp = 3 - me;

  if (g->lastplayer == 0) {
    *movex = g->field_width / 2;
    *movey = g->field_height / 2;
    ai_viktor_reset();
    goto QUIT;
  }

  lastmoves[lastmoveptr++] = XYTOFIELD(g->lastmovex, g->lastmovey);
  lastmoveptr %= LASTMOVES;

  for (n = 0; n < LASTMOVES; n++) {
    if (lastmoves[n] == 0) continue;
    if (interestingmoves_count >= MAXFIELDS) break;
    interestingmoves_count += computeinterestingfields(
      g, &interestingmoves[interestingmoves_count], lastmoves[n],
      (MAXFIELDS - interestingmoves_count < FIELDSLOOKUP) ? (MAXFIELDS - interestingmoves_count) : FIELDSLOOKUP);
  }

  /* liberties of frontier groups */
  {
    int x, y, k;
    for (y = 0; y < g->field_height; y++) {
      for (x = 0; x < g->field_width; x++) {
        int dot = KROPKI_GETPOS(g, x, y) & 3;
        if ((dot != me) && (dot != opp)) continue;
        for (k = 0; k < 4; k++) {
          int lx = x + ORTH[k][0], ly = y + ORTH[k][1];
          field_t f;
          if (!cell_empty(g, lx, ly)) continue;
          f = XYTOFIELD(lx, ly);
          if (!field_in(interestingmoves, interestingmoves_count, f)
           && (interestingmoves_count < MAXFIELDS))
            interestingmoves[interestingmoves_count++] = f;
        }
      }
    }
  }

  /* contested frontier + tactics */
  {
    struct kropki_game *passg = kropki_copygame(g, kropki_NO_WALLS);
    int x, y;
    if (passg != NULL) kropki_pass(passg, passg->nextplayer);

    for (y = 0; y < g->field_height; y++) {
      for (x = 0; x < g->field_width; x++) {
        int fri = 0, ene = 0, dx, dy, gain = 0, thr = 0;
        field_t f;
        if (KROPKI_GETPOS(g, x, y) != 0) continue;
        for (dy = -1; dy <= 1; dy++) {
          for (dx = -1; dx <= 1; dx++) {
            int p;
            if ((dx == 0) && (dy == 0)) continue;
            if (!in_bounds(g, x + dx, y + dy)) continue;
            p = KROPKI_GETPOS(g, x + dx, y + dy) & 3;
            if (p == me) fri++;
            else if (p == opp) ene++;
          }
        }
        if ((fri < 1) && (ene < 1)) continue;
        gain = move_gain(g, x, y, me);
        if (gain < 0) gain = 0;
        if (passg != NULL) {
          thr = move_gain(passg, x, y, opp);
          if (thr < 0) thr = 0;
        }
        if ((gain <= 0) && (thr <= 0) && (fri + ene < 2)) continue;
        f = XYTOFIELD(x, y);
        if (!field_in(interestingmoves, interestingmoves_count, f) && (interestingmoves_count < MAXFIELDS))
          interestingmoves[interestingmoves_count++] = f;
      }
    }
    kropki_freegame(passg);
  }

  /* near-complete pure cages -- not seeded; traps must not drive play */

  if (interestingmoves_count == 0)
    interestingmoves[interestingmoves_count++] = XYTOFIELD(g->playablefieldx, g->playablefieldy);

  for (n = 0; n < interestingmoves_count; n++) {
    for (i = interestingmoves_count - 1; i > n; i--) {
      if (interestingmoves[n] != interestingmoves[i]) continue;
      interestingmoves_count--;
      if (interestingmoves_count > i)
        memmove(&interestingmoves[i], &interestingmoves[i + 1],
                sizeof(field_t) * (interestingmoves_count - i));
    }
  }

  memset(flist, 0, sizeof(flist));
  maxcap = 0; maxthreat = 0; maxmake = 0;
  bestcap = -1; bestthreat = -1; bestmake = -1;
  {
    struct kropki_game *passg = kropki_copygame(g, kropki_NO_WALLS);
    int pass_cap = 0;
    if (passg != NULL) {
      kropki_pass(passg, passg->nextplayer);
      pass_cap = best_capture_now(passg, opp, interestingmoves, interestingmoves_count);
    }

    flistlen = 0;
    for (n = 0; n < interestingmoves_count; n++) {
      int x = FIELDX(interestingmoves[n]);
      int y = FIELDY(interestingmoves[n]);
      int gain, thr = 0, mk = 0, tr = 0;
      int post = -1, saves = 0, near_fight;
      struct kropki_game *after;

      if (KROPKI_GETPOS(g, x, y) != 0) continue;

      gain = move_gain(g, x, y, me);
      if (gain < 0) continue;
      if (passg != NULL) {
        thr = move_gain(passg, x, y, opp);
        if (thr < 0) thr = 0;
      }

      if ((gain <= 0) && (thr <= 0) && is_dead_pattern(g, x, y, me)) continue;

      after = kropki_copygame(g, kropki_NO_WALLS);
      if (after != NULL) {
        if (kropki_setpos(after, x, y, me, kropki_NO_WALLS) == 0)
          mk = best_local_capture(after, me, x, y);
        kropki_freegame(after);
      }

      tr = trap_build_score(g, x, y, me);

      near_fight = (g->lastplayer != 0)
                && (abs_i(x - (int)g->lastmovex) + abs_i(y - (int)g->lastmovey) <= 4);
      if ((thr > 0) || near_fight || (pass_cap > 0 && near_fight)) {
        post = opp_capture_after_our_move(g, x, y, me, interestingmoves, interestingmoves_count);
        if (post >= 0) saves = pass_cap - post;
      }

      flist[flistlen].x = x;
      flist[flistlen].y = y;
      flist[flistlen].capture = gain;
      flist[flistlen].threat = thr;
      flist[flistlen].makes_threat = mk;
      flist[flistlen].trap = tr;
      flist[flistlen].post_threat = post;
      flist[flistlen].saves = saves;
      flist[flistlen].shape = local_shape(g, x, y, me)
                            + mk * 36
                            + ((gain > 0) ? 50 : 0);
      /* real defense only -- never reward futile "saves" that feed a stone */
      if ((thr > 0) && (saves > 0))
        flist[flistlen].shape += 25 + saves * 15;
      else if ((post >= 0) && (saves < 0))
        flist[flistlen].shape -= 90 - saves * 25; /* fed the closing base */
      else if ((post >= 0) && (saves == 0) && (pass_cap > 0) && near_fight)
        flist[flistlen].shape -= 35; /* desperate mill near a lost group */
      /* traps: negligible */
      flist[flistlen].shape += tr / 16;

      if (gain > maxcap) { maxcap = gain; bestcap = flistlen; }
      if (mk > maxmake) { maxmake = mk; bestmake = flistlen; }
      flistlen++;
      if (flistlen >= MAXFIELDS) break;
    }
    /* raw danger vs moves that truly reduce it */
    maxthreat = 0;
    bestthreat = -1;
    for (n = 0; n < flistlen; n++) {
      if (flist[n].threat > maxthreat) maxthreat = flist[n].threat;
      if (flist[n].saves <= 0) continue;
      if ((bestthreat < 0)
       || (flist[n].saves > flist[bestthreat].saves)
       || ((flist[n].saves == flist[bestthreat].saves)
        && (flist[n].shape > flist[bestthreat].shape)))
        bestthreat = n;
    }
    kropki_freegame(passg);
  }

  if (flistlen == 0) {
    *movex = g->playablefieldx;
    *movey = g->playablefieldy;
    goto REMEMBER;
  }

  if ((bestcap >= 0) && (maxcap > 0) && ((bestthreat < 0) || (maxcap >= flist[bestthreat].saves))) {
    *movex = flist[bestcap].x;
    *movey = flist[bestcap].y;
    printf("ai_viktor: CAPTURE [%d,%d] +%d\n", *movex, *movey, maxcap);
    goto REMEMBER;
  }
  /* only defend when the move actually reduces the coming capture */
  if ((bestthreat >= 0) && (flist[bestthreat].saves > 0)
   && ((bestcap < 0) || (flist[bestthreat].saves > maxcap))) {
    mymove = bestthreat;
    for (i = 0; i < flistlen; i++) {
      if (flist[i].saves < flist[mymove].saves) continue;
      if (flist[i].saves > flist[mymove].saves) mymove = i;
      else if (flist[i].shape > flist[mymove].shape) mymove = i;
    }
    *movex = flist[mymove].x;
    *movey = flist[mymove].y;
    printf("ai_viktor: DEFEND [%d,%d] saves=%d (was threat=%d, left=%d)\n",
           *movex, *movey, flist[mymove].saves, maxthreat, flist[mymove].post_threat);
    goto REMEMBER;
  }
  /* counter-attack: make our own threat even if a futile opp threat exists */
  if ((bestmake >= 0) && (maxmake >= 1) && (maxcap <= 0)) {
    mymove = bestmake;
    for (i = 0; i < flistlen; i++) {
      if (flist[i].makes_threat < maxmake) continue;
      if (flist[i].shape > flist[mymove].shape) mymove = i;
    }
    *movex = flist[mymove].x;
    *movey = flist[mymove].y;
    printf("ai_viktor: MAKE-THREAT [%d,%d] nextcap=%d\n", *movex, *movey, maxmake);
    goto REMEMBER;
  }

  /* no auto-TRAP short-circuit -- rare cage-arming left to deep search */

  if (flistlen > SEARCHCAP) {
    for (i = 0; i < flistlen; i++) {
      int j;
      for (j = i + 1; j < flistlen; j++) {
        int si = flist[i].capture * 1000 + flist[i].saves * 220
               + flist[i].makes_threat * 180 + flist[i].shape;
        int sj = flist[j].capture * 1000 + flist[j].saves * 220
               + flist[j].makes_threat * 180 + flist[j].shape;
        if (sj > si) {
          struct fieldlist tmp = flist[i];
          flist[i] = flist[j];
          flist[j] = tmp;
        }
      }
    }
    flistlen = SEARCHCAP;
  }

  interestingmoves_count = flistlen;
  for (i = 0; i < flistlen; i++)
    interestingmoves[i] = XYTOFIELD(flist[i].x, flist[i].y);

  /* ===== 4) iterative deepening + TT under time budget ===== */
  if (tt_ensure() != 0) {
    /* fall back to a single fixed-depth search without TT */
    search_abort = 0;
    search_deadline_ms = now_ms() + TIME_BUDGET_MS;
    search_depth(g, flist, flistlen, interestingmoves, interestingmoves_count, STEPLIMIT_MIN + 2);
    mymove = pick_root_move(flist, flistlen, g);
    *movex = flist[mymove].x;
    *movey = flist[mymove].y;
    printf("ai_viktor: [%d,%d] worst=%d best=%d (no-TT depth=%d n=%d)\n",
           *movex, *movey, flist[mymove].worst, flist[mymove].best,
           STEPLIMIT_MIN + 2, flistlen);
    goto REMEMBER;
  }

  /* Keep tt_gen across moves in the same game so the TT stays warm.
   * ai_viktor_reset() bumps it between games (scores are side-relative). */
  t0 = now_ms();
  search_deadline_ms = t0 + TIME_BUDGET_MS;
  search_abort = 0;
  mymove = 0;

  for (depth = STEPLIMIT_MIN; depth <= STEPLIMIT_MAX; depth++) {
    if (now_ms() >= search_deadline_ms) break;
    search_abort = 0;
    search_depth(g, flist, flistlen, interestingmoves, interestingmoves_count, depth);
    if (search_abort) {
      printf("ai_viktor: abort at depth %d (%lu ms)\n", depth, now_ms() - t0);
      break;
    }
    mymove = pick_root_move(flist, flistlen, g);
    completed_depth = depth;
    best_x = flist[mymove].x;
    best_y = flist[mymove].y;
    best_worst = flist[mymove].worst;
    best_best = flist[mymove].best;

    /* promote PV move to front for the next iteration */
    if (mymove > 0) {
      struct fieldlist tmp = flist[0];
      flist[0] = flist[mymove];
      flist[mymove] = tmp;
      interestingmoves[0] = XYTOFIELD(flist[0].x, flist[0].y);
      interestingmoves[mymove] = XYTOFIELD(flist[mymove].x, flist[mymove].y);
      mymove = 0;
    }

    printf("ai_viktor: depth %d -> [%d,%d] worst=%d best=%d (%lu ms)\n",
           depth, best_x, best_y, best_worst, best_best, now_ms() - t0);
  }

  if (completed_depth == 0) {
    /* nothing finished -- shape heuristic fallback */
    mymove = 0;
    for (i = 1; i < flistlen; i++)
      if (flist[i].shape > flist[mymove].shape) mymove = i;
    *movex = flist[mymove].x;
    *movey = flist[mymove].y;
    best_worst = flist[mymove].worst;
    best_best = flist[mymove].best;
  } else {
    *movex = best_x;
    *movey = best_y;
    /* recover flist index for printf extras */
    mymove = 0;
    for (i = 0; i < flistlen; i++) {
      if ((flist[i].x == best_x) && (flist[i].y == best_y)) { mymove = i; break; }
    }
  }

  printf("ai_viktor: [%d,%d] worst=%d best=%d cap=%d thr=%d mk=%d trap=%d shape=%d (depth=%d n=%d %lu ms)\n",
         *movex, *movey, best_worst, best_best,
         flist[mymove].capture, flist[mymove].threat, flist[mymove].makes_threat,
         flist[mymove].trap, flist[mymove].shape, completed_depth, flistlen, now_ms() - t0);

REMEMBER:
  lastmoves[lastmoveptr++] = XYTOFIELD(*movex, *movey);
  lastmoveptr %= LASTMOVES;

QUIT:
  kropki_freegame(g);
}
