/// @name Snake Auto (AutoCalib) using System; using System.Linq; using System.Collections.Generic; const int GAME_MIN_X = 9; const int GAME_MAX_X = 28; const int GAME_MIN_Y = 11; const int GAME_MAX_Y = 30; const int BLOCKED_MIN_X = 17; const int BLOCKED_MAX_X = 20; const int BLOCKED_MIN_Y = 19; const int BLOCKED_MAX_Y = 22; const int KIND_SNAKE = 7100; const int KIND_FOOD = 5068; // Fallback controls for this room layout const int FALLBACK_UP = 769407748; const int FALLBACK_RIGHT = 769407103; const int FALLBACK_DOWN = 769407216; const int FALLBACK_LEFT = 769407068; int CTRL_UP = FALLBACK_UP; int CTRL_RIGHT = FALLBACK_RIGHT; int CTRL_DOWN = FALLBACK_DOWN; int CTRL_LEFT = FALLBACK_LEFT; List<(int x, int y)> body = new List<(int x, int y)>(256); HashSet<(int x, int y)> bodySet = new HashSet<(int x, int y)>(); (int x, int y) food = (-1, -1); string dir = "DOWN"; string lastSentDir = ""; DateTime lastCmdTime = DateTime.MinValue; bool ate = false; static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; bool Wall(int x, int y) => x < GAME_MIN_X || x > GAME_MAX_X || y < GAME_MIN_Y || y > GAME_MAX_Y; bool Blk(int x, int y) => x >= BLOCKED_MIN_X && x <= BLOCKED_MAX_X && y >= BLOCKED_MIN_Y && y <= BLOCKED_MAX_Y; void Step(int x, int y, int d, out int nx, out int ny) { nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); } int Opp(int d) => d ^ 1; int DirToIdx(string d) => d == "UP" ? 0 : d == "DOWN" ? 1 : d == "LEFT" ? 2 : 3; bool Blocked(int x, int y, bool ignoreTail) { if (Wall(x, y) || Blk(x, y)) return true; if (body.Count == 0) return false; int checkLen = ignoreTail && !ate ? body.Count - 1 : body.Count; for (int i = 0; i < checkLen; i++) if (body[i].x == x && body[i].y == y) return true; return false; } void SendDir(int dIdx) { string d = DIRS[dIdx]; if (d == lastSentDir && (DateTime.Now - lastCmdTime).TotalMilliseconds < 80) return; int id = dIdx == 0 ? CTRL_UP : dIdx == 1 ? CTRL_DOWN : dIdx == 2 ? CTRL_LEFT : CTRL_RIGHT; Send(Out["ClickFurni"], id, 0); lastCmdTime = DateTime.Now; lastSentDir = d; dir = d; } int OpenSpace(int sx, int sy, int cap) { if (Blocked(sx, sy, false)) return 0; var seen = new HashSet<(int, int)>(); var q = new Queue<(int, int)>(); q.Enqueue((sx, sy)); seen.Add((sx, sy)); int c = 0; while (q.Count > 0 && c < cap) { var p = q.Dequeue(); c++; for (int d = 0; d < 4; d++) { Step(p.Item1, p.Item2, d, out int nx, out int ny); if (!Blocked(nx, ny, false) && seen.Add((nx, ny))) q.Enqueue((nx, ny)); } } return c; } int DistTo(int sx, int sy, int tx, int ty, int cap = 180) { if (sx == tx && sy == ty) return 0; var seen = new HashSet<(int, int)>(); var q = new Queue<((int x, int y) pos, int dist)>(); q.Enqueue(((sx, sy), 0)); seen.Add((sx, sy)); while (q.Count > 0) { var cur = q.Dequeue(); if (cur.dist >= cap) return 999; for (int d = 0; d < 4; d++) { Step(cur.pos.x, cur.pos.y, d, out int nx, out int ny); if (nx == tx && ny == ty) return cur.dist + 1; if (!Blocked(nx, ny, false) && seen.Add((nx, ny))) q.Enqueue(((nx, ny), cur.dist + 1)); } } return -1; } int BestMove(int hx, int hy, int fx, int fy, int curDir) { int best = curDir; int bestScore = int.MinValue; int minSpace = Math.Max(body.Count / 4, 8); for (int d = 0; d < 4; d++) { if (d == Opp(curDir)) continue; Step(hx, hy, d, out int nx, out int ny); if (Blocked(nx, ny, true)) continue; int space = OpenSpace(nx, ny, 180); if (space < minSpace) continue; int score = 0; if (nx == fx && ny == fy) score += 10000; int distFood = DistTo(nx, ny, fx, fy, 180); if (distFood > 0 && distFood < 999) score += (220 - distFood) * 7; else if (distFood == -1) score -= 150; score += Math.Min(space, 100) * 4; if (score > bestScore) { bestScore = score; best = d; } } return best; } int Emergency(int hx, int hy, int curDir) { int best = curDir; int bestSpace = -1; for (int d = 0; d < 4; d++) { if (d == Opp(curDir)) continue; Step(hx, hy, d, out int nx, out int ny); if (Blocked(nx, ny, true)) continue; int space = OpenSpace(nx, ny, 80); if (space > bestSpace) { bestSpace = space; best = d; } } return best; } List> GetComponents(List<(long id, int kind, int x, int y)> points) { var comps = new List>(); var used = new HashSet(); foreach (var p in points) { if (!used.Add(p.id)) continue; var comp = new List<(long id, int x, int y)>(); var q = new Queue<(long id, int x, int y)>(); q.Enqueue((p.id, p.x, p.y)); while (q.Count > 0) { var cur = q.Dequeue(); comp.Add(cur); foreach (var n in points) { if (used.Contains(n.id)) continue; if (Math.Abs(cur.x - n.x) <= 1 && Math.Abs(cur.y - n.y) <= 1) { used.Add(n.id); q.Enqueue((n.id, n.x, n.y)); } } } comps.Add(comp); } return comps; } bool TryAutoCalibrateControls() { if (Self == null) return false; int sx = Self.Location.X; int sy = Self.Location.Y; var nearby = FloorItems .Where(i => i != null) .Where(i => Math.Abs(i.Location.X - sx) <= 14 && Math.Abs(i.Location.Y - sy) <= 14) .Where(i => i.Kind != KIND_SNAKE && i.Kind != KIND_FOOD) .Select(i => (id: (long)i.Id, kind: (int)i.Kind, x: i.Location.X, y: i.Location.Y)) .ToList(); if (nearby.Count == 0) return false; List<(long id, int x, int y)> best = null; double bestScore = double.MaxValue; foreach (var g in nearby.GroupBy(p => p.kind)) { if (g.Count() < 4) continue; var components = GetComponents(g.ToList()); foreach (var comp in components) { if (comp.Count < 4) continue; int minX = comp.Min(p => p.x); int maxX = comp.Max(p => p.x); int minY = comp.Min(p => p.y); int maxY = comp.Max(p => p.y); int w = maxX - minX; int h = maxY - minY; if (w > 6 || h > 6) continue; double cx = comp.Average(p => p.x); double cy = comp.Average(p => p.y); double dist = Math.Abs(cx - sx) + Math.Abs(cy - sy); double score = dist + Math.Abs(comp.Count - 4) * 0.5; if (score < bestScore) { bestScore = score; best = comp; } } } if (best == null || best.Count < 4) return false; double centerX = best.Average(p => p.x); double centerY = best.Average(p => p.y); var used = new HashSet(); long Pick(IEnumerable<(long id, int x, int y)> list) { foreach (var p in list) { if (used.Add(p.id)) return p.id; } return -1; } var upList = best.OrderBy(p => p.y).ThenBy(p => Math.Abs(p.x - centerX)).ToList(); var downList = best.OrderByDescending(p => p.y).ThenBy(p => Math.Abs(p.x - centerX)).ToList(); var leftList = best.OrderBy(p => p.x).ThenBy(p => Math.Abs(p.y - centerY)).ToList(); var rightList = best.OrderByDescending(p => p.x).ThenBy(p => Math.Abs(p.y - centerY)).ToList(); long up = Pick(upList); long down = Pick(downList); long left = Pick(leftList); long right = Pick(rightList); if (up <= 0 || down <= 0 || left <= 0 || right <= 0) return false; CTRL_UP = (int)up; CTRL_RIGHT = (int)right; CTRL_DOWN = (int)down; CTRL_LEFT = (int)left; return true; } Log("Snake Auto (AutoCalib) started"); bool calibrated = TryAutoCalibrateControls(); if (calibrated) Log($"Auto-calibrated controls: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); else Log($"Auto-calibration failed, using fallback: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); Log("Waiting for snake + food spawn..."); while (Run) { Delay(15); var curSnake = new HashSet<(int x, int y)>(); food = (-1, -1); foreach (var item in FloorItems) { if (item == null) continue; int k; try { k = (int)item.Kind; } catch { continue; } if (k == KIND_SNAKE && !Wall(item.Location.X, item.Location.Y)) curSnake.Add((item.Location.X, item.Location.Y)); else if (k == KIND_FOOD) food = (item.Location.X, item.Location.Y); } int curLen = curSnake.Count; if (curLen == 0) { body.Clear(); bodySet.Clear(); continue; } if (body.Count == 0) { var sorted = curSnake.OrderBy(p => p.y).ThenBy(p => p.x).ToList(); body = new List<(int x, int y)>(sorted); bodySet = new HashSet<(int x, int y)>(sorted); continue; } var head = body[0]; (int x, int y) newHead = (-1, -1); for (int d = 0; d < 4; d++) { Step(head.x, head.y, d, out int nx, out int ny); if (curSnake.Contains((nx, ny)) && !bodySet.Contains((nx, ny))) { newHead = (nx, ny); if ((DateTime.Now - lastCmdTime).TotalMilliseconds > 400) dir = DIRS[d]; break; } } if (newHead.x != -1) { int prevLen = body.Count; body.Insert(0, newHead); bodySet.Add(newHead); ate = curLen > prevLen; while (body.Count > curLen) { var tail = body[body.Count - 1]; bodySet.Remove(tail); body.RemoveAt(body.Count - 1); } } if (body.Count == 0 || food.x == -1) continue; head = body[0]; int curDir = DirToIdx(dir); Step(head.x, head.y, curDir, out int fx, out int fy); if (Blocked(fx, fy, true)) { SendDir(Emergency(head.x, head.y, curDir)); continue; } for (int d = 0; d < 4; d++) { if (d == Opp(curDir)) continue; Step(head.x, head.y, d, out int nx, out int ny); if (nx == food.x && ny == food.y && !Blocked(nx, ny, true)) { SendDir(d); goto nextLoop; } } SendDir(BestMove(head.x, head.y, food.x, food.y, curDir)); nextLoop:; }