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 Tiles = new List(); public double AvgZ; public HashSet StateSet = new HashSet(); 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) ? "" : n; } catch { return ""; } } List ReadAllFloorTiles() { var list = new List(); 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 BuildClusters(List tilesOfKind) { var byPos = tilesOfKind.ToDictionary(t => PosKey(t.X, t.Y), t => t); var visited = new HashSet(); var clusters = new List(); 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(); 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 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 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 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(); var yToI = new Dictionary(); 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(); 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 targetByBottomId, out List bottomTilesOrdered) { targetByBottomId = new Dictionary(); bottomTilesOrdered = new List(); 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>(); 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 ReadCurrentStatesById(HashSet ids) { var map = new Dictionary(); 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 current, Dictionary 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 targetById; List bottomTiles; if (!TryBuildTargetMap(top, bottom, out targetById, out bottomTiles)) { Log("ERROR: Could not map top pattern to bottom board."); return; } var targetIds = new HashSet(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.");