/// @name Snake Auto (Campaign) v13 using System; using System.Linq; using System.Collections.Generic; const long HEAD_ID = 769407982; const int SNAKE_KIND = 5478; static readonly int[] TARGET_KINDS = { 6559, 3666 }; const int GAME_MIN_X = 20; const int GAME_MAX_X = 65; const int GAME_MIN_Y = 20; const int GAME_MAX_Y = 65; const int CTRL_UP = 769406820; const int CTRL_DOWN = 769407116; const int CTRL_LEFT = 769407385; const int CTRL_RIGHT = 769407085; static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; DateTime lastCmd = DateTime.MinValue; DateTime lastDbg = DateTime.MinValue; int curDir = -1; string lastSentDir = ""; (int x, int y) prevHead = (-1, -1); int lastCmdDir = -1; (int x, int y) lastCmdHead = (-999, -999); int lastAttemptDir = -1; (int x, int y) lastAttemptHead = (-999, -999); DateTime lastAttemptAt = DateTime.MinValue; long lastTargetId = 0; Dictionary<(int x, int y), DateTime> tempBlocked = new Dictionary<(int, int), DateTime>(); Dictionary targetBlacklist = new Dictionary(); HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); bool InBoard(int x, int y) => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; void Dbg(string m, bool force = false) { if (!force && (DateTime.Now - lastDbg).TotalMilliseconds < 220) return; Log($"dbg: {m}"); lastDbg = DateTime.Now; } void Next(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 DirFromDelta(int dx, int dy) { if (Math.Abs(dx) >= Math.Abs(dy)) return dx < 0 ? 2 : 3; return dy < 0 ? 0 : 1; } int BtnForDir(int d) => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; bool TryGetBtnPos(int id, out int x, out int y) { var b = GetFloorItem(id); if (b == null) { x = 0; y = 0; return false; } x = b.Location.X; y = b.Location.Y; return true; } void CleanupMaps() { var now = DateTime.Now; foreach (var k in tempBlocked.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) tempBlocked.Remove(k); foreach (var k in targetBlacklist.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) targetBlacklist.Remove(k); } bool IsTempBlocked(int x, int y) => tempBlocked.TryGetValue((x, y), out var until) && until > DateTime.Now; bool Bad(int x, int y, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) { if (!InBoard(x, y)) return true; if (blocks.Contains((x, y))) return true; if (IsTempBlocked(x, y)) return true; return body.Contains((x, y)); } int Fill(int sx, int sy, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks, int cap) { if (Bad(sx, sy, body, blocks)) return 0; fillVisited.Clear(); fillQueue.Clear(); fillVisited.Add((sx, sy)); fillQueue.Enqueue((sx, sy)); int c = 0; while (fillQueue.Count > 0 && c < cap) { var p = fillQueue.Dequeue(); c++; for (int d = 0; d < 4; d++) { Next(p.x, p.y, d, out int nx, out int ny); if (!Bad(nx, ny, body, blocks) && fillVisited.Add((nx, ny))) fillQueue.Enqueue((nx, ny)); } } return c; } int Emergency((int x, int y) head, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) { int bestDir = curDir >= 0 ? curDir : 0; int bestSpace = -1; for (int d = 0; d < 4; d++) { if (curDir >= 0 && d == Opp(curDir)) continue; Next(head.x, head.y, d, out int nx, out int ny); if (Bad(nx, ny, body, blocks)) continue; int space = Fill(nx, ny, body, blocks, 260); if (space > bestSpace) { bestSpace = space; bestDir = d; } } return bestDir; } int PickDir((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) { int desired = DirFromDelta(target.x - head.x, target.y - head.y); var dirs = new List { desired, 0, 1, 2, 3 }; int bestDir = curDir >= 0 ? curDir : desired; int bestScore = int.MinValue; foreach (int d in dirs.Distinct()) { if (curDir >= 0 && d == Opp(curDir)) continue; Next(head.x, head.y, d, out int nx, out int ny); if (Bad(nx, ny, body, blocks)) continue; int dist = Math.Abs(target.x - nx) + Math.Abs(target.y - ny); int space = Fill(nx, ny, body, blocks, 260); int score = -dist * 6 + space * 2; if (d == desired) score += 25; if (curDir >= 0 && d == curDir) score += 4; if (score > bestScore) { bestScore = score; bestDir = d; } } return bestDir; } void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target, long targetId) { string nd = DIRS[d]; if (d == lastCmdDir && head == lastCmdHead && (DateTime.Now - lastCmd).TotalMilliseconds < 500) return; if (nd == lastSentDir && (DateTime.Now - lastCmd).TotalMilliseconds < 200) return; int id = BtnForDir(d); if (TryGetBtnPos(id, out int bx, out int by)) Move(bx, by); Send(Out["ClickFurni"], id, 0); lastCmd = DateTime.Now; lastSentDir = nd; lastCmdDir = d; lastCmdHead = head; // Do NOT reset attempt timer for repeated same head+dir. if (lastAttemptDir != d || lastAttemptHead != head) { lastAttemptDir = d; lastAttemptHead = head; lastAttemptAt = DateTime.Now; lastTargetId = targetId; } if (target.HasValue) Dbg($"{reason} h=({head.x},{head.y}) t=({target.Value.x},{target.Value.y}) d={nd} b={id}", true); else Dbg($"{reason} h=({head.x},{head.y}) d={nd} b={id}", true); } Log("Snake Auto (Campaign) v13 started"); Log($"target kinds: {string.Join(",", TARGET_KINDS)}"); Log($"buttons U={CTRL_UP} D={CTRL_DOWN} L={CTRL_LEFT} R={CTRL_RIGHT}"); while (Run) { Delay(24); CleanupMaps(); var headItem = GetFloorItem(HEAD_ID); if (headItem == null) { Dbg("head missing"); continue; } var head = (headItem.Location.X, headItem.Location.Y); if (!InBoard(head.Item1, head.Item2)) continue; if (prevHead.x != -1) { int hdx = head.Item1 - prevHead.x; int hdy = head.Item2 - prevHead.y; if (hdx != 0 || hdy != 0) curDir = DirFromDelta(hdx, hdy); } // Stuck: head did not move after attempted direction. if (head == lastAttemptHead && lastAttemptDir >= 0 && (DateTime.Now - lastAttemptAt).TotalMilliseconds > 560) { Next(head.Item1, head.Item2, lastAttemptDir, out int bx, out int by); tempBlocked[(bx, by)] = DateTime.Now.AddMilliseconds(2500); if (lastTargetId != 0) targetBlacklist[lastTargetId] = DateTime.Now.AddMilliseconds(3000); Dbg($"stuck: blocked ({bx},{by}), blacklisted target {lastTargetId}", true); lastAttemptDir = -1; } prevHead = head; var blocks = FloorItems .Where(i => i != null && i.Kind == 9109) .Where(i => InBoard(i.Location.X, i.Location.Y)) .Select(i => (i.Location.X, i.Location.Y)) .ToHashSet(); var body = FloorItems .Where(i => i != null && i.Kind == SNAKE_KIND) .Where(i => InBoard(i.Location.X, i.Location.Y)) .Select(i => (i.Location.X, i.Location.Y)) .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) .ToHashSet(); var targetsAll = FloorItems .Where(i => i != null && TARGET_KINDS.Contains((int)i.Kind)) .Where(i => InBoard(i.Location.X, i.Location.Y)) .Where(i => !blocks.Contains((i.Location.X, i.Location.Y))) .Select(i => (x: i.Location.X, y: i.Location.Y, id: (long)i.Id, kind: (int)i.Kind)) .ToList(); if (targetsAll.Count == 0) { int safe = Emergency(head, body, blocks); Cmd(safe, "no_target_safe", head, null, 0); continue; } var targets = targetsAll .Where(t => !targetBlacklist.TryGetValue(t.id, out var until) || until <= DateTime.Now) .ToList(); if (targets.Count == 0) targets = targetsAll; var target = targets .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) .First(); int dsel = PickDir(head, (target.x, target.y), body, blocks); Cmd(dsel, $"seek k={target.kind} id={target.id}", head, (target.x, target.y), target.id); }