xabbo-scripts/Scripts/DominoSolverV3.csx
Administrator 7a548130a3 Move all scripts into Scripts/ subfolder
Keeps the repo root clean - only README.md visible on landing page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:49:37 +01:00

250 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Xabbo.Core;
public struct Point : IEquatable<Point>
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public bool Equals(Point other) => X == other.X && Y == other.Y;
public override bool Equals(object obj) => obj is Point other && Equals(other);
public override int GetHashCode() => HashCode.Combine(X, Y);
public override string ToString() => $"({X}, {Y})";
}
// --- Configuration ---
string targetFurniName = "Number Tile Dark";
int minX = 16; int minY = 28;
int maxX = 54; int maxY = 62;
int stepDelayMilliseconds = 2500;
// --- End Configuration ---
Log("Dominosa Hybrid Solver v6 (Robust Reconciliation) Initialized.");
// ================================================================= //
// PHASE 1: FULL BOARD ANALYSIS
// ================================================================= //
Log("Phase 1: Analyzing board state...");
var numberTiles = new Dictionary<Point, IFloorItem>();
var connectionTiles = new Dictionary<Point, (Point, Point)>();
var floorMap = new Dictionary<Point, List<IFloorItem>>();
try
{
if (FloorItems == null) { Log("ERROR: Cannot access FloorItems."); return; }
foreach (IFloorItem item in FloorItems)
{
if (item == null) continue;
var p = new Point(item.Location.X, item.Location.Y);
if (!floorMap.ContainsKey(p)) floorMap[p] = new List<IFloorItem>();
floorMap[p].Add(item);
}
var numberTileLocations = new HashSet<Point>();
foreach (var item in floorMap.SelectMany(kvp => kvp.Value))
{
if (item.GetName() != targetFurniName) continue;
int x = item.Location.X;
int y = item.Location.Y;
if (x >= minX && x <= maxX && y >= minY && y <= maxY)
{
var p = new Point(x, y);
numberTiles[p] = item;
numberTileLocations.Add(p);
}
}
foreach (Point p1 in numberTileLocations)
{
Point p2_horiz = new Point(p1.X + 4, p1.Y);
if (numberTileLocations.Contains(p2_horiz)) connectionTiles[new Point(p1.X + 2, p1.Y)] = (p1, p2_horiz);
Point p2_vert = new Point(p1.X, p1.Y + 4);
if (numberTileLocations.Contains(p2_vert)) connectionTiles[new Point(p1.X, p1.Y + 2)] = (p1, p2_vert);
}
}
catch (Exception ex) { Log($"ERROR during analysis: {ex.Message}"); return; }
Log($"Analysis complete. Found {numberTiles.Count} numbers and {connectionTiles.Count} connections.");
Func<Point, bool> isGapTileConnected = (p) => {
if (floorMap.TryGetValue(p, out var stack))
return stack.Any(f => f.GetName() == "Dark Tile" && Math.Abs(f.Location.Z - 0.25) < 0.001);
return false;
};
var activeConnections = new HashSet<Point>();
foreach (var c in connectionTiles)
{
bool isHorizontal = numberTiles.ContainsKey(new Point(c.Key.X - 2, c.Key.Y));
if (isHorizontal)
{
if (isGapTileConnected(new Point(c.Key.X - 1, c.Key.Y)) && isGapTileConnected(c.Key) && isGapTileConnected(new Point(c.Key.X + 1, c.Key.Y)))
activeConnections.Add(c.Key);
}
else
{
if (isGapTileConnected(new Point(c.Key.X, c.Key.Y - 1)) && isGapTileConnected(c.Key) && isGapTileConnected(new Point(c.Key.X, c.Key.Y + 1)))
activeConnections.Add(c.Key);
}
}
Log($"Detected {activeConnections.Count} currently active connections on the board.");
// ================================================================= //
// PHASE 2: CALCULATING THE IDEAL SOLUTION (HYBRID SOLVER)
// ================================================================= //
var idealConnections = new HashSet<Point>();
var pairedTiles = new HashSet<Point>();
var usedDominoes = new HashSet<(int, int)>();
Log("Phase 2.1: Finding forced moves (deterministic pass)...");
int deterministicMoves = 0;
while (true)
{
bool moveMadeThisIteration = false;
foreach (var tilePos in numberTiles.Keys.Where(p => !pairedTiles.Contains(p)))
{
var possiblePartners = new List<Point>();
Point[] neighborChecks = { new Point(tilePos.X - 4, tilePos.Y), new Point(tilePos.X + 4, tilePos.Y), new Point(tilePos.X, tilePos.Y - 4), new Point(tilePos.X, tilePos.Y + 4) };
foreach (var partnerPos in neighborChecks)
{
if (numberTiles.ContainsKey(partnerPos) && !pairedTiles.Contains(partnerPos))
{
var domino = (Math.Min(numberTiles[tilePos].State, numberTiles[partnerPos].State), Math.Max(numberTiles[tilePos].State, numberTiles[partnerPos].State));
if (!usedDominoes.Contains(domino)) possiblePartners.Add(partnerPos);
}
}
if (possiblePartners.Count == 1)
{
var partnerPos = possiblePartners.First();
var domino = (Math.Min(numberTiles[tilePos].State, numberTiles[partnerPos].State), Math.Max(numberTiles[tilePos].State, numberTiles[partnerPos].State));
var connection = connectionTiles.First(kvp => (kvp.Value.Item1.Equals(tilePos) && kvp.Value.Item2.Equals(partnerPos)) || (kvp.Value.Item1.Equals(partnerPos) && kvp.Value.Item2.Equals(tilePos))).Key;
idealConnections.Add(connection); pairedTiles.Add(tilePos); pairedTiles.Add(partnerPos); usedDominoes.Add(domino);
moveMadeThisIteration = true; deterministicMoves++; break;
}
}
if (moveMadeThisIteration) continue;
var dominoPossibilities = new Dictionary<(int, int), List<Point>>();
foreach (var c in connectionTiles)
{
Point p1 = c.Value.Item1; Point p2 = c.Value.Item2;
if (!pairedTiles.Contains(p1) && !pairedTiles.Contains(p2))
{
var domino = (Math.Min(numberTiles[p1].State, numberTiles[p2].State), Math.Max(numberTiles[p1].State, numberTiles[p2].State));
if (!usedDominoes.Contains(domino))
{
if (!dominoPossibilities.ContainsKey(domino)) dominoPossibilities[domino] = new List<Point>();
dominoPossibilities[domino].Add(c.Key);
}
}
}
var forcedDomino = dominoPossibilities.FirstOrDefault(kvp => kvp.Value.Count == 1);
if (!forcedDomino.Equals(default(KeyValuePair<(int, int), List<Point>>)))
{
var connection = forcedDomino.Value.First();
var (p1, p2) = connectionTiles[connection];
var domino = forcedDomino.Key;
idealConnections.Add(connection); pairedTiles.Add(p1); pairedTiles.Add(p2); usedDominoes.Add(domino);
moveMadeThisIteration = true; deterministicMoves++; continue;
}
if (!moveMadeThisIteration) break;
}
Log($"Deterministic pass found {deterministicMoves} moves.");
bool success = false;
if (pairedTiles.Count == numberTiles.Count)
{
Log("Puzzle solved deterministically. No recursion needed.");
success = true;
}
else
{
Log($"Phase 2.2: Starting recursive backtracking on remaining {numberTiles.Count - pairedTiles.Count} tiles...");
var precomputedNeighbors = numberTiles.Keys.ToDictionary(p => p, p => new Point[] { new Point(p.X - 4, p.Y), new Point(p.X + 4, p.Y), new Point(p.X, p.Y - 4), new Point(p.X, p.Y + 4) }.Where(n => numberTiles.ContainsKey(n)).ToList());
bool SolveRecursive()
{
if (pairedTiles.Count == numberTiles.Count) return true;
var firstUnpaired = numberTiles.Keys.First(p => !pairedTiles.Contains(p));
foreach (var neighbor in precomputedNeighbors[firstUnpaired])
{
if (pairedTiles.Contains(neighbor)) continue;
var domino = (Math.Min(numberTiles[firstUnpaired].State, numberTiles[neighbor].State), Math.Max(numberTiles[firstUnpaired].State, numberTiles[neighbor].State));
if (usedDominoes.Contains(domino)) continue;
var connection = connectionTiles.First(kvp => (kvp.Value.Item1.Equals(firstUnpaired) && kvp.Value.Item2.Equals(neighbor)) || (kvp.Value.Item1.Equals(neighbor) && kvp.Value.Item2.Equals(firstUnpaired))).Key;
pairedTiles.Add(firstUnpaired); pairedTiles.Add(neighbor); usedDominoes.Add(domino); idealConnections.Add(connection);
if (SolveRecursive()) return true;
idealConnections.Remove(connection); usedDominoes.Remove(domino); pairedTiles.Remove(neighbor); pairedTiles.Remove(firstUnpaired);
}
return false;
}
success = SolveRecursive();
}
Log($"Calculation complete. Success: {success}. Ideal solution has {idealConnections.Count} steps.");
// ================================================================= //
// PHASE 3: RECONCILIATION AND EXECUTION (ROBUST VERSION)
// ================================================================= //
if (!success) { Log("CRITICAL ERROR: The solver could not find any valid solution for the board."); return; }
Log("Phase 3: Reconciling current state with ideal solution...");
// --- THE FIX: Manually calculate the differences to avoid LINQ bugs ---
var movesToDisconnect = new List<Point>();
foreach(Point p in activeConnections)
{
if (!idealConnections.Contains(p)) movesToDisconnect.Add(p);
}
var movesToConnect = new List<Point>();
foreach(Point p in idealConnections)
{
if (!activeConnections.Contains(p)) movesToConnect.Add(p);
}
int correctCount = activeConnections.Count - movesToDisconnect.Count;
Log($"Found {movesToDisconnect.Count} incorrect connections to UNDO.");
Log($"Found {movesToConnect.Count} missing connections to MAKE.");
Log($"Found {correctCount} connections that are already correct.");
if (movesToDisconnect.Count == 0 && movesToConnect.Count == 0)
{
Log("\nBoard is already solved! No action needed.");
return;
}
if (movesToDisconnect.Any())
{
Log("\n--- [PLAN: UNDO MOVES] ---");
movesToDisconnect.ForEach(p => Log($" -> Click {p} to disconnect"));
Log("------------------------\nExecuting... Please wait.");
Delay(2000);
for (int i = 0; i < movesToDisconnect.Count; i++)
{
var p = movesToDisconnect[i];
Log($"Undoing {i + 1}/{movesToDisconnect.Count}: MoveTo {p}");
Send(Out["MoveAvatar"], p.X, p.Y);
Delay(stepDelayMilliseconds);
}
Log("Disconnections complete.");
}
if (movesToConnect.Any())
{
Log("\n--- [PLAN: MAKE MOVES] ---");
movesToConnect.ForEach(p => Log($" -> Click {p} to connect"));
Log("------------------------\nExecuting... Please wait.");
Delay(2000);
for (int i = 0; i < movesToConnect.Count; i++)
{
var p = movesToConnect[i];
Log($"Connecting {i + 1}/{movesToConnect.Count}: MoveTo {p}");
Send(Out["MoveAvatar"], p.X, p.Y);
Delay(stepDelayMilliseconds);
}
Log("Connections complete.");
}
Log("\nReconciliation complete. Puzzle should be solved.");