Пароль на настройки готов. Супербот(гроза мобилок) готов
This commit is contained in:
parent
83c2f05a51
commit
34ca90431b
@ -11349,7 +11349,7 @@ Material:
|
||||
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
|
||||
- _EnvMatrixRotation: {r: 0, g: 0, b: 0, a: 0}
|
||||
- _FaceColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _GlowColor: {r: 1, g: 0.5024084, b: 0, a: 1}
|
||||
- _GlowColor: {r: 0, g: 0.5683422, b: 1, a: 1}
|
||||
- _MaskCoord: {r: 0, g: 0, b: 32767, a: 32767}
|
||||
- _OutlineColor: {r: 0.554717, g: 0.554717, b: 0.554717, a: 1}
|
||||
- _ReflectFaceColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@ MonoBehaviour:
|
||||
m_Name: 5mistressM3_hard
|
||||
m_EditorClassIdentifier:
|
||||
Description: "\u0444\u0438\u0448\u043A\u0430_\u0441\u043B\u043E\u0436\u043D\u044B\u0439"
|
||||
Depth: 8
|
||||
Depth: 9
|
||||
WinBonus: 1000000
|
||||
RowOfTwoBonus: 0
|
||||
RowOfThree: 0
|
||||
|
||||
174
Assets/Scripts/EnterPasswordPanel.cs
Normal file
174
Assets/Scripts/EnterPasswordPanel.cs
Normal file
@ -0,0 +1,174 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class EnterPasswordPanel : MonoBehaviour
|
||||
{
|
||||
int enteredSymbols = 0;
|
||||
int SymbolsCount = 6;
|
||||
|
||||
public AudioClip trueSound;
|
||||
public AudioClip falseSound;
|
||||
|
||||
public AnimationCurve fillCurve;
|
||||
|
||||
float clearTimer = 0;
|
||||
public Image ClearImage;
|
||||
|
||||
float exitTimer = 0;
|
||||
public Image ExitImage;
|
||||
|
||||
public string symbols = "";
|
||||
|
||||
public List<RectTransform> slots;
|
||||
public GameObject L1;
|
||||
public GameObject L2;
|
||||
public GameObject R1;
|
||||
public GameObject R2;
|
||||
public GameObject D1;
|
||||
public GameObject D2;
|
||||
private List<GameObject> spawnedObjs = new();
|
||||
|
||||
public bool truePassword = false;
|
||||
private string password;
|
||||
|
||||
List<ButtonPinMapping> availableButtons = new List<ButtonPinMapping>
|
||||
{
|
||||
ButtonPinMapping.left1,
|
||||
ButtonPinMapping.left2,
|
||||
ButtonPinMapping.right1,
|
||||
ButtonPinMapping.right2,
|
||||
ButtonPinMapping.down1,
|
||||
ButtonPinMapping.down2,
|
||||
};
|
||||
|
||||
public void Initialize(string password)
|
||||
{
|
||||
truePassword = false;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public void _Update()
|
||||
{
|
||||
//p2 очистить
|
||||
ClearProcess();
|
||||
ExitProcess();
|
||||
|
||||
//получить комбинацию по кнопке
|
||||
ButtonsProcess();
|
||||
}
|
||||
|
||||
void ClearProcess()
|
||||
{
|
||||
|
||||
if (In.GetButton(ButtonPinMapping.startTwoPlayers)) clearTimer += Time.deltaTime;
|
||||
else clearTimer -= Time.deltaTime;
|
||||
clearTimer = Mathf.Clamp(clearTimer, 0.0f, 2.0f);
|
||||
ProcessImageTimer(ClearImage, clearTimer, 2);
|
||||
|
||||
if (clearTimer == 2)
|
||||
{
|
||||
clearTimer = 0.0f;
|
||||
Clear();
|
||||
}
|
||||
|
||||
}
|
||||
private void Clear()
|
||||
{
|
||||
clearTimer = 0;
|
||||
exitTimer = 0;
|
||||
enteredSymbols = 0;
|
||||
symbols = string.Empty;
|
||||
|
||||
foreach (var go in spawnedObjs)
|
||||
{
|
||||
Destroy(go);
|
||||
}
|
||||
spawnedObjs.Clear();
|
||||
}
|
||||
|
||||
private void ExitProcess()
|
||||
{
|
||||
if (In.GetButton(ButtonPinMapping.startOnePlayer)) exitTimer += Time.deltaTime;
|
||||
else exitTimer -= Time.deltaTime;
|
||||
exitTimer = Mathf.Clamp(exitTimer, 0.0f, 2.0f);
|
||||
ProcessImageTimer(ExitImage, exitTimer, 2);
|
||||
|
||||
if (exitTimer == 2)
|
||||
{
|
||||
Clear();
|
||||
GameManager.instance.stateMachine.ChangeState(new MenuState());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ButtonsProcess()
|
||||
{
|
||||
if (enteredSymbols >= SymbolsCount) return;
|
||||
|
||||
foreach (ButtonPinMapping button in availableButtons)
|
||||
{
|
||||
if (In.GetButtonDown(button))
|
||||
{
|
||||
string combinationStr = GetStrCombination(button);
|
||||
symbols+=combinationStr;
|
||||
DisplaySymbol(combinationStr);
|
||||
enteredSymbols++;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(enteredSymbols == SymbolsCount)
|
||||
{
|
||||
if(symbols == password)
|
||||
{
|
||||
truePassword = true;
|
||||
SoundManager.instance.playClip(trueSound);
|
||||
}
|
||||
else
|
||||
{
|
||||
truePassword = false;
|
||||
SoundManager.instance.playClip(falseSound);
|
||||
}
|
||||
Clear();
|
||||
|
||||
//введи все символы надо проверка
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DisplaySymbol(string symbol)
|
||||
{
|
||||
GameObject prefab = symbol switch
|
||||
{
|
||||
"L1" => L1,
|
||||
"L2" => L2,
|
||||
"R1" => R1,
|
||||
"R2" => R2,
|
||||
"A1" => D1,
|
||||
"A2" => D2,
|
||||
};
|
||||
|
||||
GameObject spawned = Instantiate(prefab, slots[enteredSymbols]);
|
||||
spawnedObjs.Add(spawned);
|
||||
}
|
||||
|
||||
string GetStrCombination(ButtonPinMapping button)
|
||||
=> button switch
|
||||
{
|
||||
ButtonPinMapping.left1 => "L1",
|
||||
ButtonPinMapping.left2 => "L2",
|
||||
ButtonPinMapping.right1 => "R1",
|
||||
ButtonPinMapping.right2 => "R2",
|
||||
ButtonPinMapping.down1 => "A1",
|
||||
ButtonPinMapping.down2 => "A2",
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
||||
void ProcessImageTimer(Image image, float time, float maxtime)
|
||||
=> image.fillAmount = fillCurve.Evaluate(time / maxtime);
|
||||
}
|
||||
11
Assets/Scripts/EnterPasswordPanel.cs.meta
Normal file
11
Assets/Scripts/EnterPasswordPanel.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0e91130ffc40c34bbf557582d2ce828
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -67,11 +67,20 @@ public class BotThinkingState : IState
|
||||
|
||||
}
|
||||
|
||||
public bool displayed = false;
|
||||
|
||||
bool animationStarted = false;
|
||||
float startAnimTime;
|
||||
public void update()
|
||||
{
|
||||
//Алгоритм досчитал
|
||||
//подсчет времени
|
||||
if(calculatedColumn!= -1 && !displayed)
|
||||
{
|
||||
Debug.Log($"Думал: {Time.time - startTime}");
|
||||
displayed = true;
|
||||
}
|
||||
|
||||
|
||||
if (calculatedColumn != -1 && Time.time - startTime > timeToThink)
|
||||
{
|
||||
if (!animationStarted)
|
||||
@ -101,6 +110,30 @@ public class BotThinkingState : IState
|
||||
}
|
||||
void calc()
|
||||
{
|
||||
calculatedColumn = Solver.FindBestMove(GameManager.fieldModel.fieldArr, GameManager.instance.currentPlayer);
|
||||
//calculatedColumn = Solver.FindBestMove(GameManager.fieldModel.fieldArr, GameManager.instance.currentPlayer);
|
||||
|
||||
GameManager gm = GameManager.instance;
|
||||
|
||||
//Судя по счету мы находимся на последнем боте.
|
||||
bool OnBossByScore = gm.vsBotScore == gm.botsAmount - 1;
|
||||
|
||||
bool IsHardCommonDifficulty = SettingsManager.GetString("Difficulty", "Средняя") == "Сложная";
|
||||
bool IsUpperCashDifficulty = SettingsManager.GetString("BaseBotDifficulty", "Повышенная") == "Повышенная";
|
||||
|
||||
bool NeedSuperPreset = OnBossByScore && (IsHardCommonDifficulty || IsUpperCashDifficulty);
|
||||
|
||||
Debug.Log($"Использовать суперсложность: {NeedSuperPreset}\nСейчас босс: {OnBossByScore}\nСложная сложность: {IsHardCommonDifficulty}\nВключен режим заработка: {IsUpperCashDifficulty}\n");
|
||||
|
||||
|
||||
|
||||
if (NeedSuperPreset)
|
||||
{
|
||||
calculatedColumn = SolverV2.FindBestMove(GameManager.fieldModel.fieldArr, GameManager.instance.currentPlayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
calculatedColumn = Solver.FindBestMove(GameManager.fieldModel.fieldArr, GameManager.instance.currentPlayer);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,98 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
public class CheckSettingsPasswordState : IState
|
||||
{
|
||||
EnterPasswordPanel panel;
|
||||
public void enter()
|
||||
{
|
||||
Debug.Log("[ЗДЕСЬ БУДЕТ ПРОВЕРКА ПАРОЛЯ]");
|
||||
GameManager.instance.stateMachine.ChangeState(new SettingsState());
|
||||
panel = GameManager.instance.enterPasswordPanel;
|
||||
Debug.Log("[Проход в настройки]");
|
||||
|
||||
//Пароль не установлен.
|
||||
bool Pwd = SettingsManager.GetBool("PasswordOnSettings", false);
|
||||
if (!Pwd)
|
||||
{
|
||||
Debug.Log("[Пароля нет, заходим братья]");
|
||||
GameManager.instance.stateMachine.ChangeState(new SettingsState());
|
||||
return;
|
||||
}
|
||||
|
||||
//Пароль есть.
|
||||
//Получить пароль.
|
||||
string password = ExternalFilesGetter.GetSettingsPassword();
|
||||
//Если он не валидный, то сбросить настройку и пропустить.
|
||||
if (password == null)
|
||||
{
|
||||
Debug.Log("[Файла пароля нет. Сброс пароля. Заход в настройки]");
|
||||
disableCheck();
|
||||
|
||||
GameManager.instance.stateMachine.ChangeState(new SettingsState());
|
||||
return;
|
||||
}
|
||||
|
||||
if (PasswordValid(password))
|
||||
{
|
||||
// если валидный, то запускаем ввод пароля
|
||||
panel.gameObject.SetActive(true);
|
||||
panel.Initialize(password);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[пароль невалидный. Сброс пароля. Заход в настройки]");
|
||||
disableCheck();
|
||||
ExternalFilesGetter.SetPassword("");
|
||||
GameManager.instance.stateMachine.ChangeState(new SettingsState());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static void disableCheck()
|
||||
{
|
||||
Setting setting = SettingsManager.Container.TryGetSetting("PasswordOnSettings");
|
||||
setting.ChangeValue(-1);//ставим false
|
||||
SettingsManager.OnSettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void exit()
|
||||
{
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void update()
|
||||
{
|
||||
panel._Update();
|
||||
if (panel.truePassword)
|
||||
{
|
||||
GameManager.instance.stateMachine.ChangeState(new SettingsState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool PasswordValid(string password)
|
||||
{
|
||||
if(password.Length!=12) return false;
|
||||
List<string> combinations = SplitPasswordIntoPairs(password);
|
||||
List<string> validCombinations = new List<string> { "L1", "L2", "R1", "R2", "A1", "A2", "P1", "P2" };
|
||||
|
||||
foreach (string combination in combinations)
|
||||
{
|
||||
if (!validCombinations.Contains(combination)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
List<string> SplitPasswordIntoPairs(string password)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
for (int i = 0; i < password.Length; i += 2)
|
||||
{
|
||||
result.Add(password.Substring(i, 2));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -144,7 +144,7 @@ public class MenuState : IState
|
||||
//Заход в настройки
|
||||
if (ShouldGotosettings())
|
||||
{
|
||||
SoundManager.instance.playSwitch();
|
||||
SoundManager.instance.playChoose();
|
||||
//gm.stateMachine.ChangeState(new SettingsState());
|
||||
gm.stateMachine.ChangeState(new CheckSettingsPasswordState());
|
||||
return;
|
||||
|
||||
@ -425,6 +425,7 @@ public class GameManager : MonoBehaviour
|
||||
public MovesAndTimeCounter movesAndTimeCounter;
|
||||
|
||||
public GameObject setPasswordPanel;
|
||||
public EnterPasswordPanel enterPasswordPanel;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
|
||||
486
Assets/Scripts/SolverV2.cs
Normal file
486
Assets/Scripts/SolverV2.cs
Normal file
@ -0,0 +1,486 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Супер-сильный солвер 4-в-ряд на битбордах + Negamax(AlphaBeta) + TT + итеративное углубление.
|
||||
/// Возвращает индекс колонки 0..6.
|
||||
/// </summary>
|
||||
public static class SolverV2
|
||||
{
|
||||
// Поле 6x7 (стандарт Connect4)
|
||||
private const int WIDTH = 7;
|
||||
private const int HEIGHT = 6;
|
||||
private const int H1 = HEIGHT + 1; // 7: с "сентинел" битом на верх колонки
|
||||
private const int MAX_MOVES = WIDTH * HEIGHT; // 42
|
||||
|
||||
/// <summary>
|
||||
/// Можешь крутить глубину снаружи (чем выше — тем сильнее, но медленнее).
|
||||
/// Реально "очень высокие" глубины становятся возможны за счёт TT и битбордов.
|
||||
/// </summary>
|
||||
public static int MaxDepth = 21;
|
||||
|
||||
/// <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 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()
|
||||
{
|
||||
// Инициализация масок колонок
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Главный метод: верни индекс колонки 0..6, куда бросать фишку.
|
||||
/// </summary>
|
||||
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 3;
|
||||
}
|
||||
|
||||
// Камень текущего игрока
|
||||
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<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 <= 6) ? 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 ? 3 : 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 = 3;
|
||||
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 <= 6)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/SolverV2.cs.meta
Normal file
11
Assets/Scripts/SolverV2.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe0d77956814fa54285af02ad4bb0f7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because one or more lines are too long
BIN
Assets/В доступе отказано.mp3
Normal file
BIN
Assets/В доступе отказано.mp3
Normal file
Binary file not shown.
23
Assets/В доступе отказано.mp3.meta
Normal file
23
Assets/В доступе отказано.mp3.meta
Normal file
@ -0,0 +1,23 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48c70e631c1936044be943e214a1a37f
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 7
|
||||
defaultSettings:
|
||||
serializedVersion: 2
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
preloadAudioData: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Доступ разрешен.mp3
Normal file
BIN
Assets/Доступ разрешен.mp3
Normal file
Binary file not shown.
23
Assets/Доступ разрешен.mp3.meta
Normal file
23
Assets/Доступ разрешен.mp3.meta
Normal file
@ -0,0 +1,23 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a17192e2e481bd48bf553f772fdcdb4
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 7
|
||||
defaultSettings:
|
||||
serializedVersion: 2
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
preloadAudioData: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
x
Reference in New Issue
Block a user