xabbo-scripts/Scripts/Color Pattern Walker Solver.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

519 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
class Tile
{
public long Id;
public int Kind;
public string Name;
public int X;
public int Y;
public double Z;
public int State;
}
class Cluster
{
public int Kind;
public List<Tile> Tiles = new List<Tile>();
public double AvgZ;
public HashSet<int> StateSet = new HashSet<int>();
public double CenterX;
public double CenterY;
}
const int SPAWN_X = 13;
const int SPAWN_Y = 13;
const bool WAIT_FOR_SPAWN_START = true;
const int SPAWN_WAIT_TIMEOUT_MS = 180000;
int GetState(dynamic item)
{
try { return int.Parse(item.State?.ToString() ?? "0"); }
catch { return 0; }
}
int GetKind(dynamic item)
{
try { return (int)item.Kind; }
catch { return -1; }
}
string GetNameSafe(dynamic item)
{
try
{
string n = item.GetName();
return string.IsNullOrWhiteSpace(n) ? "<unknown>" : n;
}
catch { return "<unknown>"; }
}
List<Tile> ReadAllFloorTiles()
{
var list = new List<Tile>();
foreach (var item in FloorItems)
{
if (item == null) continue;
list.Add(new Tile {
Id = item.Id,
Kind = GetKind(item),
Name = GetNameSafe(item),
X = item.Location.X,
Y = item.Location.Y,
Z = item.Location.Z,
State = GetState(item)
});
}
return list;
}
string PosKey(int x, int y) => x + "," + y;
List<Cluster> BuildClusters(List<Tile> tilesOfKind)
{
var byPos = tilesOfKind.ToDictionary(t => PosKey(t.X, t.Y), t => t);
var visited = new HashSet<string>();
var clusters = new List<Cluster>();
int[] d = { -1, 0, 1 };
foreach (var t in tilesOfKind)
{
string start = PosKey(t.X, t.Y);
if (visited.Contains(start)) continue;
var q = new Queue<Tile>();
var c = new Cluster { Kind = t.Kind };
q.Enqueue(t);
visited.Add(start);
while (q.Count > 0)
{
var cur = q.Dequeue();
c.Tiles.Add(cur);
c.StateSet.Add(cur.State);
foreach (int dx in d)
foreach (int dy in d)
{
if (dx == 0 && dy == 0) continue;
string nk = PosKey(cur.X + dx, cur.Y + dy);
if (visited.Contains(nk)) continue;
if (!byPos.ContainsKey(nk)) continue;
visited.Add(nk);
q.Enqueue(byPos[nk]);
}
}
c.AvgZ = c.Tiles.Count == 0 ? 0.0 : c.Tiles.Average(x => x.Z);
c.CenterX = c.Tiles.Count == 0 ? 0.0 : c.Tiles.Average(x => x.X);
c.CenterY = c.Tiles.Count == 0 ? 0.0 : c.Tiles.Average(x => x.Y);
clusters.Add(c);
}
return clusters;
}
void LogRoomScan(List<Tile> all)
{
Log("=== Full Room Scan ===");
Log($"FloorItems total: {all.Count}");
var byKind = all.GroupBy(x => x.Kind)
.Select(g => new {
Kind = g.Key,
Count = g.Count(),
Names = g.Select(x => x.Name).Distinct().Take(3).ToArray(),
MinX = g.Min(x => x.X), MaxX = g.Max(x => x.X),
MinY = g.Min(x => x.Y), MaxY = g.Max(x => x.Y),
MinZ = g.Min(x => x.Z), MaxZ = g.Max(x => x.Z),
States = string.Join(",", g.Select(x => x.State).Distinct().OrderBy(x => x))
})
.OrderByDescending(x => x.Count)
.Take(30)
.ToList();
foreach (var k in byKind)
{
Log($"Kind {k.Kind} x{k.Count} | states [{k.States}] | bbox X[{k.MinX}-{k.MaxX}] Y[{k.MinY}-{k.MaxY}] Z[{k.MinZ:F2}-{k.MaxZ:F2}] | name {string.Join(" / ", k.Names)}");
}
}
double DistD(double x1, double y1, double x2, double y2)
{
double dx = x1 - x2;
double dy = y1 - y2;
return Math.Sqrt(dx * dx + dy * dy);
}
bool TryDetectBoards(List<Tile> all, int spawnX, int spawnY, out Cluster top, out Cluster bottom)
{
top = null;
bottom = null;
var byKind = all.GroupBy(x => x.Kind).ToList();
Cluster bestA = null, bestB = null;
int bestScore = -1;
foreach (var g in byKind)
{
var clusters = BuildClusters(g.ToList())
.Where(c => c.Tiles.Count >= 9)
.OrderByDescending(c => c.Tiles.Count)
.ToList();
if (clusters.Count < 2) continue;
for (int i = 0; i < clusters.Count; i++)
{
for (int j = i + 1; j < clusters.Count; j++)
{
var a = clusters[i];
var b = clusters[j];
int minCount = Math.Min(a.Tiles.Count, b.Tiles.Count);
int diff = Math.Abs(a.Tiles.Count - b.Tiles.Count);
int score = (minCount * 10) - diff;
if (a.StateSet.Count > 1) score += 5;
if (b.StateSet.Count > 1) score += 5;
if (score > bestScore)
{
bestScore = score;
bestA = a;
bestB = b;
}
}
}
}
if (bestA == null || bestB == null) return false;
// Prefer the board whose center is closer to your spawn as bottom/play board.
double da = DistD(bestA.CenterX, bestA.CenterY, spawnX, spawnY);
double db = DistD(bestB.CenterX, bestB.CenterY, spawnX, spawnY);
if (Math.Abs(da - db) >= 2.0)
{
if (da <= db) { bottom = bestA; top = bestB; }
else { bottom = bestB; top = bestA; }
}
else
{
// Fallback if both are similarly far away.
if (bestA.AvgZ >= bestB.AvgZ) { top = bestA; bottom = bestB; }
else { top = bestB; bottom = bestA; }
}
return true;
}
bool WaitForRoundSpawn(int spawnX, int spawnY, int timeoutMs)
{
Log($"Waiting for round start spawn at {spawnX}:{spawnY}...");
int elapsed = 0;
while (elapsed < timeoutMs)
{
if (Self != null && Self.Location != null)
{
if (Self.Location.X == spawnX && Self.Location.Y == spawnY)
{
Log("Spawn detected. Round started.");
Delay(350);
return true;
}
}
Delay(200);
elapsed += 200;
}
Log("Spawn wait timeout reached. Continuing anyway.");
return false;
}
Dictionary<string, Tile> BuildIndexedMap(Cluster c, out int w, out int h)
{
var xs = c.Tiles.Select(t => t.X).Distinct().OrderBy(x => x).ToList();
var ys = c.Tiles.Select(t => t.Y).Distinct().OrderBy(y => y).ToList();
w = xs.Count;
h = ys.Count;
var xToI = new Dictionary<int, int>();
var yToI = new Dictionary<int, int>();
for (int i = 0; i < xs.Count; i++) xToI[xs[i]] = i;
for (int i = 0; i < ys.Count; i++) yToI[ys[i]] = i;
var map = new Dictionary<string, Tile>();
foreach (var t in c.Tiles)
{
int xi = xToI[t.X];
int yi = yToI[t.Y];
map[PosKey(xi, yi)] = t;
}
return map;
}
bool TryBuildTargetMap(Cluster top, Cluster bottom, out Dictionary<long, int> targetByBottomId, out List<Tile> bottomTilesOrdered)
{
targetByBottomId = new Dictionary<long, int>();
bottomTilesOrdered = new List<Tile>();
int bw, bh, tw, th;
var bMap = BuildIndexedMap(bottom, out bw, out bh);
var tMap = BuildIndexedMap(top, out tw, out th);
if (bw != tw || bh != th)
{
Log($"WARN: Different board dimensions: top {tw}x{th}, bottom {bw}x{bh}. Trying overlap map.");
}
int w = Math.Min(bw, tw);
int h = Math.Min(bh, th);
if (w <= 0 || h <= 0) return false;
var transforms = new List<Func<int, int, (int x, int y)>>();
transforms.Add((x, y) => (x, y));
transforms.Add((x, y) => (w - 1 - x, y));
transforms.Add((x, y) => (x, h - 1 - y));
transforms.Add((x, y) => (w - 1 - x, h - 1 - y));
if (w == h)
{
transforms.Add((x, y) => (y, x));
transforms.Add((x, y) => (w - 1 - y, x));
transforms.Add((x, y) => (y, h - 1 - x));
transforms.Add((x, y) => (w - 1 - y, h - 1 - x));
}
int bestIdx = 0;
int bestMatches = -1;
for (int ti = 0; ti < transforms.Count; ti++)
{
int matches = 0;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
var bKey = PosKey(x, y);
if (!bMap.ContainsKey(bKey)) continue;
var tr = transforms[ti](x, y);
var tKey = PosKey(tr.x, tr.y);
if (tMap.ContainsKey(tKey)) matches++;
}
}
if (matches > bestMatches)
{
bestMatches = matches;
bestIdx = ti;
}
}
var bestTf = transforms[bestIdx];
Log($"Mapping transform index: {bestIdx}, overlap: {bestMatches}");
foreach (var kv in bMap)
{
var p = kv.Key.Split(',');
int x = int.Parse(p[0]);
int y = int.Parse(p[1]);
if (x >= w || y >= h) continue;
var tr = bestTf(x, y);
var tKey = PosKey(tr.x, tr.y);
if (!tMap.ContainsKey(tKey)) continue;
var bTile = kv.Value;
var tTile = tMap[tKey];
targetByBottomId[bTile.Id] = tTile.State;
bottomTilesOrdered.Add(bTile);
}
return targetByBottomId.Count > 0;
}
Dictionary<long, int> ReadCurrentStatesById(HashSet<long> ids)
{
var map = new Dictionary<long, int>();
foreach (var item in FloorItems)
{
if (item == null) continue;
long id = item.Id;
if (!ids.Contains(id)) continue;
map[id] = GetState(item);
}
return map;
}
int CircularNeed(int current, int target, int mod)
{
int d = (target - current) % mod;
if (d < 0) d += mod;
return d;
}
int CalcObjective(Dictionary<long, int> current, Dictionary<long, int> target, int cycle)
{
int s = 0;
foreach (var kv in target)
{
if (!current.ContainsKey(kv.Key)) continue;
s += CircularNeed(current[kv.Key], kv.Value, cycle);
}
return s;
}
bool WaitUntilAt(int tx, int ty, int timeoutMs)
{
int elapsed = 0;
while (elapsed < timeoutMs)
{
if (Self != null && Self.Location != null && Self.Location.X == tx && Self.Location.Y == ty)
return true;
Delay(120);
elapsed += 120;
}
return false;
}
int Dist(int x1, int y1, int x2, int y2)
{
return Math.Abs(x1 - x2) + Math.Abs(y1 - y2);
}
Log("=== Color Pattern Walker Solver ===");
if (WAIT_FOR_SPAWN_START)
WaitForRoundSpawn(SPAWN_X, SPAWN_Y, SPAWN_WAIT_TIMEOUT_MS);
var allTiles = ReadAllFloorTiles();
if (allTiles.Count == 0)
{
Log("ERROR: No floor items found.");
return;
}
LogRoomScan(allTiles);
Cluster top, bottom;
if (!TryDetectBoards(allTiles, SPAWN_X, SPAWN_Y, out top, out bottom))
{
Log("ERROR: Could not auto-detect top template board + bottom play board.");
Log("Tip: run ColorPuzzleScanner first and tell me tile Kind/Name, then I hard-bind it.");
return;
}
Log($"Detected kind: {top.Kind}");
Log($"Top tiles: {top.Tiles.Count}, avgZ={top.AvgZ:F2}");
Log($"Bottom tiles: {bottom.Tiles.Count}, avgZ={bottom.AvgZ:F2}");
Dictionary<long, int> targetById;
List<Tile> bottomTiles;
if (!TryBuildTargetMap(top, bottom, out targetById, out bottomTiles))
{
Log("ERROR: Could not map top pattern to bottom board.");
return;
}
var targetIds = new HashSet<long>(targetById.Keys);
var current = ReadCurrentStatesById(targetIds);
if (current.Count == 0)
{
Log("ERROR: Could not read current bottom states.");
return;
}
int maxStateSeen = 0;
foreach (var v in targetById.Values) if (v > maxStateSeen) maxStateSeen = v;
foreach (var v in current.Values) if (v > maxStateSeen) maxStateSeen = v;
int cycle = maxStateSeen + 1;
if (cycle < 2 || cycle > 8) cycle = 4;
Log($"Mapped tiles: {targetById.Count}");
Log($"State cycle guessed: {cycle}");
int objective = CalcObjective(current, targetById, cycle);
Log($"Initial distance: {objective}");
if (objective == 0)
{
Log("Already solved.");
return;
}
if (Self == null || Self.Location == null)
{
Log("ERROR: Self position unavailable.");
return;
}
var rng = new Random();
int stagnation = 0;
const int MAX_MOVES = 1200;
for (int step = 1; step <= MAX_MOVES; step++)
{
var meX = Self?.Location?.X ?? -1;
var meY = Self?.Location?.Y ?? -1;
Tile chosen = null;
var needTiles = bottomTiles
.Where(t => current.ContainsKey(t.Id) && targetById.ContainsKey(t.Id))
.Select(t => new {
Tile = t,
Need = CircularNeed(current[t.Id], targetById[t.Id], cycle),
D = (meX >= 0 && meY >= 0) ? Dist(meX, meY, t.X, t.Y) : 9999
})
.Where(x => x.Need > 0)
.OrderByDescending(x => x.Need)
.ThenBy(x => x.D)
.ToList();
if (needTiles.Count == 0)
{
Log("Solved (need list empty).");
break;
}
if (stagnation >= 12)
{
chosen = needTiles[rng.Next(needTiles.Count)].Tile;
}
else
{
chosen = needTiles[0].Tile;
}
Log($"[{step}] Move to ({chosen.X},{chosen.Y}) id={chosen.Id} state={current[chosen.Id]} target={targetById[chosen.Id]}");
Move(chosen.X, chosen.Y);
bool arrived = WaitUntilAt(chosen.X, chosen.Y, 2800);
if (!arrived)
{
Move(chosen.X, chosen.Y);
WaitUntilAt(chosen.X, chosen.Y, 2000);
}
Delay(220);
current = ReadCurrentStatesById(targetIds);
int newObj = CalcObjective(current, targetById, cycle);
int delta = objective - newObj;
Log($" distance: {objective} -> {newObj} (delta {delta})");
if (newObj <= 0)
{
Log("=== Done: bottom now matches top pattern ===");
return;
}
if (newObj < objective) stagnation = 0;
else stagnation++;
objective = newObj;
Delay(120);
}
var finalStates = ReadCurrentStatesById(targetIds);
int finalObj = CalcObjective(finalStates, targetById, cycle);
if (finalObj == 0)
Log("=== Done: bottom now matches top pattern ===");
else
Log($"Stopped. Remaining distance: {finalObj}. Re-run script to continue.");