Keeps the repo root clean - only README.md visible on landing page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
287 lines
8.4 KiB
C#
287 lines
8.4 KiB
C#
/// @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<long, DateTime> targetBlacklist = new Dictionary<long, DateTime>();
|
|
|
|
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<int> { 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);
|
|
} |