From 923d889a8fb61f8474473789e82075875bdffa39 Mon Sep 17 00:00:00 2001 From: "Valeriano A.R" Date: Sat, 2 Nov 2019 22:34:07 +0100 Subject: [PATCH] TetrisBot --- VAR.ScreenAutomation/Bots/TetrisBot.cs | 761 ++++++++++++++++++ .../VAR.ScreenAutomation.csproj | 1 + 2 files changed, 762 insertions(+) create mode 100644 VAR.ScreenAutomation/Bots/TetrisBot.cs diff --git a/VAR.ScreenAutomation/Bots/TetrisBot.cs b/VAR.ScreenAutomation/Bots/TetrisBot.cs new file mode 100644 index 0000000..c61cbde --- /dev/null +++ b/VAR.ScreenAutomation/Bots/TetrisBot.cs @@ -0,0 +1,761 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using VAR.ScreenAutomation.Interfaces; + +namespace VAR.ScreenAutomation.Bots +{ + public class TetrisBot : IAutomationBot + { + private TetrisGrid _grid = new TetrisGrid(); + + private List _currentShape = new List(); + private TetrisGrid _workGrid = new TetrisGrid(); + + private bool _shapeFound = false; + private int _shapeX; + private int _shapeY; + private double _bestEvaluation = double.MinValue; + private int _bestXOffset = 0; + private int _bestRotation = 0; + + public string Name => "Tetris"; + + public void Init(IOutputHandler output) + { + _currentShape.Add(new TetrisShape()); + _currentShape.Add(new TetrisShape()); + _currentShape.Add(new TetrisShape()); + _currentShape.Add(new TetrisShape()); + output.Clean(); + } + + public Bitmap Process(Bitmap bmpInput, IOutputHandler output) + { + // Initialize grid + _grid.SampleFromBitmap(bmpInput); + _grid.MarkGround(); + + // Identify current tetronino + _shapeFound = false; + if (_grid.SearchFirstCell(1, out _shapeX, out _shapeY)) + { + _currentShape[0].SampleFromGrid(_grid, _shapeX, _shapeY, 1); + _shapeFound = _currentShape[0].IsValid(); + for (int i = 1; i < 4; i++) + { + _currentShape[i].RotateCW(_currentShape[i - 1]); + } + } + + // Search best action + _bestEvaluation = double.MinValue; + _bestXOffset = 0; + _bestRotation = 0; + if (_shapeFound) + { + _workGrid.SampleOther(_grid, 2, 2); + if (_currentShape[0].Drop(_workGrid, _shapeX, _shapeY, 1)) + { + _bestXOffset = 0; + _bestRotation = 0; + _bestEvaluation = EvaluateWorkingGrid(); + } + + int offsetX = 1; + double newEvaluation; + + for (int rotation = 0; rotation < 4; rotation++) + { + // Check positive offset + offsetX = 1; + do + { + _workGrid.SampleOther(_grid, 2, 2); + if (_currentShape[rotation].Drop(_workGrid, _shapeX + offsetX, _shapeY, 1)) + { + newEvaluation = EvaluateWorkingGrid(); + if (newEvaluation > _bestEvaluation) + { + _bestEvaluation = newEvaluation; + _bestXOffset = offsetX; + _bestRotation = rotation; + } + } + else + { + break; + } + offsetX++; + } while (true); + + // Check negative offset + offsetX = -1; + do + { + _workGrid.SampleOther(_grid, 2, 2); + if (_currentShape[rotation].Drop(_workGrid, _shapeX + offsetX, _shapeY, 1)) + { + newEvaluation = EvaluateWorkingGrid(); + if (newEvaluation > _bestEvaluation) + { + _bestEvaluation = newEvaluation; + _bestXOffset = offsetX; + _bestRotation = rotation; + } + } + else + { + break; + } + offsetX--; + } while (true); + } + } + else + { + _workGrid.SampleOther(_grid, 2, 2); + } + + // DEBUG Show information + _workGrid.SampleOther(_grid, 2, 2); + if (_shapeFound) + { + _currentShape[_bestRotation].Drop(_workGrid, _shapeX + _bestXOffset, _shapeY, 1); + } + _workGrid.Draw(bmpInput); + + return bmpInput; + } + + private double EvaluateWorkingGrid() + { + //return _workGrid.Evaluate( + // aggregateHeightWeight: -0.510066, + // completeLinesWeight: 0.760666, + // holesWeight: -0.35663, + // bumpinessWeight: -0.184483, + // maxHeightWeight: 0); + return _workGrid.Evaluate( + aggregateHeightWeight: -0.6, + completeLinesWeight: 0.8, + holesWeight: -0.5, + bumpinessWeight: -0.2, + maxHeightWeight: -0.25); + } + + private bool _canShot = true; + private int _lastShotShapeY = 0; + private DateTime _lastShotDateTime; + + public string ResponseKeys() + { + if (_shapeFound == false) { return string.Empty; } + + if (_canShot == false && (_shapeY < _lastShotShapeY || _lastShotDateTime.AddMilliseconds(500) < DateTime.UtcNow)) + { + _canShot = true; + } + + if (_bestRotation == 0 && _bestXOffset == 0) + { + if (_canShot) + { + _canShot = false; + _lastShotShapeY = _shapeY; + _lastShotDateTime = DateTime.UtcNow; + return " "; + } + } + + if (_bestRotation != 0) { return "{UP}"; } + if (_bestXOffset < 0) { return "{LEFT}"; } + if (_bestXOffset > 0) { return "{RIGHT}"; } + + return string.Empty; + } + } + + public class TetrisShape + { + public const int ShapeSize = 4; + + private byte[][] _cells = null; + + private int _count = 0; + + public TetrisShape(byte[][] cells = null) + { + _cells = new byte[ShapeSize][]; + for (int y = 0; y < ShapeSize; y++) + { + _cells[y] = new byte[ShapeSize]; + } + + _count = 0; + if (cells != null) + { + for (int j = 0; j < ShapeSize; j++) + { + if (j >= cells.Length) { break; } + for (int i = 0; i < ShapeSize; i++) + { + if (i >= cells[j].Length) { break; } + _cells[j][i] = cells[j][i]; + if (_cells[j][i] != 0) { _count++; } + } + } + } + } + + public int GetCount() + { + return _count; + } + + private static List _defaultShapes = null; + + public bool IsValid() + { + if (_defaultShapes == null) + { + _defaultShapes = new List + { + // I + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, 1, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, }, + new byte[]{ 1, }, + new byte[]{ 1, }, + new byte[]{ 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, }, + new byte[]{ 1, }, + new byte[]{ 1, }, + new byte[]{ 1, }, + }), + + // J + new TetrisShape(new byte[][]{ + new byte[]{ 1, }, + new byte[]{ 1, 1, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, }, + new byte[]{ 1, }, + new byte[]{ 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, 1, }, + new byte[]{ 0, 0, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 0, 1, }, + new byte[]{ 0, 1, }, + new byte[]{ 1, 1, }, + }), + + // L + new TetrisShape(new byte[][]{ + new byte[]{ 0, 0, 1, }, + new byte[]{ 1, 1, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, }, + new byte[]{ 1, }, + new byte[]{ 1, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, 1, }, + new byte[]{ 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, }, + new byte[]{ 0, 1, }, + new byte[]{ 0, 1, }, + }), + + // S + new TetrisShape(new byte[][]{ + new byte[]{ 0, 1, 1, }, + new byte[]{ 1, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, }, + new byte[]{ 1, 1, }, + new byte[]{ 0, 1, }, + }), + + // T + new TetrisShape(new byte[][]{ + new byte[]{ 0, 1, }, + new byte[]{ 1, 1, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, }, + new byte[]{ 1, 1, }, + new byte[]{ 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, 1, }, + new byte[]{ 0, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 0, 1, }, + new byte[]{ 1, 1, }, + new byte[]{ 0, 1, }, + }), + + // Z + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, }, + new byte[]{ 0, 1, 1, }, + }), + new TetrisShape(new byte[][]{ + new byte[]{ 0, 1, }, + new byte[]{ 1, 1, }, + new byte[]{ 1, }, + }), + + // O + new TetrisShape(new byte[][]{ + new byte[]{ 1, 1, }, + new byte[]{ 1, 1, }, + }) + }; + } + + if (_count != 4) { return false; } + bool matchesAnyDefault = _defaultShapes.Any(ts => CompareShape(ts)); + return matchesAnyDefault; + } + + public void Copy(TetrisShape shape) + { + for (int j = 0; j < ShapeSize; j++) + { + for (int i = 0; i < ShapeSize; i++) + { + _cells[j][i] = shape._cells[j][i]; + _count = shape._count; + } + } + } + + public bool CompareShape(TetrisShape shape) + { + for (int j = 0; j < ShapeSize; j++) + { + for (int i = 0; i < ShapeSize; i++) + { + if (_cells[j][i] != shape._cells[j][i]) + { + return false; + } + } + } + return true; + } + + public void SampleFromGrid(TetrisGrid grid, int x, int y, byte value) + { + _count = 0; + for (int j = 0; j < ShapeSize; j++) + { + for (int i = 0; i < ShapeSize; i++) + { + if (grid.Get(x + i, y + j) == value) + { + _cells[j][i] = 1; + _count++; + } + else + { + _cells[j][i] = 0; + } + } + } + } + + public void PutOnGrid(TetrisGrid grid, int x, int y, byte value) + { + for (int j = 0; j < ShapeSize; j++) + { + for (int i = 0; i < ShapeSize; i++) + { + if (_cells[j][i] == 0) { continue; } + grid.Set(x + i, y + j, value); + } + } + } + + public bool CheckIntersection(TetrisGrid grid, int x, int y) + { + for (int j = 0; j < ShapeSize; j++) + { + for (int i = 0; i < ShapeSize; i++) + { + if (_cells[j][i] == 0) { continue; } + if (grid.Get(x + i, y + j) != 0) + { + return true; + } + } + } + return false; + } + + public bool Drop(TetrisGrid grid, int x, int y, byte value) + { + if (CheckIntersection(grid, x, y)) { return false; } + while (CheckIntersection(grid, x, y + 1) == false) + { + y++; + } + PutOnGrid(grid, x, y, value); + return true; + } + + public bool SearchFirstCell(byte value, out int x, out int y) + { + x = -1; + y = -1; + for (int j = 0; j < ShapeSize && y == -1; j++) + { + for (int i = 0; i < ShapeSize && y == -1; i++) + { + if (_cells[j][i] == value) + { + y = j; + } + } + } + if (y == -1) { return false; } + for (int i = 0; i < ShapeSize && x == -1; i++) + { + for (int j = 0; j < ShapeSize && x == -1; j++) + { + if (_cells[j][i] == value) + { + x = i; + } + } + } + if (x == -1) { return false; } + return true; + } + + public void Offset(int x, int y) + { + _count = 0; + for (int j = 0; j < ShapeSize; j++) + { + for (int i = 0; i < ShapeSize; i++) + { + if ((j + y) < ShapeSize && (i + x) < ShapeSize) + { + _cells[j][i] = _cells[j + y][i + x]; + if (_cells[j][i] != 0) + { + _count++; + } + } + else + { + _cells[j][i] = 0; + } + } + } + } + + public void RotateCW(TetrisShape shape) + { + for (int j = 0; j < ShapeSize; j++) + { + for (int i = 0; i < ShapeSize; i++) + { + _cells[i][ShapeSize - (j + 1)] = shape._cells[j][i]; + } + } + _count = shape._count; + + if (SearchFirstCell(1, out int offsetX, out int offsetY)) + { + Offset(offsetX, offsetY); + } + } + + public void Print(IOutputHandler output) + { + for (int y = 0; y < ShapeSize; y++) + { + StringBuilder sbLine = new StringBuilder(); + for (int x = 0; x < ShapeSize; x++) + { + if (_cells[y][x] == 0) + { + sbLine.Append(".."); + } + else + { + sbLine.Append("[]"); + } + } + output.AddLine(sbLine.ToString()); + } + } + } + + public class TetrisGrid + { + public const int GridWidth = 10; + public const int GridHeight = 24; + + private byte[][] _grid = null; + + private int[] _heights = null; + + public TetrisGrid() + { + _grid = new byte[GridHeight][]; + for (int y = 0; y < GridHeight; y++) + { + _grid[y] = new byte[GridWidth]; + } + _heights = new int[GridWidth]; + } + + public byte Get(int x, int y) + { + if (x >= GridWidth || x < 0) { return 0xFF; } + if (y >= GridHeight || y < 0) { return 0xFF; } + return _grid[y][x]; + } + + public void Set(int x, int y, byte value) + { + if (x >= GridWidth || x < 0) { return; } + if (y >= GridHeight || y < 0) { return; } + _grid[y][x] = value; + } + + public void SampleFromBitmap(Bitmap bmp) + { + float xStep = bmp.Width / GridWidth; + float yStep = bmp.Height / GridHeight; + for (int y = 0; y < GridHeight; y++) + { + for (int x = 0; x < GridWidth; x++) + { + Color color = bmp.GetPixel( + x: (int)((x * xStep) + (xStep / 2)), + y: (int)((y * yStep) + (yStep / 2))); + if (color.R > 128 || color.G > 128 || color.B > 128) + { + _grid[y][x] = 1; + } + else + { + _grid[y][x] = 0; + } + } + } + } + + public void MarkGround() + { + for (int i = 0; i < GridWidth; i++) + { + if (_grid[GridHeight - 1][i] == 1) + { + FloodFill(i, GridHeight - 1, 1, 2); + } + } + } + + public void FloodFill(int x, int y, byte expectedValue, byte fillValue) + { + if (x >= GridWidth || x < 0) { return; } + if (y >= GridHeight || y < 0) { return; } + if (_grid[y][x] != expectedValue) { return; } + _grid[y][x] = fillValue; + FloodFill(x - 1, y - 1, expectedValue, fillValue); + FloodFill(x - 1, y + 0, expectedValue, fillValue); + FloodFill(x - 1, y + 1, expectedValue, fillValue); + FloodFill(x + 0, y - 1, expectedValue, fillValue); + FloodFill(x + 0, y + 1, expectedValue, fillValue); + FloodFill(x + 1, y - 1, expectedValue, fillValue); + FloodFill(x + 1, y + 0, expectedValue, fillValue); + FloodFill(x + 1, y + 1, expectedValue, fillValue); + } + + public void SampleOther(TetrisGrid grid, byte value, byte setValue = 1) + { + for (int y = 0; y < GridHeight; y++) + { + for (int x = 0; x < GridWidth; x++) + { + if (grid._grid[y][x] == value) + { + _grid[y][x] = setValue; + } + else + { + _grid[y][x] = 0; + } + } + } + } + + public bool SearchFirstCell(byte value, out int x, out int y) + { + x = -1; + y = -1; + for (int j = 0; j < GridHeight && y == -1; j++) + { + for (int i = 0; i < GridWidth && y == -1; i++) + { + if (_grid[j][i] == value) + { + y = j; + } + } + } + if (y == -1) { return false; } + for (int i = 0; i < GridWidth && x == -1; i++) + { + for (int j = 0; j < GridHeight && x == -1; j++) + { + if (_grid[j][i] == value) + { + x = i; + } + } + } + if (x == -1) { return false; } + return true; + } + + public double Evaluate(double aggregateHeightWeight, double completeLinesWeight, double holesWeight, double bumpinessWeight, double maxHeightWeight) + { + // Calculte aggregate height + for (int i = 0; i < GridWidth; i++) + { + int j = 0; + while (j < GridHeight && _grid[j][i] == 0) { j++; } + _heights[i] = GridHeight - j; + } + double agregateHeight = _heights.Sum(); + + // Calculate complete lines + int completeLines = 0; + for (int y = 0; y < GridHeight; y++) + { + bool complete = true; + for (int x = 0; x < GridWidth; x++) + { + if (_grid[y][x] == 0) + { + complete = false; + break; + } + } + if (complete) { completeLines++; } + } + + // Calculate holes + int holes = 0; + for (int y = 1; y < GridHeight; y++) + { + for (int x = 0; x < GridWidth; x++) + { + if (_grid[y - 1][x] == 0 && _grid[y][x] != 0) + { + holes++; + } + } + } + + // Calculate bumpiness + int bumpines = 0; + for (int i = 1; i < GridWidth; i++) + { + bumpines += Math.Abs(_heights[i] - _heights[i - 1]); + } + + // Calculate max height + int maxHeight = _heights.Max(); + + // Evaluate formula + double evaluationValue = + aggregateHeightWeight * agregateHeight + + completeLinesWeight * completeLines + + holesWeight * holes + + bumpinessWeight * bumpines + + maxHeightWeight * maxHeight + + 0; + return evaluationValue; + } + + public void Print(IOutputHandler output) + { + for (int y = 0; y < GridHeight; y++) + { + StringBuilder sbLine = new StringBuilder(); + for (int x = 0; x < GridWidth; x++) + { + if (_grid[y][x] == 0) + { + sbLine.Append(".."); + } + else if (_grid[y][x] == 1) + { + sbLine.Append("$$"); + } + else + { + sbLine.Append("[]"); + } + } + output.AddLine(sbLine.ToString()); + } + } + + public void Draw(Bitmap bmp) + { + float xStep = bmp.Width / (float)GridWidth; + float yStep = bmp.Height / (float)GridHeight; + using (Graphics g = Graphics.FromImage(bmp)) + { + g.Clear(Color.Black); + for (int y = 0; y < GridHeight; y++) + { + for (int x = 0; x < GridWidth; x++) + { + Brush br; + if (_grid[y][x] == 0) + { + br = null; + } + else if (_grid[y][x] == 1) + { + br = Brushes.Blue; + } + else + { + br = Brushes.Red; + } + if (br == null) { continue; } + g.FillRectangle(br, xStep * x, yStep * y, xStep, yStep); + } + } + } + } + + } +} diff --git a/VAR.ScreenAutomation/VAR.ScreenAutomation.csproj b/VAR.ScreenAutomation/VAR.ScreenAutomation.csproj index faddfd1..91ef47e 100644 --- a/VAR.ScreenAutomation/VAR.ScreenAutomation.csproj +++ b/VAR.ScreenAutomation/VAR.ScreenAutomation.csproj @@ -50,6 +50,7 @@ +