xabbo-scripts/Scripts/Tetris-Sirjonas2.csx
Administrator 7a548130a3 Move all scripts into Scripts/ subfolder
Keeps the repo root clean - only README.md visible on landing page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:49:37 +01:00

1117 lines
47 KiB
C#

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 = 70; // ms between same direction commands (was 120)
int verifyTimeout = 150; // ms to wait for move confirmation (was 250)
int downDelay = 60; // ms between down presses (was 100)
int landingTimeout = 250; // ms without movement = piece landed (was 400)
int swapCooldown = 250; // ms between swap actions (was 400)
// ─────────────────────────────────────────────────────────────────────────────────
// 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 = 200; // ms to wait after drop before detecting new piece (was 350)
HashSet<int> pieceIds = new HashSet<int>();
// ─────────────────────────────────────────────────────────────────────────────────
// 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<Point>
{
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<string, int[]> {
// 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<int> pieceKinds = new HashSet<int> {
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<int> ignoreKinds = new HashSet<int> {
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<Point> 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<Point>();
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<Point> GetShapePoints(char piece, int rot)
{
string key = $"{piece}{rot}";
if (!shapes.ContainsKey(key)) return new List<Point>();
var def = shapes[key];
var pts = new List<Point>();
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<int, int, bool> 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<Point> GetFieldBlocks()
{
var result = new HashSet<Point>();
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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point>(field);
var piecePoints = new HashSet<Point>();
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<Point>();
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<Point> 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<Point> field)
{
var (bx, br, _) = FindBestPlacementForPiece(piece, field);
return (bx, br);
}
// ═══════════════════════════════════════════════════════════════════════════════
// SWAP DECISION LOGIC
// ═══════════════════════════════════════════════════════════════════════════════
bool ShouldSwap(char currentPiece, char heldOrNextPiece, HashSet<Point> 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<Point> 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<Point>(), -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<Point>(), -1, -1, "?", '?', -1);
}
// Keep old method for ID-based tracking when we have valid IDs
(List<Point> positions, int minX, int minY, string shape, char piece, int rot) GetPieceState()
{
var positions = new List<Point>();
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 - FAST 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(15); // Faster loop (was 30)
// 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(100);
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}");
lastDropTime = DateTime.Now;
pieceIds.Clear();
pieceActive = false;
currentPiece = '?';
targetX = -1;
targetRot = -1;
positioned = false;
Delay(150); // Wait for piece to land (was 300)
}
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.");