502 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Numerics;
/// <summary>
/// Супер-сильный солвер 4-в-ряд на битбордах + Negamax(AlphaBeta) + TT + итеративное углубление.
/// Возвращает индекс колонки 0..6.
/// </summary>
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;
/// <summary>
/// Можешь крутить глубину снаружи (чем выше — тем сильнее, но медленнее).
/// Реально "очень высокие" глубины становятся возможны за счёт TT и битбордов.
/// </summary>
public static int MaxDepth = 20;
/// <summary>
/// Ограничение на размер TT (чтобы не раздувать память). При переполнении — чистим.
/// </summary>
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<ulong, TTEntry> TT = new Dictionary<ulong, TTEntry>(1 << 20);
static SolverV2()
{
WIDTH = Board.Instance.Columns;
HEIGHT = Board.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<int> { 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();
}
/// <summary>
/// Главный метод: верни индекс колонки 0..6, куда бросать фишку.
/// </summary>
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)
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 == 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<int> 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<int> 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<int> 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<ulong>(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();
}
}