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 Hybrid Solver v6 (Robust Reconciliation) Initialized."); // ================================================================= // // PHASE 1: FULL BOARD ANALYSIS // ================================================================= // 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."); // ================================================================= // // PHASE 2: CALCULATING THE IDEAL SOLUTION (HYBRID SOLVER) // ================================================================= // var idealConnections = new HashSet(); var pairedTiles = new HashSet(); 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[] 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>(); 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(); dominoPossibilities[domino].Add(c.Key); } } } var forcedDomino = dominoPossibilities.FirstOrDefault(kvp => kvp.Value.Count == 1); if (!forcedDomino.Equals(default(KeyValuePair<(int, int), List>))) { 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(); foreach(Point p in activeConnections) { if (!idealConnections.Contains(p)) movesToDisconnect.Add(p); } var movesToConnect = new List(); 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.");