using System;
using System.Collections.Generic;
using System.Numerics;
public static class SolverV2
{
// Поле 6x7 (стандарт Connect4)
private const int WIDTH = 7;
private const int HEIGHT = 6;
private static int CenterCol => WIDTH / 2;
private static int H1 => HEIGHT + 1; //"сентинел" битом на верх колонки
private static int MAX_MOVES => WIDTH * HEIGHT;
///
/// Можешь крутить глубину снаружи (чем выше — тем сильнее, но медленнее).
/// Реально "очень высокие" глубины становятся возможны за счёт TT и битбордов.
///
public static int MaxDepth = 20;
///
/// Ограничение на размер TT (чтобы не раздувать память). При переполнении — чистим.
///
public static int TranspositionMaxEntries = 1_200_000;
// Порядок ходов: центр -> к краям (лучше и для игры, и для отсечений)
//private static readonly int[] MoveOrder = { 3, 2, 4, 1, 5, 0, 6 };
private static readonly int[] MoveOrder;
// Битовые маски по колонкам
private static readonly ulong[] BottomMask = new ulong[WIDTH];
private static readonly ulong[] ColumnMask = new ulong[WIDTH];
private static readonly ulong[] TopPlayableMask = new ulong[WIDTH]; // верхняя игровая клетка (6-я строка)
// Все выигрышные "четвёрки" (для быстрой эвристики)
private static readonly ulong[] WinningLineMasks;
// TT
private enum Bound : byte { Exact, Lower, Upper }
private struct TTEntry
{
public short Value; // -21..21 + эвристика
public byte Depth; // глубина, на которой посчитано
public Bound Bound;
public byte BestCol; // лучший ход (для ordering)
}
private static readonly Dictionary TT = new Dictionary(1 << 20);
static SolverV2()
{
MoveOrder = GenerateMoveOrder();
// Инициализация масок колонок
for (int c = 0; c < WIDTH; c++)
{
BottomMask[c] = 1UL << (c * H1);
ColumnMask[c] = ((1UL << HEIGHT) - 1UL) << (c * H1); // 6 бит игрового столбца
TopPlayableMask[c] = 1UL << (c * H1 + (HEIGHT - 1)); // верхняя игровая клетка (row=5)
}
// Генерация всех выигрышных линий (четвёрок)
WinningLineMasks = BuildWinningMasks();
}
private static int[] GenerateMoveOrder()
{
int center = WIDTH / 2;
var order = new List { center };
for (int i = 1; i <= center; i++)
{
if (center - i >= 0) order.Add(center - i);
if (center + i < WIDTH) order.Add(center + i);
}
return order.ToArray();
}
public static int FindBestMove(Team[,] board, Team currentPlayer)
{
if (board == null) throw new ArgumentNullException(nameof(board));
if (board.GetLength(0) != HEIGHT || board.GetLength(1) != WIDTH)
return Solver.FindBestMove(board, currentPlayer);
// Конвертируем в битборды
ulong p1 = 0, p2 = 0;
for (int r = 0; r < HEIGHT; r++)
{
for (int c = 0; c < WIDTH; c++)
{
var cs = board[r, c];
if (cs == Team.None) continue;
// В битбордах row=0 снизу. В массиве чаще row=0 сверху => инвертируем
int br = (HEIGHT - 1) - r; // bottom=0
ulong bit = 1UL << (c * H1 + br);
if (cs == Team.Player1) p1 |= bit;
else if (cs == Team.Player2) p2 |= bit;
}
}
ulong mask = p1 | p2;
if (mask == 0)
{
// На пустом поле лучший ход — центр
return WIDTH / 2;
}
// Камень текущего игрока
ulong currentBits = (currentPlayer == Team.Player1) ? p1 : p2;
var pos = new Position(currentBits, mask);
// Если TT распух — чистим (простая стратегия)
if (TT.Count > TranspositionMaxEntries) TT.Clear();
// На всякий: если центр забит и вообще нет ходов
if (pos.PossibleMovesMask() == 0) return FirstLegalColumn(pos);
// Итеративное углубление
int bestCol = FirstLegalColumn(pos);
int maxDepth = Math.Min(MaxDepth, pos.RemainingMoves());
// aspiration: можно добавить, но и так хорошо
for (int depth = 1; depth <= maxDepth; depth++)
{
int alpha = -INF;
int beta = INF;
int localBestCol = bestCol;
int localBestScore = -INF;
// ordering: если в TT есть лучший ход для корня — попробуем первым
int ttBest = TryGetTTBestMove(pos, out int ttCol) ? ttCol : -1;
// Ходы (сначала ttBest, потом центр-ориентированный порядок)
Span cols = stackalloc int[WIDTH];
int n = BuildOrderedMoves(pos, cols, ttBest);
for (int i = 0; i < n; i++)
{
int col = cols[i];
ulong move = pos.MoveMask(col);
if (move == 0) continue;
var child = pos;
child.Play(move);
int score = -Negamax(child, depth - 1, -beta, -alpha);
if (score > localBestScore)
{
localBestScore = score;
localBestCol = col;
}
if (score > alpha) alpha = score;
if (alpha >= beta) break; // отсечение
}
bestCol = localBestCol;
// Если нашли гарантированный выигрыш — можно заканчивать раньше
if (localBestScore >= WinScore(pos))
break;
}
return bestCol;
}
// -------------------- Core search --------------------
private const int INF = 10_000;
// Оценка победы "чем быстрее — тем лучше"
private static int WinScore(Position p)
{
// Максимальный выигрыш в терминах "сколько ходов до конца"
// В классических решателях Connect4 используют диапазон [-21..21]
return (p.RemainingMoves() + 1) / 2;
}
private static int LossScore(Position p) => -WinScore(p);
private static int Negamax(Position p, int depth, int alpha, int beta)
{
// Если соперник уже выиграл предыдущим ходом, мы в проигрыше
if (HasFour(p.OpponentBits()))
return LossScore(p);
ulong possible = p.PossibleMovesMask();
if (possible == 0)
return 0; // ничья
// Быстрая проверка: есть ли выигрышный ход прямо сейчас
if (p.HasImmediateWinningMove())
return WinScore(p);
if (depth <= 0)
return Evaluate(p);
// TT probe
ulong key = p.Key();
if (TT.TryGetValue(key, out TTEntry entry) && entry.Depth >= depth)
{
int v = entry.Value;
switch (entry.Bound)
{
case Bound.Exact: return v;
case Bound.Lower: alpha = Math.Max(alpha, v); break;
case Bound.Upper: beta = Math.Min(beta, v); break;
}
if (alpha >= beta) return v;
}
int originalAlpha = alpha;
int bestCol = 255;
int bestValue = -INF;
// ordering: TT best first
int ttBest = (entry.BestCol <= WIDTH - 1) ? entry.BestCol : -1;
Span cols = stackalloc int[WIDTH];
int n = BuildOrderedMoves(p, cols, ttBest);
for (int i = 0; i < n; i++)
{
int col = cols[i];
ulong move = p.MoveMask(col);
if (move == 0) continue;
var child = p;
child.Play(move);
int score = -Negamax(child, depth - 1, -beta, -alpha);
if (score > bestValue)
{
bestValue = score;
bestCol = col;
}
if (score > alpha) alpha = score;
if (alpha >= beta) break;
}
// Store TT
TTEntry store = new TTEntry
{
Value = (short)bestValue,
Depth = (byte)depth,
BestCol = (byte)(bestCol == 255 ? CenterCol : bestCol),
Bound = Bound.Exact
};
if (bestValue <= originalAlpha) store.Bound = Bound.Upper;
else if (bestValue >= beta) store.Bound = Bound.Lower;
TT[key] = store;
return bestValue;
}
// -------------------- Evaluation --------------------
private static int Evaluate(Position p)
{
// Быстрая эвристика: сколько "открытых" выигрышных линий у текущего игрока минус у оппонента,
// плюс небольшой приоритет центра (чтобы игра была сильнее на малой глубине).
ulong cur = p.CurrentBits;
ulong opp = p.OpponentBits();
int curOpen = 0;
int oppOpen = 0;
for (int i = 0; i < WinningLineMasks.Length; i++)
{
ulong m = WinningLineMasks[i];
// если в линии есть камни оппонента — линия не годится для текущего
if ((opp & m) == 0) curOpen++;
// если в линии есть камни текущего — линия не годится для оппонента
if ((cur & m) == 0) oppOpen++;
}
int centerBonus = CenterControlBonus(cur) - CenterControlBonus(opp);
// Масштабируем, чтобы не перебить "мат" значения (±21)
// 1) open-lines дают "стратегию", 2) центр слегка помогает
int score = (curOpen - oppOpen) + 2 * centerBonus;
// Сжимаем в разумный диапазон
return Math.Clamp(score / 4, -20, 20);
}
private static int CenterControlBonus(ulong bits)
{
// колонка 3 (центр)
int c = CenterCol;
ulong col = ((1UL << HEIGHT) - 1UL) << (c * H1);
int count = PopCount(bits & col);
return count; // чем больше в центре, тем лучше
}
// -------------------- Bitboard Position --------------------
private struct Position
{
// Биты текущего игрока (того, кто ходит сейчас)
public ulong CurrentBits;
// Биты всех занятых клеток
public ulong Mask;
public Position(ulong current, ulong mask)
{
CurrentBits = current;
Mask = mask;
}
public int RemainingMoves() => MAX_MOVES - PopCount(Mask);
public ulong OpponentBits() => Mask ^ CurrentBits;
public ulong Key()
{
// Достаточно устойчивый ключ (не крипто), быстрый
return CurrentBits ^ (Mask << 1);
}
public ulong PossibleMovesMask()
{
// Все возможные ходы: для каждой колонки — бит "самого нижнего пустого слота"
ulong moves = 0;
for (int c = 0; c < WIDTH; c++)
moves |= MoveMask(c);
return moves;
}
public ulong MoveMask(int col)
{
// move = (mask + bottomMask) & columnMask
// даёт бит первой пустой ячейки в колонке, либо 0 если колонка полная
ulong m = (Mask + BottomMask[col]) & ColumnMask[col];
return m;
}
public void Play(ulong move)
{
// классическая схема:
// 1) после хода "текущий игрок" меняется => CurrentBits ^= Mask
// 2) добавляем камень => Mask |= move
CurrentBits ^= Mask;
Mask |= move;
}
public bool HasImmediateWinningMove()
{
// проверяем каждый ход: приводит ли к 4-в-ряд у текущего игрока
for (int i = 0; i < WIDTH; i++)
{
int c = MoveOrder[i];
ulong mv = MoveMask(c);
if (mv == 0) continue;
ulong after = CurrentBits | mv;
if (HasFour(after)) return true;
}
return false;
}
}
// -------------------- Helpers --------------------
private static int BuildOrderedMoves(Position p, Span cols, int ttBest)
{
int n = 0;
// 1) TT best первым (если легален)
if (ttBest >= 0 && ttBest < WIDTH && p.MoveMask(ttBest) != 0)
cols[n++] = ttBest;
// 2) остальное — центр->края, без дубликатов
for (int i = 0; i < MoveOrder.Length; i++)
{
int c = MoveOrder[i];
if (c == ttBest) continue;
if (p.MoveMask(c) != 0)
cols[n++] = c;
}
return n;
}
private static bool TryGetTTBestMove(Position p, out int bestCol)
{
if (TT.TryGetValue(p.Key(), out var e) && e.BestCol <= WIDTH - 1)
{
bestCol = e.BestCol;
return true;
}
bestCol = -1;
return false;
}
private static int FirstLegalColumn(Position p)
{
for (int i = 0; i < MoveOrder.Length; i++)
{
int c = MoveOrder[i];
if (p.MoveMask(c) != 0) return c;
}
return 0;
}
private static bool HasFour(ulong b)
{
// Вертикаль (shift 1)
ulong m = b & (b >> 1);
if ((m & (m >> 2)) != 0) return true;
// Горизонталь (shift H1)
m = b & (b >> H1);
if ((m & (m >> (2 * H1))) != 0) return true;
// Диагональ /
m = b & (b >> (H1 - 1));
if ((m & (m >> (2 * (H1 - 1)))) != 0) return true;
// Диагональ \
m = b & (b >> (H1 + 1));
if ((m & (m >> (2 * (H1 + 1)))) != 0) return true;
return false;
}
private static int PopCount(ulong x)
{
#if NET5_0_OR_GREATER
return (int)BitOperations.PopCount(x);
#else
// fallback
int c = 0;
while (x != 0) { x &= (x - 1); c++; }
return c;
#endif
}
private static ulong[] BuildWinningMasks()
{
var list = new List(100);
// Перебор всех 4-в-ряд масок в нашей бит-разметке
for (int c = 0; c < WIDTH; c++)
{
for (int r = 0; r < HEIGHT; r++)
{
// стартовая клетка: (c,r), где r=0 снизу
int idx = c * H1 + r;
ulong bit = 1UL << idx;
// вертикаль (r..r+3)
if (r + 3 < HEIGHT)
{
ulong m = bit | (bit << 1) | (bit << 2) | (bit << 3);
list.Add(m);
}
// горизонталь (c..c+3)
if (c + 3 < WIDTH)
{
ulong m = bit | (bit << H1) | (bit << (2 * H1)) | (bit << (3 * H1));
list.Add(m);
}
// диагональ \ (c..c+3, r..r+3)
if (c + 3 < WIDTH && r + 3 < HEIGHT)
{
int s = H1 + 1;
ulong m = bit | (bit << s) | (bit << (2 * s)) | (bit << (3 * s));
list.Add(m);
}
// диагональ / (c..c+3, r..r-3) => тут старт должен быть достаточно высоким
if (c + 3 < WIDTH && r - 3 >= 0)
{
int s = H1 - 1;
ulong m = bit | (bit << s) | (bit << (2 * s)) | (bit << (3 * s));
list.Add(m);
}
}
}
return list.ToArray();
}
}