xabbo-scripts/Scripts/ColorPuzzleSolver_AutoCalib.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

513 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
// Color Puzzle Solver v2
// - Auto calibration of arrow -> move mapping
// - Waits for real state change after every click
const int TILE_KIND = 3696;
const int ARROW_KIND = 17851;
const int GRID_X_MIN = 36;
const int GRID_X_MAX = 39;
const int GRID_Y_MIN = 27;
const int GRID_Y_MAX = 30;
const int CLICK_SETTLE_DELAY_MS = 250;
const int WAIT_CHANGE_TIMEOUT_MS = 6000;
const int WAIT_CHANGE_POLL_MS = 120;
const int MAX_STEPS = 140;
const int BFS_MAX_NODES = 4_000_000;
const int IDA_MAX_SEC = 12;
const bool AUTO_QUEUE_START = true;
const long TRANSPORTER_ID = 759030883;
const int QUEUE_CLICK_INTERVAL_MS = 5000;
const int WAIT_PUZZLE_POLL_MS = 250;
const int WAIT_PUZZLE_LOG_MS = 5000;
const bool REQUIRE_SELF_IN_PLAYZONE = true;
const int PLAY_X_MIN = 34;
const int PLAY_X_MAX = 41;
const int PLAY_Y_MIN = 26;
const int PLAY_Y_MAX = 31;
const bool REQUIRE_SELF_MIN_Z = true;
const double SELF_MIN_Z = 17.0;
const bool REQUIRE_PLAYER_QUEUE_CLEAR = true;
const string WATCH_PLAYER_NAME = "gracie";
const int WATCH_QUEUE_X = 33;
const int WATCH_QUEUE_Y = 19;
const double WATCH_QUEUE_Z = 4.5;
const double WATCH_QUEUE_Z_TOL = 1.0;
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; }
}
uint EncodeGrid(int[,] g)
{
uint s = 0;
for (int r = 0; r < 4; r++)
for (int c = 0; c < 4; c++)
s |= ((uint)(g[r, c] & 3)) << (2 * (r * 4 + c));
return s;
}
bool TryReadGrid(out uint state, out string dump)
{
int[,] grid = new int[4, 4];
bool[,] found = new bool[4, 4];
foreach (var item in FloorItems)
{
if (item == null) continue;
if (GetKind(item) != TILE_KIND) continue;
int x = item.Location.X;
int y = item.Location.Y;
double z = item.Location.Z;
if (x < GRID_X_MIN || x > GRID_X_MAX) continue;
if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue;
if (z < 18.4) continue;
int row = y - GRID_Y_MIN;
int col = x - GRID_X_MIN;
grid[row, col] = GetState(item);
found[row, col] = true;
}
int cnt = 0;
for (int r = 0; r < 4; r++)
for (int c = 0; c < 4; c++)
if (found[r, c]) cnt++;
if (cnt < 16)
{
state = 0;
dump = "";
return false;
}
state = EncodeGrid(grid);
dump = string.Join(" | ", Enumerable.Range(0, 4).Select(r =>
$"R{r}[{grid[r,0]},{grid[r,1]},{grid[r,2]},{grid[r,3]}]"));
return true;
}
uint RowLeft(uint s, int r)
{
int sh = r * 8;
uint row = (s >> sh) & 0xFFu;
uint rot = ((row >> 2) | (row << 6)) & 0xFFu;
return (s & ~(0xFFu << sh)) | (rot << sh);
}
uint RowRight(uint s, int r)
{
int sh = r * 8;
uint row = (s >> sh) & 0xFFu;
uint rot = ((row << 2) | (row >> 6)) & 0xFFu;
return (s & ~(0xFFu << sh)) | (rot << sh);
}
uint ColUp(uint s, int c)
{
int b = c * 2;
uint v0 = (s >> b) & 3u;
uint v1 = (s >> (b + 8)) & 3u;
uint v2 = (s >> (b + 16)) & 3u;
uint v3 = (s >> (b + 24)) & 3u;
uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24));
return (s & mask) | (v1 << b) | (v2 << (b + 8)) | (v3 << (b + 16)) | (v0 << (b + 24));
}
uint ColDown(uint s, int c)
{
int b = c * 2;
uint v0 = (s >> b) & 3u;
uint v1 = (s >> (b + 8)) & 3u;
uint v2 = (s >> (b + 16)) & 3u;
uint v3 = (s >> (b + 24)) & 3u;
uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24));
return (s & mask) | (v3 << b) | (v0 << (b + 8)) | (v1 << (b + 16)) | (v2 << (b + 24));
}
uint ApplyMove(uint s, int m)
{
if (m < 4) return RowLeft(s, m);
if (m < 8) return RowRight(s, m - 4);
if (m < 12) return ColUp(s, m - 8);
return ColDown(s, m - 12);
}
int InverseMove(int m)
{
if (m < 4) return m + 4;
if (m < 8) return m - 4;
if (m < 12) return m + 4;
return m - 4;
}
string MoveName(int m)
{
if (m < 4) return $"Row{m} LEFT";
if (m < 8) return $"Row{m - 4} RIGHT";
if (m < 12) return $"Col{m - 8} UP";
return $"Col{m - 12} DOWN";
}
int DetectMove(uint before, uint after)
{
int hit = -1;
for (int m = 0; m < 16; m++)
{
if (ApplyMove(before, m) != after) continue;
if (hit != -1) return -2;
hit = m;
}
return hit;
}
List<int> SolveBfs(uint start, uint goal)
{
if (start == goal) return new List<int>();
var visited = new Dictionary<uint, (uint parent, int move)>();
var queue = new Queue<uint>();
visited[start] = (start, -1);
queue.Enqueue(start);
int nodes = 0;
bool found = false;
while (queue.Count > 0 && nodes < BFS_MAX_NODES)
{
uint cur = queue.Dequeue();
nodes++;
for (int m = 0; m < 16; m++)
{
uint nxt = ApplyMove(cur, m);
if (visited.ContainsKey(nxt)) continue;
visited[nxt] = (cur, m);
if (nxt == goal)
{
found = true;
queue.Clear();
break;
}
queue.Enqueue(nxt);
}
}
if (!found) return null;
var sol = new List<int>();
uint s = goal;
while (s != start)
{
var p = visited[s];
sol.Add(p.move);
s = p.parent;
}
sol.Reverse();
return sol;
}
List<int> SolveIda(uint start, uint goal)
{
if (start == goal) return new List<int>();
var t0 = DateTime.Now;
int H(uint st)
{
int mis = 0;
for (int i = 0; i < 16; i++)
{
int a = (int)((st >> (i * 2)) & 3u);
int b = (int)((goal >> (i * 2)) & 3u);
if (a != b) mis++;
}
return (mis + 3) / 4;
}
List<int> best = null;
bool timeout = false;
bool Dfs(uint st, List<int> path, int maxDepth)
{
if (timeout) return false;
if ((DateTime.Now - t0).TotalSeconds > IDA_MAX_SEC)
{
timeout = true;
return false;
}
if (st == goal)
{
best = new List<int>(path);
return true;
}
int h = H(st);
if (path.Count + h > maxDepth) return false;
int block = path.Count > 0 ? InverseMove(path[path.Count - 1]) : -1;
for (int m = 0; m < 16; m++)
{
if (m == block) continue;
path.Add(m);
if (Dfs(ApplyMove(st, m), path, maxDepth)) return true;
path.RemoveAt(path.Count - 1);
if (timeout) return false;
}
return false;
}
int d0 = H(start);
for (int d = d0; d <= 22 && !timeout; d++)
{
if (Dfs(start, new List<int>(), d)) break;
}
return best;
}
List<int> Solve(uint start, uint goal)
{
var bfs = SolveBfs(start, goal);
if (bfs != null) return bfs;
return SolveIda(start, goal);
}
bool ClickAndWaitChange(long furniId, uint before, out uint after, out string dumpAfter)
{
Send(Out["ClickFurni"], (int)furniId, 0);
Delay(CLICK_SETTLE_DELAY_MS);
int waited = 0;
while (waited < WAIT_CHANGE_TIMEOUT_MS)
{
if (TryReadGrid(out after, out dumpAfter) && after != before)
return true;
Delay(WAIT_CHANGE_POLL_MS);
waited += WAIT_CHANGE_POLL_MS;
}
after = before;
dumpAfter = "";
return false;
}
Dictionary<string, long> ReadArrowIds()
{
var arrowIds = new Dictionary<string, long>();
foreach (var item in FloorItems)
{
if (item == null) continue;
if (GetKind(item) != ARROW_KIND) continue;
int x = item.Location.X;
int y = item.Location.Y;
if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX)
arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id;
else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX)
arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id;
else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX)
arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id;
else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX)
arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id;
}
return arrowIds;
}
bool IsSelfInPlayZone()
{
try
{
int x = Self.Location.X;
int y = Self.Location.Y;
double z = Self.Location.Z;
bool inRect = x >= PLAY_X_MIN && x <= PLAY_X_MAX && y >= PLAY_Y_MIN && y <= PLAY_Y_MAX;
bool inZ = !REQUIRE_SELF_MIN_Z || z >= SELF_MIN_Z;
return inRect && inZ;
}
catch
{
return false;
}
}
bool IsWatchedPlayerAtQueueSpot()
{
try
{
var u = Users.FirstOrDefault(x =>
x != null &&
x.Name != null &&
x.Name.Equals(WATCH_PLAYER_NAME, StringComparison.OrdinalIgnoreCase));
if (u == null || u.Location == null) return false;
int x = u.Location.X;
int y = u.Location.Y;
double z = u.Location.Z;
return x == WATCH_QUEUE_X && y == WATCH_QUEUE_Y && Math.Abs(z - WATCH_QUEUE_Z) <= WATCH_QUEUE_Z_TOL;
}
catch
{
return false;
}
}
Log("=== Color Puzzle Auto-Solver (AutoCalib + WaitChange) ===");
Dictionary<string, long> arrowIds = null;
uint current;
string dumpNow;
int sinceQueueClick = QUEUE_CLICK_INTERVAL_MS;
int sinceLog = WAIT_PUZZLE_LOG_MS;
while (true)
{
bool hasGrid = TryReadGrid(out current, out dumpNow);
var probeArrows = ReadArrowIds();
bool hasArrows = probeArrows.Count == 16;
bool inPlayZone = !REQUIRE_SELF_IN_PLAYZONE || IsSelfInPlayZone();
bool queueClear = !REQUIRE_PLAYER_QUEUE_CLEAR || !IsWatchedPlayerAtQueueSpot();
if (hasGrid && hasArrows && inPlayZone && queueClear)
{
arrowIds = probeArrows;
break;
}
if (AUTO_QUEUE_START && sinceQueueClick >= QUEUE_CLICK_INTERVAL_MS)
{
Send(Out["ClickFurni"], (int)TRANSPORTER_ID, 0);
Log($"Queue: Klick Transporter {TRANSPORTER_ID}...");
sinceQueueClick = 0;
}
if (sinceLog >= WAIT_PUZZLE_LOG_MS)
{
string selfPos = "?";
try { selfPos = $"{Self.Location.X},{Self.Location.Y},{Self.Location.Z:F2}"; } catch { }
Log($"Warte auf Spielstart... Grid={(hasGrid ? "ok" : "no")}, Pfeile={probeArrows.Count}/16, InZone={(inPlayZone ? "yes" : "no")}, QueueClear={(queueClear ? "yes" : "no")}, Self={selfPos}");
sinceLog = 0;
}
Delay(WAIT_PUZZLE_POLL_MS);
sinceQueueClick += WAIT_PUZZLE_POLL_MS;
sinceLog += WAIT_PUZZLE_POLL_MS;
}
Log("Puzzle erkannt. Starte Solver...");
Log($"Pfeile: {arrowIds.Count}/16");
int[] targetRows = new int[4];
bool targetFound = false;
foreach (var item in FloorItems)
{
if (item == null) continue;
if (GetKind(item) != TILE_KIND) continue;
if (item.Location.X != 41) continue;
int y = item.Location.Y;
if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue;
targetRows[y - GRID_Y_MIN] = GetState(item);
targetFound = true;
}
if (!targetFound) targetRows = new[] { 1, 2, 3, 0 };
int[,] tgt = new int[4, 4];
for (int r = 0; r < 4; r++)
for (int c = 0; c < 4; c++)
tgt[r, c] = targetRows[r];
uint goal = EncodeGrid(tgt);
Log($"Ziel: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}");
Log($"Start: {dumpNow}");
var moveToKey = new Dictionary<int, string>();
var keyToMove = new Dictionary<string, int>();
var allKeys = arrowIds.Keys.OrderBy(k => k).ToList();
for (int step = 1; step <= MAX_STEPS; step++)
{
if (current == goal)
{
Log("=== Geloest: alle 4 Reihen korrekt ===");
return;
}
var plan = Solve(current, goal);
if (plan == null || plan.Count == 0)
{
Log("ERROR: Kein Plan vom aktuellen Zustand.");
return;
}
int wanted = plan[0];
string key;
bool probing = false;
if (moveToKey.ContainsKey(wanted))
{
key = moveToKey[wanted];
}
else
{
key = allKeys.FirstOrDefault(k => !keyToMove.ContainsKey(k));
if (key == null)
{
key = allKeys[0];
}
probing = true;
}
long id = arrowIds[key];
Log($"[{step}] want {MoveName(wanted)} | click {key}" + (probing ? " (probe)" : ""));
if (!ClickAndWaitChange(id, current, out uint after, out string dumpAfter))
{
Log(" Kein Move erkannt (Timeout), gleicher Schritt nochmal.");
continue;
}
int actual = DetectMove(current, after);
if (actual >= 0)
{
moveToKey[actual] = key;
keyToMove[key] = actual;
if (actual != wanted)
Log($" AutoCalib: {key} == {MoveName(actual)} (nicht {MoveName(wanted)})");
}
else if (actual == -1)
{
Log($" Unbekannter Transition-Delta, weiter mit Re-Plan. State: {dumpAfter}");
}
else
{
Log($" Mehrdeutiger Delta, weiter mit Re-Plan. State: {dumpAfter}");
}
current = after;
if (step % 10 == 0)
Log($" Calib: {moveToKey.Count}/16 Moves gemappt");
}
Log("Nicht fertig in MAX_STEPS. Script einfach nochmal starten.");