xabbo-scripts/Scripts/Duck Dodging Pathfinder AI.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

491 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
public struct Point : IEquatable<Point>
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public static implicit operator Point((int x, int y) tuple) => new Point(tuple.x, tuple.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);
public static bool operator ==(Point left, Point right) => left.Equals(right);
public static bool operator !=(Point left, Point right) => !(left == right);
public override string ToString() => $"({X},{Y})";
}
public class Tile
{
public int X { get; set; }
public int Y { get; set; }
public double Z { get; set; }
public Point XY => new Point(X, Y);
public Tile(int x, int y, double z = 0.0) { X = x; Y = y; Z = z; }
}
public class Duck
{
public long id { get; set; }
public Point pos { get; set; }
public Point lastpos { get; set; }
public Point vel { get; set; }
public DateTime lastseen { get; set; }
public Queue<Point> trail { get; set; } = new Queue<Point>(10);
public double spd { get; set; }
}
HashSet<Point> tiles = new HashSet<Point> {
(13,13),(14,13),(15,13),(16,13),(17,13),(18,13),(19,13),(20,13),(21,13),(22,13),(23,13),(24,13),(25,13),
(13,14),(17,14),(21,14),(25,14),
(13,15),(14,15),(15,15),(17,15),(18,15),(20,15),(21,15),(23,15),(24,15),(25,15),
(13,16),(15,16),(18,16),(20,16),(23,16),(25,16),
(13,17),(15,17),(16,17),(17,17),(18,17),(19,17),(20,17),(21,17),(22,17),(23,17),(25,17),
(13,18),(15,18),(19,18),(23,18),(25,18),
(13,19),(14,19),(15,19),(17,19),(18,19),(19,19),(20,19),(21,19),(23,19),(24,19),(25,19),
(13,20),(15,20),(16,20),(17,20),(21,20),(22,20),(23,20),(25,20),
(12,21),(13,21),(17,21),(18,21),(19,21),(20,21),(21,21),(25,21),(26,21),
(13,22),(15,22),(16,22),(17,22),(21,22),(22,22),(23,22),(25,22),
(13,23),(14,23),(15,23),(17,23),(18,23),(19,23),(20,23),(21,23),(23,23),(24,23),(25,23),
(13,24),(15,24),(19,24),(23,24),(25,24),
(13,25),(15,25),(16,25),(17,25),(18,25),(19,25),(20,25),(21,25),(22,25),(23,25),(25,25),
(13,26),(15,26),(18,26),(20,26),(23,26),(25,26),
(13,27),(14,27),(15,27),(17,27),(18,27),(20,27),(21,27),(23,27),(24,27),(25,27),
(13,28),(17,28),(21,28),(25,28),
(13,29),(14,29),(15,29),(16,29),(17,29),(18,29),(19,29),(20,29),(21,29),(22,29),(23,29),(24,29),(25,29)
};
Dictionary<Point, List<Point>> adj = new Dictionary<Point, List<Point>>();
Point[] dirs = { (0,1), (0,-1), (1,0), (-1,0), (1,1), (1,-1), (-1,1), (-1,-1) };
foreach(var t in tiles)
{
var n = new List<Point>();
foreach(var d in dirs)
{
Point p = new Point(t.X + d.X, t.Y + d.Y);
if(tiles.Contains(p)) n.Add(p);
}
adj[t] = n;
}
Dictionary<long, Duck> ducks = new Dictionary<long, Duck>();
Tile tgt = null;
Point lastcmd = default(Point);
DateTime cmdtime = DateTime.MinValue;
Point prev = default(Point);
Point curr = default(Point);
Queue<Point> hist = new Queue<Point>(5);
int stuck = 0;
HashSet<Point> danger = new HashSet<Point>();
Point dest = default(Point);
bool forcedest = false;
DateTime desttime = DateTime.MinValue;
Point getpos()
{
if (Self == null) return default(Point);
if (tgt != null) return tgt.XY;
if (!lastcmd.Equals(default(Point)) && (DateTime.UtcNow - cmdtime).TotalMilliseconds < 250)
return lastcmd;
if (Self.Location != null) return new Point(Self.Location.X, Self.Location.Y);
return default(Point);
}
void go(int x, int y)
{
Move(x, y);
lastcmd = new Point(x, y);
cmdtime = DateTime.UtcNow;
tgt = null;
}
HashSet<Point> predict(int frames)
{
var zones = new HashSet<Point>();
foreach(var d in ducks.Values)
{
zones.Add(d.pos);
if(!d.vel.Equals(default(Point)))
{
for(int i = 1; i <= frames; i++)
{
Point pred = new Point(
d.pos.X + d.vel.X * i,
d.pos.Y + d.vel.Y * i
);
if(tiles.Contains(pred))
zones.Add(pred);
}
}
if(frames >= 1 && adj.ContainsKey(d.pos))
{
foreach(var n in adj[d.pos])
zones.Add(n);
}
if(frames >= 2)
{
foreach(var d1 in dirs)
{
Point p1 = new Point(d.pos.X + d1.X, d.pos.Y + d1.Y);
if(tiles.Contains(p1))
{
zones.Add(p1);
foreach(var d2 in dirs)
{
Point p2 = new Point(p1.X + d2.X, p1.Y + d2.Y);
if(tiles.Contains(p2))
zones.Add(p2);
}
}
}
}
}
return zones;
}
Point findsafe(Point me, HashSet<Point> bad)
{
if(!bad.Any()) return tiles.First();
double best = double.MinValue;
Point spot = me;
foreach(var t in tiles)
{
if(bad.Contains(t)) continue;
double score = 0;
double mindist = double.MaxValue;
foreach(var b in bad)
{
double d = Math.Abs(t.X - b.X) + Math.Abs(t.Y - b.Y);
mindist = Math.Min(mindist, d);
score += d;
}
score += mindist * 100;
if(adj.ContainsKey(t))
{
int exits = adj[t].Count(n => !bad.Contains(n));
score += exits * 10;
}
double dist = Math.Abs(t.X - me.X) + Math.Abs(t.Y - me.Y);
score -= dist * 0.5;
if(score > best)
{
best = score;
spot = t;
}
}
return spot;
}
Point pathto(Point me, Point goal, HashSet<Point> d1, HashSet<Point> d2, HashSet<Point> d3)
{
if(!adj.ContainsKey(me)) return me;
if(hist.Count >= 3)
{
var last3 = hist.TakeLast(3).ToArray();
if(last3[0] == last3[2] && last3[0] != last3[1])
{
stuck++;
if(stuck > 2)
{
var esc = adj[me]
.Where(n => !d1.Contains(n))
.OrderBy(n => Guid.NewGuid())
.FirstOrDefault();
if(!esc.Equals(default(Point)))
{
stuck = 0;
return esc;
}
}
}
else stuck = 0;
}
double best = double.MinValue;
Point move = me;
foreach(var n in adj[me])
{
if(d1.Contains(n)) continue;
double score = 0;
if(d2.Contains(n)) score -= 800;
if(d3.Contains(n)) score -= 400;
double dist = Math.Abs(n.X - goal.X) + Math.Abs(n.Y - goal.Y);
score -= dist * 100;
foreach(var duck in ducks.Values)
{
double dd = Math.Abs(n.X - duck.pos.X) + Math.Abs(n.Y - duck.pos.Y);
score += dd * 10;
}
if(adj.ContainsKey(n))
{
int safe = adj[n].Count(x => !d1.Contains(x));
score += safe * 20;
if(safe == 0 && d2.Contains(n))
score -= 2000;
}
if(!prev.Equals(default(Point)) && n.Equals(prev))
score -= 50;
if(score > best)
{
best = score;
move = n;
}
}
return move;
}
Point getmove(Point me, Point goal, HashSet<Point> d1, HashSet<Point> d2, HashSet<Point> d3)
{
if(!adj.ContainsKey(me)) return me;
if(hist.Count >= 3)
{
var last3 = hist.TakeLast(3).ToArray();
if(last3[0] == last3[2] && last3[0] != last3[1])
{
stuck++;
if(stuck > 1)
{
var any = adj[me]
.Where(n => !d1.Contains(n))
.OrderBy(n => d2.Contains(n) ? 1 : 0)
.FirstOrDefault();
if(!any.Equals(default(Point)))
{
stuck = 0;
return any;
}
}
}
else stuck = 0;
}
double best = double.MinValue;
Point move = me;
foreach(var n in adj[me])
{
if(d1.Contains(n)) continue;
if(!prev.Equals(default(Point)) && n.Equals(prev))
continue;
double score = 0;
if(d2.Contains(n)) score -= 1000;
if(d3.Contains(n)) score -= 500;
double dist = Math.Abs(n.X - goal.X) + Math.Abs(n.Y - goal.Y);
score -= dist * 10;
foreach(var duck in ducks.Values)
{
double dd = Math.Abs(n.X - duck.pos.X) + Math.Abs(n.Y - duck.pos.Y);
score += dd * 20;
}
if(adj.ContainsKey(n))
{
int exits = adj[n].Count(x => !d1.Contains(x) && !d2.Contains(x));
score += exits * 50;
}
if(score > best)
{
best = score;
move = n;
}
}
return move;
}
OnIntercept(Out["MoveAvatar"], e => {
var pkt = e.Packet;
int x = pkt.ReadInt();
int y = pkt.ReadInt();
dest = new Point(x, y);
forcedest = true;
desttime = DateTime.UtcNow;
});
OnEnteredRoom(e => {
ducks.Clear();
hist.Clear();
prev = default(Point);
stuck = 0;
forcedest = false;
dest = default(Point);
});
OnIntercept(In["WiredMovements"], e => {
var pkt = e.Packet;
int cnt = pkt.ReadInt();
for(int i = 0; i < cnt; i++)
{
pkt.ReadInt();
int fx = pkt.ReadInt();
int fy = pkt.ReadInt();
int tx = pkt.ReadInt();
int ty = pkt.ReadInt();
pkt.ReadString();
pkt.ReadString();
int id = pkt.ReadInt();
pkt.ReadInt();
pkt.ReadInt();
long fid = id;
Point newp = new Point(tx, ty);
Point oldp = new Point(fx, fy);
if(!ducks.ContainsKey(fid))
{
ducks[fid] = new Duck { id = fid };
}
var d = ducks[fid];
d.lastpos = d.pos;
d.pos = newp;
d.vel = new Point(tx - fx, ty - fy);
d.lastseen = DateTime.UtcNow;
d.trail.Enqueue(newp);
if(d.trail.Count > 10) d.trail.Dequeue();
if((d.lastseen - DateTime.UtcNow).TotalSeconds < 1)
{
d.spd = Math.Sqrt(Math.Pow(d.vel.X, 2) + Math.Pow(d.vel.Y, 2));
}
}
});
OnIntercept(In["UserUpdate"], e => {
if(Self == null) return;
var pkt = e.Packet;
int num = pkt.ReadInt();
for(int i = 0; i < num; i++)
{
int idx = pkt.ReadInt();
int x = pkt.ReadInt();
int y = pkt.ReadInt();
string z = pkt.ReadString();
pkt.ReadInt();
pkt.ReadInt();
string act = pkt.ReadString();
if(idx == Self.Index)
{
prev = curr;
curr = new Point(x, y);
hist.Enqueue(curr);
if(hist.Count > 5) hist.Dequeue();
if(forcedest && curr.Equals(dest))
{
forcedest = false;
}
if(act.Contains("/mv"))
{
var parts = act.Split(new[] {' ', '/', ','}, StringSplitOptions.RemoveEmptyEntries);
if(parts.Length >= 4 && parts[0] == "mv")
{
if(int.TryParse(parts[1], out int mx) &&
int.TryParse(parts[2], out int my) &&
double.TryParse(parts[3], NumberStyles.Any, CultureInfo.InvariantCulture, out double mz))
{
tgt = new Tile(mx, my, mz);
lastcmd = default(Point);
}
}
}
else if(act.EndsWith("//") && !act.Contains("/mv"))
{
tgt = null;
}
}
}
});
while(Run)
{
try
{
Point me = getpos();
if(me.Equals(default(Point))) { Delay(20); continue; }
if(ducks.Any() || forcedest)
{
var d1 = predict(1);
var d2 = predict(2);
var d3 = predict(3);
Point goal;
Point next = me;
if(forcedest && tiles.Contains(dest))
{
if((DateTime.UtcNow - desttime).TotalSeconds > 30)
{
forcedest = false;
goal = findsafe(me, d1);
next = getmove(me, goal, d1, d2, d3);
}
else
{
goal = dest;
next = pathto(me, goal, d1, d2, d3);
}
}
else
{
goal = findsafe(me, d1);
next = getmove(me, goal, d1, d2, d3);
}
if(!next.Equals(me))
{
go(next.X, next.Y);
}
}
}
catch(Exception ex)
{
}
Delay(20);
}