Keeps the repo root clean - only README.md visible on landing page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
245 lines
7.2 KiB
C#
245 lines
7.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
const int CLICK_DELAY = 2500;
|
|
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;
|
|
|
|
Dictionary<int, int> tileIdByColor = new Dictionary<int, int>();
|
|
int[,] grid = new int[GRID_W, GRID_H];
|
|
|
|
int GetState(dynamic item) { try { return int.Parse(item.State?.ToString() ?? "0"); } catch { return 0; } }
|
|
int GetId(dynamic item) { try { return (int)item.Id; } catch { return 0; } }
|
|
int GetKind(dynamic item) { try { return (int)item.Kind; } catch { return -1; } }
|
|
|
|
void ReadGrid()
|
|
{
|
|
Array.Clear(grid, 0, grid.Length);
|
|
tileIdByColor.Clear();
|
|
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), id = GetId(item);
|
|
if (gx >= 0 && gx < GRID_W && gy >= 0 && gy < GRID_H)
|
|
{
|
|
grid[gx, gy] = state;
|
|
if (!tileIdByColor.ContainsKey(state)) tileIdByColor[state] = id;
|
|
}
|
|
}
|
|
}
|
|
|
|
HashSet<(int,int)> Flood(HashSet<(int,int)> area, int color)
|
|
{
|
|
var result = new HashSet<(int,int)>(area);
|
|
var q = new Queue<(int,int)>();
|
|
foreach (var p in area) q.Enqueue(p);
|
|
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 (result.Contains((nx,ny))) continue;
|
|
if (grid[nx,ny] == color) { result.Add((nx,ny)); q.Enqueue((nx,ny)); }
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
HashSet<(int,int)> InitArea()
|
|
{
|
|
var area = new HashSet<(int,int)>{(0,0)};
|
|
return Flood(area, grid[0,0]);
|
|
}
|
|
|
|
HashSet<int> BorderColors(HashSet<(int,int)> area)
|
|
{
|
|
var c = new HashSet<int>();
|
|
foreach (var (x,y) in area)
|
|
{
|
|
if (x > 0 && !area.Contains((x-1,y))) c.Add(grid[x-1,y]);
|
|
if (x < GRID_W-1 && !area.Contains((x+1,y))) c.Add(grid[x+1,y]);
|
|
if (y > 0 && !area.Contains((x,y-1))) c.Add(grid[x,y-1]);
|
|
if (y < GRID_H-1 && !area.Contains((x,y+1))) c.Add(grid[x,y+1]);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
HashSet<int> RemainingColors(HashSet<(int,int)> area)
|
|
{
|
|
var colors = new HashSet<int>();
|
|
for (int x = 0; x < GRID_W; x++)
|
|
for (int y = 0; y < GRID_H; y++)
|
|
if (!area.Contains((x,y))) colors.Add(grid[x,y]);
|
|
return colors;
|
|
}
|
|
|
|
int CountRegions(HashSet<(int,int)> area)
|
|
{
|
|
var visited = new bool[GRID_W, GRID_H];
|
|
foreach (var (x,y) in area) visited[x,y] = true;
|
|
int regions = 0;
|
|
|
|
for (int sx = 0; sx < GRID_W; sx++)
|
|
{
|
|
for (int sy = 0; sy < GRID_H; sy++)
|
|
{
|
|
if (visited[sx,sy]) continue;
|
|
regions++;
|
|
var q = new Queue<(int,int)>();
|
|
q.Enqueue((sx,sy));
|
|
visited[sx,sy] = true;
|
|
int c = grid[sx,sy];
|
|
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 (visited[nx,ny]) continue;
|
|
if (grid[nx,ny] == c) { visited[nx,ny] = true; q.Enqueue((nx,ny)); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return regions;
|
|
}
|
|
|
|
int Heuristic(HashSet<(int,int)> area)
|
|
{
|
|
var remaining = RemainingColors(area);
|
|
int regions = CountRegions(area);
|
|
return Math.Max(remaining.Count, (regions + 1) / 2);
|
|
}
|
|
|
|
bool EliminatesColor(HashSet<(int,int)> area, int color)
|
|
{
|
|
var newArea = Flood(area, color);
|
|
for (int x = 0; x < GRID_W; x++)
|
|
for (int y = 0; y < GRID_H; y++)
|
|
if (!newArea.Contains((x,y)) && grid[x,y] == color) return false;
|
|
return true;
|
|
}
|
|
|
|
int Eval(HashSet<(int,int)> area, int depth, int alpha)
|
|
{
|
|
if (area.Count == TOTAL) return 1000 - depth;
|
|
if (depth <= 0) return area.Count - Heuristic(area) * 10;
|
|
|
|
var borders = BorderColors(area);
|
|
int best = -999;
|
|
|
|
var moves = borders.Select(c => {
|
|
var next = Flood(area, c);
|
|
bool elim = EliminatesColor(area, c);
|
|
return (c, next.Count - area.Count, elim, next);
|
|
}).OrderByDescending(m => m.Item3 ? 1000 : 0)
|
|
.ThenByDescending(m => m.Item2).ToList();
|
|
|
|
foreach (var (c, gain, elim, next) in moves)
|
|
{
|
|
int score = Eval(next, depth - 1, best);
|
|
if (score > best) best = score;
|
|
if (best >= alpha) break;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
(int color, int score) BestMove(HashSet<(int,int)> area, int depth)
|
|
{
|
|
var borders = BorderColors(area);
|
|
int bestC = 0, bestS = -9999;
|
|
|
|
var moves = borders.Select(c => {
|
|
var next = Flood(area, c);
|
|
bool elim = EliminatesColor(area, c);
|
|
return (c, next.Count - area.Count, elim, next);
|
|
}).OrderByDescending(m => m.Item3 ? 1000 : 0)
|
|
.ThenByDescending(m => m.Item2).ToList();
|
|
|
|
foreach (var (c, gain, elim, next) in moves)
|
|
{
|
|
int score;
|
|
if (next.Count == TOTAL) score = 10000;
|
|
else score = Eval(next, depth - 1, bestS) + (elim ? 50 : 0);
|
|
|
|
if (score > bestS) { bestS = score; bestC = c; }
|
|
}
|
|
return (bestC, bestS);
|
|
}
|
|
|
|
List<int> Solve()
|
|
{
|
|
var moves = new List<int>();
|
|
var area = InitArea();
|
|
|
|
while (area.Count < TOTAL && moves.Count < 32)
|
|
{
|
|
int remaining = TOTAL - area.Count;
|
|
int depth = remaining > 200 ? 3 : remaining > 100 ? 4 : remaining > 50 ? 5 : 6;
|
|
|
|
var (c, s) = BestMove(area, depth);
|
|
if (c == 0) break;
|
|
|
|
moves.Add(c);
|
|
area = Flood(area, c);
|
|
}
|
|
return moves;
|
|
}
|
|
|
|
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 BOT - RESEARCH BASED");
|
|
Log("═══════════════════════════════════════════");
|
|
|
|
while (Run)
|
|
{
|
|
ReadGrid();
|
|
var area = InitArea();
|
|
Log($"Start: {area.Count}/{TOTAL}");
|
|
|
|
if (area.Count == TOTAL) { Log("COMPLETE!"); Delay(2000); continue; }
|
|
|
|
Log("Solving...");
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
var solution = Solve();
|
|
sw.Stop();
|
|
Log($"Solution: {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);
|
|
}
|