xabbo-scripts/Scripts/DominoSolverV2.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

193 lines
6.7 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 Backtracking Solver v4 Initialized.");
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.");
Log("Phase 2: Calculating ideal solution with backtracking...");
var idealConnections = new HashSet<Point>();
var pairedTiles = new HashSet<Point>();
var usedDominoes = new HashSet<(int, int)>();
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.FirstOrDefault(p => !pairedTiles.Contains(p));
if (firstUnpaired.Equals(default(Point))) return true;
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;
}
bool success = SolveRecursive();
Log($"Calculation complete. Success: {success}. Ideal solution has {idealConnections.Count} steps.");
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...");
var movesToDisconnect = activeConnections.Except(idealConnections).ToList();
var movesToConnect = idealConnections.Except(activeConnections).ToList();
Log($"Found {movesToDisconnect.Count} incorrect connections to UNDO.");
Log($"Found {movesToConnect.Count} missing connections to MAKE.");
Log($"Found {activeConnections.Intersect(idealConnections).Count()} 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.");