xabbo-scripts/Scripts/Snake Auto (Campaign) v7.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

336 lines
8.5 KiB
C#

/// @name Snake Auto (Campaign) v7
using System;
using System.Linq;
using System.Collections.Generic;
const long HEAD_ID = 769407982;
const int TARGET_KIND = 3666;
const int SNAKE_KIND = 5478;
const int GAME_MIN_X = 37;
const int GAME_MAX_X = 59;
const int GAME_MIN_Y = 29;
const int GAME_MAX_Y = 51;
// Initial guess; script auto-corrects while running.
const int BTN_TL = 769407085;
const int BTN_TR = 769406820;
const int BTN_BL = 769407385;
const int BTN_BR = 769407116;
// dirs: 0=UP 1=DOWN 2=LEFT 3=RIGHT
Dictionary<int, int> dirBtn = new Dictionary<int, int>
{
[0] = BTN_TR,
[1] = BTN_BR,
[2] = BTN_BL,
[3] = BTN_TL
};
static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" };
DateTime lastCmdTime = DateTime.MinValue;
DateTime lastInfo = DateTime.MinValue;
DateTime lastDebug = DateTime.MinValue;
string lastSentDir = "";
int curDir = -1;
(int x, int y) prevHead = (-1, -1);
int pendingBtn = 0;
(int x, int y) pendingHead = (-1, -1);
DateTime pendingAt = DateTime.MinValue;
HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>();
Queue<(int x, int y)> fillQueue = new Queue<(int, int)>();
HashSet<(int x, int y)> pathVisited = new HashSet<(int, int)>();
Queue<((int x, int y) pos, int dist)> pathQueue = new Queue<((int, 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 msg, bool force = false)
{
if (!force && (DateTime.Now - lastDebug).TotalMilliseconds < 120) return;
Log($"dbg: {msg}");
lastDebug = 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;
}
bool TryGetButtonPos(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 LearnMappingFromMovement((int x, int y) head)
{
if (pendingBtn == 0) return;
if ((DateTime.Now - pendingAt).TotalMilliseconds > 1200)
{
pendingBtn = 0;
return;
}
int dx = head.x - pendingHead.x;
int dy = head.y - pendingHead.y;
if (dx == 0 && dy == 0) return;
int movedDir = DirFromDelta(dx, dy);
int oldBtn = dirBtn[movedDir];
if (oldBtn != pendingBtn)
{
dirBtn[movedDir] = pendingBtn;
// keep mapping one-to-one by swapping previous owner
foreach (var kv in dirBtn.ToList())
{
if (kv.Key != movedDir && kv.Value == pendingBtn)
{
dirBtn[kv.Key] = oldBtn;
break;
}
}
Dbg($"calib: {DIRS[movedDir]} now={pendingBtn} swapped_old={oldBtn}", true);
Dbg($"map: U={dirBtn[0]} D={dirBtn[1]} L={dirBtn[2]} R={dirBtn[3]}", true);
}
pendingBtn = 0;
}
void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target)
{
string nd = DIRS[d];
if (nd == lastSentDir && (DateTime.Now - lastCmdTime).TotalMilliseconds < 110)
return;
int id = dirBtn[d];
if (TryGetButtonPos(id, out int bx, out int by))
Move(bx, by);
Send(Out["ClickFurni"], id, 0);
pendingBtn = id;
pendingHead = head;
pendingAt = DateTime.Now;
lastCmdTime = DateTime.Now;
lastSentDir = nd;
if (target.HasValue)
Dbg($"reason={reason} head=({head.x},{head.y}) target=({target.Value.x},{target.Value.y}) dir={nd} btn={id}", true);
else
Dbg($"reason={reason} head=({head.x},{head.y}) dir={nd} btn={id}", true);
}
bool Bad(int x, int y, HashSet<(int x, int y)> body)
{
if (!InBoard(x, y)) return true;
return body.Contains((x, y));
}
int Fill(int sx, int sy, HashSet<(int x, int y)> body, int cap)
{
if (Bad(sx, sy, body)) 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) && fillVisited.Add((nx, ny)))
fillQueue.Enqueue((nx, ny));
}
}
return c;
}
int Dist(int sx, int sy, int tx, int ty, HashSet<(int x, int y)> body, int cap)
{
if (sx == tx && sy == ty) return 0;
pathVisited.Clear();
pathQueue.Clear();
pathVisited.Add((sx, sy));
pathQueue.Enqueue(((sx, sy), 0));
while (pathQueue.Count > 0)
{
var cur = pathQueue.Dequeue();
if (cur.dist >= cap) return 999;
for (int d = 0; d < 4; d++)
{
Next(cur.pos.x, cur.pos.y, d, out int nx, out int ny);
if (nx == tx && ny == ty) return cur.dist + 1;
if (!Bad(nx, ny, body) && pathVisited.Add((nx, ny)))
pathQueue.Enqueue(((nx, ny), cur.dist + 1));
}
}
return -1;
}
int Emergency((int x, int y) head, HashSet<(int x, int y)> body)
{
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)) continue;
int space = Fill(nx, ny, body, 120);
if (space > bestSpace)
{
bestSpace = space;
bestDir = d;
}
}
return bestDir;
}
int Decide((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body)
{
int bestDir = curDir >= 0 ? curDir : 0;
int bestScore = int.MinValue;
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)) continue;
int space = Fill(nx, ny, body, 200);
if (space < 10) continue;
int dist = Dist(nx, ny, target.x, target.y, body, 200);
int score = 0;
if (nx == target.x && ny == target.y) score += 15000;
if (dist > 0 && dist < 999) score += (240 - dist) * 8;
else if (dist == -1) score -= 300;
score += Math.Min(space, 120) * 4;
if (curDir >= 0 && d == curDir) score += 2;
if (score > bestScore)
{
bestScore = score;
bestDir = d;
}
}
return bestDir;
}
Log("Snake Auto (Campaign) v7 started");
Log($"init map: U={dirBtn[0]} D={dirBtn[1]} L={dirBtn[2]} R={dirBtn[3]}");
while (Run)
{
Delay(18);
var headItem = GetFloorItem(HEAD_ID);
if (headItem == null)
{
if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500)
{
Log("waiting: head not visible");
lastInfo = DateTime.Now;
}
continue;
}
var head = (headItem.Location.X, headItem.Location.Y);
if (!InBoard(head.Item1, head.Item2))
continue;
LearnMappingFromMovement(head);
if (prevHead.x != -1)
{
int dx = head.Item1 - prevHead.x;
int dy = head.Item2 - prevHead.y;
if (dx != 0 || dy != 0)
curDir = DirFromDelta(dx, dy);
}
prevHead = head;
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 targets = FloorItems
.Where(i => i != null && i.Kind == TARGET_KIND)
.Where(i => InBoard(i.Location.X, i.Location.Y))
.Select(i => (x: i.Location.X, y: i.Location.Y))
.ToList();
if (targets.Count == 0)
{
if ((DateTime.Now - lastInfo).TotalMilliseconds > 500)
{
Dbg($"reason=no_target_hold head=({head.Item1},{head.Item2})", true);
lastInfo = DateTime.Now;
}
continue;
}
var target = targets
.OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2))
.First();
if (curDir >= 0)
{
Next(head.Item1, head.Item2, curDir, out int fx, out int fy);
if (Bad(fx, fy, body))
{
int emergency = Emergency(head, body);
Cmd(emergency, "front_blocked", head, target);
continue;
}
}
int best = Decide(head, target, body);
Cmd(best, "seek_target", head, target);
}