Keeps the repo root clean - only README.md visible on landing page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
578 lines
15 KiB
C#
578 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
// Color Puzzle Solver (fix): solves all 4 rows reliably.
|
|
// Keeps desync handling + auto direction flip detection.
|
|
|
|
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_DELAY_MS = 950;
|
|
|
|
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; }
|
|
}
|
|
|
|
int[,] ReadGridFromRoom()
|
|
{
|
|
int[,] g = 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 r = y - GRID_Y_MIN;
|
|
int c = x - GRID_X_MIN;
|
|
g[r, c] = GetState(item);
|
|
found[r, c] = true;
|
|
}
|
|
|
|
int cnt = 0;
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
if (found[r, c]) cnt++;
|
|
|
|
return cnt == 16 ? g : null;
|
|
}
|
|
|
|
string GridDump(int[,] g)
|
|
{
|
|
return string.Join(" | ", Enumerable.Range(0, 4).Select(r =>
|
|
$"R{r}[{g[r,0]},{g[r,1]},{g[r,2]},{g[r,3]}]"));
|
|
}
|
|
|
|
void SimMove(int[,] g, int m)
|
|
{
|
|
if (m < 4)
|
|
{
|
|
int r = m;
|
|
int t = g[r, 0]; g[r, 0] = g[r, 1]; g[r, 1] = g[r, 2]; g[r, 2] = g[r, 3]; g[r, 3] = t;
|
|
}
|
|
else if (m < 8)
|
|
{
|
|
int r = m - 4;
|
|
int t = g[r, 3]; g[r, 3] = g[r, 2]; g[r, 2] = g[r, 1]; g[r, 1] = g[r, 0]; g[r, 0] = t;
|
|
}
|
|
else if (m < 12)
|
|
{
|
|
int c = m - 8;
|
|
int t = g[0, c]; g[0, c] = g[1, c]; g[1, c] = g[2, c]; g[2, c] = g[3, c]; g[3, c] = t;
|
|
}
|
|
else
|
|
{
|
|
int c = m - 12;
|
|
int t = g[3, c]; g[3, c] = g[2, c]; g[2, c] = g[1, c]; g[1, c] = g[0, c]; g[0, c] = t;
|
|
}
|
|
}
|
|
|
|
List<int> SolveLayerByLayer(int[,] srcGrid, int[] tgtRows)
|
|
{
|
|
int[,] g = new int[4, 4];
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
g[r, c] = srcGrid[r, c];
|
|
|
|
var moves = new List<int>();
|
|
|
|
void Do(int m) { moves.Add(m); SimMove(g, m); }
|
|
|
|
void DoRowRight(int r, int times)
|
|
{
|
|
times = ((times % 4) + 4) % 4;
|
|
if (times == 3) { Do(r); return; }
|
|
for (int i = 0; i < times; i++) Do(r + 4);
|
|
}
|
|
void DoRowLeft(int r, int times)
|
|
{
|
|
times = ((times % 4) + 4) % 4;
|
|
if (times == 3) { Do(r + 4); return; }
|
|
for (int i = 0; i < times; i++) Do(r);
|
|
}
|
|
void DoColUp(int c, int times)
|
|
{
|
|
times = ((times % 4) + 4) % 4;
|
|
if (times == 3) { Do(c + 12); return; }
|
|
for (int i = 0; i < times; i++) Do(c + 8);
|
|
}
|
|
void DoColDown(int c, int times)
|
|
{
|
|
times = ((times % 4) + 4) % 4;
|
|
if (times == 3) { Do(c + 8); return; }
|
|
for (int i = 0; i < times; i++) Do(c + 12);
|
|
}
|
|
|
|
int C0 = tgtRows[0];
|
|
for (int c = 0; c < 4; c++)
|
|
{
|
|
if (g[0, c] == C0) continue;
|
|
|
|
int foundRow = -1;
|
|
for (int r = 1; r <= 3; r++)
|
|
if (g[r, c] == C0) { foundRow = r; break; }
|
|
|
|
if (foundRow >= 0)
|
|
{
|
|
DoColUp(c, foundRow);
|
|
}
|
|
else
|
|
{
|
|
bool found = false;
|
|
for (int r = 1; r <= 3 && !found; r++)
|
|
for (int c2 = 0; c2 < 4 && !found; c2++)
|
|
if (c2 != c && g[r, c2] == C0)
|
|
{
|
|
DoRowRight(r, (c - c2 + 4) % 4);
|
|
DoColUp(c, r);
|
|
found = true;
|
|
}
|
|
if (!found) return null;
|
|
}
|
|
}
|
|
|
|
int C1 = tgtRows[1];
|
|
for (int pass = 0; pass < 8; pass++)
|
|
{
|
|
int colW = -1;
|
|
for (int c = 0; c < 4; c++) if (g[1, c] != C1) { colW = c; break; }
|
|
if (colW < 0) break;
|
|
|
|
int srcR = -1, srcC = -1;
|
|
for (int r = 2; r <= 3 && srcR < 0; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
if (g[r, c] == C1) { srcR = r; srcC = c; break; }
|
|
if (srcR < 0) return null;
|
|
|
|
if (srcC != colW) DoRowRight(srcR, (colW - srcC + 4) % 4);
|
|
|
|
int k2 = srcR - 1;
|
|
DoRowLeft(1, 1);
|
|
DoColUp(colW, k2);
|
|
DoRowRight(1, 1);
|
|
DoColDown(colW, k2);
|
|
}
|
|
|
|
int C2 = tgtRows[2];
|
|
for (int pass = 0; pass < 8; pass++)
|
|
{
|
|
int colW = -1;
|
|
for (int c = 0; c < 4; c++) if (g[2, c] != C2) { colW = c; break; }
|
|
if (colW < 0) break;
|
|
|
|
int srcC = -1;
|
|
for (int c = 0; c < 4; c++) if (g[3, c] == C2) { srcC = c; break; }
|
|
if (srcC < 0) return null;
|
|
|
|
if (srcC != colW) DoRowRight(3, (colW - srcC + 4) % 4);
|
|
|
|
DoRowLeft(2, 1);
|
|
DoColUp(colW, 1);
|
|
DoRowRight(2, 1);
|
|
DoColDown(colW, 1);
|
|
}
|
|
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
if (g[r, c] != tgtRows[r]) return null;
|
|
|
|
bool changed = true;
|
|
while (changed)
|
|
{
|
|
changed = false;
|
|
for (int i = 0; i < moves.Count - 1; i++)
|
|
{
|
|
int a = moves[i], b = moves[i + 1];
|
|
bool cancel = false;
|
|
if (a < 4 && b == a + 4) cancel = true;
|
|
if (a >= 4 && a < 8 && b == a - 4) cancel = true;
|
|
if (a >= 8 && a < 12 && b == a + 4) cancel = true;
|
|
if (a >= 12 && b == a - 4) cancel = true;
|
|
if (cancel)
|
|
{
|
|
moves.RemoveAt(i + 1);
|
|
moves.RemoveAt(i);
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return moves;
|
|
}
|
|
|
|
bool IsSolvedForTarget(int[,] g, int[] targetRows)
|
|
{
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
if (g[r, c] != targetRows[r]) return false;
|
|
return true;
|
|
}
|
|
|
|
bool TryReadTargetRows(out int[] targetRows)
|
|
{
|
|
targetRows = null;
|
|
|
|
var byX = new Dictionary<int, (int[] states, bool[] found, int count)>();
|
|
|
|
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;
|
|
if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue;
|
|
if (x >= GRID_X_MIN && x <= GRID_X_MAX) continue;
|
|
|
|
if (!byX.ContainsKey(x))
|
|
byX[x] = (new int[4], new bool[4], 0);
|
|
|
|
var entry = byX[x];
|
|
int r = y - GRID_Y_MIN;
|
|
if (!entry.found[r])
|
|
{
|
|
entry.states[r] = GetState(item);
|
|
entry.found[r] = true;
|
|
entry.count++;
|
|
byX[x] = entry;
|
|
}
|
|
}
|
|
|
|
if (byX.Count == 0) return false;
|
|
|
|
var best = byX
|
|
.Select(kvp => new
|
|
{
|
|
X = kvp.Key,
|
|
States = kvp.Value.states,
|
|
Count = kvp.Value.count,
|
|
Dist = kvp.Key < GRID_X_MIN ? (GRID_X_MIN - kvp.Key) : (kvp.Key - GRID_X_MAX)
|
|
})
|
|
.OrderByDescending(x => x.Count)
|
|
.ThenBy(x => x.Dist)
|
|
.First();
|
|
|
|
if (best.Count < 4) return false;
|
|
|
|
targetRows = new int[4];
|
|
for (int r = 0; r < 4; r++) targetRows[r] = best.States[r];
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
uint ApplyMoveBits(uint s, int m)
|
|
{
|
|
if (m < 4)
|
|
{
|
|
int sh = m * 8;
|
|
uint row = (s >> sh) & 0xFFu;
|
|
uint rot = ((row >> 2) | (row << 6)) & 0xFFu;
|
|
return (s & ~(0xFFu << sh)) | (rot << sh);
|
|
}
|
|
if (m < 8)
|
|
{
|
|
int sh = (m - 4) * 8;
|
|
uint row = (s >> sh) & 0xFFu;
|
|
uint rot = ((row << 2) | (row >> 6)) & 0xFFu;
|
|
return (s & ~(0xFFu << sh)) | (rot << sh);
|
|
}
|
|
if (m < 12)
|
|
{
|
|
int b = (m - 8) * 2;
|
|
uint v0 = (s >> b) & 3u, v1 = (s >> (b + 8)) & 3u, v2 = (s >> (b + 16)) & 3u, 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));
|
|
}
|
|
{
|
|
int b = (m - 12) * 2;
|
|
uint v0 = (s >> b) & 3u, v1 = (s >> (b + 8)) & 3u, v2 = (s >> (b + 16)) & 3u, 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));
|
|
}
|
|
}
|
|
|
|
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[,] ApplyMovesToCopy(int[,] src, List<int> moves)
|
|
{
|
|
int[,] g = new int[4, 4];
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
g[r, c] = src[r, c];
|
|
foreach (int m in moves) SimMove(g, m);
|
|
return g;
|
|
}
|
|
|
|
Log("=== Color Puzzle Solver (fix all rows) ===");
|
|
|
|
var grid = ReadGridFromRoom();
|
|
if (grid == null)
|
|
{
|
|
Log("ERROR: Could not read full 4x4 grid.");
|
|
return;
|
|
}
|
|
Log($"Start: {GridDump(grid)}");
|
|
|
|
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, 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;
|
|
}
|
|
|
|
if (arrowIds.Count < 16)
|
|
{
|
|
Log($"ERROR: Missing arrows ({arrowIds.Count}/16).");
|
|
return;
|
|
}
|
|
|
|
int[] detectedTarget;
|
|
if (!TryReadTargetRows(out detectedTarget))
|
|
{
|
|
detectedTarget = new[] { 1, 2, 3, 0 };
|
|
Log("WARN: Target tiles not fully detected, using fallback target rows 1,2,3,0.");
|
|
}
|
|
|
|
var candidateTargets = new List<int[]>();
|
|
void AddTargetCandidate(int[] t)
|
|
{
|
|
if (t == null || t.Length != 4) return;
|
|
if (!candidateTargets.Any(x => x[0] == t[0] && x[1] == t[1] && x[2] == t[2] && x[3] == t[3]))
|
|
candidateTargets.Add(new[] { t[0], t[1], t[2], t[3] });
|
|
}
|
|
|
|
AddTargetCandidate(detectedTarget);
|
|
AddTargetCandidate(new[] { detectedTarget[3], detectedTarget[2], detectedTarget[1], detectedTarget[0] });
|
|
AddTargetCandidate(new[] { 1, 2, 3, 0 });
|
|
AddTargetCandidate(new[] { 0, 3, 2, 1 });
|
|
|
|
List<int> solution = null;
|
|
int[] targetRows = null;
|
|
|
|
foreach (var candidate in candidateTargets)
|
|
{
|
|
var s = SolveLayerByLayer(grid, candidate);
|
|
if (s == null || s.Count == 0) continue;
|
|
|
|
var check = ApplyMovesToCopy(grid, s);
|
|
if (!IsSolvedForTarget(check, candidate)) continue;
|
|
|
|
if (solution == null || s.Count < solution.Count)
|
|
{
|
|
solution = s;
|
|
targetRows = candidate;
|
|
}
|
|
}
|
|
|
|
if (solution == null || targetRows == null)
|
|
{
|
|
Log("ERROR: Could not build a valid full 4-row plan.");
|
|
return;
|
|
}
|
|
|
|
Log($"Target rows chosen: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}");
|
|
Log($"Plan length: {solution.Count} moves");
|
|
|
|
bool[] rowFlip = new bool[4];
|
|
bool[] colFlip = new bool[4];
|
|
|
|
string KeyForMove(int m)
|
|
{
|
|
if (m < 4) { int r = m; return rowFlip[r] ? $"right_{r}" : $"left_{r}"; }
|
|
if (m < 8) { int r = m - 4; return rowFlip[r] ? $"left_{r}" : $"right_{r}"; }
|
|
if (m < 12) { int c = m - 8; return colFlip[c] ? $"down_{c}" : $"up_{c}"; }
|
|
int cc = m - 12; return colFlip[cc] ? $"up_{cc}" : $"down_{cc}";
|
|
}
|
|
|
|
int[,] tgtGrid = new int[4, 4];
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
tgtGrid[r, c] = targetRows[r];
|
|
|
|
uint goalState = EncodeGrid(tgtGrid);
|
|
uint currentState = EncodeGrid(grid);
|
|
|
|
int moveIdx = 0;
|
|
int retries = 0;
|
|
const int MAX_RETRIES = 3;
|
|
|
|
while (moveIdx < solution.Count)
|
|
{
|
|
if (currentState == goalState)
|
|
{
|
|
Log("=== Solved: all 4 rows complete ===");
|
|
return;
|
|
}
|
|
|
|
int move = solution[moveIdx];
|
|
uint expected = ApplyMoveBits(currentState, move);
|
|
string key = KeyForMove(move);
|
|
|
|
if (!arrowIds.ContainsKey(key))
|
|
{
|
|
Log($"ERROR: Arrow '{key}' not found.");
|
|
return;
|
|
}
|
|
|
|
long id = arrowIds[key];
|
|
Log($"[{moveIdx + 1}/{solution.Count}] {MoveName(move)} via {key}");
|
|
Send(Out["ClickFurni"], (int)id, 0);
|
|
Delay(CLICK_DELAY_MS);
|
|
|
|
var newGrid = ReadGridFromRoom();
|
|
if (newGrid == null)
|
|
{
|
|
Log("WARN: Grid read failed, retry...");
|
|
Delay(400);
|
|
continue;
|
|
}
|
|
|
|
uint afterState = EncodeGrid(newGrid);
|
|
|
|
if (afterState == expected)
|
|
{
|
|
currentState = afterState;
|
|
moveIdx++;
|
|
retries = 0;
|
|
continue;
|
|
}
|
|
|
|
uint invExpected = ApplyMoveBits(currentState, InverseMove(move));
|
|
if (afterState == invExpected)
|
|
{
|
|
if (move < 8)
|
|
{
|
|
int r = move < 4 ? move : move - 4;
|
|
rowFlip[r] = !rowFlip[r];
|
|
Log($"Auto-fix: Row {r} direction flipped.");
|
|
}
|
|
else
|
|
{
|
|
int c = move < 12 ? move - 8 : move - 12;
|
|
colFlip[c] = !colFlip[c];
|
|
Log($"Auto-fix: Col {c} direction flipped.");
|
|
}
|
|
|
|
grid = newGrid;
|
|
currentState = afterState;
|
|
|
|
var replan = SolveLayerByLayer(grid, targetRows);
|
|
if (replan == null)
|
|
{
|
|
Log("ERROR: Replan failed after direction flip.");
|
|
return;
|
|
}
|
|
|
|
solution = replan;
|
|
moveIdx = 0;
|
|
retries = 0;
|
|
Log($"Replan: {solution.Count} moves");
|
|
continue;
|
|
}
|
|
|
|
if (afterState == currentState)
|
|
{
|
|
retries++;
|
|
if (retries >= MAX_RETRIES)
|
|
{
|
|
Log("WARN: Click had no effect multiple times, replan.");
|
|
grid = newGrid;
|
|
var replan = SolveLayerByLayer(grid, targetRows);
|
|
if (replan == null)
|
|
{
|
|
Log("ERROR: Replan failed after no-effect clicks.");
|
|
return;
|
|
}
|
|
solution = replan;
|
|
moveIdx = 0;
|
|
retries = 0;
|
|
}
|
|
else
|
|
{
|
|
Log("Click no effect, retrying...");
|
|
Delay(300);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
Log($"Desync detected. New grid: {GridDump(newGrid)}");
|
|
grid = newGrid;
|
|
currentState = afterState;
|
|
|
|
if (IsSolvedForTarget(grid, targetRows))
|
|
{
|
|
Log("=== Solved: all 4 rows complete ===");
|
|
return;
|
|
}
|
|
|
|
var desyncReplan = SolveLayerByLayer(grid, targetRows);
|
|
if (desyncReplan == null)
|
|
{
|
|
Log("ERROR: Replan failed after desync.");
|
|
return;
|
|
}
|
|
|
|
solution = desyncReplan;
|
|
moveIdx = 0;
|
|
retries = 0;
|
|
Log($"Replan after desync: {solution.Count} moves");
|
|
}
|
|
|
|
if (currentState == goalState)
|
|
Log("=== Solved: all 4 rows complete ===");
|
|
else
|
|
Log("Moves done. Please check if room state changed externally.");
|