using System; using System.Collections.Generic; using System.Numerics; /// /// Супер-сильный солвер 4-в-ряд на битбордах + Negamax(AlphaBeta) + TT + итеративное углубление. /// Возвращает индекс колонки 0..6. /// public static class SolverV2 { // Поле 6x7 (стандарт Connect4) private static int WIDTH = 7; private static 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() { WIDTH = GameManager.instance.Cols; HEIGHT = GameManager.instance.Rows; 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(); } /// /// Главный метод: верни индекс колонки 0..6, куда бросать фишку. /// public static int FindBestMove(CellState[,] board, CellState currentPlayer) { if (board == null) throw new ArgumentNullException(nameof(board)); if (board.GetLength(0) != HEIGHT || board.GetLength(1) != WIDTH) throw new ArgumentException($"Ожидается board размера [{HEIGHT},{WIDTH}]"); // Конвертируем в битборды 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 == CellState.Empty) continue; // В битбордах row=0 снизу. В массиве чаще row=0 сверху => инвертируем int br = (HEIGHT - 1) - r; // bottom=0 ulong bit = 1UL << (c * H1 + br); if (cs == CellState.Player1) p1 |= bit; else if (cs == CellState.Player2) p2 |= bit; } } ulong mask = p1 | p2; if (mask == 0) { // На пустом поле лучший ход — центр return WIDTH / 2; } // Камень текущего игрока ulong currentBits = (currentPlayer == CellState.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(); } }