Keeps the repo root clean - only README.md visible on landing page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
513 lines
16 KiB
C#
513 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
// ============================================================
|
|
// COLOR PUZZLE AUTO-SOLVER (Loopover 4x4)
|
|
// Layer-by-layer Ansatz: loest ALLE 4 Reihen zuverlaessig.
|
|
// Behaelt Anti-Desync + Auto-Flip-Erkennung bei.
|
|
// ============================================================
|
|
|
|
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; }
|
|
}
|
|
|
|
Log("=== Color Puzzle Auto-Solver (Layer-by-Layer) ===");
|
|
|
|
// ── 1. Read grid as array ─────────────────────────────────
|
|
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, 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;
|
|
g[y - GRID_Y_MIN, x - GRID_X_MIN] = GetState(item);
|
|
found[y - GRID_Y_MIN, x - GRID_X_MIN] = true;
|
|
}
|
|
int cnt = 0;
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
if (found[r, c]) cnt++;
|
|
if (cnt < 16) return null;
|
|
return g;
|
|
}
|
|
|
|
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]}]"));
|
|
}
|
|
|
|
var grid = ReadGridFromRoom();
|
|
if (grid == null)
|
|
{
|
|
Log("ERROR: Konnte Grid nicht lesen (nicht alle 16 Tiles gefunden).");
|
|
return;
|
|
}
|
|
Log($"Start: {GridDump(grid)}");
|
|
|
|
// ── 2. Read arrows ────────────────────────────────────────
|
|
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;
|
|
}
|
|
Log($"Pfeile: {arrowIds.Count}/16");
|
|
if (arrowIds.Count < 16) { Log("ERROR: Nicht alle Pfeile gefunden!"); return; }
|
|
|
|
// ── 3. Read target ────────────────────────────────────────
|
|
int[] targetRows = new int[4];
|
|
bool targetFound = false;
|
|
foreach (var item in FloorItems)
|
|
{
|
|
if (item == null) continue;
|
|
if (GetKind(item) != TILE_KIND) continue;
|
|
if (item.Location.X != 41) continue;
|
|
int y = item.Location.Y;
|
|
if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue;
|
|
targetRows[y - GRID_Y_MIN] = GetState(item);
|
|
targetFound = true;
|
|
}
|
|
if (!targetFound) targetRows = new[] { 1, 2, 3, 0 };
|
|
Log($"Ziel: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}");
|
|
|
|
// ── 4. Layer-by-Layer Solver ──────────────────────────────
|
|
// Move encoding: 0-3=RowLeft(0-3), 4-7=RowRight(0-3),
|
|
// 8-11=ColUp(0-3), 12-15=ColDown(0-3)
|
|
|
|
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";
|
|
}
|
|
|
|
// Simulate a single move on a grid copy
|
|
void SimMove(int[,] g, int m)
|
|
{
|
|
if (m < 4) { // RowLeft
|
|
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) { // RowRight
|
|
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) { // ColUp
|
|
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 { // ColDown
|
|
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)
|
|
{
|
|
// Work on a copy
|
|
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; } // 1x RowLeft is cheaper
|
|
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; } // 1x ColDown is cheaper
|
|
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);
|
|
}
|
|
|
|
// ── Phase 1: Solve Row 0 ─────────────────────────────
|
|
// Use free column rotations + row shifts on rows 1-3.
|
|
int C0 = tgtRows[0];
|
|
for (int c = 0; c < 4; c++)
|
|
{
|
|
if (g[0,c] == C0) continue;
|
|
|
|
// Look in same column
|
|
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
|
|
{
|
|
// Find C0 anywhere in rows 1-3
|
|
bool found = false;
|
|
for (int r = 1; r <= 3 && !found; r++)
|
|
for (int c2 = 0; c2 < 4 && !found; c2++)
|
|
{
|
|
if (c2 == c) continue;
|
|
if (g[r,c2] == C0)
|
|
{
|
|
DoRowRight(r, (c - c2 + 4) % 4);
|
|
DoColUp(c, r);
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found) return null; // should never happen
|
|
}
|
|
}
|
|
|
|
// ── Phase 2: Solve Row 1 (protecting Row 0) ──────────
|
|
// Commutator [RowLeft(1,k1), ColUp(c,k2), RowRight(1,k1), ColDown(c,k2)]
|
|
// creates a 3-cycle in rows 1+ only. Row 0 stays intact.
|
|
int C1 = tgtRows[1];
|
|
for (int pass = 0; pass < 4; 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;
|
|
|
|
// Move C1 to (srcR, colW) via row shift (safe: rows 2-3 only)
|
|
if (srcC != colW)
|
|
DoRowRight(srcR, (colW - srcC + 4) % 4);
|
|
|
|
int k2 = srcR - 1; // 1 or 2
|
|
DoRowLeft(1, 1);
|
|
DoColUp(colW, k2);
|
|
DoRowRight(1, 1);
|
|
DoColDown(colW, k2);
|
|
}
|
|
|
|
// ── Phase 3: Solve Rows 2-3 (protecting Rows 0-1) ───
|
|
// Commutator with r1=2, k2=1 only touches rows 2-3.
|
|
int C2 = tgtRows[2];
|
|
for (int pass = 0; pass < 4; 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);
|
|
}
|
|
|
|
// Verify
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
if (g[r,c] != tgtRows[r]) return null;
|
|
|
|
// Optimize: remove consecutive inverse pairs
|
|
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;
|
|
}
|
|
|
|
// ── 5. Check if already solved ────────────────────────────
|
|
bool IsGridSolved(int[,] g)
|
|
{
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
if (g[r,c] != targetRows[r]) return false;
|
|
return true;
|
|
}
|
|
|
|
if (IsGridSolved(grid))
|
|
{
|
|
Log("Puzzle ist bereits geloest!");
|
|
return;
|
|
}
|
|
|
|
// ── 6. Solve ──────────────────────────────────────────────
|
|
var solution = SolveLayerByLayer(grid, targetRows);
|
|
if (solution == null || solution.Count == 0)
|
|
{
|
|
Log("ERROR: Solver konnte keine Loesung finden!");
|
|
Log("Moegliche Gruende: Farb-Verteilung nicht 4x je Farbe, oder falsche Ziel-Zuordnung.");
|
|
return;
|
|
}
|
|
|
|
Log($"Loesung gefunden: {solution.Count} Moves");
|
|
for (int i = 0; i < solution.Count; i++)
|
|
Log($" {i+1}. {MoveName(solution[i])}");
|
|
|
|
// ── 7. Execute with verification ──────────────────────────
|
|
// Track arrow direction flips (auto-detect reversed arrows)
|
|
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}";
|
|
}
|
|
|
|
// Encode grid as uint for quick comparison
|
|
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;
|
|
}
|
|
|
|
// Compute expected state after a move (using bit ops for speed)
|
|
uint ApplyMoveBits(uint s, int m)
|
|
{
|
|
if (m < 4) { // RowLeft
|
|
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) { // RowRight
|
|
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) { // ColUp
|
|
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));
|
|
}
|
|
{ // ColDown
|
|
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;
|
|
}
|
|
|
|
Log("\nFuehre Moves aus...");
|
|
uint currentState = EncodeGrid(grid);
|
|
uint goalState = EncodeGrid(new int[4,4]); // temp
|
|
{
|
|
int[,] tgt = new int[4,4];
|
|
for (int r = 0; r < 4; r++)
|
|
for (int c = 0; c < 4; c++)
|
|
tgt[r,c] = targetRows[r];
|
|
goalState = EncodeGrid(tgt);
|
|
}
|
|
|
|
int moveIdx = 0;
|
|
int retries = 0;
|
|
const int MAX_RETRIES = 3;
|
|
|
|
while (moveIdx < solution.Count)
|
|
{
|
|
if (currentState == goalState)
|
|
{
|
|
Log("=== Puzzle geloest! Alle 4 Reihen korrekt! ===");
|
|
return;
|
|
}
|
|
|
|
int move = solution[moveIdx];
|
|
uint expected = ApplyMoveBits(currentState, move);
|
|
string key = KeyForMove(move);
|
|
|
|
if (!arrowIds.ContainsKey(key))
|
|
{
|
|
Log($"ERROR: Arrow '{key}' nicht gefunden!");
|
|
return;
|
|
}
|
|
|
|
long id = arrowIds[key];
|
|
Log($" [{moveIdx+1}/{solution.Count}] {MoveName(move)} via {key}");
|
|
Send(Out["ClickFurni"], (int)id, 0);
|
|
Delay(CLICK_DELAY_MS);
|
|
|
|
// Re-read grid to verify
|
|
var newGrid = ReadGridFromRoom();
|
|
if (newGrid == null)
|
|
{
|
|
Log("WARN: Grid-Read fehlgeschlagen, retry...");
|
|
Delay(400);
|
|
continue;
|
|
}
|
|
|
|
uint afterState = EncodeGrid(newGrid);
|
|
|
|
if (afterState == expected)
|
|
{
|
|
// Move worked as expected
|
|
currentState = afterState;
|
|
moveIdx++;
|
|
retries = 0;
|
|
continue;
|
|
}
|
|
|
|
// Check if arrow direction was reversed
|
|
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} Richtung gespiegelt.");
|
|
} else {
|
|
int c = move < 12 ? move - 8 : move - 12;
|
|
colFlip[c] = !colFlip[c];
|
|
Log($" Auto-Fix: Col {c} Richtung gespiegelt.");
|
|
}
|
|
currentState = afterState;
|
|
// Don't advance moveIdx - the move did the opposite, re-plan
|
|
Log(" Re-plane von neuem Zustand...");
|
|
grid = newGrid;
|
|
solution = SolveLayerByLayer(grid, targetRows);
|
|
if (solution == null) { Log("ERROR: Re-Plan fehlgeschlagen!"); return; }
|
|
moveIdx = 0;
|
|
retries = 0;
|
|
Log($" Neuer Plan: {solution.Count} Moves");
|
|
continue;
|
|
}
|
|
|
|
if (afterState == currentState)
|
|
{
|
|
// Click had no effect
|
|
retries++;
|
|
if (retries >= MAX_RETRIES)
|
|
{
|
|
Log("WARN: Klick ohne Effekt nach 3 Versuchen, re-plane...");
|
|
grid = newGrid;
|
|
solution = SolveLayerByLayer(grid, targetRows);
|
|
if (solution == null) { Log("ERROR: Re-Plan fehlgeschlagen!"); return; }
|
|
moveIdx = 0;
|
|
retries = 0;
|
|
}
|
|
else
|
|
{
|
|
Log(" Klick ohne Effekt, retry...");
|
|
Delay(300);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Desync: grid changed unexpectedly (maybe another player or lag)
|
|
Log($" Desync! Neuer Zustand: {GridDump(newGrid)}");
|
|
Log(" Re-plane von neuem Zustand...");
|
|
grid = newGrid;
|
|
currentState = afterState;
|
|
|
|
if (IsGridSolved(grid))
|
|
{
|
|
Log("=== Puzzle geloest! Alle 4 Reihen korrekt! ===");
|
|
return;
|
|
}
|
|
|
|
solution = SolveLayerByLayer(grid, targetRows);
|
|
if (solution == null) { Log("ERROR: Re-Plan fehlgeschlagen!"); return; }
|
|
moveIdx = 0;
|
|
retries = 0;
|
|
Log($" Neuer Plan: {solution.Count} Moves");
|
|
}
|
|
|
|
if (currentState == goalState)
|
|
Log("=== Puzzle geloest! Alle 4 Reihen korrekt! ===");
|
|
else
|
|
Log("Alle Moves ausgefuehrt. Grid pruefen ob geloest.");
|