using System; using System.Collections.Generic; using System.Linq; using Xabbo.Core; public struct Point : IEquatable { 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(); var connectionTiles = new Dictionary(); var floorMap = new Dictionary>(); 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(); floorMap[p].Add(item); } var numberTileLocations = new HashSet(); 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 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(); 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(); var pairedTiles = new HashSet(); 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.");