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

221 lines
8.6 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 = 18; int minY = 30;
int maxX = 40; int maxY = 48;
int stepDelayMilliseconds = 2500;
// --- End Configuration ---
Log("Dominosa Solver v3 (Unique Domino Fix) 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 (REWRITTEN)
// ================================================================= //
Log("Phase 2: Calculating ideal solution with unique domino rule...");
var idealConnections = new HashSet<Point>();
var pairedTiles = new HashSet<Point>();
var usedDominoes = new HashSet<(int, int)>(); // THE FIX: This tracks used domino pairs.
while (pairedTiles.Count < numberTiles.Count)
{
bool moveMadeThisIteration = false;
// Strategy 1: Find tile with only one possible unpaired neighbor.
foreach (var tilePos in numberTiles.Keys.Where(p => !pairedTiles.Contains(p)))
{
var possibleNeighbors = 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 neighborPos in neighborChecks)
if (numberTiles.ContainsKey(neighborPos) && !pairedTiles.Contains(neighborPos))
possibleNeighbors.Add(neighborPos);
if (possibleNeighbors.Count == 1)
{
var partnerPos = possibleNeighbors.First();
var domino = (Math.Min(numberTiles[tilePos].State, numberTiles[partnerPos].State), Math.Max(numberTiles[tilePos].State, numberTiles[partnerPos].State));
if (!usedDominoes.Contains(domino))
{
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); // Mark domino as used
moveMadeThisIteration = true;
break;
}
}
}
if (moveMadeThisIteration) continue;
// Strategy 2: Find domino type with only one possible placement.
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)) // Only consider unused domino types
{
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); // Mark domino as used
moveMadeThisIteration = true;
continue;
}
if (!moveMadeThisIteration) break;
}
Log($"Calculation complete. Ideal solution has {idealConnections.Count} unique steps.");
// ================================================================= //
// PHASE 3: RECONCILIATION AND EXECUTION
// ================================================================= //
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.");