Habbo Hotel automation scripts including: - Game solvers (Snake, Color Puzzle, Tetris, Flappy Bird, Flood-IT) - Room utilities (Autogate, One-Way Door, Furni Scanner) - Bot tools (Heal Bot, Pet Trainer, User Collector) - Trading & economy (Furni-Matic, Seed Trade, Trade Spam) Cleaned up: removed 5 duplicates and 2 broken scripts, renamed 37 gibberish filenames to descriptive names.
1115 lines
47 KiB
C#
1115 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 = 60; // ms between same direction commands (increased)
|
|
int verifyTimeout = 230; // ms to wait for move confirmation
|
|
int downDelay = 80; // ms between down presses (if not using instant)
|
|
int landingTimeout = 300; // 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<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 - 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."); |