492 lines
17 KiB
C#
492 lines
17 KiB
C#
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;
|
||
|
||
/// <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()
|
||
{
|
||
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();
|
||
}
|
||
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<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();
|
||
}
|
||
}
|