/// <summary> /// Распознаёт открытый клиент игры по скриншоту. /// Анализирует состояние игры и делает ход при помощи мыши. /// </summary> /// <returns>Было ли состояние игры распознано как корректное</returns> public bool MakeMovement() { start = Stopwatch.StartNew(); var screen = GetScreenshot(); Logger.Log(screen); gameState = GetGameState(screen); if (gameState == null) { List<Point> closes = templatesStorage.GetPositions("Close", screen, true); if (closes.Count > 0) { Mouse.Move(closes[0]); Thread.Sleep(200); Mouse.LeftClick(); Thread.Sleep(200); Mouse.Move(0, 0); return true; } else { closes = templatesStorage.GetPositions("RandomPlayerGame", screen, true); if (closes.Count > 0) { Mouse.Move(closes[0]); Thread.Sleep(200); Mouse.LeftClick(); Thread.Sleep(200); Mouse.Move(0, 0); Thread.Sleep(5000); return true; } } return false; } var Ai = new ClassicAI(gameState); var movement = Ai.GetGoodMovement(3); MakeMovement(movement); Logger.Log(movement); return true; }
/// <summary> /// Получает все возможные состояния игры, получаемые из /// исходного состояния за один ход /// </summary> /// <param name="source">Исходное состояние</param> /// <returns>Словарь возможных состояний</returns> public Dictionary<Movement, ClassicGameState> GetAllMovements(ClassicGameState source) { Validate(source); var result = new Dictionary<Movement, ClassicGameState>(); if (source.GameOver()) return result; AddSwaps(result, source); if (result.Count == 0) return result; // некорректное состояние игры if (source.CurrentPlayer.Bombs > 0) AddExplosiveItemUsages(result, source, isBomb: true); if (source.CurrentPlayer.Dynamits > 0) AddExplosiveItemUsages(result, source, isBomb: false); AddEmptyMovement(result, source); return result; }
private ClassicGameState GetGameState(Bitmap screen) { gameEntities = GetGameEntities(screen); var inBoardEntities = gameEntities .Where(pair => EnumerateInBoardEntities() .Contains(pair.Value)) .ToDictionary(pair => pair.Key, pair => pair.Value); if (inBoardEntities.Count != 63) { return null; } leftTopBoardCell = GetLeftTopBoardCell(inBoardEntities); cellWidth = GetMaxDistance(inBoardEntities.Keys, p => p.X) / (BoardWidth - 1); cellHeight = GetMaxDistance(inBoardEntities.Keys, p => p.Y) / (BoardHeight - 1); var simpleBoard = GetSimpleBoard(inBoardEntities); var gameState = new ClassicGameState(simpleBoard); // Hp... var hps = templatesStorage.GetPositions("HpHeart", screen, true); if (hps.Count == 2) { var pos1 = hps[0]; int width = templatesStorage.GetTemplate("HpHeart").Width; double hpLeft = 0; for (int i = 1; i <= 227; ++i) { var toCheck = pos1; toCheck.X += width + i; if (i > 180) toCheck.Y += 7; var pixel = screen.GetPixel(toCheck.X, toCheck.Y); if (Math.Max(pixel.R, pixel.G) > 180) hpLeft++; } gameState.CurrentPlayer.Hp.Current = (int)Math.Round(hpLeft / 227 * gameState.CurrentPlayer.Hp.Max); if (gameState.CurrentPlayer.Hp.Current == 0) gameState.CurrentPlayer.Hp.Current = 1; pos1 = hps[1]; hpLeft = 0; for (int i = 1; i <= 227; ++i) { var toCheck = pos1; toCheck.X -= i; if (i > 180) toCheck.Y += 7; var pixel = screen.GetPixel(toCheck.X, toCheck.Y); if (Math.Max(pixel.R, pixel.G) > 180) hpLeft++; } gameState.AnotherPlayer.Hp.Current = (int)Math.Round(hpLeft / 227 * gameState.AnotherPlayer.Hp.Max); if (gameState.AnotherPlayer.Hp.Current == 0) gameState.AnotherPlayer.Hp.Current = 1; Console.WriteLine("Hps: {0} {1}", gameState.CurrentPlayer.Hp.Current, gameState.AnotherPlayer.Hp.Current); } /* gameState.CurrentPlayer.Hp.Current = 50; gameState.AnotherPlayer.Hp.Current = 50; */ foreach (var entity in EnumerateNotInBoardEntities()) { int countLeft = GetNotInBoardEntitiesCount(entity, true); int countRight = GetNotInBoardEntitiesCount(entity, false); var name = entity + "s"; if (entity == "TurtleShell") { if (countRight > 0) gameState.CurrentPlayer.Bombs -= 10; } else { gameState.CurrentPlayer.GetType() .GetProperty(entity + "s").SetValue(gameState.CurrentPlayer, countLeft); gameState.AnotherPlayer.GetType() .GetProperty(entity + "s").SetValue(gameState.AnotherPlayer, countRight); } } return gameState; }
private KeyValuePair<Movement, ClassicGameState> GetGoodMovementImpl(ClassicGameState state, int depth, ref int switches, CancellationToken ct) { // Все ходы CurrentPlayer. В этих состояниях CurrentPlayer == Enemy var map = state.Rules.GetAllMovements(state); if (map.Count == 0) return new KeyValuePair<Movement, ClassicGameState>(null, state); var result = new KeyValuePair<Movement,ClassicGameState>(); if (depth == maxRecursionDepth) { // Наихудший ход для Enemy GetBestState(map, -1, ref result); } else { var resultArray = new KeyValuePair<Movement, ClassicGameState>[map.Count]; // Тут храним количество смен игроков. Так как игра может закончится // внезапно, формула, использующая maxRecursionDepth не годится var resultSwitches = new int[map.Count]; ct.ThrowIfCancellationRequested(); if (depth == 1) // запускаем параллельный алгоритм { Parallel.ForEach(map, (pair, loopState, iteration) => { resultArray[iteration] = new KeyValuePair<Movement,ClassicGameState>( pair.Key, GetGoodMovementImpl(pair.Value, depth + 1, ref resultSwitches[iteration], ct).Value); }); } else { int index = 0; foreach (var pair in map) { resultArray[index] = new KeyValuePair<Movement,ClassicGameState>(pair.Key, GetGoodMovementImpl(pair.Value, depth + 1, ref resultSwitches[index], ct).Value); ++index; } } var max = double.NegativeInfinity; int minSwitches = int.MaxValue; for (int i = 0; i < resultArray.Length; ++i) { int mult = resultSwitches[i] % 2 == 0 ? -1 : 1; var currHpRatio = GetCurrentHpRatio(resultArray[i].Value) * mult; if (currHpRatio > max || currHpRatio > 10000 && resultSwitches[i] < minSwitches) { minSwitches = resultSwitches[i]; switches = resultSwitches[i]; max = mult * GetCurrentHpRatio(resultArray[i].Value); result = resultArray[i]; } } } ++switches; return result; }
/// <summary> /// Соотносит текущие жизни игроков в игре state /// </summary> double GetCurrentHpRatio(ClassicGameState state) { return GetHpRatio(state.CurrentPlayer, state.AnotherPlayer); }
public ClassicAI(ClassicGameState state) { this.state = state; }
private static void ChangeGameState(ClassicGameState state, double heal, double damage, int bombs, int dynamits, int diamonds) { state.CurrentPlayer.Hp.AddHealth((int)Math.Round(heal)); state.AnotherPlayer.Hp.AddHealth(-(int)Math.Round(damage)); state.CurrentPlayer.Bombs += bombs; state.CurrentPlayer.Dynamits += dynamits; }
private void Validate(ClassicGameState source) { if (source.Board.Width != _width || source.Board.Height != _height) throw new ArgumentException("Incorrect source size"); }
private ClassicGameState GetStateAfterExplosition(Point[] explositionShape, ClassicGameState state, bool isBomb) { double heal; double damage; int addBombs = isBomb ? -1 : 0; int addDynamits = isBomb ? 0 : -1; int addDiamonds; var result = state.Clone(); ExplodeOnce(explositionShape, result.Board, out heal, out damage, out addDiamonds); // TODO: вынести информацию о доп. уроне в класс Player damage += isBomb ? +2 : +3; addBombs = Math.Min(addBombs, 1); addDynamits = Math.Min(addDynamits, 1); ChangeGameState(result, heal, damage, addBombs, addDynamits, addDiamonds); ExplodeSecondariesOnly(result); return result; }
/// <summary> /// Получает новое состояние игры при помощи обмена элементов и последующими взрывами /// </summary> private ClassicGameState GetStateAfterSwap(Point first, Point second, ClassicGameState source) { Swap(first, second, source.Board); var result = Explode(source); Swap(first, second, source.Board); return result; }
/// <summary> /// Изменяет состояние игры, производя только вторичные взрывы в соответствии с правилами игры /// </summary> /// <param name="result"></param> private void ExplodeSecondariesOnly(ClassicGameState result) { double heal = 0; double damage = 0; int bombs = 0; int dynamits = 0; int diamonds = 0; while (IsDetonationRequired(result.Board)) { double newHeal; double newDamage; int newBombs; int newDynamits; ExplodeOnce(result.Board, out newHeal, out newDamage, out newBombs, out newDynamits, out diamonds); heal += newHeal * _secondaryBouncesMultiplier; damage += newDamage * _secondaryBouncesMultiplier; bombs += newBombs; dynamits += newDynamits; } ChangeGameState(result, heal, damage, bombs, dynamits, diamonds); result.SwapPlayers(); }
/// <summary> /// Получает новое состояние игры, путём взрывов элементов в соответствии с правилами. /// Производит все серии взрывов. Смена хода также произодится. /// </summary> /// <param name="source"></param> /// <returns></returns> private ClassicGameState Explode(ClassicGameState source) { double heal; double damage; int bombs; int dynamits; int diamonds; var result = source.Clone(); // Первая серия взрывов ExplodeOnce(result.Board, out heal, out damage, out bombs, out dynamits, out diamonds); ChangeGameState(result, heal, damage, bombs, dynamits, diamonds); ExplodeSecondariesOnly(result); return result; }
/// <summary> /// Добавляет в словарь ходов, все возможные ходы с обменом элементов /// </summary> /// <param name="dest"></param> /// <param name="state"></param> private void AddSwaps(Dictionary<Movement, ClassicGameState> dest, ClassicGameState state) { foreach (var movement in GetUncheckedSwaps(state.Board)) { var classicMovement = (ClassicMovement)movement; Point first = classicMovement.First; Point second = classicMovement.Second; if (IsSwapCorrect(first, second, state.Board)) { var newMovement = new ClassicMovement { First = first, Second = second }; dest.Add(movement, GetStateAfterSwap(first, second, state)); } } }
/// <summary> /// Добавляет в словарь ходов, все возможные ходы с использованием взрывчатки /// </summary> private void AddExplosiveItemUsages(Dictionary<Movement, ClassicGameState> dest, ClassicGameState state, bool isBomb) { var current = isBomb ? ClassicMovementKind.Bomb : ClassicMovementKind.Dynamit; var explosiveItemGetShape = isBomb ? (Func<Point, Point[]>)GetBombShape : GetDynamitShape; for (int x = 1; x <= state.Board.Width; ++x) for (int y = 1; y <= state.Board.Height; ++y) { Point center = new Point(x, y); var shape = explosiveItemGetShape(center); ClassicMovement movement = new ClassicMovement { First = center, Kind = current }; dest.Add(movement, GetStateAfterExplosition(shape, state, current == ClassicMovementKind.Bomb)); } }
private void AddEmptyMovement(Dictionary<Movement, ClassicGameState> result, ClassicGameState source) { var movement = new ClassicMovement { Kind = ClassicMovementKind.Empty }; var state = source.Clone(); state.SwapPlayers(); result.Add(movement, state); }