using System; using System.Collections.Generic; using System.Linq; // ═══════════════════════════════════════════════════════════════════════════════ // TETRIS BOT V2 - Enhanced with Instant Drop, Hold/Swap, and Preview Look-ahead // ═══════════════════════════════════════════════════════════════════════════════ // ───────────────────────────────────────────────────────────────────────────────── // TIMING CONFIGURATION (tune these for optimal performance) // ───────────────────────────────────────────────────────────────────────────────── int sameCommandDelay = 120; // ms between same direction commands (increased) int verifyTimeout = 250; // ms to wait for move confirmation int downDelay = 100; // ms between down presses (if not using instant) int landingTimeout = 400; // ms without movement = piece landed int swapCooldown = 400; // ms between swap actions // ───────────────────────────────────────────────────────────────────────────────── // FIELD COORDINATES // ───────────────────────────────────────────────────────────────────────────────── const int FIELD_MIN_X = 15; const int FIELD_MAX_X = 24; const int FIELD_MIN_Y = 12; const int FIELD_MAX_Y = 32; // ───────────────────────────────────────────────────────────────────────────────── // SPAWN AREA (where controlled blocks appear) // ───────────────────────────────────────────────────────────────────────────────── const int SPAWN_MIN_X = 18; const int SPAWN_MAX_X = 21; const int SPAWN_MIN_Y = 12; const int SPAWN_MAX_Y = 13; // ───────────────────────────────────────────────────────────────────────────────── // PREVIEW AREAS (next 3 pieces) // ───────────────────────────────────────────────────────────────────────────────── const int PREVIEW1_MIN_X = 9; const int PREVIEW1_MAX_X = 13; const int PREVIEW1_MIN_Y = 14; const int PREVIEW1_MAX_Y = 16; const int PREVIEW2_MIN_X = 9; const int PREVIEW2_MAX_X = 13; const int PREVIEW2_MIN_Y = 18; const int PREVIEW2_MAX_Y = 21; const int PREVIEW3_MIN_X = 9; const int PREVIEW3_MAX_X = 13; const int PREVIEW3_MIN_Y = 22; const int PREVIEW3_MAX_Y = 25; // ───────────────────────────────────────────────────────────────────────────────── // HOLD/SWAP AREA // ───────────────────────────────────────────────────────────────────────────────── const int HOLD_MIN_X = 24; const int HOLD_MAX_X = 26; const int HOLD_MIN_Y = 13; const int HOLD_MAX_Y = 18; // ───────────────────────────────────────────────────────────────────────────────── // CONTROL FURNI IDs // ───────────────────────────────────────────────────────────────────────────────── const int CTRL_LEFT = 2147418115; const int CTRL_RIGHT = 2147418116; const int CTRL_DOWN = 2147418113; const int CTRL_INSTANT_DOWN = 2147418114; const int CTRL_ROTATE = 2147418118; // TODO: UPDATE THIS WITH CORRECT ID! const int CTRL_SWAP = 2147418117; // ───────────────────────────────────────────────────────────────────────────────── // COMMAND TIMESTAMPS // ───────────────────────────────────────────────────────────────────────────────── DateTime lastLeftCmd = DateTime.MinValue; DateTime lastRightCmd = DateTime.MinValue; DateTime lastRotateCmd = DateTime.MinValue; DateTime lastDownCmd = DateTime.MinValue; DateTime lastSwapCmd = DateTime.MinValue; DateTime lastMoveDetected = DateTime.MinValue; DateTime spawnTime = DateTime.MinValue; // ───────────────────────────────────────────────────────────────────────────────── // PENDING COMMAND TRACKING (for verification) // ───────────────────────────────────────────────────────────────────────────────── int pendingLeftFromX = -1; int pendingRightFromX = -1; int pendingRotateFromRot = -1; // ───────────────────────────────────────────────────────────────────────────────── // CURRENT STATE // ───────────────────────────────────────────────────────────────────────────────── int targetX = -1; int targetRot = -1; int currentX = -1; int currentRot = -1; char currentPiece = '?'; bool pieceActive = false; bool positioned = false; bool useInstantDrop = true; // Set to false if instant drop causes issues DateTime lastDropTime = DateTime.MinValue; int dropCooldown = 350; // ms to wait after drop before detecting new piece HashSet pieceIds = new HashSet(); // ───────────────────────────────────────────────────────────────────────────────── // HOLD/SWAP STATE // ───────────────────────────────────────────────────────────────────────────────── char heldPiece = '?'; bool canSwap = true; bool hasSwappedThisTurn = false; // ───────────────────────────────────────────────────────────────────────────────── // PREVIEW STATE // ───────────────────────────────────────────────────────────────────────────────── char nextPiece1 = '?'; char nextPiece2 = '?'; char nextPiece3 = '?'; // ═══════════════════════════════════════════════════════════════════════════════ // DATA STRUCTURES // ═══════════════════════════════════════════════════════════════════════════════ 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); } // ═══════════════════════════════════════════════════════════════════════════════ // TETROMINO SHAPES (normalized to top-left corner) // ═══════════════════════════════════════════════════════════════════════════════ var shapes = new Dictionary { // I-piece: 4 wide, 2 rotations ["I0"] = new[] {0,0, 1,0, 2,0, 3,0}, ["I1"] = new[] {0,0, 0,1, 0,2, 0,3}, // O-piece: 2x2, 1 rotation (no change) ["O0"] = new[] {0,0, 1,0, 0,1, 1,1}, // T-piece: 4 rotations ["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}, // S-piece: 2 rotations ["S0"] = new[] {1,0, 2,0, 0,1, 1,1}, ["S1"] = new[] {0,0, 0,1, 1,1, 1,2}, // Z-piece: 2 rotations ["Z0"] = new[] {0,0, 1,0, 1,1, 2,1}, ["Z1"] = new[] {1,0, 0,1, 1,1, 0,2}, // L-piece: 4 rotations ["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}, // J-piece: 4 rotations ["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} }; // ═══════════════════════════════════════════════════════════════════════════════ // BLOCK DETECTION - Using Kind property (TypeId) // ═══════════════════════════════════════════════════════════════════════════════ // Piece Kind values (confirmed from debug) HashSet pieceKinds = new HashSet { 7100, // I piece 7101, // J piece 7108, // T piece 7121, // Z piece 7124, // L piece 7138, // S piece 5482, // O piece }; // IGNORE these Kinds completely HashSet ignoreKinds = new HashSet { 4071, // Decoration/frame 10469, // Shadow/overlay (always 5 blocks, follows active piece) }; int GetKind(dynamic item) { try { return (int)item.Kind; } catch { return -1; } } bool IsPieceKind(int kind) => pieceKinds.Contains(kind); bool IsIgnoredKind(int kind) => ignoreKinds.Contains(kind); // Wrapper for backward compatibility - checks Kind of item bool IsBlockItem(dynamic item) { int kind = GetKind(item); if (kind < 0) return false; if (IsIgnoredKind(kind)) return false; return IsPieceKind(kind); } // ═══════════════════════════════════════════════════════════════════════════════ // COORDINATE HELPERS // ═══════════════════════════════════════════════════════════════════════════════ 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; bool IsInPreview1(int x, int y) => x >= PREVIEW1_MIN_X && x <= PREVIEW1_MAX_X && y >= PREVIEW1_MIN_Y && y <= PREVIEW1_MAX_Y; bool IsInPreview2(int x, int y) => x >= PREVIEW2_MIN_X && x <= PREVIEW2_MAX_X && y >= PREVIEW2_MIN_Y && y <= PREVIEW2_MAX_Y; bool IsInPreview3(int x, int y) => x >= PREVIEW3_MIN_X && x <= PREVIEW3_MAX_X && y >= PREVIEW3_MIN_Y && y <= PREVIEW3_MAX_Y; bool IsInHold(int x, int y) => x >= HOLD_MIN_X && x <= HOLD_MAX_X && y >= HOLD_MIN_Y && y <= HOLD_MAX_Y; // ═══════════════════════════════════════════════════════════════════════════════ // COMMAND SENDERS // ═══════════════════════════════════════════════════════════════════════════════ 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; } void SendInstantDown() { Send(Out["ClickFurni"], CTRL_INSTANT_DOWN, 0); lastDownCmd = DateTime.Now; } void SendSwap() { Send(Out["ClickFurni"], CTRL_SWAP, 0); lastSwapCmd = DateTime.Now; hasSwappedThisTurn = true; } // ═══════════════════════════════════════════════════════════════════════════════ // COMMAND TIMING CHECKS // ═══════════════════════════════════════════════════════════════════════════════ 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; bool CanSwap() => (DateTime.Now - lastSwapCmd).TotalMilliseconds >= swapCooldown && !hasSwappedThisTurn; // ═══════════════════════════════════════════════════════════════════════════════ // SHAPE IDENTIFICATION // ═══════════════════════════════════════════════════════════════════════════════ 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; } // ═══════════════════════════════════════════════════════════════════════════════ // PREVIEW READING // ═══════════════════════════════════════════════════════════════════════════════ char ReadPreviewPiece(Func isInArea) { var blocks = new List<(int kind, int x, int y)>(); foreach (var item in FloorItems) { if (item == null) continue; int kind = GetKind(item); if (!IsPieceKind(kind)) continue; int x = item.Location.X; int y = item.Location.Y; if (isInArea(x, y)) blocks.Add((kind, x, y)); } // Group by kind and find a group of exactly 4 var byType = blocks.GroupBy(b => b.kind).ToList(); foreach (var group in byType) { if (group.Count() != 4) continue; var positions = group.Select(b => new Point(b.x, b.y)).ToList(); string shape = IdentifyShape(positions); if (shape != "?") return GetPiece(shape); } return '?'; } void UpdatePreviews() { nextPiece1 = ReadPreviewPiece(IsInPreview1); nextPiece2 = ReadPreviewPiece(IsInPreview2); nextPiece3 = ReadPreviewPiece(IsInPreview3); } char ReadHeldPiece() { // First check HOLD area (X24-26, Y13-18) var blocks = new List<(int kind, int x, int y)>(); foreach (var item in FloorItems) { if (item == null) continue; int kind = GetKind(item); if (!IsPieceKind(kind)) continue; int x = item.Location.X; int y = item.Location.Y; if (IsInHold(x, y)) blocks.Add((kind, x, y)); } // Group by kind and find a group of exactly 4 var byType = blocks.GroupBy(b => b.kind).ToList(); foreach (var group in byType) { if (group.Count() != 4) continue; var positions = group.Select(b => new Point(b.x, b.y)).ToList(); string shape = IdentifyShape(positions); if (shape != "?") return GetPiece(shape); } // If not found in HOLD, check SWAP-AREA (X27-30, Y14-20) where held pieces go after swap var swapBlocks = new List<(int kind, int x, int y)>(); foreach (var item in FloorItems) { if (item == null) continue; int kind = GetKind(item); if (!IsPieceKind(kind)) continue; int x = item.Location.X; int y = item.Location.Y; if (x >= 27 && x <= 30 && y >= 14 && y <= 20) swapBlocks.Add((kind, x, y)); } var byTypeSwap = swapBlocks.GroupBy(b => b.kind).ToList(); foreach (var group in byTypeSwap) { if (group.Count() != 4) continue; var positions = group.Select(b => new Point(b.x, b.y)).ToList(); string shape = IdentifyShape(positions); if (shape != "?") return GetPiece(shape); } return '?'; } // ═══════════════════════════════════════════════════════════════════════════════ // FIELD STATE // ═══════════════════════════════════════════════════════════════════════════════ HashSet GetFieldBlocks() { var result = new HashSet(); foreach (var item in FloorItems) { if (item == null) continue; int kind = GetKind(item); if (!IsPieceKind(kind)) 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; } // ═══════════════════════════════════════════════════════════════════════════════ // SIMULATION & SCORING (El-Tetris inspired heuristics) // ═══════════════════════════════════════════════════════════════════════════════ 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 width = FIELD_MAX_X - FIELD_MIN_X + 1; int[] h = new int[width]; 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; } int GetBumpiness(int[] heights) { int bump = 0; for (int i = 0; i < heights.Length - 1; i++) bump += Math.Abs(heights[i] - heights[i + 1]); return bump; } 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; // Validate all blocks land within field 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; } // Create new field state with piece placed 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); } // Count lines cleared and piece cells involved 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); } } } // Simulate gravity after line clear 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; } // Calculate features 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); var heights = GetHeights(newField); int bumpiness = GetBumpiness(heights); int maxH = heights.Length > 0 ? heights.Max() : 0; int aggregateHeight = heights.Sum(); // El-Tetris inspired weights with additions 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; score += -0.51 * bumpiness; score += -0.18 * aggregateHeight; // Bonus for clearing lines score += linesCleared * 500; // Extra bonus for Tetris (4 lines) if (linesCleared == 4) score += 2000; // Emergency mode when stack is high if (maxH >= 15) { score += linesCleared * 3000; score -= maxH * 100; } return score; } // ═══════════════════════════════════════════════════════════════════════════════ // BEST PLACEMENT FINDER (with optional look-ahead) // ═══════════════════════════════════════════════════════════════════════════════ (int x, int rot, double score) FindBestPlacementForPiece(char piece, HashSet field) { int bestX = (FIELD_MIN_X + FIELD_MAX_X) / 2; int 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, bestScore); } (int x, int rot) FindBestPlacement(char piece, HashSet field) { var (bx, br, _) = FindBestPlacementForPiece(piece, field); return (bx, br); } // ═══════════════════════════════════════════════════════════════════════════════ // SWAP DECISION LOGIC // ═══════════════════════════════════════════════════════════════════════════════ bool ShouldSwap(char currentPiece, char heldOrNextPiece, HashSet field) { if (heldOrNextPiece == '?') return false; var (_, _, currentScore) = FindBestPlacementForPiece(currentPiece, field); var (_, _, alternateScore) = FindBestPlacementForPiece(heldOrNextPiece, field); // Swap if alternate piece gives significantly better score return alternateScore > currentScore + 500; } // ═══════════════════════════════════════════════════════════════════════════════ // PIECE STATE GETTER - Position-based for rooms that recreate items on move // ═══════════════════════════════════════════════════════════════════════════════ (List positions, int minX, int minY, string shape, char piece, int rot) GetActivePieceInField() { // Find blocks in the upper portion of the field (active piece area) var fieldBlocks = new List<(int id, int kind, int x, int y)>(); foreach (var item in FloorItems) { if (item == null) continue; int kind = GetKind(item); if (!IsPieceKind(kind)) continue; int x = item.Location.X; int y = item.Location.Y; // Look in upper half of field for active piece if (IsInField(x, y) && y <= FIELD_MIN_Y + 10) fieldBlocks.Add(((int)item.Id, kind, x, y)); } if (fieldBlocks.Count < 4) return (new List(), -1, -1, "?", '?', -1); // Group by kind - pieces of same type have same kind var byKind = fieldBlocks.GroupBy(b => b.kind) .OrderBy(g => g.Min(b => b.y)) // Prioritize topmost groups .ToList(); // For each kind, try the topmost 4 blocks foreach (var group in byKind) { if (group.Count() < 4) continue; // Take the 4 blocks with lowest Y (topmost) var top4 = group.OrderBy(b => b.y).ThenBy(b => b.x).Take(4).ToList(); var positions = top4.Select(b => new Point(b.x, b.y)).ToList(); string shape = IdentifyShape(positions); if (shape != "?") { int minX = positions.Min(p => p.X); int minY = positions.Min(p => p.Y); // Store these IDs for movement tracking pieceIds.Clear(); foreach (var b in top4) pieceIds.Add(b.id); return (positions, minX, minY, shape, GetPiece(shape), GetRot(shape)); } } return (new List(), -1, -1, "?", '?', -1); } // Keep old method for ID-based tracking when we have valid IDs (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) { // IDs lost - try position-based detection return GetActivePieceInField(); } 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); } // ═══════════════════════════════════════════════════════════════════════════════ // MOVEMENT DETECTION INTERCEPTOR // ═══════════════════════════════════════════════════════════════════════════════ OnIntercept(In["ObjectUpdate"], e => { var p = e.Packet; int id = p.ReadInt(); if (pieceIds.Contains(id)) lastMoveDetected = DateTime.Now; }); // ═══════════════════════════════════════════════════════════════════════════════ // MAIN LOOP // ═══════════════════════════════════════════════════════════════════════════════ Log("═══════════════════════════════════════════"); Log(" TETRIS BOT V2 - Enhanced Edition"); Log(" Features: Instant Drop, Hold/Swap, Preview"); Log("═══════════════════════════════════════════"); Log($"Field: X{FIELD_MIN_X}-{FIELD_MAX_X}, Y{FIELD_MIN_Y}-{FIELD_MAX_Y}"); Log($"Spawn: X{SPAWN_MIN_X}-{SPAWN_MAX_X}, Y{SPAWN_MIN_Y}-{SPAWN_MAX_Y}"); Log(""); Log("Block types: I=7100, J=7101, T=7108, Z=7121, L=7124, S=7138, O=5482"); Log("Ghost (ignored): 10469"); Log(""); // Initial preview scan UpdatePreviews(); heldPiece = ReadHeldPiece(); Log($"Previews: Next1={nextPiece1} Next2={nextPiece2} Next3={nextPiece3} Hold={heldPiece}"); Log(""); Log("Press Ctrl+C to stop"); Log(""); while (Run) { Delay(30); // 30ms between iterations to prevent command spam // Update preview pieces periodically UpdatePreviews(); heldPiece = ReadHeldPiece(); // ───────────────────────────────────────────────────────────────────────── // SPAWN DETECTION - Look for new piece at top of field // ───────────────────────────────────────────────────────────────────────── if (!pieceActive) { // Wait for cooldown after last drop if ((DateTime.Now - lastDropTime).TotalMilliseconds < dropCooldown) continue; // Look for blocks in the top area of field (Y12-18) var topBlocks = new List<(int id, int kind, int x, int y)>(); foreach (var item in FloorItems) { if (item == null) continue; int kind = GetKind(item); if (!IsPieceKind(kind)) continue; int x = item.Location.X; int y = item.Location.Y; // Top of field area (expanded range) if (x >= FIELD_MIN_X && x <= FIELD_MAX_X && y >= FIELD_MIN_Y && y <= FIELD_MIN_Y + 6) topBlocks.Add(((int)item.Id, kind, x, y)); } // Group by kind var byKind = topBlocks.GroupBy(b => b.kind).ToList(); // For EACH kind, try the TOPMOST 4 blocks (lowest Y values) foreach (var group in byKind.OrderBy(g => g.Min(b => b.y))) { if (group.Count() < 4) continue; // Take the 4 blocks with lowest Y (topmost), then by X var top4 = group.OrderBy(b => b.y).ThenBy(b => b.x).Take(4).ToList(); var positions = top4.Select(b => new Point(b.x, b.y)).ToList(); string shape = IdentifyShape(positions); if (shape != "?") { pieceIds.Clear(); foreach (var b in top4) pieceIds.Add(b.id); currentPiece = GetPiece(shape); currentRot = GetRot(shape); currentX = positions.Min(p => p.X); // Log kind for debugging int blockKind = top4[0].kind; var field = GetFieldBlocks(); // Check if we should swap with held piece hasSwappedThisTurn = false; char swapCandidate = heldPiece != '?' ? heldPiece : nextPiece1; if (CanSwap() && ShouldSwap(currentPiece, swapCandidate, field)) { Log($"SWAP: {currentPiece} -> {swapCandidate}"); SendSwap(); pieceIds.Clear(); pieceActive = false; Delay(150); break; } 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($"NEW: {currentPiece}{currentRot} [Kind:{blockKind}] @ X{currentX} → X{targetX} r{targetRot} (Next:{nextPiece1},{nextPiece2},{nextPiece3})"); break; } } continue; } // ───────────────────────────────────────────────────────────────────────── // PIECE STATE UPDATE - Handle ID changes during movement // ───────────────────────────────────────────────────────────────────────── var state = GetPieceState(); // If we lost the piece but still have blocks in field, try to re-acquire if (state.positions.Count != 4 || state.piece == '?') { // Piece might have changed IDs or moved - try position-based detection state = GetActivePieceInField(); } if (state.positions.Count != 4 || state.piece == '?') { // Still can't find piece - it may have landed or we lost it double timeSinceSpawn = (DateTime.Now - spawnTime).TotalMilliseconds; if (timeSinceSpawn > 200) // Give some time for piece to appear { pieceIds.Clear(); pieceActive = false; currentPiece = '?'; targetX = -1; targetRot = -1; positioned = false; } continue; } // Update piece if it changed (rotation wall kick or similar) if (state.piece != '?' && state.piece != currentPiece) { Log($" Piece changed: {currentPiece} -> {state.piece}"); currentPiece = state.piece; // Recalculate target for new piece var field = GetFieldBlocks(); var (bx, br) = FindBestPlacement(currentPiece, field); targetX = bx; targetRot = br; positioned = false; } int newX = state.minX; int newRot = state.rot != -1 ? state.rot : currentRot; // Verify pending commands completed 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; } // ───────────────────────────────────────────────────────────────────────── // LANDING DETECTION // ───────────────────────────────────────────────────────────────────────── double timeSinceMove = (DateTime.Now - lastMoveDetected).TotalMilliseconds; if (timeSinceMove > landingTimeout) { pieceIds.Clear(); pieceActive = false; currentPiece = '?'; targetX = -1; targetRot = -1; positioned = false; continue; } // ───────────────────────────────────────────────────────────────────────── // POSITIONING LOGIC // ───────────────────────────────────────────────────────────────────────── int maxRot = GetMaxRotations(currentPiece); int rotNeeded = (targetRot - currentRot + maxRot) % maxRot; int xDiff = targetX - currentX; if (rotNeeded == 0 && xDiff == 0) positioned = true; // ───────────────────────────────────────────────────────────────────────── // DROP WHEN POSITIONED // ───────────────────────────────────────────────────────────────────────── if (positioned) { if (useInstantDrop) { // Use instant drop for maximum speed SendInstantDown(); Log($"DROP: {currentPiece} at X{currentX}"); pieceIds.Clear(); pieceActive = false; currentPiece = '?'; targetX = -1; targetRot = -1; positioned = false; Delay(300); // Wait for piece to land and new piece to spawn } else if (CanDown()) { SendDown(); } continue; } // ───────────────────────────────────────────────────────────────────────── // MOVEMENT EXECUTION // ───────────────────────────────────────────────────────────────────────── 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; // Rotate first (higher priority) if (rotNeeded > 0) { if (rotateTimeout && CanRotate()) { SendRotate(); } else if (!rotatePending && CanRotate()) { SendRotate(); } } // Horizontal movement 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(); } } } Log(""); Log("Bot stopped.");