using System; using System.Collections.Generic; using System.Linq; int sameCommandDelay = 75; int verifyTimeout = 250; int downDelay = 50; int landingTimeout = 400; int botPosX = 18; int botPosY = 28; int waitAreaMinX = 5; int waitAreaMaxX = 8; int waitAreaMinY = 19; int waitAreaMaxY = 22; int walkToX = 8; int walkToY = 23; const int FIELD_MIN_X = 11; const int FIELD_MAX_X = 20; const int FIELD_MIN_Y = 3; const int FIELD_MAX_Y = 22; const int SPAWN_MIN_X = 14; const int SPAWN_MAX_X = 17; const int SPAWN_MIN_Y = 3; const int SPAWN_MAX_Y = 5; const int CTRL_LEFT = 256117487; const int CTRL_DOWN = 256117486; const int CTRL_RIGHT = 256117485; const int CTRL_ROTATE = 256117484; DateTime lastLeftCmd = DateTime.MinValue; DateTime lastRightCmd = DateTime.MinValue; DateTime lastRotateCmd = DateTime.MinValue; DateTime lastDownCmd = DateTime.MinValue; DateTime lastMoveDetected = DateTime.MinValue; DateTime spawnTime = DateTime.MinValue; int pendingLeftFromX = -1; int pendingRightFromX = -1; int pendingRotateFromRot = -1; int targetX = -1; int targetRot = -1; int currentX = -1; int currentRot = -1; char currentPiece = '?'; bool pieceActive = false; bool positioned = false; HashSet pieceIds = new HashSet(); public struct Point : IEquatable { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } public bool Equals(Point other) => X == other.X && Y == other.Y; public override bool Equals(object obj) => obj is Point other && Equals(other); public override int GetHashCode() => HashCode.Combine(X, Y); } var shapes = new Dictionary { ["I0"] = new[] {0,0,1,0,2,0,3,0}, ["I1"] = new[] {0,0,0,1,0,2,0,3}, ["O0"] = new[] {0,0,1,0,0,1,1,1}, ["T0"] = new[] {0,0,1,0,2,0,1,1}, ["T1"] = new[] {1,0,0,1,1,1,1,2}, ["T2"] = new[] {1,0,0,1,1,1,2,1}, ["T3"] = new[] {0,0,0,1,1,1,0,2}, ["S0"] = new[] {1,0,2,0,0,1,1,1}, ["S1"] = new[] {0,0,0,1,1,1,1,2}, ["Z0"] = new[] {0,0,1,0,1,1,2,1}, ["Z1"] = new[] {1,0,0,1,1,1,0,2}, ["L0"] = new[] {0,0,0,1,0,2,1,2}, ["L1"] = new[] {0,0,1,0,2,0,0,1}, ["L2"] = new[] {0,0,1,0,1,1,1,2}, ["L3"] = new[] {2,0,0,1,1,1,2,1}, ["J0"] = new[] {1,0,1,1,0,2,1,2}, ["J1"] = new[] {0,0,0,1,1,1,2,1}, ["J2"] = new[] {0,0,1,0,0,1,0,2}, ["J3"] = new[] {0,0,1,0,2,0,2,1} }; HashSet ignoredItems = new HashSet { "Large Block 18", "Großer Bauklotz 18", "Large Block 13", "Großer Bauklotz 13" }; bool IsBlockItem(string name) { if (string.IsNullOrEmpty(name)) return false; if (ignoredItems.Contains(name)) return false; return name.StartsWith("Großer Bauklotz") || name.StartsWith("Large Block"); } bool IsInField(int x, int y) => x >= FIELD_MIN_X && x <= FIELD_MAX_X && y >= FIELD_MIN_Y && y <= FIELD_MAX_Y; bool IsInSpawn(int x, int y) => x >= SPAWN_MIN_X && x <= SPAWN_MAX_X && y >= SPAWN_MIN_Y && y <= SPAWN_MAX_Y; void SendLeft() { pendingLeftFromX = currentX; Send(Out["ClickFurni"], CTRL_LEFT, 0); lastLeftCmd = DateTime.Now; } void SendRight() { pendingRightFromX = currentX; Send(Out["ClickFurni"], CTRL_RIGHT, 0); lastRightCmd = DateTime.Now; } void SendRotate() { pendingRotateFromRot = currentRot; Send(Out["ClickFurni"], CTRL_ROTATE, 0); lastRotateCmd = DateTime.Now; } void SendDown() { Send(Out["ClickFurni"], CTRL_DOWN, 0); lastDownCmd = DateTime.Now; } bool CanLeft() => (DateTime.Now - lastLeftCmd).TotalMilliseconds >= sameCommandDelay; bool CanRight() => (DateTime.Now - lastRightCmd).TotalMilliseconds >= sameCommandDelay; bool CanRotate() => (DateTime.Now - lastRotateCmd).TotalMilliseconds >= sameCommandDelay; bool CanDown() => (DateTime.Now - lastDownCmd).TotalMilliseconds >= downDelay; string IdentifyShape(List positions) { if (positions.Count != 4) return "?"; int minX = positions.Min(p => p.X); int minY = positions.Min(p => p.Y); var norm = positions.Select(p => new Point(p.X - minX, p.Y - minY)).OrderBy(p => p.Y).ThenBy(p => p.X).ToList(); foreach (var kvp in shapes) { var pts = new List(); for (int i = 0; i < 8; i += 2) pts.Add(new Point(kvp.Value[i], kvp.Value[i+1])); pts = pts.OrderBy(p => p.Y).ThenBy(p => p.X).ToList(); bool match = true; for (int i = 0; i < 4; i++) if (!norm[i].Equals(pts[i])) { match = false; break; } if (match) return kvp.Key; } return "?"; } char GetPiece(string s) => s != "?" && s.Length >= 1 ? s[0] : '?'; int GetRot(string s) => s != "?" && s.Length >= 2 ? int.Parse(s.Substring(1)) : -1; List GetShapePoints(char piece, int rot) { string key = $"{piece}{rot}"; if (!shapes.ContainsKey(key)) return new List(); var def = shapes[key]; var pts = new List(); for (int i = 0; i < 8; i += 2) pts.Add(new Point(def[i], def[i+1])); return pts; } int GetMaxRotations(char piece) { if (piece == 'O') return 1; if (piece == 'I' || piece == 'S' || piece == 'Z') return 2; return 4; } HashSet GetFieldBlocks() { var result = new HashSet(); foreach (var item in FloorItems) { if (item == null) continue; if (!IsBlockItem(item.GetName())) continue; int x = item.Location.X; int y = item.Location.Y; if (IsInField(x, y) && !pieceIds.Contains((int)item.Id)) result.Add(new Point(x, y)); } return result; } int SimulateDrop(char piece, int rot, int startX, HashSet field) { var shape = GetShapePoints(piece, rot); if (shape.Count != 4) return -1; for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y + 1; y++) { foreach (var p in shape) { int px = startX + p.X; int py = y + p.Y; if (py > FIELD_MAX_Y || field.Contains(new Point(px, py))) return y - 1; } } return FIELD_MAX_Y; } int CountHoles(HashSet field) { int holes = 0; for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) { bool blockFound = false; for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) { if (field.Contains(new Point(x, y))) blockFound = true; else if (blockFound) holes++; } } return holes; } int CountHoleDepth(HashSet field) { int depth = 0; for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) { int above = 0; for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) { if (field.Contains(new Point(x, y))) above++; else if (above > 0) depth += above; } } return depth; } int CountRowsWithHoles(HashSet field) { int count = 0; for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) { for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) { if (!field.Contains(new Point(x, y))) { for (int yy = FIELD_MIN_Y; yy < y; yy++) { if (field.Contains(new Point(x, yy))) { count++; goto nextRow; } } } } nextRow:; } return count; } int CountRowTransitions(HashSet field) { int t = 0; for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) { bool last = true; for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) { bool cur = field.Contains(new Point(x, y)); if (cur != last) t++; last = cur; } if (!last) t++; } return t; } int CountColTransitions(HashSet field) { int t = 0; for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) { bool last = true; for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) { bool cur = field.Contains(new Point(x, y)); if (cur != last) t++; last = cur; } if (!last) t++; } return t; } int CountWellSums(HashSet field) { int wellSum = 0; for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) { for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) { if (field.Contains(new Point(x, y))) continue; bool lb = (x == FIELD_MIN_X) || field.Contains(new Point(x - 1, y)); bool rb = (x == FIELD_MAX_X) || field.Contains(new Point(x + 1, y)); if (lb && rb) { int d = 1; for (int yy = y + 1; yy <= FIELD_MAX_Y; yy++) { if (field.Contains(new Point(x, yy))) break; bool lb2 = (x == FIELD_MIN_X) || field.Contains(new Point(x - 1, yy)); bool rb2 = (x == FIELD_MAX_X) || field.Contains(new Point(x + 1, yy)); if (lb2 && rb2) d++; else break; } wellSum += (d * (d + 1)) / 2; } } } return wellSum; } int[] GetHeights(HashSet field) { int[] h = new int[10]; for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) { for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) { if (field.Contains(new Point(x, y))) { h[x - FIELD_MIN_X] = FIELD_MAX_Y - y + 1; break; } } } return h; } double ScorePlacement(char piece, int rot, int x, HashSet field) { var shape = GetShapePoints(piece, rot); if (shape.Count != 4) return double.MinValue; int landY = SimulateDrop(piece, rot, x, field); if (landY < FIELD_MIN_Y) return double.MinValue; foreach (var p in shape) { int px = x + p.X; int py = landY + p.Y; if (px < FIELD_MIN_X || px > FIELD_MAX_X || py > FIELD_MAX_Y) return double.MinValue; } var newField = new HashSet(field); var piecePoints = new HashSet(); foreach (var p in shape) { var pt = new Point(x + p.X, landY + p.Y); newField.Add(pt); piecePoints.Add(pt); } int linesCleared = 0; int pieceCellsCleared = 0; for (int row = FIELD_MIN_Y; row <= FIELD_MAX_Y; row++) { bool full = true; for (int col = FIELD_MIN_X; col <= FIELD_MAX_X; col++) if (!newField.Contains(new Point(col, row))) { full = false; break; } if (full) { linesCleared++; for (int col = FIELD_MIN_X; col <= FIELD_MAX_X; col++) { var pt = new Point(col, row); if (piecePoints.Contains(pt)) pieceCellsCleared++; newField.Remove(pt); } } } if (linesCleared > 0) { var dropped = new HashSet(); for (int row = FIELD_MAX_Y; row >= FIELD_MIN_Y; row--) { for (int col = FIELD_MIN_X; col <= FIELD_MAX_X; col++) { var pt = new Point(col, row); if (newField.Contains(pt)) { newField.Remove(pt); int newRow = row; while (newRow < FIELD_MAX_Y && !dropped.Contains(new Point(col, newRow + 1))) newRow++; dropped.Add(new Point(col, newRow)); } } } newField = dropped; } int pieceH = shape.Max(p => p.Y) + 1; double landingHeight = (FIELD_MAX_Y - landY + 1) + (pieceH - 1) / 2.0; double erodedPieceCells = linesCleared * pieceCellsCleared; int rowTrans = CountRowTransitions(newField); int colTrans = CountColTransitions(newField); int holes = CountHoles(newField); int wellSums = CountWellSums(newField); int holeDepth = CountHoleDepth(newField); int rowsWithHoles = CountRowsWithHoles(newField); double score = 0; score += -12.63 * landingHeight; score += 6.60 * erodedPieceCells; score += -9.22 * rowTrans; score += -19.77 * colTrans; score += -13.08 * holes; score += -10.49 * wellSums; score += -1.61 * holeDepth; score += -24.04 * rowsWithHoles; var heights = GetHeights(newField); int maxH = heights.Max(); if (maxH >= 12) { score += linesCleared * 5000; score -= maxH * 50; } return score; } (int, int) FindBestPlacement(char piece, HashSet field) { int bestX = 15, bestRot = 0; double bestScore = double.MinValue; int maxRot = GetMaxRotations(piece); for (int rot = 0; rot < maxRot; rot++) { var shape = GetShapePoints(piece, rot); if (shape.Count != 4) continue; int w = shape.Max(p => p.X) + 1; for (int px = FIELD_MIN_X; px <= FIELD_MAX_X - w + 1; px++) { double s = ScorePlacement(piece, rot, px, field); if (s > bestScore) { bestScore = s; bestX = px; bestRot = rot; } } } return (bestX, bestRot); } (List positions, int minX, int minY, string shape, char piece, int rot) GetPieceState() { var positions = new List(); foreach (var item in FloorItems) { if (item == null) continue; if (!pieceIds.Contains((int)item.Id)) continue; positions.Add(new Point(item.Location.X, item.Location.Y)); } if (positions.Count != 4) return (positions, -1, -1, "?", '?', -1); int minX = positions.Min(p => p.X); int minY = positions.Min(p => p.Y); string shape = IdentifyShape(positions); char piece = GetPiece(shape); int rot = GetRot(shape); return (positions, minX, minY, shape, piece, rot); } OnIntercept(In["ObjectUpdate"], e => { var p = e.Packet; int id = p.ReadInt(); if (pieceIds.Contains(id)) lastMoveDetected = DateTime.Now; }); Log("TETRIS BOT"); Log($"Play position: ({botPosX},{botPosY})"); while (Run) { Delay(1); if (Self == null) continue; int myX = Self.Location.X; int myY = Self.Location.Y; if (myX >= waitAreaMinX && myX <= waitAreaMaxX && myY >= waitAreaMinY && myY <= waitAreaMaxY) { Move(walkToX, walkToY); Delay(500); continue; } if (myX != botPosX || myY != botPosY) { pieceIds.Clear(); pieceActive = false; currentPiece = '?'; targetX = -1; targetRot = -1; positioned = false; Delay(100); continue; } if (!pieceActive) { var spawnBlocks = new List<(int id, int x, int y)>(); foreach (var item in FloorItems) { if (item == null) continue; if (!IsBlockItem(item.GetName())) continue; int x = item.Location.X; int y = item.Location.Y; if (IsInSpawn(x, y)) spawnBlocks.Add(((int)item.Id, x, y)); } if (spawnBlocks.Count == 4) { pieceIds.Clear(); foreach (var b in spawnBlocks) pieceIds.Add(b.id); var positions = spawnBlocks.Select(b => new Point(b.x, b.y)).ToList(); string shape = IdentifyShape(positions); currentPiece = GetPiece(shape); currentRot = GetRot(shape); currentX = positions.Min(p => p.X); if (currentPiece != '?') { var field = GetFieldBlocks(); var (bx, br) = FindBestPlacement(currentPiece, field); targetX = bx; targetRot = br; positioned = false; pieceActive = true; spawnTime = DateTime.Now; lastMoveDetected = DateTime.Now; pendingLeftFromX = -1; pendingRightFromX = -1; pendingRotateFromRot = -1; Log($"{currentPiece}{currentRot} @ X{currentX} -> X{targetX} r{targetRot}"); } } continue; } var state = GetPieceState(); if (state.positions.Count != 4) { pieceIds.Clear(); pieceActive = false; currentPiece = '?'; targetX = -1; targetRot = -1; positioned = false; continue; } int newX = state.minX; int newRot = state.rot != -1 ? state.rot : currentRot; if (newX != currentX) { if (pendingLeftFromX != -1 && newX < pendingLeftFromX) pendingLeftFromX = -1; if (pendingRightFromX != -1 && newX > pendingRightFromX) pendingRightFromX = -1; currentX = newX; } if (newRot != currentRot) { pendingRotateFromRot = -1; currentRot = newRot; } double timeSinceMove = (DateTime.Now - lastMoveDetected).TotalMilliseconds; if (timeSinceMove > landingTimeout) { pieceIds.Clear(); pieceActive = false; currentPiece = '?'; targetX = -1; targetRot = -1; positioned = false; continue; } int maxRot = GetMaxRotations(currentPiece); int rotNeeded = (targetRot - currentRot + maxRot) % maxRot; int xDiff = targetX - currentX; if (rotNeeded == 0 && xDiff == 0) positioned = true; if (positioned) { if (CanDown()) SendDown(); continue; } double timeSinceLeft = (DateTime.Now - lastLeftCmd).TotalMilliseconds; double timeSinceRight = (DateTime.Now - lastRightCmd).TotalMilliseconds; double timeSinceRotate = (DateTime.Now - lastRotateCmd).TotalMilliseconds; bool leftPending = pendingLeftFromX != -1 && timeSinceLeft < verifyTimeout; bool rightPending = pendingRightFromX != -1 && timeSinceRight < verifyTimeout; bool rotatePending = pendingRotateFromRot != -1 && timeSinceRotate < verifyTimeout; bool leftTimeout = pendingLeftFromX != -1 && timeSinceLeft >= verifyTimeout; bool rightTimeout = pendingRightFromX != -1 && timeSinceRight >= verifyTimeout; bool rotateTimeout = pendingRotateFromRot != -1 && timeSinceRotate >= verifyTimeout; if (rotNeeded > 0) { if (rotateTimeout && CanRotate()) { SendRotate(); } else if (!rotatePending && CanRotate()) { SendRotate(); } } if (xDiff < 0) { if (leftTimeout && CanLeft()) { SendLeft(); } else if (!leftPending && CanLeft()) { SendLeft(); } } else if (xDiff > 0) { if (rightTimeout && CanRight()) { SendRight(); } else if (!rightPending && CanRight()) { SendRight(); } } }