using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; const int CLICK_DELAY = 3000; const int GAME_MIN_X = 9; const int GAME_MAX_X = 26; const int GAME_MIN_Y = 12; const int GAME_MAX_Y = 29; const int GRID_W = 18; const int GRID_H = 18; const int TOTAL = 324; const int KIND_TILE = 3666; const int MAX_CALC_MS = 4000; int[,] grid = new int[GRID_W, GRID_H]; int[,] component = new int[GRID_W, GRID_H]; int numComponents = 0; int[] compColorArr; int[][] compNeighborsArr; 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; } } void ReadGrid() { Array.Clear(grid, 0, grid.Length); foreach (var item in FloorItems) { if (item == null) continue; if (GetKind(item) != KIND_TILE) continue; int x = item.Location.X, y = item.Location.Y; if (x < GAME_MIN_X || x > GAME_MAX_X || y < GAME_MIN_Y || y > GAME_MAX_Y) continue; int gx = x - GAME_MIN_X, gy = GAME_MAX_Y - y; int state = GetState(item); if (gx >= 0 && gx < GRID_W && gy >= 0 && gy < GRID_H) grid[gx, gy] = state; } } void BuildComponents() { for (int x = 0; x < GRID_W; x++) for (int y = 0; y < GRID_H; y++) component[x,y] = -1; var colors = new List(); var neighbors = new List>(); numComponents = 0; for (int sx = 0; sx < GRID_W; sx++) { for (int sy = 0; sy < GRID_H; sy++) { if (component[sx,sy] >= 0) continue; int cid = numComponents++; int color = grid[sx,sy]; colors.Add(color); neighbors.Add(new HashSet()); var q = new Queue<(int,int)>(); q.Enqueue((sx,sy)); component[sx,sy] = cid; while (q.Count > 0) { var (x,y) = q.Dequeue(); foreach (var (nx,ny) in new[]{(x-1,y),(x+1,y),(x,y-1),(x,y+1)}) { if (nx < 0 || nx >= GRID_W || ny < 0 || ny >= GRID_H) continue; if (component[nx,ny] >= 0) { if (component[nx,ny] != cid) neighbors[cid].Add(component[nx,ny]); continue; } if (grid[nx,ny] == color) { component[nx,ny] = cid; q.Enqueue((nx,ny)); } } } } } for (int c = 0; c < numComponents; c++) foreach (int n in neighbors[c].ToList()) neighbors[n].Add(c); compColorArr = colors.ToArray(); compNeighborsArr = neighbors.Select(s => s.ToArray()).ToArray(); } // HashSet based - slower but correct for any size HashSet FloodComp(HashSet flooded, int color) { var result = new HashSet(flooded); bool changed = true; while (changed) { changed = false; for (int c = 0; c < numComponents; c++) { if (result.Contains(c)) continue; if (compColorArr[c] != color) continue; foreach (int n in compNeighborsArr[c]) { if (result.Contains(n)) { result.Add(c); changed = true; break; } } } } return result; } HashSet BorderColors(HashSet flooded) { var colors = new HashSet(); foreach (int c in flooded) foreach (int n in compNeighborsArr[c]) if (!flooded.Contains(n)) colors.Add(compColorArr[n]); return colors; } // Heuristic: max BFS distance on component graph int Heuristic(HashSet flooded) { if (flooded.Count == numComponents) return 0; var dist = new int[numComponents]; for (int i = 0; i < numComponents; i++) dist[i] = flooded.Contains(i) ? 0 : 999; var q = new Queue(); foreach (int i in flooded) q.Enqueue(i); int maxD = 0; while (q.Count > 0) { int c = q.Dequeue(); foreach (int n in compNeighborsArr[c]) { int nd = dist[c] + 1; if (nd < dist[n]) { dist[n] = nd; maxD = Math.Max(maxD, nd); q.Enqueue(n); } } } return maxD; } // LOOKAHEAD GREEDY - key to good solutions! int EvalLookahead(HashSet flooded, int depth) { if (flooded.Count == numComponents) return 10000 - depth; if (depth <= 0) return flooded.Count * 10 - Heuristic(flooded); var borders = BorderColors(flooded); int best = flooded.Count; foreach (int c in borders) { var next = FloodComp(flooded, c); int score = EvalLookahead(next, depth - 1); if (score > best) best = score; } return best; } List SolveLookaheadGreedy(HashSet flooded, int lookahead) { var moves = new List(); while (flooded.Count < numComponents && moves.Count < 40) { var borders = BorderColors(flooded); int bestC = 0, bestScore = -99999; foreach (int c in borders) { var next = FloodComp(flooded, c); int score; if (next.Count == numComponents) score = 100000; else score = EvalLookahead(next, lookahead - 1); if (score > bestScore) { bestScore = score; bestC = c; } } if (bestC == 0) break; moves.Add(bestC); flooded = FloodComp(flooded, bestC); } return moves; } // IDA* for optimal solution volatile int globalBest = 99; List globalBestMoves = null; object lockObj = new object(); DateTime startTime; bool DFS(HashSet flooded, List moves, int maxDepth) { if ((DateTime.Now - startTime).TotalMilliseconds > MAX_CALC_MS) return false; if (flooded.Count == numComponents) { lock (lockObj) { if (moves.Count < globalBest) { globalBest = moves.Count; globalBestMoves = new List(moves); } } return true; } int h = Heuristic(flooded); if (moves.Count + h >= globalBest) return false; if (moves.Count >= maxDepth) return false; var borders = BorderColors(flooded); // Order by gain var sorted = borders.Select(c => { var next = FloodComp(flooded, c); return (c, next.Count - flooded.Count, next); }).OrderByDescending(x => x.Item2).ToList(); bool found = false; foreach (var (c, gain, next) in sorted) { moves.Add(c); if (DFS(next, moves, maxDepth)) found = true; moves.RemoveAt(moves.Count - 1); if ((DateTime.Now - startTime).TotalMilliseconds > MAX_CALC_MS) break; } return found; } List Solve(HashSet startFlooded) { startTime = DateTime.Now; // Step 1: Fast lookahead greedy (depth 4) var greedy4 = SolveLookaheadGreedy(new HashSet(startFlooded), 4); Log($"Lookahead-4: {greedy4.Count} moves"); globalBest = greedy4.Count; globalBestMoves = greedy4; // Step 2: Try deeper lookahead if we have time if ((DateTime.Now - startTime).TotalMilliseconds < 1000) { var greedy5 = SolveLookaheadGreedy(new HashSet(startFlooded), 5); Log($"Lookahead-5: {greedy5.Count} moves"); if (greedy5.Count < globalBest) { globalBest = greedy5.Count; globalBestMoves = greedy5; } } // Step 3: Parallel IDA* to find optimal int h = Heuristic(startFlooded); var borders = BorderColors(startFlooded); var firstMoves = borders.Select(c => { var next = FloodComp(startFlooded, c); return (c, next.Count - startFlooded.Count, next); }).OrderByDescending(x => x.Item2).ToList(); Parallel.ForEach(firstMoves, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 2 }, fm => { if ((DateTime.Now - startTime).TotalMilliseconds > MAX_CALC_MS) return; var (c, gain, next) = fm; var moves = new List { c }; for (int maxD = h; maxD < globalBest; maxD++) { if ((DateTime.Now - startTime).TotalMilliseconds > MAX_CALC_MS) break; DFS(next, moves, maxD); } }); return globalBestMoves; } HashSet InitFlooded() { var flooded = new HashSet { component[0,0] }; return FloodComp(flooded, compColorArr[component[0,0]]); } void Click(int color) { foreach (var item in FloorItems) { if (item == null) continue; if (GetKind(item) != KIND_TILE) continue; int x = item.Location.X, y = item.Location.Y; if (x < GAME_MIN_X || x > GAME_MAX_X || y < GAME_MIN_Y || y > GAME_MAX_Y) continue; int state = GetState(item); if (state == color) { Send(Out["ClickFurni"], (int)item.Id, 0); return; } } } Log("═══════════════════════════════════════════"); Log(" FLOOD-IT - LOOKAHEAD + IDA*"); Log("═══════════════════════════════════════════"); while (Run) { ReadGrid(); BuildComponents(); var flooded = InitFlooded(); Log($"Components: {numComponents}, Flooded: {flooded.Count}"); if (flooded.Count == numComponents) { Log("COMPLETE!"); Delay(2000); continue; } Log("Solving..."); var sw = System.Diagnostics.Stopwatch.StartNew(); var solution = Solve(flooded); sw.Stop(); Log($"BEST: {string.Join(",", solution)} ({solution.Count} moves) in {sw.ElapsedMilliseconds}ms"); foreach (var c in solution) { Click(c); Log($"Click {c}"); Delay(CLICK_DELAY); } Delay(1000); }