Keeps the repo root clean - only README.md visible on landing page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
361 lines
10 KiB
C#
361 lines
10 KiB
C#
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<int>();
|
|
var neighbors = new List<HashSet<int>>();
|
|
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<int>());
|
|
|
|
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<int> FloodComp(HashSet<int> flooded, int color)
|
|
{
|
|
var result = new HashSet<int>(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<int> BorderColors(HashSet<int> flooded)
|
|
{
|
|
var colors = new HashSet<int>();
|
|
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<int> 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<int>();
|
|
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<int> 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<int> SolveLookaheadGreedy(HashSet<int> flooded, int lookahead)
|
|
{
|
|
var moves = new List<int>();
|
|
|
|
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<int> globalBestMoves = null;
|
|
object lockObj = new object();
|
|
DateTime startTime;
|
|
|
|
bool DFS(HashSet<int> flooded, List<int> 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<int>(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<int> Solve(HashSet<int> startFlooded)
|
|
{
|
|
startTime = DateTime.Now;
|
|
|
|
// Step 1: Fast lookahead greedy (depth 4)
|
|
var greedy4 = SolveLookaheadGreedy(new HashSet<int>(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<int>(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<int> { c };
|
|
|
|
for (int maxD = h; maxD < globalBest; maxD++)
|
|
{
|
|
if ((DateTime.Now - startTime).TotalMilliseconds > MAX_CALC_MS) break;
|
|
DFS(next, moves, maxD);
|
|
}
|
|
});
|
|
|
|
return globalBestMoves;
|
|
}
|
|
|
|
HashSet<int> InitFlooded()
|
|
{
|
|
var flooded = new HashSet<int> { 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);
|
|
}
|