From 7bfc390ed63752635860873e9560249051ede686 Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 16 Mar 2026 09:38:59 +0100 Subject: [PATCH] Initial commit: 68 Xabbo Scripter scripts Habbo Hotel automation scripts including: - Game solvers (Snake, Color Puzzle, Tetris, Flappy Bird, Flood-IT) - Room utilities (Autogate, One-Way Door, Furni Scanner) - Bot tools (Heal Bot, Pet Trainer, User Collector) - Trading & economy (Furni-Matic, Seed Trade, Trade Spam) Cleaned up: removed 5 duplicates and 2 broken scripts, renamed 37 gibberish filenames to descriptive names. --- Auto Fishing [28.12.25] (MidMan).csx | 101 ++ Auto Healer Medic Bot.csx | 131 ++ Autogate.csx | 156 ++ BC Items legen (boost).csx | 56 + Cabbage Placer Loop.csx | 47 + Catalog Exporter.csx | 60 + Color Matching Puzzle Auto.csx | 155 ++ Color Matching Simple.csx | 101 ++ Color Pattern Walker Fixed.csx | 635 ++++++++ Color Pattern Walker Solver.csx | 518 ++++++ Color Puzzle AutoCalib Queue.csx | 482 ++++++ Color Puzzle AutoCalib Solver.csx | 415 +++++ Color Puzzle BFS-IDA Solver.csx | 418 +++++ Color Puzzle Layer Solver.csx | 512 ++++++ Color Puzzle Queue Watcher.csx | 133 ++ Color Puzzle Solver 4-Row Fix.csx | 577 +++++++ Color Puzzle Solver Simple.csx | 477 ++++++ Color Puzzle State Logger.csx | 148 ++ Color Run Path Recorder.csx | 536 +++++++ Dance Switcher.csx | 13 + Dave 2.csx | 30 + Duck Dodging Pathfinder AI.csx | 491 ++++++ Enter-One-Way-Door #1.csx | 59 + FURNIMATIC.csx | 578 +++++++ Find Item by ID.csx | 21 + Flappy Bird [28.12.25] V1.0.csx | 165 ++ Flood-IT [28.12.25] V1.0.csx | 244 +++ Floor Items CSV Exporter.csx | 97 ++ Furni-Matic Auto-Eco.csx | 367 +++++ Furni-Matic Unboxing.csx | 20 + Furni.csx | 366 +++++ Furniture Position Debug.csx | 60 + Heal Bot.csx | 97 ++ Heart.csx | 373 +++++ One-Way Door Auto Enter.csx | 512 ++++++ Pet Level Scanner.csx | 33 + Pet Trainer Auto v84.csx | 2177 ++++++++++++++++++++++++++ Random Walker Bot.csx | 12 + Roller Gate Bot.csx | 122 ++ Roller Surf Game Bot.csx | 172 ++ Room Furni Scanner.csx | 31 + Room Queue Auto Joiner.csx | 50 + SOLVER.csx | 399 +++++ Seed trade (simple).csx | 3 + Seed trade (split).csx | 9 + Sit Spam.csx | 9 + Snake Auto (AutoCalib).csx | 404 +++++ Snake Auto (Campaign) v1.csx | 116 ++ Snake Auto (Campaign) v10.csx | 286 ++++ Snake Auto (Campaign) v11.csx | 293 ++++ Snake Auto (Campaign) v12.csx | 285 ++++ Snake Auto (Campaign) v13.csx | 287 ++++ Snake Auto (Campaign) v4.csx | 282 ++++ Snake Auto (Campaign) v5.csx | 269 ++++ Snake Auto (Campaign) v6.csx | 287 ++++ Snake Auto (Campaign) v7.csx | 336 ++++ Snake Auto (Campaign) v8.csx | 171 ++ Snake Auto (Campaign) v9.csx | 233 +++ Snake Auto (Campaign).csx | 115 ++ Snake Debug Watch.csx | 37 + Snake Target Probe.csx | 47 + Snake [28.12.25] V1.0.csx | 343 ++++ Tetris [28.12.25] V1.0.csx | 1115 +++++++++++++ Trade Spam.csx | 35 + User Collector FI Server.csx | 95 ++ User Collector Hopper Advanced.csx | 245 +++ User Collector NL Server.csx | 95 ++ Wired Message Blocker.csx | 28 + hello-once.csx | 2 + room-scan-basic.csx | 12 + room-scan-starter.csx | 50 + room-scan-topk.csx | 20 + 72 files changed, 17656 insertions(+) create mode 100644 Auto Fishing [28.12.25] (MidMan).csx create mode 100644 Auto Healer Medic Bot.csx create mode 100644 Autogate.csx create mode 100644 BC Items legen (boost).csx create mode 100644 Cabbage Placer Loop.csx create mode 100644 Catalog Exporter.csx create mode 100644 Color Matching Puzzle Auto.csx create mode 100644 Color Matching Simple.csx create mode 100644 Color Pattern Walker Fixed.csx create mode 100644 Color Pattern Walker Solver.csx create mode 100644 Color Puzzle AutoCalib Queue.csx create mode 100644 Color Puzzle AutoCalib Solver.csx create mode 100644 Color Puzzle BFS-IDA Solver.csx create mode 100644 Color Puzzle Layer Solver.csx create mode 100644 Color Puzzle Queue Watcher.csx create mode 100644 Color Puzzle Solver 4-Row Fix.csx create mode 100644 Color Puzzle Solver Simple.csx create mode 100644 Color Puzzle State Logger.csx create mode 100644 Color Run Path Recorder.csx create mode 100644 Dance Switcher.csx create mode 100644 Dave 2.csx create mode 100644 Duck Dodging Pathfinder AI.csx create mode 100644 Enter-One-Way-Door #1.csx create mode 100644 FURNIMATIC.csx create mode 100644 Find Item by ID.csx create mode 100644 Flappy Bird [28.12.25] V1.0.csx create mode 100644 Flood-IT [28.12.25] V1.0.csx create mode 100644 Floor Items CSV Exporter.csx create mode 100644 Furni-Matic Auto-Eco.csx create mode 100644 Furni-Matic Unboxing.csx create mode 100644 Furni.csx create mode 100644 Furniture Position Debug.csx create mode 100644 Heal Bot.csx create mode 100644 Heart.csx create mode 100644 One-Way Door Auto Enter.csx create mode 100644 Pet Level Scanner.csx create mode 100644 Pet Trainer Auto v84.csx create mode 100644 Random Walker Bot.csx create mode 100644 Roller Gate Bot.csx create mode 100644 Roller Surf Game Bot.csx create mode 100644 Room Furni Scanner.csx create mode 100644 Room Queue Auto Joiner.csx create mode 100644 SOLVER.csx create mode 100644 Seed trade (simple).csx create mode 100644 Seed trade (split).csx create mode 100644 Sit Spam.csx create mode 100644 Snake Auto (AutoCalib).csx create mode 100644 Snake Auto (Campaign) v1.csx create mode 100644 Snake Auto (Campaign) v10.csx create mode 100644 Snake Auto (Campaign) v11.csx create mode 100644 Snake Auto (Campaign) v12.csx create mode 100644 Snake Auto (Campaign) v13.csx create mode 100644 Snake Auto (Campaign) v4.csx create mode 100644 Snake Auto (Campaign) v5.csx create mode 100644 Snake Auto (Campaign) v6.csx create mode 100644 Snake Auto (Campaign) v7.csx create mode 100644 Snake Auto (Campaign) v8.csx create mode 100644 Snake Auto (Campaign) v9.csx create mode 100644 Snake Auto (Campaign).csx create mode 100644 Snake Debug Watch.csx create mode 100644 Snake Target Probe.csx create mode 100644 Snake [28.12.25] V1.0.csx create mode 100644 Tetris [28.12.25] V1.0.csx create mode 100644 Trade Spam.csx create mode 100644 User Collector FI Server.csx create mode 100644 User Collector Hopper Advanced.csx create mode 100644 User Collector NL Server.csx create mode 100644 Wired Message Blocker.csx create mode 100644 hello-once.csx create mode 100644 room-scan-basic.csx create mode 100644 room-scan-starter.csx create mode 100644 room-scan-topk.csx diff --git a/Auto Fishing [28.12.25] (MidMan).csx b/Auto Fishing [28.12.25] (MidMan).csx new file mode 100644 index 0000000..5b2208c --- /dev/null +++ b/Auto Fishing [28.12.25] (MidMan).csx @@ -0,0 +1,101 @@ +/* + AUTO-FISHER V10 - SILENT HISTORY + - Zeigt NUR noch FΓ€nge und wichtige Events an + - Kein "Angel ausgeworfen" Spam mehr + - Clean Log (ohne BBCode) & Rare Tracker aktiv +*/ + +using System; +using System.Threading.Tasks; +using System.Text.RegularExpressions; + +// --- EINSTELLUNGEN --- +int itemId = 2147419751; // ID deiner Angel/Tile + +// Trigger +string triggerCatch = "Rhaz caught"; +string triggerShark = "attacked by a shark"; +string triggerStunOver = "stun effect has worn off"; + +Log("--- Auto-Fisher V10 (History Mode) gestartet ---"); + +string CleanMessage(string input) +{ + return Regex.Replace(input, @"\[.*?\]", ""); +} + +void Click() +{ + if (!Run) return; + Send(Out["ClickFurni"], itemId, 0); + // HIER HABEN WIR DEN LOG ENTFERNT + // Damit bleibt deine History sauber! +} + +void HandleMessage(InterceptArgs e) +{ + try + { + var p = e.Packet; + p.ReadInt(); + string rawMsg = p.ReadString(); + string lowerMsg = rawMsg.ToLower(); + string cleanMsg = CleanMessage(rawMsg); + + // 1. FANG + if (lowerMsg.Contains(triggerCatch.ToLower())) + { + // Rarity Tracker + string rarityLog = ""; + if (lowerMsg.Contains("exotic")) rarityLog = "🟣 EXOTIC"; + else if (lowerMsg.Contains("legendary")) rarityLog = "🟑 LEGENDARY"; + else if (lowerMsg.Contains("epic")) rarityLog = "πŸ”΄ EPIC"; + else if (lowerMsg.Contains("rare")) rarityLog = "πŸ”΅ RARE"; + + // Nur besondere Fische kriegen eine Zeitstempel-Hervorhebung + if (rarityLog != "") + Log($"πŸ’Ž {rarityLog} ({DateTime.Now:HH:mm:ss})"); + + // Der normale Log-Eintrag fΓΌr die History + Log($"βœ… {cleanMsg}"); + + Task.Run(() => + { + int ms = new Random().Next(500, 1500); + System.Threading.Thread.Sleep(ms); + if (Run) Click(); + }); + } + + // 2. HAI + else if (lowerMsg.Contains(triggerShark.ToLower())) + { + Log($"⚠️ HAI-ANGRIFF! ({CleanMessage(rawMsg)})"); + // Wir warten stillschweigend auf das Ende + } + + // 3. STUN VORBEI + else if (lowerMsg.Contains(triggerStunOver.ToLower())) + { + Log($"πŸŽ‰ Stun vorbei - weiter geht's!"); + + Task.Run(() => + { + int reactionTime = new Random().Next(500, 1200); + System.Threading.Thread.Sleep(reactionTime); + if (Run) Click(); + }); + } + } + catch { } +} + +OnIntercept(In["Chat"], e => HandleMessage(e)); +OnIntercept(In["Shout"], e => HandleMessage(e)); +OnIntercept(In["Whisper"], e => HandleMessage(e)); + +// Erster Klick +Click(); +Log("(Angel ist aktiv - warte auf ersten Fisch...)"); + +while(Run) Delay(1000); \ No newline at end of file diff --git a/Auto Healer Medic Bot.csx b/Auto Healer Medic Bot.csx new file mode 100644 index 0000000..13e5fa5 --- /dev/null +++ b/Auto Healer Medic Bot.csx @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Sulakore.Communication; +using Sulakore.Modules; +using Tangine; + +namespace HabboHealBot +{ + [Module("Auto Medic", "Automates the :offer heal process")] + [Author("User")] + public class HealBot : Extension + { + // Status-Variable, damit Prozesse nicht unterbrochen werden + private bool _isBusy = false; + + // Speichert die Zuordnung von Index (Raum-ID) zu EntityID (Datenbank-ID) + // Das wird benΓΆtigt, weil der Chat den Index sendet, der Klick aber oft die ID braucht. + private Dictionary _userMap = new Dictionary(); + + public HealBot() + { + // Events registrieren + Triggers.In(In.Chat, OnChat); // Wenn jemand spricht + Triggers.In(In.Users, OnUsers); // Wenn User den Raum betreten + Triggers.In(In.RoomReady, OnRoomReady); // Wenn wir den Raum betreten (Reset) + } + + // 1. Liste zurΓΌcksetzen wenn wir den Raum wechseln + private void OnRoomReady(DataInterceptedEventArgs e) + { + _userMap.Clear(); + _isBusy = false; + } + + // 2. User tracken (Index zu ID Zuordnung) + private void OnUsers(DataInterceptedEventArgs e) + { + var parser = e.Packet; + int count = parser.ReadInt(); + + for (int i = 0; i < count; i++) + { + int id = parser.ReadInt(); // Entity ID (Datenbank ID) + string name = parser.ReadString(); + string motto = parser.ReadString(); + string look = parser.ReadString(); + int index = parser.ReadInt(); // Room Index + + // Restliche Daten ΓΌberspringen (Koordinaten etc.) + // Hinweis: Die Struktur kann je nach Server leicht variieren, + // aber ID und Index kommen meist zuerst. + + if (!_userMap.ContainsKey(index)) + { + _userMap.Add(index, id); + } + } + } + + // 3. Auf "heal" im Chat reagieren + private void OnChat(DataInterceptedEventArgs e) + { + // Wenn wir gerade schon jemanden heilen -> Ignorieren + if (_isBusy) return; + + int index = e.Packet.ReadInt(); + string message = e.Packet.ReadString(); + + // PrΓΌfen ob "heal" (groß/kleinschreibung egal) vorkommt + if (message.ToLower().Contains("heal")) + { + // PrΓΌfen, ob wir die ID zu diesem User haben + if (_userMap.ContainsKey(index)) + { + int targetId = _userMap[index]; + + // Prozess starten (Async damit der Main-Thread nicht blockiert) + Task.Run(() => PerformHealRoutine(targetId)); + } + } + } + + private async Task PerformHealRoutine(int targetUserId) + { + _isBusy = true; // Blockieren + Console.WriteLine($"[Bot] Healing User ID: {targetUserId}..."); + + try + { + // Schritt 1: :offer senden + await SendPacketAsync(Out.Shout, ":offer", 0); + await Task.Delay(600); // Warten bis MenΓΌ da ist (Evtl. anpassen je nach Lag) + + // Schritt 2: 1 senden (Heal auswΓ€hlen) + await SendPacketAsync(Out.Shout, "1", 0); + await Task.Delay(600); // Warten auf "Click the user" Prompt + + // Schritt 3: User anklicken + // Auf den meisten Servern ist "Anklicken" das Paket "GetSelectedBadges" + // oder einfach das Abfragen der User-Info. + // Paket-Struktur: {Header} {Int: UserID} + await SendPacketAsync(Out.GetSelectedBadges, targetUserId); + + // Falls "GetSelectedBadges" auf deinem Server nicht als Klick zΓ€hlt, + // probiere stattdessen Out.RoomUserAction oder Out.LookTo + + Console.WriteLine("[Bot] Heal sequence finished."); + } + catch (Exception ex) + { + Console.WriteLine("[Bot] Error: " + ex.Message); + } + finally + { + // Prozess freigeben + await Task.Delay(500); // Kurzer Cooldown + _isBusy = false; + } + } + + // Helfer fΓΌr asynchrones Senden + private async Task SendPacketAsync(ushort header, params object[] values) + { + await SendToServerAsync(header, values); + } + } +} \ No newline at end of file diff --git a/Autogate.csx b/Autogate.csx new file mode 100644 index 0000000..9f628e3 --- /dev/null +++ b/Autogate.csx @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +public struct Point : IEquatable +{ + public int X { get; } + public int Y { get; } + public Point(int x, int y) { X = x; Y = y; } + public static implicit operator Point((int x, int y) tuple) => new Point(tuple.x, tuple.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 static bool operator ==(Point left, Point right) => left.Equals(right); + public static bool operator !=(Point left, Point right) => !(left == right); + public override string ToString() => $"({X},{Y})"; +} + +Log("started"); + +const string FURNI_NAME_CONTAINS_TEXT = "One Way Gate"; +Regex mvRegex = new Regex(@"/mv (\d+),(\d+),([\d\.]+)/"); + +void CheckAndEnterGateGeneral(int userTileX, int userTileY) +{ + if (FloorItems == null) return; + + foreach (var item in FloorItems) + { + if (item == null || item.Location == null) continue; + + string itemName = null; + try { itemName = item.GetName(); } catch { continue; } + + if (itemName != null && itemName.Contains(FURNI_NAME_CONTAINS_TEXT)) + { + int gateX = item.Location.X; + int gateY = item.Location.Y; + int gateDir = item.Direction; + long gateId = item.Id; + bool shouldEnter = false; + + if (gateDir == 0) { if (userTileX == gateX && userTileY == gateY - 1) shouldEnter = true; } + else if (gateDir == 2) { if (userTileX == gateX + 1 && userTileY == gateY) shouldEnter = true; } + else if (gateDir == 4) { if (userTileX == gateX && userTileY == gateY + 1) shouldEnter = true; } + else if (gateDir == 6) { if (userTileX == gateX - 1 && userTileY == gateY) shouldEnter = true; } + + if (shouldEnter) + { + Log($"User tile ({userTileX},{userTileY}) matches criteria for Gate (ID {gateId}, Name: '{itemName}') at ({gateX},{gateY} Dir:{gateDir}) via GeneralCheck. Sending packet."); + Send(Out.EnterOneWayDoor, gateId); + return; + } + } + } +} + +void HandleUserUpdate(dynamic e) +{ + if (Self == null) return; + var packet = e.Packet; + int numUpdates = packet.ReadInt(); + for (int i = 0; i < numUpdates; i++) + { + int entityIndex = packet.ReadInt(); + int currentX = packet.ReadInt(); + int currentY = packet.ReadInt(); + packet.ReadString(); + packet.ReadInt(); + packet.ReadInt(); + string action = packet.ReadString(); + + if (entityIndex == Self.Index) + { + Point tileToConsiderForGate; + Match match = mvRegex.Match(action); + if (match.Success) + { + try + { + int targetX = int.Parse(match.Groups[1].Value); + int targetY = int.Parse(match.Groups[2].Value); + tileToConsiderForGate = new Point(targetX, targetY); + } + catch { tileToConsiderForGate = new Point(currentX, currentY); } + } + else { tileToConsiderForGate = new Point(currentX, currentY); } + CheckAndEnterGateGeneral(tileToConsiderForGate.X, tileToConsiderForGate.Y); + } + } +} + +void HandleObjectUpdate(dynamic e) +{ + if (Self == null || Self.Location == null) return; + + var packet = e.Packet; + + int furniIdFromPacket = packet.ReadInt(); + packet.ReadInt(); + int itemXFromPacket = packet.ReadInt(); + int itemYFromPacket = packet.ReadInt(); + int itemNewDirectionFromPacket = packet.ReadInt(); + + var itemInstance = FloorItems?.FirstOrDefault(f => f != null && f.Id == furniIdFromPacket); + if (itemInstance != null) + { + string itemName = null; + try { itemName = itemInstance.GetName(); } catch { return; } + + if (itemName != null && itemName.Contains(FURNI_NAME_CONTAINS_TEXT)) + { + bool shouldEnter = false; + int userX = Self.Location.X; + int userY = Self.Location.Y; + + if (itemNewDirectionFromPacket == 0) { if (userX == itemXFromPacket && userY == itemYFromPacket - 1) shouldEnter = true; } + else if (itemNewDirectionFromPacket == 2) { if (userX == itemXFromPacket + 1 && userY == itemYFromPacket) shouldEnter = true; } + else if (itemNewDirectionFromPacket == 4) { if (userX == itemXFromPacket && userY == itemYFromPacket + 1) shouldEnter = true; } + else if (itemNewDirectionFromPacket == 6) { if (userX == itemXFromPacket - 1 && userY == itemYFromPacket) shouldEnter = true; } + + if (shouldEnter) + { + Log($"User at ({userX},{userY}) matches criteria for Gate ID {furniIdFromPacket} ({itemXFromPacket},{itemYFromPacket}) with NewDir:{itemNewDirectionFromPacket} from ObjectUpdate. Sending packet."); + Send(Out.EnterOneWayDoor, furniIdFromPacket); + } + } + } +} + +void InitializeStateAndCheckGates(dynamic eventArgs) +{ + if (Self != null && Self.Location != null) + { + Point initialPosition = new Point(Self.Location.X, Self.Location.Y); + CheckAndEnterGateGeneral(initialPosition.X, initialPosition.Y); + } +} + +OnIntercept(In["UserUpdate"], e => HandleUserUpdate(e)); +OnIntercept(In["ObjectUpdate"], e => HandleObjectUpdate(e)); +OnEnteredRoom(e => InitializeStateAndCheckGates(e)); + +if (Self != null && Self.Location != null) { + InitializeStateAndCheckGates(null); +} + +while(Run) +{ + try { } + catch (Exception ex) { Log($"MAIN LOOP ERROR: {ex.GetType().Name} - {ex.Message}"); } + if (!Run) break; + Delay(30); +} +Log("closed"); \ No newline at end of file diff --git a/BC Items legen (boost).csx b/BC Items legen (boost).csx new file mode 100644 index 0000000..bcd6d39 --- /dev/null +++ b/BC Items legen (boost).csx @@ -0,0 +1,56 @@ +var furnitures = new Dictionary { + { 880616572, (25, 13) }, + { 880617415, (13, 13) }, + { 880617090, (13, 29) }, + { 880617208, (25, 29) } +}; + +var lastusedid = -1; +var nearfurni = false; + +Log("Furniture proximity trigger started"); +Log($"Monitoring {furnitures.Count} furniture items"); + +bool isnearfurniture(int x, int y, out int furniid) { + foreach (var furni in furnitures) { + var dx = Math.Abs(x - furni.Value.x); + var dy = Math.Abs(y - furni.Value.y); + + if (dx <= 1 && dy <= 1) { + furniid = furni.Key; + return true; + } + } + furniid = -1; + return false; +} + +while (Run) { + if (Self == null || Self.Location == null) { + Delay(100); + continue; + } + + var x = Self.Location.X; + var y = Self.Location.Y; + + if (isnearfurniture(x, y, out int furniid)) { + if (!nearfurni || furniid != lastusedid) { + Send(Out["UseFurniture"], furniid, 0); + Log($"Used furniture {furniid} from position ({x},{y})"); + lastusedid = furniid; + nearfurni = true; + } + } + else { + if (nearfurni) { + Log("Left furniture area"); + nearfurni = false; + lastusedid = -1; + } + } + + Delay(100); +} + +Log("Proximity trigger stopped"); \ No newline at end of file diff --git a/Cabbage Placer Loop.csx b/Cabbage Placer Loop.csx new file mode 100644 index 0000000..70ef00c --- /dev/null +++ b/Cabbage Placer Loop.csx @@ -0,0 +1,47 @@ +Log("=== Cabbage Placer Loop (Alle Tiles) ==="); + +// Raumgrenzen +int minX = 4; +int maxX = 11; +int minY = 1; +int maxY = 13; + +EnsureInventory(); +Delay(100); + +while (Run) { + bool hasCabbage = true; + + for (int x = minX; x <= maxX && Run && hasCabbage; x++) { + for (int y = maxY; y >= minY && Run && hasCabbage; y--) { + + long cabbageId = -1; + string cabbageName = ""; + + foreach (var item in Inventory) { + string name = item.GetName().ToLower(); + if (name.Contains("cabbage")) { + cabbageId = item.Id; + cabbageName = item.GetName(); + break; + } + } + + if (cabbageId != -1) { + Log($"Platziere: {cabbageName} auf ({x}, {y})..."); + Send(Out["PlaceObject"], $"-{cabbageId} {x} {y} 0"); + Delay(100); + EnsureInventory(); + Delay(100); + } else { + Log("Kein Cabbage mehr im Inventar!"); + hasCabbage = false; + } + } + } + + Log("Durchlauf abgeschlossen. Starte neu..."); + Delay(500); + EnsureInventory(); + Delay(100); +} \ No newline at end of file diff --git a/Catalog Exporter.csx b/Catalog Exporter.csx new file mode 100644 index 0000000..c68777e --- /dev/null +++ b/Catalog Exporter.csx @@ -0,0 +1,60 @@ +/// @name Catalog Scraper + +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Encodings.Web; + +var catalog = GetCatalog(); +var nodes = catalog.Where(x => x.Id > 0).ToArray(); +var pages = new List<(ICatalogPageNode Node, ICatalogPage Page)>(); +for (int i = 0; i < nodes.Length; i++) { + var node = nodes[i]; + Status($"Loading page {i+1}/{nodes.Length}..."); + pages.Add((node, GetCatalogPage(node))); + await Task.Delay(300); +} + +var opts = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true +}; + +static bool IsFurni(IItem it) => it.Type == ItemType.Floor || it.Type == ItemType.Wall; + +string json = JsonSerializer.Serialize( + pages.Select(x => new { + PageId = x.Node.Id, + x.Node.Name, + x.Node.Title, + x.Node.Icon, + x.Node.IsVisible, + x.Page.LayoutCode, + x.Page.Images, + x.Page.Texts, + x.Page.AcceptSeasonCurrencyAsCredits, + Offers = x.Page.Offers.Select(offer => new { + offer.Id, + offer.FurniLine, + offer.PriceInCredits, + offer.PriceInActivityPoints, + ActivityPointType = offer.ActivityPointType.ToString(), + offer.CanPurchaseAsGift, + offer.CanPurchaseMultiple, + offer.ClubLevel, + Products = offer.Products.Select(product => new { + Identifier = IsFurni(product) ? product.GetIdentifier() : null, + Name = IsFurni(product) ? product.GetIdentifier() : null, + Type = product.Type.ToString(), + product.Variant, + product.Count, + product.IsLimited + }) + }) + }), + opts +); + +Directory.CreateDirectory("catalog"); +File.WriteAllText($"catalog/{DateTime.Now:yyyyMMddHHmmssfff}.json", json); + diff --git a/Color Matching Puzzle Auto.csx b/Color Matching Puzzle Auto.csx new file mode 100644 index 0000000..37a76d5 --- /dev/null +++ b/Color Matching Puzzle Auto.csx @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +const int SOURCE_MIN_X = 18; +const int SOURCE_MAX_X = 29; +const int SOURCE_MIN_Y = 25; +const int SOURCE_MAX_Y = 36; + +const int FIELD_MIN_X = 18; +const int FIELD_MAX_X = 29; +const int FIELD_MIN_Y = 9; +const int FIELD_MAX_Y = 20; + +int GetColorNumber(string name) +{ + var match = Regex.Match(name, @"\d+$"); + return match.Success ? int.Parse(match.Value) : -1; +} + +bool InField(int x, int y) => x >= FIELD_MIN_X && x <= FIELD_MAX_X && y >= FIELD_MIN_Y && y <= FIELD_MAX_Y; + +Log("=== WAITING FOR FIELD POSITION ==="); + +while (true) +{ + int selfX = Self.Location.X; + int selfY = Self.Location.Y; + + if (selfX == 14 && selfY >= 16 && selfY <= 35) + { + Log("Moving to (14, 15)..."); + await SendAsync(Out["MoveAvatar"], 14, 15); + await Task.Delay(500); + continue; + } + + if (InField(selfX, selfY)) + { + Log($"In field at ({selfX}, {selfY}), starting!"); + break; + } + + await Task.Delay(100); +} + +Log("=== AUTO DETECTING COLORS ==="); + +var selectors = new Dictionary(); +foreach (var item in FloorItems) +{ + string name = item.GetName(); + if (name.Contains("Cylinder Block") || name.Contains("Hemisphere Block")) + { + int colorNum = GetColorNumber(name); + if (colorNum > 0 && !selectors.ContainsKey(colorNum)) + { + selectors[colorNum] = (int)item.Id; + Log($"Selector: {name} -> Color {colorNum}"); + } + } +} +Log($"Total selectors: {selectors.Count}"); + +var sourcePixels = new Dictionary<(int x, int y), int>(); +foreach (var item in FloorItems) +{ + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + if (x < SOURCE_MIN_X || x > SOURCE_MAX_X || y < SOURCE_MIN_Y || y > SOURCE_MAX_Y) continue; + if (z < 0.7 || z > 1.0) continue; + int colorNum = GetColorNumber(item.GetName()); + if (colorNum > 0) + sourcePixels[(x, y)] = colorNum; +} +Log($"Source pixels: {sourcePixels.Count}"); + +int offsetY = SOURCE_MIN_Y - FIELD_MIN_Y; +int changed = 0; + +for (int attempt = 1; attempt <= 3; attempt++) +{ + var fieldTiles = new Dictionary<(int x, int y), (int id, int colorNum)>(); + foreach (var item in FloorItems) + { + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + if (x < FIELD_MIN_X || x > FIELD_MAX_X || y < FIELD_MIN_Y || y > FIELD_MAX_Y) continue; + + int colorNum = GetColorNumber(item.GetName()); + + if (z > 0.5) + fieldTiles[(x, y)] = ((int)item.Id, colorNum); + else if (!fieldTiles.ContainsKey((x, y))) + fieldTiles[(x, y)] = ((int)item.Id, -1); + } + + var toChange = new List<(int fieldId, int srcColor)>(); + foreach (var src in sourcePixels) + { + int fieldX = src.Key.x; + int fieldY = src.Key.y - offsetY; + int srcColor = src.Value; + + if (!fieldTiles.TryGetValue((fieldX, fieldY), out var field)) continue; + if (field.colorNum == srcColor) continue; + if (!selectors.ContainsKey(srcColor)) continue; + + toChange.Add((field.id, srcColor)); + } + + if (toChange.Count == 0) + { + Log($"Attempt {attempt}: All correct!"); + break; + } + + Log($"Attempt {attempt}: {toChange.Count} to change"); + + var sorted = toChange.OrderBy(x => x.srcColor).ToList(); + int lastColor = -1; + + foreach (var item in sorted) + { + if (item.srcColor != lastColor) + { + await SendAsync(Out["ClickFurni"], selectors[item.srcColor], 0); + await Task.Delay(50); + lastColor = item.srcColor; + } + + await SendAsync(Out["ClickFurni"], item.fieldId, 0); + await Task.Delay(50); + changed++; + } + + await Task.Delay(500); +} + +Log("=== SUBMITTING ==="); +var badge = FloorItems.FirstOrDefault(f => f.GetName() == "Badge Display Case"); +if (badge != null) +{ + await SendAsync(Out["ClickFurni"], (int)badge.Id, 0); + Log($"Clicked Badge Display Case (ID: {badge.Id})"); +} +else +{ + Log("Badge Display Case not found!"); +} + +Log($"Done! Total changed: {changed}"); diff --git a/Color Matching Simple.csx b/Color Matching Simple.csx new file mode 100644 index 0000000..d597dd7 --- /dev/null +++ b/Color Matching Simple.csx @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +const int SOURCE_MIN_X = 18; +const int SOURCE_MAX_X = 29; +const int SOURCE_MIN_Y = 25; +const int SOURCE_MAX_Y = 36; + +const int FIELD_MIN_X = 18; +const int FIELD_MAX_X = 29; +const int FIELD_MIN_Y = 9; +const int FIELD_MAX_Y = 20; + +int GetColorNumber(string name) +{ + var match = Regex.Match(name, @"\d+$"); + return match.Success ? int.Parse(match.Value) : -1; +} + +Log("=== AUTO DETECTING COLORS ==="); + +var selectors = new Dictionary(); +foreach (var item in FloorItems) +{ + string name = item.GetName(); + if (name.Contains("Cylinder Block") || name.Contains("Hemisphere Block")) + { + int colorNum = GetColorNumber(name); + if (colorNum > 0 && !selectors.ContainsKey(colorNum)) + { + selectors[colorNum] = (int)item.Id; + Log($"Selector: {name} -> Color {colorNum}"); + } + } +} +Log($"Total selectors: {selectors.Count}"); + +var sourcePixels = new Dictionary<(int x, int y), int>(); +foreach (var item in FloorItems) +{ + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + if (x < SOURCE_MIN_X || x > SOURCE_MAX_X || y < SOURCE_MIN_Y || y > SOURCE_MAX_Y) continue; + if (z < 0.7 || z > 1.0) continue; + int colorNum = GetColorNumber(item.GetName()); + if (colorNum > 0) + sourcePixels[(x, y)] = colorNum; +} +Log($"Source pixels: {sourcePixels.Count}"); + +var fieldTiles = new Dictionary<(int x, int y), int>(); +foreach (var item in FloorItems) +{ + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + if (x < FIELD_MIN_X || x > FIELD_MAX_X || y < FIELD_MIN_Y || y > FIELD_MAX_Y) continue; + if (z > 0.5) continue; + fieldTiles[(x, y)] = (int)item.Id; +} +Log($"Field tiles: {fieldTiles.Count}"); + +int offsetY = SOURCE_MIN_Y - FIELD_MIN_Y; + +var toChange = new List<(int fieldId, int srcColor)>(); +foreach (var src in sourcePixels) +{ + int fieldX = src.Key.x; + int fieldY = src.Key.y - offsetY; + int srcColor = src.Value; + + if (!fieldTiles.TryGetValue((fieldX, fieldY), out int fieldId)) continue; + if (!selectors.ContainsKey(srcColor)) continue; + + toChange.Add((fieldId, srcColor)); +} + +var sorted = toChange.OrderBy(x => x.srcColor).ToList(); +Log($"To change: {sorted.Count}"); + +int lastColor = -1; +int changed = 0; + +foreach (var item in sorted) +{ + if (item.srcColor != lastColor) + { + await SendAsync(Out["ClickFurni"], selectors[item.srcColor], 0); + await Task.Delay(50); + lastColor = item.srcColor; + } + + await SendAsync(Out["ClickFurni"], item.fieldId, 0); + await Task.Delay(50); + changed++; +} + +Log($"Done! Changed: {changed}"); \ No newline at end of file diff --git a/Color Pattern Walker Fixed.csx b/Color Pattern Walker Fixed.csx new file mode 100644 index 0000000..341657e --- /dev/null +++ b/Color Pattern Walker Fixed.csx @@ -0,0 +1,635 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +class Cell +{ + public long Id; + public int X; + public int Y; + public int State; + public int Kind; + public double Z; +} + +const int SOL_MIN_X = 4; +const int SOL_MAX_X = 9; +const int SOL_MIN_Y = 1; +const int SOL_MAX_Y = 8; + +const int PLAY_MIN_X = 8; +const int PLAY_MAX_X = 13; +const int PLAY_MIN_Y = 14; +const int PLAY_MAX_Y = 21; + +const int SPAWN_X = 13; +const int SPAWN_Y = 13; +const int SPAWN_WAIT_MS = 180000; + +const int FORCED_CYCLE = 5; +const bool ASSUME_START_ALL_ZERO = true; + +const int STEP_WAIT_MS = 420; +const int MOVE_COMMAND_INTERVAL_MS = 70; +const int MOVE_SETTLE_MS = 0; +const int PERIODIC_SYNC_EVERY_STEPS = 12; +const int PATH_BURST_MAX_STEPS = 10; +const int MAX_STEPS = 5000; + +string K(int x, int y) => x + "," + y; + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +string GetNameSafe(dynamic item) +{ + try + { + string n = item.GetName(); + return string.IsNullOrWhiteSpace(n) ? "" : n; + } + catch { return ""; } +} + +bool InRect(int x, int y, int minX, int maxX, int minY, int maxY) +{ + return x >= minX && x <= maxX && y >= minY && y <= maxY; +} + +bool InPlay(int x, int y) +{ + return InRect(x, y, PLAY_MIN_X, PLAY_MAX_X, PLAY_MIN_Y, PLAY_MAX_Y); +} + +void FullRoomScanLog() +{ + var all = new List(); + foreach (var it in FloorItems) + { + if (it == null) continue; + all.Add(it); + } + + Log("=== Full Room Scan ==="); + Log($"FloorItems total: {all.Count}"); + + var byKind = all.GroupBy(x => GetKind(x)) + .Select(g => new { + Kind = g.Key, + Count = g.Count(), + States = string.Join(",", g.Select(x => GetState(x)).Distinct().OrderBy(x => x)), + Name = g.Select(x => GetNameSafe(x)).FirstOrDefault() + }) + .OrderByDescending(x => x.Count) + .Take(25) + .ToList(); + + foreach (var k in byKind) + Log($"Kind {k.Kind} x{k.Count} states[{k.States}] name={k.Name}"); +} + +bool WaitForSpawn() +{ + Log($"Waiting for round spawn on {SPAWN_X}:{SPAWN_Y}..."); + int elapsed = 0; + while (elapsed < SPAWN_WAIT_MS) + { + if (Self != null && Self.Location != null && Self.Location.X == SPAWN_X && Self.Location.Y == SPAWN_Y) + { + Log("Spawn detected, starting solver."); + Delay(300); + return true; + } + Delay(200); + elapsed += 200; + } + Log("Spawn timeout. Starting anyway."); + return false; +} + +List CollectCellsInRect(int minX, int maxX, int minY, int maxY) +{ + var raw = new List(); + foreach (var it in FloorItems) + { + if (it == null) continue; + int x = it.Location.X; + int y = it.Location.Y; + if (!InRect(x, y, minX, maxX, minY, maxY)) continue; + raw.Add(new Cell { + Id = it.Id, + X = x, + Y = y, + State = GetState(it), + Kind = GetKind(it), + Z = it.Location.Z + }); + } + + if (raw.Count == 0) return new List(); + + int targetCount = (maxX - minX + 1) * (maxY - minY + 1); + + var bestKind = raw.GroupBy(c => c.Kind) + .Select(g => new { + Kind = g.Key, + CoordCount = g.Select(c => K(c.X, c.Y)).Distinct().Count(), + Count = g.Count() + }) + .OrderByDescending(x => x.CoordCount) + .ThenByDescending(x => x.Count) + .First(); + + var cellsOfKind = raw.Where(c => c.Kind == bestKind.Kind).ToList(); + + var bestPerCoord = new List(); + foreach (var g in cellsOfKind.GroupBy(c => K(c.X, c.Y))) + { + var top = g.OrderByDescending(c => c.Z).First(); + bestPerCoord.Add(top); + } + + Log($"Rect X[{minX}-{maxX}] Y[{minY}-{maxY}] -> kind {bestKind.Kind}, coords {bestPerCoord.Count}/{targetCount}"); + return bestPerCoord; +} + +Dictionary IndexCells(List cells) +{ + var d = new Dictionary(); + foreach (var c in cells) d[K(c.X, c.Y)] = c; + return d; +} + +Dictionary ReadCurrentPlayStates(HashSet ids) +{ + var d = new Dictionary(); + foreach (var it in FloorItems) + { + if (it == null) continue; + long id = it.Id; + if (!ids.Contains(id)) continue; + d[id] = GetState(it); + } + return d; +} + +int Need(int current, int target, int cycle) +{ + int d = (target - current) % cycle; + if (d < 0) d += cycle; + return d; +} + +int Objective(Dictionary cur, Dictionary target, int cycle) +{ + int sum = 0; + foreach (var kv in target) + { + if (!cur.ContainsKey(kv.Key)) continue; + sum += Need(cur[kv.Key], kv.Value, cycle); + } + return sum; +} + +int HardObjectiveFromRoom(List playCells, Dictionary targetByPlayId, int cycle, int[] needs) +{ + var ids = new HashSet(targetByPlayId.Keys); + var cur = ReadCurrentPlayStates(ids); + for (int i = 0; i < playCells.Count; i++) + { + long id = playCells[i].Id; + int val = cur.ContainsKey(id) ? cur[id] : 0; + needs[i] = Need(val, targetByPlayId[id], cycle); + } + return needs.Sum(); +} + +int ApplyNeedStep(int[] needs, int idx, int cycle) +{ + int d = needs[idx]; + if (d > 0) + { + needs[idx] = d - 1; + return -1; + } + + needs[idx] = cycle - 1; + return cycle - 1; +} + +bool WaitUntilAt(int tx, int ty) +{ + int elapsed = 0; + while (elapsed < STEP_WAIT_MS) + { + if (Self != null && Self.Location != null && Self.Location.X == tx && Self.Location.Y == ty) + return true; + Delay(40); + elapsed += 40; + } + return false; +} + +List<(int x, int y)> Neigh4(int x, int y) +{ + var list = new List<(int, int)> { + (x + 1, y), + (x - 1, y), + (x, y + 1), + (x, y - 1), + (x + 1, y + 1), + (x + 1, y - 1), + (x - 1, y + 1), + (x - 1, y - 1) + }; + return list.Where(p => InPlay(p.Item1, p.Item2)).ToList(); +} + +int Dist(int x1, int y1, int x2, int y2) +{ + return Math.Abs(x1 - x2) + Math.Abs(y1 - y2); +} + +int ReadSelfX(int fallback) +{ + try { return Self.Location.X; } + catch { return fallback; } +} + +int ReadSelfY(int fallback) +{ + try { return Self.Location.Y; } + catch { return fallback; } +} + +DateTime _lastMoveCmd = DateTime.MinValue; +void FastMove(int x, int y) +{ + int since = (int)(DateTime.UtcNow - _lastMoveCmd).TotalMilliseconds; + if (since < MOVE_COMMAND_INTERVAL_MS) + Delay(MOVE_COMMAND_INTERVAL_MS - since); + Move(x, y); + _lastMoveCmd = DateTime.UtcNow; +} + +(int nx, int ny)? NextStepToTarget(int sx, int sy, int tx, int ty) +{ + (int x, int y) start = (sx, sy); + (int x, int y) goal = (tx, ty); + if (start == goal) return null; + + var q = new Queue<(int x, int y)>(); + var vis = new HashSet(); + var prev = new Dictionary(); + q.Enqueue(start); + vis.Add(K(start.x, start.y)); + + while (q.Count > 0) + { + var cur = q.Dequeue(); + foreach (var n in Neigh4(cur.x, cur.y)) + { + string nk = K(n.x, n.y); + if (vis.Contains(nk)) continue; + vis.Add(nk); + prev[nk] = cur; + if (n == goal) + { + var node = goal; + while (true) + { + var pk = K(node.x, node.y); + var pnode = prev[pk]; + if (pnode == start) return node; + node = pnode; + } + } + q.Enqueue(n); + } + } + return null; +} + +List<(int x, int y)> BuildPathToTarget(int sx, int sy, int tx, int ty) +{ + (int x, int y) start = (sx, sy); + (int x, int y) goal = (tx, ty); + var empty = new List<(int x, int y)>(); + if (start == goal) return empty; + + var q = new Queue<(int x, int y)>(); + var vis = new HashSet(); + var prev = new Dictionary(); + q.Enqueue(start); + vis.Add(K(start.x, start.y)); + + while (q.Count > 0) + { + var cur = q.Dequeue(); + foreach (var n in Neigh4(cur.x, cur.y)) + { + string nk = K(n.x, n.y); + if (vis.Contains(nk)) continue; + vis.Add(nk); + prev[nk] = cur; + if (n == goal) + { + var rev = new List<(int x, int y)>(); + var node = goal; + while (node != start) + { + rev.Add(node); + node = prev[K(node.x, node.y)]; + } + rev.Reverse(); + return rev; + } + q.Enqueue(n); + } + } + return empty; +} + +Log("=== Color Pattern Walker Solver (fixed bounds) ==="); +WaitForSpawn(); +FullRoomScanLog(); + +var solCells = CollectCellsInRect(SOL_MIN_X, SOL_MAX_X, SOL_MIN_Y, SOL_MAX_Y); +var playCells = CollectCellsInRect(PLAY_MIN_X, PLAY_MAX_X, PLAY_MIN_Y, PLAY_MAX_Y); + +int expectedSol = (SOL_MAX_X - SOL_MIN_X + 1) * (SOL_MAX_Y - SOL_MIN_Y + 1); +int expectedPlay = (PLAY_MAX_X - PLAY_MIN_X + 1) * (PLAY_MAX_Y - PLAY_MIN_Y + 1); + +if (solCells.Count < expectedSol || playCells.Count < expectedPlay) +{ + Log($"ERROR: Board incomplete. Solution {solCells.Count}/{expectedSol}, Play {playCells.Count}/{expectedPlay}"); + return; +} + +var solMap = IndexCells(solCells); +var playMap = IndexCells(playCells); + +var targetByPlayId = new Dictionary(); +foreach (var p in playCells) +{ + int sx = p.X - 4; + int sy = p.Y - 13; + string sk = K(sx, sy); + if (!solMap.ContainsKey(sk)) continue; + targetByPlayId[p.Id] = solMap[sk].State; +} + +if (targetByPlayId.Count != expectedPlay) +{ + Log($"ERROR: Could not map all play cells to solution cells ({targetByPlayId.Count}/{expectedPlay})."); + return; +} + +var ids = new HashSet(targetByPlayId.Keys); +var cur = new Dictionary(); +if (ASSUME_START_ALL_ZERO) +{ + foreach (var id in ids) cur[id] = 0; + Log("Using round-start baseline: all play tiles = state 0."); +} +else +{ + cur = ReadCurrentPlayStates(ids); + if (cur.Count != expectedPlay) + { + Log($"ERROR: Could not read all current play states ({cur.Count}/{expectedPlay})."); + return; + } +} + +int cycle = FORCED_CYCLE; +if (cycle < 2) cycle = 5; + +Log($"State cycle: {cycle}"); + +var coordToIdx = new Dictionary(); +var idToIdx = new Dictionary(); +for (int i = 0; i < playCells.Count; i++) +{ + var c = playCells[i]; + coordToIdx[K(c.X, c.Y)] = i; + idToIdx[c.Id] = i; +} + +int[] needs = new int[playCells.Count]; +for (int i = 0; i < playCells.Count; i++) +{ + long id = playCells[i].Id; + needs[i] = Need(cur[id], targetByPlayId[id], cycle); +} + +int obj = needs.Sum(); +Log($"Initial objective: {obj}"); +if (obj == 0) { Log("Already solved."); return; } + +if (Self == null || Self.Location == null) +{ + Log("ERROR: No self location."); + return; +} + +int cx = Self.Location.X; +int cy = Self.Location.Y; + +if (!InPlay(cx, cy)) +{ + var bestEntry = playCells + .OrderBy(c => Dist(cx, cy, c.X, c.Y)) + .First(); + FastMove(bestEntry.X, bestEntry.Y); + WaitUntilAt(bestEntry.X, bestEntry.Y); + cur = ReadCurrentPlayStates(ids); + for (int i = 0; i < playCells.Count; i++) + { + long id = playCells[i].Id; + needs[i] = Need(cur[id], targetByPlayId[id], cycle); + } + cx = ReadSelfX(bestEntry.X); + cy = ReadSelfY(bestEntry.Y); + obj = needs.Sum(); + Log($"After entry objective: {obj}"); +} + +int stagnation = 0; +int prevX = -999; +int prevY = -999; +int sinceResync = 0; +var burstPath = new List<(int x, int y)>(); +int burstIndex = 0; + +for (int step = 1; step <= MAX_STEPS; step++) +{ + if (!InPlay(cx, cy)) + { + Log("WARN: Left play field unexpectedly, moving back."); + var back = playCells.OrderBy(c => Dist(cx, cy, c.X, c.Y)).First(); + FastMove(back.X, back.Y); + WaitUntilAt(back.X, back.Y); + cx = ReadSelfX(back.X); + cy = ReadSelfY(back.Y); + cur = ReadCurrentPlayStates(ids); + for (int i = 0; i < playCells.Count; i++) + { + long id = playCells[i].Id; + needs[i] = Need(cur[id], targetByPlayId[id], cycle); + } + obj = needs.Sum(); + sinceResync = 0; + burstPath.Clear(); + burstIndex = 0; + continue; + } + + if (obj == 0) + { + obj = HardObjectiveFromRoom(playCells, targetByPlayId, cycle, needs); + int standNeed = 999; + if (InPlay(cx, cy) && coordToIdx.ContainsKey(K(cx, cy))) + standNeed = needs[coordToIdx[K(cx, cy)]]; + + if (obj == 0 && standNeed == 0) + { + Log("=== Done: bottom matches solution (hard check OK) ==="); + return; + } + } + + var neighbors = Neigh4(cx, cy); + if (neighbors.Count == 0) + { + Log("ERROR: No neighbors on play field."); + return; + } + + int unresolved = 0; + for (int i = 0; i < needs.Length; i++) + if (needs[i] > 0) unresolved++; + + if (burstIndex >= burstPath.Count) + { + var needy = playCells + .Select(c => new { + Cell = c, + Need = needs[idToIdx[c.Id]], + D = Dist(cx, cy, c.X, c.Y) + }) + .Where(x => x.Need > 0) + .OrderByDescending(x => x.Need) + .ThenBy(x => x.D) + .FirstOrDefault(); + + if (needy != null) + { + var path = BuildPathToTarget(cx, cy, needy.Cell.X, needy.Cell.Y); + if (path.Count > 0) + { + burstPath = path.Take(PATH_BURST_MAX_STEPS).ToList(); + burstIndex = 0; + } + } + + if (burstIndex >= burstPath.Count) + { + var fallback = neighbors + .Select(n => new { + N = n, + Need = needs[coordToIdx[K(n.x, n.y)]], + Back = (n.x == prevX && n.y == prevY) ? 1 : 0 + }) + .OrderByDescending(x => x.Need) + .ThenBy(x => x.Back) + .First(); + burstPath = new List<(int x, int y)> { fallback.N }; + burstIndex = 0; + } + } + + (int x, int y) bestN = burstPath[burstIndex]; + burstIndex++; + + int idxChosen = coordToIdx[K(bestN.x, bestN.y)]; + if (needs[idxChosen] == 0 && unresolved <= 8) + { + stagnation++; + } + else + { + stagnation = 0; + } + + if (stagnation >= 12) + { + burstPath.Clear(); + burstIndex = 0; + stagnation = 0; + } + + prevX = cx; + prevY = cy; + int oldObj = obj; + + FastMove(bestN.x, bestN.y); + if (MOVE_SETTLE_MS > 0) Delay(MOVE_SETTLE_MS); + + // Predictive advance: keep running without stop-go per step. + cx = bestN.x; + cy = bestN.y; + + if (InPlay(cx, cy) && coordToIdx.ContainsKey(K(cx, cy))) + { + int landedIdx = coordToIdx[K(cx, cy)]; + obj += ApplyNeedStep(needs, landedIdx, cycle); + } + else + { + sinceResync = 20; + } + + sinceResync++; + if (sinceResync >= PERIODIC_SYNC_EVERY_STEPS || stagnation >= 8) + { + Delay(120); + cx = ReadSelfX(cx); + cy = ReadSelfY(cy); + cur = ReadCurrentPlayStates(ids); + for (int i = 0; i < playCells.Count; i++) + { + long id = playCells[i].Id; + needs[i] = Need(cur[id], targetByPlayId[id], cycle); + } + obj = needs.Sum(); + sinceResync = 0; + } + + int newObj = obj; + Log($"[{step}] ({cx},{cy}) objective {oldObj} -> {newObj}"); +} + +obj = HardObjectiveFromRoom(playCells, targetByPlayId, cycle, needs); +int finalStandNeed = 999; +if (Self != null && Self.Location != null) +{ + int fx = ReadSelfX(-9999); + int fy = ReadSelfY(-9999); + if (InPlay(fx, fy) && coordToIdx.ContainsKey(K(fx, fy))) + finalStandNeed = needs[coordToIdx[K(fx, fy)]]; +} + +if (obj == 0 && finalStandNeed == 0) + Log("=== Done: bottom matches solution (hard check OK) ==="); +else + Log($"Stopped after max steps. Remaining objective: {obj}"); diff --git a/Color Pattern Walker Solver.csx b/Color Pattern Walker Solver.csx new file mode 100644 index 0000000..e2de95c --- /dev/null +++ b/Color Pattern Walker Solver.csx @@ -0,0 +1,518 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +class Tile +{ + public long Id; + public int Kind; + public string Name; + public int X; + public int Y; + public double Z; + public int State; +} + +class Cluster +{ + public int Kind; + public List Tiles = new List(); + public double AvgZ; + public HashSet StateSet = new HashSet(); + public double CenterX; + public double CenterY; +} + +const int SPAWN_X = 13; +const int SPAWN_Y = 13; +const bool WAIT_FOR_SPAWN_START = true; +const int SPAWN_WAIT_TIMEOUT_MS = 180000; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +string GetNameSafe(dynamic item) +{ + try + { + string n = item.GetName(); + return string.IsNullOrWhiteSpace(n) ? "" : n; + } + catch { return ""; } +} + +List ReadAllFloorTiles() +{ + var list = new List(); + foreach (var item in FloorItems) + { + if (item == null) continue; + list.Add(new Tile { + Id = item.Id, + Kind = GetKind(item), + Name = GetNameSafe(item), + X = item.Location.X, + Y = item.Location.Y, + Z = item.Location.Z, + State = GetState(item) + }); + } + return list; +} + +string PosKey(int x, int y) => x + "," + y; + +List BuildClusters(List tilesOfKind) +{ + var byPos = tilesOfKind.ToDictionary(t => PosKey(t.X, t.Y), t => t); + var visited = new HashSet(); + var clusters = new List(); + + int[] d = { -1, 0, 1 }; + foreach (var t in tilesOfKind) + { + string start = PosKey(t.X, t.Y); + if (visited.Contains(start)) continue; + + var q = new Queue(); + var c = new Cluster { Kind = t.Kind }; + q.Enqueue(t); + visited.Add(start); + + while (q.Count > 0) + { + var cur = q.Dequeue(); + c.Tiles.Add(cur); + c.StateSet.Add(cur.State); + + foreach (int dx in d) + foreach (int dy in d) + { + if (dx == 0 && dy == 0) continue; + string nk = PosKey(cur.X + dx, cur.Y + dy); + if (visited.Contains(nk)) continue; + if (!byPos.ContainsKey(nk)) continue; + visited.Add(nk); + q.Enqueue(byPos[nk]); + } + } + + c.AvgZ = c.Tiles.Count == 0 ? 0.0 : c.Tiles.Average(x => x.Z); + c.CenterX = c.Tiles.Count == 0 ? 0.0 : c.Tiles.Average(x => x.X); + c.CenterY = c.Tiles.Count == 0 ? 0.0 : c.Tiles.Average(x => x.Y); + clusters.Add(c); + } + + return clusters; +} + +void LogRoomScan(List all) +{ + Log("=== Full Room Scan ==="); + Log($"FloorItems total: {all.Count}"); + + var byKind = all.GroupBy(x => x.Kind) + .Select(g => new { + Kind = g.Key, + Count = g.Count(), + Names = g.Select(x => x.Name).Distinct().Take(3).ToArray(), + MinX = g.Min(x => x.X), MaxX = g.Max(x => x.X), + MinY = g.Min(x => x.Y), MaxY = g.Max(x => x.Y), + MinZ = g.Min(x => x.Z), MaxZ = g.Max(x => x.Z), + States = string.Join(",", g.Select(x => x.State).Distinct().OrderBy(x => x)) + }) + .OrderByDescending(x => x.Count) + .Take(30) + .ToList(); + + foreach (var k in byKind) + { + Log($"Kind {k.Kind} x{k.Count} | states [{k.States}] | bbox X[{k.MinX}-{k.MaxX}] Y[{k.MinY}-{k.MaxY}] Z[{k.MinZ:F2}-{k.MaxZ:F2}] | name {string.Join(" / ", k.Names)}"); + } +} + +double DistD(double x1, double y1, double x2, double y2) +{ + double dx = x1 - x2; + double dy = y1 - y2; + return Math.Sqrt(dx * dx + dy * dy); +} + +bool TryDetectBoards(List all, int spawnX, int spawnY, out Cluster top, out Cluster bottom) +{ + top = null; + bottom = null; + + var byKind = all.GroupBy(x => x.Kind).ToList(); + Cluster bestA = null, bestB = null; + int bestScore = -1; + + foreach (var g in byKind) + { + var clusters = BuildClusters(g.ToList()) + .Where(c => c.Tiles.Count >= 9) + .OrderByDescending(c => c.Tiles.Count) + .ToList(); + if (clusters.Count < 2) continue; + + for (int i = 0; i < clusters.Count; i++) + { + for (int j = i + 1; j < clusters.Count; j++) + { + var a = clusters[i]; + var b = clusters[j]; + int minCount = Math.Min(a.Tiles.Count, b.Tiles.Count); + int diff = Math.Abs(a.Tiles.Count - b.Tiles.Count); + int score = (minCount * 10) - diff; + if (a.StateSet.Count > 1) score += 5; + if (b.StateSet.Count > 1) score += 5; + + if (score > bestScore) + { + bestScore = score; + bestA = a; + bestB = b; + } + } + } + } + + if (bestA == null || bestB == null) return false; + + // Prefer the board whose center is closer to your spawn as bottom/play board. + double da = DistD(bestA.CenterX, bestA.CenterY, spawnX, spawnY); + double db = DistD(bestB.CenterX, bestB.CenterY, spawnX, spawnY); + if (Math.Abs(da - db) >= 2.0) + { + if (da <= db) { bottom = bestA; top = bestB; } + else { bottom = bestB; top = bestA; } + } + else + { + // Fallback if both are similarly far away. + if (bestA.AvgZ >= bestB.AvgZ) { top = bestA; bottom = bestB; } + else { top = bestB; bottom = bestA; } + } + + return true; +} + +bool WaitForRoundSpawn(int spawnX, int spawnY, int timeoutMs) +{ + Log($"Waiting for round start spawn at {spawnX}:{spawnY}..."); + int elapsed = 0; + while (elapsed < timeoutMs) + { + if (Self != null && Self.Location != null) + { + if (Self.Location.X == spawnX && Self.Location.Y == spawnY) + { + Log("Spawn detected. Round started."); + Delay(350); + return true; + } + } + Delay(200); + elapsed += 200; + } + + Log("Spawn wait timeout reached. Continuing anyway."); + return false; +} + +Dictionary BuildIndexedMap(Cluster c, out int w, out int h) +{ + var xs = c.Tiles.Select(t => t.X).Distinct().OrderBy(x => x).ToList(); + var ys = c.Tiles.Select(t => t.Y).Distinct().OrderBy(y => y).ToList(); + w = xs.Count; + h = ys.Count; + + var xToI = new Dictionary(); + var yToI = new Dictionary(); + for (int i = 0; i < xs.Count; i++) xToI[xs[i]] = i; + for (int i = 0; i < ys.Count; i++) yToI[ys[i]] = i; + + var map = new Dictionary(); + foreach (var t in c.Tiles) + { + int xi = xToI[t.X]; + int yi = yToI[t.Y]; + map[PosKey(xi, yi)] = t; + } + return map; +} + +bool TryBuildTargetMap(Cluster top, Cluster bottom, out Dictionary targetByBottomId, out List bottomTilesOrdered) +{ + targetByBottomId = new Dictionary(); + bottomTilesOrdered = new List(); + + int bw, bh, tw, th; + var bMap = BuildIndexedMap(bottom, out bw, out bh); + var tMap = BuildIndexedMap(top, out tw, out th); + + if (bw != tw || bh != th) + { + Log($"WARN: Different board dimensions: top {tw}x{th}, bottom {bw}x{bh}. Trying overlap map."); + } + + int w = Math.Min(bw, tw); + int h = Math.Min(bh, th); + if (w <= 0 || h <= 0) return false; + + var transforms = new List>(); + transforms.Add((x, y) => (x, y)); + transforms.Add((x, y) => (w - 1 - x, y)); + transforms.Add((x, y) => (x, h - 1 - y)); + transforms.Add((x, y) => (w - 1 - x, h - 1 - y)); + if (w == h) + { + transforms.Add((x, y) => (y, x)); + transforms.Add((x, y) => (w - 1 - y, x)); + transforms.Add((x, y) => (y, h - 1 - x)); + transforms.Add((x, y) => (w - 1 - y, h - 1 - x)); + } + + int bestIdx = 0; + int bestMatches = -1; + + for (int ti = 0; ti < transforms.Count; ti++) + { + int matches = 0; + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var bKey = PosKey(x, y); + if (!bMap.ContainsKey(bKey)) continue; + var tr = transforms[ti](x, y); + var tKey = PosKey(tr.x, tr.y); + if (tMap.ContainsKey(tKey)) matches++; + } + } + if (matches > bestMatches) + { + bestMatches = matches; + bestIdx = ti; + } + } + + var bestTf = transforms[bestIdx]; + Log($"Mapping transform index: {bestIdx}, overlap: {bestMatches}"); + + foreach (var kv in bMap) + { + var p = kv.Key.Split(','); + int x = int.Parse(p[0]); + int y = int.Parse(p[1]); + if (x >= w || y >= h) continue; + + var tr = bestTf(x, y); + var tKey = PosKey(tr.x, tr.y); + if (!tMap.ContainsKey(tKey)) continue; + + var bTile = kv.Value; + var tTile = tMap[tKey]; + targetByBottomId[bTile.Id] = tTile.State; + bottomTilesOrdered.Add(bTile); + } + + return targetByBottomId.Count > 0; +} + +Dictionary ReadCurrentStatesById(HashSet ids) +{ + var map = new Dictionary(); + foreach (var item in FloorItems) + { + if (item == null) continue; + long id = item.Id; + if (!ids.Contains(id)) continue; + map[id] = GetState(item); + } + return map; +} + +int CircularNeed(int current, int target, int mod) +{ + int d = (target - current) % mod; + if (d < 0) d += mod; + return d; +} + +int CalcObjective(Dictionary current, Dictionary target, int cycle) +{ + int s = 0; + foreach (var kv in target) + { + if (!current.ContainsKey(kv.Key)) continue; + s += CircularNeed(current[kv.Key], kv.Value, cycle); + } + return s; +} + +bool WaitUntilAt(int tx, int ty, int timeoutMs) +{ + int elapsed = 0; + while (elapsed < timeoutMs) + { + if (Self != null && Self.Location != null && Self.Location.X == tx && Self.Location.Y == ty) + return true; + Delay(120); + elapsed += 120; + } + return false; +} + +int Dist(int x1, int y1, int x2, int y2) +{ + return Math.Abs(x1 - x2) + Math.Abs(y1 - y2); +} + +Log("=== Color Pattern Walker Solver ==="); + +if (WAIT_FOR_SPAWN_START) + WaitForRoundSpawn(SPAWN_X, SPAWN_Y, SPAWN_WAIT_TIMEOUT_MS); + +var allTiles = ReadAllFloorTiles(); +if (allTiles.Count == 0) +{ + Log("ERROR: No floor items found."); + return; +} + +LogRoomScan(allTiles); + +Cluster top, bottom; +if (!TryDetectBoards(allTiles, SPAWN_X, SPAWN_Y, out top, out bottom)) +{ + Log("ERROR: Could not auto-detect top template board + bottom play board."); + Log("Tip: run ColorPuzzleScanner first and tell me tile Kind/Name, then I hard-bind it."); + return; +} + +Log($"Detected kind: {top.Kind}"); +Log($"Top tiles: {top.Tiles.Count}, avgZ={top.AvgZ:F2}"); +Log($"Bottom tiles: {bottom.Tiles.Count}, avgZ={bottom.AvgZ:F2}"); + +Dictionary targetById; +List bottomTiles; +if (!TryBuildTargetMap(top, bottom, out targetById, out bottomTiles)) +{ + Log("ERROR: Could not map top pattern to bottom board."); + return; +} + +var targetIds = new HashSet(targetById.Keys); +var current = ReadCurrentStatesById(targetIds); +if (current.Count == 0) +{ + Log("ERROR: Could not read current bottom states."); + return; +} + +int maxStateSeen = 0; +foreach (var v in targetById.Values) if (v > maxStateSeen) maxStateSeen = v; +foreach (var v in current.Values) if (v > maxStateSeen) maxStateSeen = v; +int cycle = maxStateSeen + 1; +if (cycle < 2 || cycle > 8) cycle = 4; + +Log($"Mapped tiles: {targetById.Count}"); +Log($"State cycle guessed: {cycle}"); + +int objective = CalcObjective(current, targetById, cycle); +Log($"Initial distance: {objective}"); +if (objective == 0) +{ + Log("Already solved."); + return; +} + +if (Self == null || Self.Location == null) +{ + Log("ERROR: Self position unavailable."); + return; +} + +var rng = new Random(); +int stagnation = 0; +const int MAX_MOVES = 1200; + +for (int step = 1; step <= MAX_MOVES; step++) +{ + var meX = Self?.Location?.X ?? -1; + var meY = Self?.Location?.Y ?? -1; + + Tile chosen = null; + + var needTiles = bottomTiles + .Where(t => current.ContainsKey(t.Id) && targetById.ContainsKey(t.Id)) + .Select(t => new { + Tile = t, + Need = CircularNeed(current[t.Id], targetById[t.Id], cycle), + D = (meX >= 0 && meY >= 0) ? Dist(meX, meY, t.X, t.Y) : 9999 + }) + .Where(x => x.Need > 0) + .OrderByDescending(x => x.Need) + .ThenBy(x => x.D) + .ToList(); + + if (needTiles.Count == 0) + { + Log("Solved (need list empty)."); + break; + } + + if (stagnation >= 12) + { + chosen = needTiles[rng.Next(needTiles.Count)].Tile; + } + else + { + chosen = needTiles[0].Tile; + } + + Log($"[{step}] Move to ({chosen.X},{chosen.Y}) id={chosen.Id} state={current[chosen.Id]} target={targetById[chosen.Id]}"); + Move(chosen.X, chosen.Y); + + bool arrived = WaitUntilAt(chosen.X, chosen.Y, 2800); + if (!arrived) + { + Move(chosen.X, chosen.Y); + WaitUntilAt(chosen.X, chosen.Y, 2000); + } + + Delay(220); + current = ReadCurrentStatesById(targetIds); + int newObj = CalcObjective(current, targetById, cycle); + int delta = objective - newObj; + Log($" distance: {objective} -> {newObj} (delta {delta})"); + + if (newObj <= 0) + { + Log("=== Done: bottom now matches top pattern ==="); + return; + } + + if (newObj < objective) stagnation = 0; + else stagnation++; + + objective = newObj; + Delay(120); +} + +var finalStates = ReadCurrentStatesById(targetIds); +int finalObj = CalcObjective(finalStates, targetById, cycle); +if (finalObj == 0) + Log("=== Done: bottom now matches top pattern ==="); +else + Log($"Stopped. Remaining distance: {finalObj}. Re-run script to continue."); diff --git a/Color Puzzle AutoCalib Queue.csx b/Color Puzzle AutoCalib Queue.csx new file mode 100644 index 0000000..7054213 --- /dev/null +++ b/Color Puzzle AutoCalib Queue.csx @@ -0,0 +1,482 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// Color Puzzle Solver v2 +// - Auto calibration of arrow -> move mapping +// - Waits for real state change after every click + +const int TILE_KIND = 3696; +const int ARROW_KIND = 17851; +const int GRID_X_MIN = 36; +const int GRID_X_MAX = 39; +const int GRID_Y_MIN = 27; +const int GRID_Y_MAX = 30; + +const int CLICK_SETTLE_DELAY_MS = 250; +const int WAIT_CHANGE_TIMEOUT_MS = 6000; +const int WAIT_CHANGE_POLL_MS = 120; +const int MAX_STEPS = 140; +const int BFS_MAX_NODES = 4_000_000; +const int IDA_MAX_SEC = 12; + +const bool AUTO_QUEUE_START = true; +const long TRANSPORTER_ID = 759030883; +const int QUEUE_CLICK_INTERVAL_MS = 5000; +const int WAIT_PUZZLE_POLL_MS = 250; +const int WAIT_PUZZLE_LOG_MS = 5000; +const bool REQUIRE_SELF_IN_PLAYZONE = true; +const int PLAY_X_MIN = 34; +const int PLAY_X_MAX = 41; +const int PLAY_Y_MIN = 26; +const int PLAY_Y_MAX = 31; +const bool REQUIRE_SELF_MIN_Z = true; +const double SELF_MIN_Z = 17.0; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +uint EncodeGrid(int[,] g) +{ + uint s = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + s |= ((uint)(g[r, c] & 3)) << (2 * (r * 4 + c)); + return s; +} + +bool TryReadGrid(out uint state, out string dump) +{ + int[,] grid = new int[4, 4]; + bool[,] found = new bool[4, 4]; + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + + int row = y - GRID_Y_MIN; + int col = x - GRID_X_MIN; + grid[row, col] = GetState(item); + found[row, col] = true; + } + + int cnt = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (found[r, c]) cnt++; + + if (cnt < 16) + { + state = 0; + dump = ""; + return false; + } + + state = EncodeGrid(grid); + dump = string.Join(" | ", Enumerable.Range(0, 4).Select(r => + $"R{r}[{grid[r,0]},{grid[r,1]},{grid[r,2]},{grid[r,3]}]")); + return true; +} + +uint RowLeft(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row >> 2) | (row << 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint RowRight(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row << 2) | (row >> 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint ColUp(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v1 << b) | (v2 << (b + 8)) | (v3 << (b + 16)) | (v0 << (b + 24)); +} + +uint ColDown(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v3 << b) | (v0 << (b + 8)) | (v1 << (b + 16)) | (v2 << (b + 24)); +} + +uint ApplyMove(uint s, int m) +{ + if (m < 4) return RowLeft(s, m); + if (m < 8) return RowRight(s, m - 4); + if (m < 12) return ColUp(s, m - 8); + return ColDown(s, m - 12); +} + +int InverseMove(int m) +{ + if (m < 4) return m + 4; + if (m < 8) return m - 4; + if (m < 12) return m + 4; + return m - 4; +} + +string MoveName(int m) +{ + if (m < 4) return $"Row{m} LEFT"; + if (m < 8) return $"Row{m - 4} RIGHT"; + if (m < 12) return $"Col{m - 8} UP"; + return $"Col{m - 12} DOWN"; +} + +int DetectMove(uint before, uint after) +{ + int hit = -1; + for (int m = 0; m < 16; m++) + { + if (ApplyMove(before, m) != after) continue; + if (hit != -1) return -2; + hit = m; + } + return hit; +} + +List SolveBfs(uint start, uint goal) +{ + if (start == goal) return new List(); + + var visited = new Dictionary(); + var queue = new Queue(); + visited[start] = (start, -1); + queue.Enqueue(start); + int nodes = 0; + bool found = false; + + while (queue.Count > 0 && nodes < BFS_MAX_NODES) + { + uint cur = queue.Dequeue(); + nodes++; + + for (int m = 0; m < 16; m++) + { + uint nxt = ApplyMove(cur, m); + if (visited.ContainsKey(nxt)) continue; + visited[nxt] = (cur, m); + if (nxt == goal) + { + found = true; + queue.Clear(); + break; + } + queue.Enqueue(nxt); + } + } + + if (!found) return null; + + var sol = new List(); + uint s = goal; + while (s != start) + { + var p = visited[s]; + sol.Add(p.move); + s = p.parent; + } + sol.Reverse(); + return sol; +} + +List SolveIda(uint start, uint goal) +{ + if (start == goal) return new List(); + var t0 = DateTime.Now; + + int H(uint st) + { + int mis = 0; + for (int i = 0; i < 16; i++) + { + int a = (int)((st >> (i * 2)) & 3u); + int b = (int)((goal >> (i * 2)) & 3u); + if (a != b) mis++; + } + return (mis + 3) / 4; + } + + List best = null; + bool timeout = false; + + bool Dfs(uint st, List path, int maxDepth) + { + if (timeout) return false; + if ((DateTime.Now - t0).TotalSeconds > IDA_MAX_SEC) + { + timeout = true; + return false; + } + + if (st == goal) + { + best = new List(path); + return true; + } + + int h = H(st); + if (path.Count + h > maxDepth) return false; + + int block = path.Count > 0 ? InverseMove(path[path.Count - 1]) : -1; + for (int m = 0; m < 16; m++) + { + if (m == block) continue; + path.Add(m); + if (Dfs(ApplyMove(st, m), path, maxDepth)) return true; + path.RemoveAt(path.Count - 1); + if (timeout) return false; + } + return false; + } + + int d0 = H(start); + for (int d = d0; d <= 22 && !timeout; d++) + { + if (Dfs(start, new List(), d)) break; + } + return best; +} + +List Solve(uint start, uint goal) +{ + var bfs = SolveBfs(start, goal); + if (bfs != null) return bfs; + return SolveIda(start, goal); +} + +bool ClickAndWaitChange(long furniId, uint before, out uint after, out string dumpAfter) +{ + Send(Out["ClickFurni"], (int)furniId, 0); + Delay(CLICK_SETTLE_DELAY_MS); + + int waited = 0; + while (waited < WAIT_CHANGE_TIMEOUT_MS) + { + if (TryReadGrid(out after, out dumpAfter) && after != before) + return true; + Delay(WAIT_CHANGE_POLL_MS); + waited += WAIT_CHANGE_POLL_MS; + } + + after = before; + dumpAfter = ""; + return false; +} + +Dictionary ReadArrowIds() +{ + var arrowIds = new Dictionary(); + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != ARROW_KIND) continue; + int x = item.Location.X; + int y = item.Location.Y; + + if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id; + else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id; + else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id; + else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id; + } + + return arrowIds; +} + +bool IsSelfInPlayZone() +{ + try + { + int x = Self.Location.X; + int y = Self.Location.Y; + double z = Self.Location.Z; + bool inRect = x >= PLAY_X_MIN && x <= PLAY_X_MAX && y >= PLAY_Y_MIN && y <= PLAY_Y_MAX; + bool inZ = !REQUIRE_SELF_MIN_Z || z >= SELF_MIN_Z; + return inRect && inZ; + } + catch + { + return false; + } +} + +Log("=== Color Puzzle Auto-Solver (AutoCalib + WaitChange) ==="); + +Dictionary arrowIds = null; +uint current; +string dumpNow; + +int sinceQueueClick = QUEUE_CLICK_INTERVAL_MS; +int sinceLog = WAIT_PUZZLE_LOG_MS; + +while (true) +{ + bool hasGrid = TryReadGrid(out current, out dumpNow); + var probeArrows = ReadArrowIds(); + bool hasArrows = probeArrows.Count == 16; + bool inPlayZone = !REQUIRE_SELF_IN_PLAYZONE || IsSelfInPlayZone(); + + if (hasGrid && hasArrows && inPlayZone) + { + arrowIds = probeArrows; + break; + } + + if (AUTO_QUEUE_START && sinceQueueClick >= QUEUE_CLICK_INTERVAL_MS) + { + Send(Out["ClickFurni"], (int)TRANSPORTER_ID, 0); + Log($"Queue: Klick Transporter {TRANSPORTER_ID}..."); + sinceQueueClick = 0; + } + + if (sinceLog >= WAIT_PUZZLE_LOG_MS) + { + string selfPos = "?"; + try { selfPos = $"{Self.Location.X},{Self.Location.Y},{Self.Location.Z:F2}"; } catch { } + Log($"Warte auf Spielstart... Grid={(hasGrid ? "ok" : "no")}, Pfeile={probeArrows.Count}/16, InZone={(inPlayZone ? "yes" : "no")}, Self={selfPos}"); + sinceLog = 0; + } + + Delay(WAIT_PUZZLE_POLL_MS); + sinceQueueClick += WAIT_PUZZLE_POLL_MS; + sinceLog += WAIT_PUZZLE_POLL_MS; +} + +Log("Puzzle erkannt. Starte Solver..."); +Log($"Pfeile: {arrowIds.Count}/16"); + +int[] targetRows = new int[4]; +bool targetFound = false; +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + if (item.Location.X != 41) continue; + int y = item.Location.Y; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + + targetRows[y - GRID_Y_MIN] = GetState(item); + targetFound = true; +} +if (!targetFound) targetRows = new[] { 1, 2, 3, 0 }; + +int[,] tgt = new int[4, 4]; +for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + tgt[r, c] = targetRows[r]; + +uint goal = EncodeGrid(tgt); +Log($"Ziel: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}"); + +Log($"Start: {dumpNow}"); + +var moveToKey = new Dictionary(); +var keyToMove = new Dictionary(); +var allKeys = arrowIds.Keys.OrderBy(k => k).ToList(); + +for (int step = 1; step <= MAX_STEPS; step++) +{ + if (current == goal) + { + Log("=== Geloest: alle 4 Reihen korrekt ==="); + return; + } + + var plan = Solve(current, goal); + if (plan == null || plan.Count == 0) + { + Log("ERROR: Kein Plan vom aktuellen Zustand."); + return; + } + + int wanted = plan[0]; + string key; + bool probing = false; + + if (moveToKey.ContainsKey(wanted)) + { + key = moveToKey[wanted]; + } + else + { + key = allKeys.FirstOrDefault(k => !keyToMove.ContainsKey(k)); + if (key == null) + { + key = allKeys[0]; + } + probing = true; + } + + long id = arrowIds[key]; + Log($"[{step}] want {MoveName(wanted)} | click {key}" + (probing ? " (probe)" : "")); + + if (!ClickAndWaitChange(id, current, out uint after, out string dumpAfter)) + { + Log(" Kein Move erkannt (Timeout), gleicher Schritt nochmal."); + continue; + } + + int actual = DetectMove(current, after); + if (actual >= 0) + { + moveToKey[actual] = key; + keyToMove[key] = actual; + if (actual != wanted) + Log($" AutoCalib: {key} == {MoveName(actual)} (nicht {MoveName(wanted)})"); + } + else if (actual == -1) + { + Log($" Unbekannter Transition-Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + else + { + Log($" Mehrdeutiger Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + + current = after; + + if (step % 10 == 0) + Log($" Calib: {moveToKey.Count}/16 Moves gemappt"); +} + +Log("Nicht fertig in MAX_STEPS. Script einfach nochmal starten."); diff --git a/Color Puzzle AutoCalib Solver.csx b/Color Puzzle AutoCalib Solver.csx new file mode 100644 index 0000000..220fd8e --- /dev/null +++ b/Color Puzzle AutoCalib Solver.csx @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// Color Puzzle Solver v2 +// - Auto calibration of arrow -> move mapping +// - Waits for real state change after every click + +const int TILE_KIND = 3696; +const int ARROW_KIND = 17851; +const int GRID_X_MIN = 36; +const int GRID_X_MAX = 39; +const int GRID_Y_MIN = 27; +const int GRID_Y_MAX = 30; + +const int CLICK_SETTLE_DELAY_MS = 250; +const int WAIT_CHANGE_TIMEOUT_MS = 6000; +const int WAIT_CHANGE_POLL_MS = 120; +const int MAX_STEPS = 140; +const int BFS_MAX_NODES = 4_000_000; +const int IDA_MAX_SEC = 12; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +uint EncodeGrid(int[,] g) +{ + uint s = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + s |= ((uint)(g[r, c] & 3)) << (2 * (r * 4 + c)); + return s; +} + +bool TryReadGrid(out uint state, out string dump) +{ + int[,] grid = new int[4, 4]; + bool[,] found = new bool[4, 4]; + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + + int row = y - GRID_Y_MIN; + int col = x - GRID_X_MIN; + grid[row, col] = GetState(item); + found[row, col] = true; + } + + int cnt = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (found[r, c]) cnt++; + + if (cnt < 16) + { + state = 0; + dump = ""; + return false; + } + + state = EncodeGrid(grid); + dump = string.Join(" | ", Enumerable.Range(0, 4).Select(r => + $"R{r}[{grid[r,0]},{grid[r,1]},{grid[r,2]},{grid[r,3]}]")); + return true; +} + +uint RowLeft(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row >> 2) | (row << 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint RowRight(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row << 2) | (row >> 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint ColUp(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v1 << b) | (v2 << (b + 8)) | (v3 << (b + 16)) | (v0 << (b + 24)); +} + +uint ColDown(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v3 << b) | (v0 << (b + 8)) | (v1 << (b + 16)) | (v2 << (b + 24)); +} + +uint ApplyMove(uint s, int m) +{ + if (m < 4) return RowLeft(s, m); + if (m < 8) return RowRight(s, m - 4); + if (m < 12) return ColUp(s, m - 8); + return ColDown(s, m - 12); +} + +int InverseMove(int m) +{ + if (m < 4) return m + 4; + if (m < 8) return m - 4; + if (m < 12) return m + 4; + return m - 4; +} + +string MoveName(int m) +{ + if (m < 4) return $"Row{m} LEFT"; + if (m < 8) return $"Row{m - 4} RIGHT"; + if (m < 12) return $"Col{m - 8} UP"; + return $"Col{m - 12} DOWN"; +} + +int DetectMove(uint before, uint after) +{ + int hit = -1; + for (int m = 0; m < 16; m++) + { + if (ApplyMove(before, m) != after) continue; + if (hit != -1) return -2; + hit = m; + } + return hit; +} + +List SolveBfs(uint start, uint goal) +{ + if (start == goal) return new List(); + + var visited = new Dictionary(); + var queue = new Queue(); + visited[start] = (start, -1); + queue.Enqueue(start); + int nodes = 0; + bool found = false; + + while (queue.Count > 0 && nodes < BFS_MAX_NODES) + { + uint cur = queue.Dequeue(); + nodes++; + + for (int m = 0; m < 16; m++) + { + uint nxt = ApplyMove(cur, m); + if (visited.ContainsKey(nxt)) continue; + visited[nxt] = (cur, m); + if (nxt == goal) + { + found = true; + queue.Clear(); + break; + } + queue.Enqueue(nxt); + } + } + + if (!found) return null; + + var sol = new List(); + uint s = goal; + while (s != start) + { + var p = visited[s]; + sol.Add(p.move); + s = p.parent; + } + sol.Reverse(); + return sol; +} + +List SolveIda(uint start, uint goal) +{ + if (start == goal) return new List(); + var t0 = DateTime.Now; + + int H(uint st) + { + int mis = 0; + for (int i = 0; i < 16; i++) + { + int a = (int)((st >> (i * 2)) & 3u); + int b = (int)((goal >> (i * 2)) & 3u); + if (a != b) mis++; + } + return (mis + 3) / 4; + } + + List best = null; + bool timeout = false; + + bool Dfs(uint st, List path, int maxDepth) + { + if (timeout) return false; + if ((DateTime.Now - t0).TotalSeconds > IDA_MAX_SEC) + { + timeout = true; + return false; + } + + if (st == goal) + { + best = new List(path); + return true; + } + + int h = H(st); + if (path.Count + h > maxDepth) return false; + + int block = path.Count > 0 ? InverseMove(path[path.Count - 1]) : -1; + for (int m = 0; m < 16; m++) + { + if (m == block) continue; + path.Add(m); + if (Dfs(ApplyMove(st, m), path, maxDepth)) return true; + path.RemoveAt(path.Count - 1); + if (timeout) return false; + } + return false; + } + + int d0 = H(start); + for (int d = d0; d <= 22 && !timeout; d++) + { + if (Dfs(start, new List(), d)) break; + } + return best; +} + +List Solve(uint start, uint goal) +{ + var bfs = SolveBfs(start, goal); + if (bfs != null) return bfs; + return SolveIda(start, goal); +} + +bool ClickAndWaitChange(long furniId, uint before, out uint after, out string dumpAfter) +{ + Send(Out["ClickFurni"], (int)furniId, 0); + Delay(CLICK_SETTLE_DELAY_MS); + + int waited = 0; + while (waited < WAIT_CHANGE_TIMEOUT_MS) + { + if (TryReadGrid(out after, out dumpAfter) && after != before) + return true; + Delay(WAIT_CHANGE_POLL_MS); + waited += WAIT_CHANGE_POLL_MS; + } + + after = before; + dumpAfter = ""; + return false; +} + +Log("=== Color Puzzle Auto-Solver (AutoCalib + WaitChange) ==="); + +var arrowIds = new Dictionary(); +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != ARROW_KIND) continue; + int x = item.Location.X; + int y = item.Location.Y; + + if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id; + else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id; + else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id; + else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id; +} + +Log($"Pfeile: {arrowIds.Count}/16"); +if (arrowIds.Count < 16) +{ + Log("ERROR: Nicht alle 16 Pfeile gefunden."); + return; +} + +int[] targetRows = new int[4]; +bool targetFound = false; +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + if (item.Location.X != 41) continue; + int y = item.Location.Y; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + + targetRows[y - GRID_Y_MIN] = GetState(item); + targetFound = true; +} +if (!targetFound) targetRows = new[] { 1, 2, 3, 0 }; + +int[,] tgt = new int[4, 4]; +for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + tgt[r, c] = targetRows[r]; + +uint goal = EncodeGrid(tgt); +Log($"Ziel: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}"); + +if (!TryReadGrid(out uint current, out string dumpNow)) +{ + Log("ERROR: Grid nicht lesbar."); + return; +} +Log($"Start: {dumpNow}"); + +var moveToKey = new Dictionary(); +var keyToMove = new Dictionary(); +var allKeys = arrowIds.Keys.OrderBy(k => k).ToList(); + +for (int step = 1; step <= MAX_STEPS; step++) +{ + if (current == goal) + { + Log("=== Geloest: alle 4 Reihen korrekt ==="); + return; + } + + var plan = Solve(current, goal); + if (plan == null || plan.Count == 0) + { + Log("ERROR: Kein Plan vom aktuellen Zustand."); + return; + } + + int wanted = plan[0]; + string key; + bool probing = false; + + if (moveToKey.ContainsKey(wanted)) + { + key = moveToKey[wanted]; + } + else + { + key = allKeys.FirstOrDefault(k => !keyToMove.ContainsKey(k)); + if (key == null) + { + key = allKeys[0]; + } + probing = true; + } + + long id = arrowIds[key]; + Log($"[{step}] want {MoveName(wanted)} | click {key}" + (probing ? " (probe)" : "")); + + if (!ClickAndWaitChange(id, current, out uint after, out string dumpAfter)) + { + Log(" Kein Move erkannt (Timeout), gleicher Schritt nochmal."); + continue; + } + + int actual = DetectMove(current, after); + if (actual >= 0) + { + moveToKey[actual] = key; + keyToMove[key] = actual; + if (actual != wanted) + Log($" AutoCalib: {key} == {MoveName(actual)} (nicht {MoveName(wanted)})"); + } + else if (actual == -1) + { + Log($" Unbekannter Transition-Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + else + { + Log($" Mehrdeutiger Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + + current = after; + + if (step % 10 == 0) + Log($" Calib: {moveToKey.Count}/16 Moves gemappt"); +} + +Log("Nicht fertig in MAX_STEPS. Script einfach nochmal starten."); diff --git a/Color Puzzle BFS-IDA Solver.csx b/Color Puzzle BFS-IDA Solver.csx new file mode 100644 index 0000000..7810aca --- /dev/null +++ b/Color Puzzle BFS-IDA Solver.csx @@ -0,0 +1,418 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// ============================================================ +// COLOR PUZZLE AUTO-SOLVER (Loopover 4x4) +// Liest Grid + Ziel aus dem Raum, loest per BFS/IDA*, +// klickt die Pfeil-Buttons automatisch. +// ============================================================ + +const int TILE_KIND = 3696; +const int ARROW_KIND = 17851; +const int GRID_X_MIN = 36; +const int GRID_X_MAX = 39; +const int GRID_Y_MIN = 27; +const int GRID_Y_MAX = 30; +const int CLICK_DELAY = 700; +const int BFS_MAX_NODES = 8_000_000; +const int IDA_MAX_SEC = 15; + +// Set true if arrows push tiles INTO the grid (opposite direction) +const bool REVERSE_ARROWS = false; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +Log("=== Color Puzzle Auto-Solver ==="); + +// ── 1. Read puzzle grid from room ────────────────────────── +int[,] grid = new int[4, 4]; +bool[,] gridFound = new bool[4, 4]; + +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + int x = item.Location.X, y = item.Location.Y; + double z = item.Location.Z; + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + int col = x - GRID_X_MIN; + int row = y - GRID_Y_MIN; + grid[row, col] = GetState(item); + gridFound[row, col] = true; +} + +int foundCount = 0; +for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (gridFound[r, c]) foundCount++; + +if (foundCount < 16) +{ + Log($"ERROR: Nur {foundCount}/16 Grid-Tiles gefunden!"); + Log("Bist du im richtigen Raum?"); + return; +} + +Log("Aktuelles Grid:"); +for (int r = 0; r < 4; r++) + Log($" Row {r}: [{grid[r,0]}, {grid[r,1]}, {grid[r,2]}, {grid[r,3]}]"); + +// ── 2. Define *fixed* target pattern (ignore room indicators) ────────── +// Mapping from Scanner (Floor:3696): +// State 1 = grΓΌn, State 2 = rot, State 3 = blau, State 0 = bunt (3‑Farben‑Tile) +// GewΓΌnschtes Endbild (von oben nach unten): +// Row 0: alles grΓΌn (1) +// Row 1: alles rot (2) +// Row 2: alles blau (3) +// Row 3: alles bunt (0) + +int[,] target = new int[4, 4]; +for (int c = 0; c < 4; c++) +{ + target[0, c] = 1; // grΓΌn + target[1, c] = 2; // rot + target[2, c] = 3; // blau + target[3, c] = 0; // bunt +} + +Log("Ziel-Grid (fest vorgegeben):"); +for (int r = 0; r < 4; r++) + Log($" Row {r}: [{target[r,0]}, {target[r,1]}, {target[r,2]}, {target[r,3]}]"); + +// ── 3. Read arrow button IDs ────────────────────────────── +var arrowIds = new Dictionary(); + +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != ARROW_KIND) continue; + int x = item.Location.X, y = item.Location.Y; + + if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id; + else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id; + else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id; + else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id; +} + +Log($"Arrow-Buttons gefunden: {arrowIds.Count}/16"); +if (arrowIds.Count < 16) +{ + Log("WARN: Nicht alle 16 Pfeile gefunden!"); + foreach (var kv in arrowIds) Log($" {kv.Key} = {kv.Value}"); +} + +// ── 4. State encoding (2 bits per cell, 32 bits total) ──── +// Bits 0-1: grid[0,0], Bits 2-3: grid[0,1], ... Bits 30-31: grid[3,3] +// Row r = bits [r*8 .. r*8+7] + +uint Encode(int[,] g) +{ + uint s = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + s |= ((uint)(g[r, c] & 3)) << (2 * (r * 4 + c)); + return s; +} + +// ── 5. Bit-manipulation move functions ──────────────────── +// RowLeft: [c0,c1,c2,c3] β†’ [c1,c2,c3,c0] = rotate byte RIGHT by 2 +uint RowLeft(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row >> 2) | (row << 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +// RowRight: [c0,c1,c2,c3] β†’ [c3,c0,c1,c2] = rotate byte LEFT by 2 +uint RowRight(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row << 2) | (row >> 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +// ColUp: [r0,r1,r2,r3] β†’ [r1,r2,r3,r0] +uint ColUp(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v1 << b) | (v2 << (b + 8)) | (v3 << (b + 16)) | (v0 << (b + 24)); +} + +// ColDown: [r0,r1,r2,r3] β†’ [r3,r0,r1,r2] +uint ColDown(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v3 << b) | (v0 << (b + 8)) | (v1 << (b + 16)) | (v2 << (b + 24)); +} + +// Move encoding: 0-3=RowLeft(0-3), 4-7=RowRight(0-3), 8-11=ColUp(0-3), 12-15=ColDown(0-3) +uint ApplyMove(uint s, int m) +{ + if (m < 4) return RowLeft(s, m); + if (m < 8) return RowRight(s, m - 4); + if (m < 12) return ColUp(s, m - 8); + return ColDown(s, m - 12); +} + +int InverseMove(int m) +{ + if (m < 4) return m + 4; + if (m < 8) return m - 4; + if (m < 12) return m + 4; + return m - 4; +} + +string MoveName(int m) +{ + if (m < 4) return $"Row{m} LEFT"; + if (m < 8) return $"Row{m-4} RIGHT"; + if (m < 12) return $"Col{m-8} UP"; + return $"Col{m-12} DOWN"; +} + +// ── 6. Solve ────────────────────────────────────────────── +uint startState = Encode(grid); +uint goalState = Encode(target); + +if (startState == goalState) +{ + Log("Puzzle ist bereits geloest!"); + return; +} + +List solution = null; + +// ── 6a. BFS ─────────────────────────────────────────────── +Log($"Starte BFS (max {BFS_MAX_NODES:N0} Nodes)..."); +var startBfs = DateTime.Now; + +var visited = new Dictionary(); +var queue = new Queue(); +visited[startState] = (startState, -1); +queue.Enqueue(startState); + +bool solved = false; +int nodesExplored = 0; + +while (queue.Count > 0 && !solved && nodesExplored < BFS_MAX_NODES) +{ + uint current = queue.Dequeue(); + nodesExplored++; + + if (nodesExplored % 2_000_000 == 0) + Log($" BFS: {nodesExplored:N0} States, Queue: {queue.Count:N0}"); + + for (int m = 0; m < 16; m++) + { + uint next = ApplyMove(current, m); + if (visited.ContainsKey(next)) continue; + visited[next] = (current, m); + if (next == goalState) + { + solved = true; + break; + } + queue.Enqueue(next); + } +} + +if (solved) +{ + solution = new List(); + uint s = goalState; + while (s != startState) + { + var (parent, move) = visited[s]; + solution.Add(move); + s = parent; + } + solution.Reverse(); + var bfsTime = (DateTime.Now - startBfs).TotalMilliseconds; + Log($"BFS Loesung: {solution.Count} Moves in {bfsTime:F0}ms ({nodesExplored:N0} States)"); +} +else +{ + Log($"BFS: Keine Loesung in {nodesExplored:N0} Nodes."); + visited.Clear(); + visited = null; + queue.Clear(); + queue = null; + + // ── 6b. IDA* Fallback ───────────────────────────────── + Log($"Starte IDA* (max {IDA_MAX_SEC}s)..."); + var startIda = DateTime.Now; + + int Heuristic(uint st) + { + int mis = 0; + for (int i = 0; i < 16; i++) + { + int sv = (int)((st >> (i * 2)) & 3u); + int gv = (int)((goalState >> (i * 2)) & 3u); + if (sv != gv) mis++; + } + return (mis + 3) / 4; + } + + List bestSol = null; + int bestLen = 30; + bool timeout = false; + + bool DFS(uint state, List moves, int maxDepth) + { + if (timeout) return false; + if ((DateTime.Now - startIda).TotalSeconds > IDA_MAX_SEC) + { + timeout = true; + return false; + } + + if (state == goalState) + { + if (moves.Count < bestLen) + { + bestLen = moves.Count; + bestSol = new List(moves); + } + return true; + } + + int h = Heuristic(state); + if (moves.Count + h > maxDepth) return false; + if (moves.Count >= bestLen - 1) return false; + + int lastInv = moves.Count > 0 ? InverseMove(moves[moves.Count - 1]) : -1; + + bool found = false; + for (int m = 0; m < 16; m++) + { + if (m == lastInv) continue; + uint next = ApplyMove(state, m); + moves.Add(m); + if (DFS(next, moves, maxDepth)) found = true; + moves.RemoveAt(moves.Count - 1); + if (timeout) break; + } + return found; + } + + int startH = Heuristic(startState); + for (int depth = startH; depth <= 20 && !timeout; depth++) + { + Log($" IDA* Tiefe {depth}..."); + DFS(startState, new List(), depth); + if (bestSol != null) break; + } + + if (bestSol != null) + { + solution = bestSol; + var idaTime = (DateTime.Now - startIda).TotalMilliseconds; + Log($"IDA* Loesung: {solution.Count} Moves in {idaTime:F0}ms"); + } + else + { + Log("ERROR: Keine Loesung gefunden!"); + Log("Moegliche Gruende:"); + Log(" - Puzzle-State hat sich geaendert"); + Log(" - Target-Zuordnung ist falsch"); + return; + } +} + +// ── 7. Show solution ────────────────────────────────────── +Log("Loesungs-Schritte:"); +for (int i = 0; i < solution.Count; i++) + Log($" {i+1}. {MoveName(solution[i])}"); + +// ── 8. Execute moves via ClickFurni ────────────────────── +Log("Fuehre Moves aus..."); + +foreach (int m in solution) +{ + string dir; + int idx; + + if (REVERSE_ARROWS) + { + // Reversed: solver says LEFT β†’ click RIGHT arrow (push from right) + if (m < 4) { dir = "right"; idx = m; } + else if (m < 8) { dir = "left"; idx = m - 4; } + else if (m < 12) { dir = "down"; idx = m - 8; } + else { dir = "up"; idx = m - 12; } + } + else + { + // Normal: solver says LEFT β†’ click LEFT arrow + if (m < 4) { dir = "left"; idx = m; } + else if (m < 8) { dir = "right"; idx = m - 4; } + else if (m < 12) { dir = "up"; idx = m - 8; } + else { dir = "down"; idx = m - 12; } + } + + string key = $"{dir}_{idx}"; + if (!arrowIds.ContainsKey(key)) + { + Log($"ERROR: Arrow '{key}' nicht gefunden!"); + return; + } + + long arrowId = arrowIds[key]; + Log($" Click: {MoveName(m)} -> {key} (ID: {arrowId})"); + Send(Out["ClickFurni"], (int)arrowId, 0); + Delay(CLICK_DELAY); +} + +// ── 9. Verify final grid ──────────────────────────────────── +int[,] finalGrid = new int[4, 4]; +bool[,] finalFound = new bool[4, 4]; + +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + int x = item.Location.X, y = item.Location.Y; + double z = item.Location.Z; + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + int col = x - GRID_X_MIN; + int row = y - GRID_Y_MIN; + finalGrid[row, col] = GetState(item); + finalFound[row, col] = true; +} + +Log("Finales Grid nach Ausfuehrung:"); +for (int r = 0; r < 4; r++) + Log($" Row {r}: [{finalGrid[r,0]}, {finalGrid[r,1]}, {finalGrid[r,2]}, {finalGrid[r,3]}]"); + +Log("=== Puzzle geloest (internes Ziel erreicht) ==="); diff --git a/Color Puzzle Layer Solver.csx b/Color Puzzle Layer Solver.csx new file mode 100644 index 0000000..458ca83 --- /dev/null +++ b/Color Puzzle Layer Solver.csx @@ -0,0 +1,512 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// ============================================================ +// COLOR PUZZLE AUTO-SOLVER (Loopover 4x4) +// Layer-by-layer Ansatz: loest ALLE 4 Reihen zuverlaessig. +// Behaelt Anti-Desync + Auto-Flip-Erkennung bei. +// ============================================================ + +const int TILE_KIND = 3696; +const int ARROW_KIND = 17851; +const int GRID_X_MIN = 36; +const int GRID_X_MAX = 39; +const int GRID_Y_MIN = 27; +const int GRID_Y_MAX = 30; +const int CLICK_DELAY_MS = 950; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +Log("=== Color Puzzle Auto-Solver (Layer-by-Layer) ==="); + +// ── 1. Read grid as array ───────────────────────────────── +int[,] ReadGridFromRoom() +{ + int[,] g = new int[4, 4]; + bool[,] found = new bool[4, 4]; + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + int x = item.Location.X, y = item.Location.Y; + double z = item.Location.Z; + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + g[y - GRID_Y_MIN, x - GRID_X_MIN] = GetState(item); + found[y - GRID_Y_MIN, x - GRID_X_MIN] = true; + } + int cnt = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (found[r, c]) cnt++; + if (cnt < 16) return null; + return g; +} + +string GridDump(int[,] g) +{ + return string.Join(" | ", Enumerable.Range(0, 4).Select(r => + $"R{r}[{g[r,0]},{g[r,1]},{g[r,2]},{g[r,3]}]")); +} + +var grid = ReadGridFromRoom(); +if (grid == null) +{ + Log("ERROR: Konnte Grid nicht lesen (nicht alle 16 Tiles gefunden)."); + return; +} +Log($"Start: {GridDump(grid)}"); + +// ── 2. Read arrows ──────────────────────────────────────── +var arrowIds = new Dictionary(); +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != ARROW_KIND) continue; + int x = item.Location.X, y = item.Location.Y; + if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id; + else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id; + else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id; + else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id; +} +Log($"Pfeile: {arrowIds.Count}/16"); +if (arrowIds.Count < 16) { Log("ERROR: Nicht alle Pfeile gefunden!"); return; } + +// ── 3. Read target ──────────────────────────────────────── +int[] targetRows = new int[4]; +bool targetFound = false; +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + if (item.Location.X != 41) continue; + int y = item.Location.Y; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + targetRows[y - GRID_Y_MIN] = GetState(item); + targetFound = true; +} +if (!targetFound) targetRows = new[] { 1, 2, 3, 0 }; +Log($"Ziel: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}"); + +// ── 4. Layer-by-Layer Solver ────────────────────────────── +// Move encoding: 0-3=RowLeft(0-3), 4-7=RowRight(0-3), +// 8-11=ColUp(0-3), 12-15=ColDown(0-3) + +string MoveName(int m) +{ + if (m < 4) return $"Row{m} LEFT"; + if (m < 8) return $"Row{m-4} RIGHT"; + if (m < 12) return $"Col{m-8} UP"; + return $"Col{m-12} DOWN"; +} + +// Simulate a single move on a grid copy +void SimMove(int[,] g, int m) +{ + if (m < 4) { // RowLeft + int r = m; + int t = g[r,0]; g[r,0]=g[r,1]; g[r,1]=g[r,2]; g[r,2]=g[r,3]; g[r,3]=t; + } else if (m < 8) { // RowRight + int r = m-4; + int t = g[r,3]; g[r,3]=g[r,2]; g[r,2]=g[r,1]; g[r,1]=g[r,0]; g[r,0]=t; + } else if (m < 12) { // ColUp + int c = m-8; + int t = g[0,c]; g[0,c]=g[1,c]; g[1,c]=g[2,c]; g[2,c]=g[3,c]; g[3,c]=t; + } else { // ColDown + int c = m-12; + int t = g[3,c]; g[3,c]=g[2,c]; g[2,c]=g[1,c]; g[1,c]=g[0,c]; g[0,c]=t; + } +} + +List SolveLayerByLayer(int[,] srcGrid, int[] tgtRows) +{ + // Work on a copy + int[,] g = new int[4,4]; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + g[r,c] = srcGrid[r,c]; + + var moves = new List(); + + void Do(int m) { moves.Add(m); SimMove(g, m); } + + void DoRowRight(int r, int times) { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(r); return; } // 1x RowLeft is cheaper + for (int i = 0; i < times; i++) Do(r + 4); + } + void DoRowLeft(int r, int times) { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(r + 4); return; } + for (int i = 0; i < times; i++) Do(r); + } + void DoColUp(int c, int times) { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(c + 12); return; } // 1x ColDown is cheaper + for (int i = 0; i < times; i++) Do(c + 8); + } + void DoColDown(int c, int times) { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(c + 8); return; } + for (int i = 0; i < times; i++) Do(c + 12); + } + + // ── Phase 1: Solve Row 0 ───────────────────────────── + // Use free column rotations + row shifts on rows 1-3. + int C0 = tgtRows[0]; + for (int c = 0; c < 4; c++) + { + if (g[0,c] == C0) continue; + + // Look in same column + int foundRow = -1; + for (int r = 1; r <= 3; r++) + if (g[r,c] == C0) { foundRow = r; break; } + + if (foundRow >= 0) + { + DoColUp(c, foundRow); + } + else + { + // Find C0 anywhere in rows 1-3 + bool found = false; + for (int r = 1; r <= 3 && !found; r++) + for (int c2 = 0; c2 < 4 && !found; c2++) + { + if (c2 == c) continue; + if (g[r,c2] == C0) + { + DoRowRight(r, (c - c2 + 4) % 4); + DoColUp(c, r); + found = true; + } + } + if (!found) return null; // should never happen + } + } + + // ── Phase 2: Solve Row 1 (protecting Row 0) ────────── + // Commutator [RowLeft(1,k1), ColUp(c,k2), RowRight(1,k1), ColDown(c,k2)] + // creates a 3-cycle in rows 1+ only. Row 0 stays intact. + int C1 = tgtRows[1]; + for (int pass = 0; pass < 4; pass++) + { + int colW = -1; + for (int c = 0; c < 4; c++) + if (g[1,c] != C1) { colW = c; break; } + if (colW < 0) break; + + int srcR = -1, srcC = -1; + for (int r = 2; r <= 3 && srcR < 0; r++) + for (int c = 0; c < 4; c++) + if (g[r,c] == C1) { srcR = r; srcC = c; break; } + if (srcR < 0) return null; + + // Move C1 to (srcR, colW) via row shift (safe: rows 2-3 only) + if (srcC != colW) + DoRowRight(srcR, (colW - srcC + 4) % 4); + + int k2 = srcR - 1; // 1 or 2 + DoRowLeft(1, 1); + DoColUp(colW, k2); + DoRowRight(1, 1); + DoColDown(colW, k2); + } + + // ── Phase 3: Solve Rows 2-3 (protecting Rows 0-1) ─── + // Commutator with r1=2, k2=1 only touches rows 2-3. + int C2 = tgtRows[2]; + for (int pass = 0; pass < 4; pass++) + { + int colW = -1; + for (int c = 0; c < 4; c++) + if (g[2,c] != C2) { colW = c; break; } + if (colW < 0) break; + + int srcC = -1; + for (int c = 0; c < 4; c++) + if (g[3,c] == C2) { srcC = c; break; } + if (srcC < 0) return null; + + if (srcC != colW) + DoRowRight(3, (colW - srcC + 4) % 4); + + DoRowLeft(2, 1); + DoColUp(colW, 1); + DoRowRight(2, 1); + DoColDown(colW, 1); + } + + // Verify + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (g[r,c] != tgtRows[r]) return null; + + // Optimize: remove consecutive inverse pairs + bool changed = true; + while (changed) + { + changed = false; + for (int i = 0; i < moves.Count - 1; i++) + { + int a = moves[i], b = moves[i+1]; + bool cancel = false; + if (a < 4 && b == a + 4) cancel = true; + if (a >= 4 && a < 8 && b == a - 4) cancel = true; + if (a >= 8 && a < 12 && b == a + 4) cancel = true; + if (a >= 12 && b == a - 4) cancel = true; + if (cancel) + { + moves.RemoveAt(i + 1); + moves.RemoveAt(i); + changed = true; + break; + } + } + } + + return moves; +} + +// ── 5. Check if already solved ──────────────────────────── +bool IsGridSolved(int[,] g) +{ + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (g[r,c] != targetRows[r]) return false; + return true; +} + +if (IsGridSolved(grid)) +{ + Log("Puzzle ist bereits geloest!"); + return; +} + +// ── 6. Solve ────────────────────────────────────────────── +var solution = SolveLayerByLayer(grid, targetRows); +if (solution == null || solution.Count == 0) +{ + Log("ERROR: Solver konnte keine Loesung finden!"); + Log("Moegliche Gruende: Farb-Verteilung nicht 4x je Farbe, oder falsche Ziel-Zuordnung."); + return; +} + +Log($"Loesung gefunden: {solution.Count} Moves"); +for (int i = 0; i < solution.Count; i++) + Log($" {i+1}. {MoveName(solution[i])}"); + +// ── 7. Execute with verification ────────────────────────── +// Track arrow direction flips (auto-detect reversed arrows) +bool[] rowFlip = new bool[4]; +bool[] colFlip = new bool[4]; + +string KeyForMove(int m) +{ + if (m < 4) { + int r = m; + return rowFlip[r] ? $"right_{r}" : $"left_{r}"; + } + if (m < 8) { + int r = m - 4; + return rowFlip[r] ? $"left_{r}" : $"right_{r}"; + } + if (m < 12) { + int c = m - 8; + return colFlip[c] ? $"down_{c}" : $"up_{c}"; + } + int cc = m - 12; + return colFlip[cc] ? $"up_{cc}" : $"down_{cc}"; +} + +// Encode grid as uint for quick comparison +uint EncodeGrid(int[,] g) +{ + uint s = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + s |= ((uint)(g[r,c] & 3)) << (2 * (r * 4 + c)); + return s; +} + +// Compute expected state after a move (using bit ops for speed) +uint ApplyMoveBits(uint s, int m) +{ + if (m < 4) { // RowLeft + int sh = m * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row >> 2) | (row << 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); + } + if (m < 8) { // RowRight + int sh = (m-4) * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row << 2) | (row >> 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); + } + if (m < 12) { // ColUp + int b = (m-8) * 2; + uint v0=(s>>b)&3u, v1=(s>>(b+8))&3u, v2=(s>>(b+16))&3u, v3=(s>>(b+24))&3u; + uint mask = ~(3u<>b)&3u, v1=(s>>(b+8))&3u, v2=(s>>(b+16))&3u, v3=(s>>(b+24))&3u; + uint mask = ~(3u<= MAX_RETRIES) + { + Log("WARN: Klick ohne Effekt nach 3 Versuchen, re-plane..."); + grid = newGrid; + solution = SolveLayerByLayer(grid, targetRows); + if (solution == null) { Log("ERROR: Re-Plan fehlgeschlagen!"); return; } + moveIdx = 0; + retries = 0; + } + else + { + Log(" Klick ohne Effekt, retry..."); + Delay(300); + } + continue; + } + + // Desync: grid changed unexpectedly (maybe another player or lag) + Log($" Desync! Neuer Zustand: {GridDump(newGrid)}"); + Log(" Re-plane von neuem Zustand..."); + grid = newGrid; + currentState = afterState; + + if (IsGridSolved(grid)) + { + Log("=== Puzzle geloest! Alle 4 Reihen korrekt! ==="); + return; + } + + solution = SolveLayerByLayer(grid, targetRows); + if (solution == null) { Log("ERROR: Re-Plan fehlgeschlagen!"); return; } + moveIdx = 0; + retries = 0; + Log($" Neuer Plan: {solution.Count} Moves"); +} + +if (currentState == goalState) + Log("=== Puzzle geloest! Alle 4 Reihen korrekt! ==="); +else + Log("Alle Moves ausgefuehrt. Grid pruefen ob geloest."); diff --git a/Color Puzzle Queue Watcher.csx b/Color Puzzle Queue Watcher.csx new file mode 100644 index 0000000..7a8a976 --- /dev/null +++ b/Color Puzzle Queue Watcher.csx @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +Log("started"); + +const string FURNI_NAME_CONTAINS_TEXT = "One Way Gate"; +Regex mvRegex = new Regex(@"/mv (\d+),(\d+),([\d\.]+)"); + +// Cache: trigger-tile -> (gateId, gateX, gateY, direction) +Dictionary<(int x, int y), (long id, int gx, int gy, int dir)> triggerMap = new(); + +(int dx, int dy) GetTriggerOffset(int dir) => dir switch +{ + 0 => (0, -1), + 2 => (1, 0), + 4 => (0, 1), + 6 => (-1, 0), + _ => (0, 0) +}; + +void RebuildGateCache() +{ + triggerMap.Clear(); + if (FloorItems == null) return; + + foreach (var item in FloorItems) + { + if (item?.Location == null) continue; + string name = null; + try { name = item.GetName(); } catch { continue; } + if (name == null || !name.Contains(FURNI_NAME_CONTAINS_TEXT)) continue; + + var (dx, dy) = GetTriggerOffset(item.Direction); + var trigger = (item.Location.X + dx, item.Location.Y + dy); + triggerMap[trigger] = (item.Id, item.Location.X, item.Location.Y, item.Direction); + } + Log($"Gate cache built: {triggerMap.Count} gates indexed"); +} + +void TryEnterGate(int userX, int userY) +{ + if (triggerMap.TryGetValue((userX, userY), out var gate)) + { + Log($"Match at ({userX},{userY}) for Gate ID {gate.id} at ({gate.gx},{gate.gy} Dir:{gate.dir}). Sending packet."); + Send(Out.EnterOneWayDoor, gate.id); + } +} + +void HandleUserUpdate(dynamic e) +{ + if (Self == null) return; + var packet = e.Packet; + int numUpdates = packet.ReadInt(); + for (int i = 0; i < numUpdates; i++) + { + int entityIndex = packet.ReadInt(); + int currentX = packet.ReadInt(); + int currentY = packet.ReadInt(); + packet.ReadString(); + packet.ReadInt(); + packet.ReadInt(); + string action = packet.ReadString(); + + if (entityIndex == Self.Index) + { + int checkX = currentX, checkY = currentY; + Match match = mvRegex.Match(action); + if (match.Success) + { + int.TryParse(match.Groups[1].Value, out checkX); + int.TryParse(match.Groups[2].Value, out checkY); + } + TryEnterGate(checkX, checkY); + } + } +} + +void HandleObjectUpdate(dynamic e) +{ + if (Self?.Location == null) return; + + var packet = e.Packet; + int furniId = packet.ReadInt(); + packet.ReadInt(); + int itemX = packet.ReadInt(); + int itemY = packet.ReadInt(); + int newDir = packet.ReadInt(); + + // Update cache for this specific gate + var item = FloorItems?.FirstOrDefault(f => f != null && f.Id == furniId); + if (item != null) + { + string name = null; + try { name = item.GetName(); } catch { return; } + if (name != null && name.Contains(FURNI_NAME_CONTAINS_TEXT)) + { + // Remove old trigger entry for this gate + var toRemove = triggerMap.Where(kv => kv.Value.id == furniId).Select(kv => kv.Key).ToList(); + foreach (var key in toRemove) triggerMap.Remove(key); + + // Add new trigger position + var (dx, dy) = GetTriggerOffset(newDir); + triggerMap[(itemX + dx, itemY + dy)] = (furniId, itemX, itemY, newDir); + + // Check if user is on new trigger tile + TryEnterGate(Self.Location.X, Self.Location.Y); + } + } +} + +void OnRoomReady(dynamic e) +{ + RebuildGateCache(); + if (Self?.Location != null) + TryEnterGate(Self.Location.X, Self.Location.Y); +} + +OnIntercept(In["UserUpdate"], e => HandleUserUpdate(e)); +OnIntercept(In["ObjectUpdate"], e => HandleObjectUpdate(e)); +OnEnteredRoom(e => OnRoomReady(e)); + +// Sofort beim Start Cache bauen und prΓΌfen (schon im Raum) +RebuildGateCache(); +if (Self?.Location != null) + TryEnterGate(Self.Location.X, Self.Location.Y); + +while (Run) +{ + Delay(30); +} +Log("closed"); diff --git a/Color Puzzle Solver 4-Row Fix.csx b/Color Puzzle Solver 4-Row Fix.csx new file mode 100644 index 0000000..9f82d36 --- /dev/null +++ b/Color Puzzle Solver 4-Row Fix.csx @@ -0,0 +1,577 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// Color Puzzle Solver (fix): solves all 4 rows reliably. +// Keeps desync handling + auto direction flip detection. + +const int TILE_KIND = 3696; +const int ARROW_KIND = 17851; +const int GRID_X_MIN = 36; +const int GRID_X_MAX = 39; +const int GRID_Y_MIN = 27; +const int GRID_Y_MAX = 30; +const int CLICK_DELAY_MS = 950; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +int[,] ReadGridFromRoom() +{ + int[,] g = new int[4, 4]; + bool[,] found = new bool[4, 4]; + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + + int r = y - GRID_Y_MIN; + int c = x - GRID_X_MIN; + g[r, c] = GetState(item); + found[r, c] = true; + } + + int cnt = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (found[r, c]) cnt++; + + return cnt == 16 ? g : null; +} + +string GridDump(int[,] g) +{ + return string.Join(" | ", Enumerable.Range(0, 4).Select(r => + $"R{r}[{g[r,0]},{g[r,1]},{g[r,2]},{g[r,3]}]")); +} + +void SimMove(int[,] g, int m) +{ + if (m < 4) + { + int r = m; + int t = g[r, 0]; g[r, 0] = g[r, 1]; g[r, 1] = g[r, 2]; g[r, 2] = g[r, 3]; g[r, 3] = t; + } + else if (m < 8) + { + int r = m - 4; + int t = g[r, 3]; g[r, 3] = g[r, 2]; g[r, 2] = g[r, 1]; g[r, 1] = g[r, 0]; g[r, 0] = t; + } + else if (m < 12) + { + int c = m - 8; + int t = g[0, c]; g[0, c] = g[1, c]; g[1, c] = g[2, c]; g[2, c] = g[3, c]; g[3, c] = t; + } + else + { + int c = m - 12; + int t = g[3, c]; g[3, c] = g[2, c]; g[2, c] = g[1, c]; g[1, c] = g[0, c]; g[0, c] = t; + } +} + +List SolveLayerByLayer(int[,] srcGrid, int[] tgtRows) +{ + int[,] g = new int[4, 4]; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + g[r, c] = srcGrid[r, c]; + + var moves = new List(); + + void Do(int m) { moves.Add(m); SimMove(g, m); } + + void DoRowRight(int r, int times) + { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(r); return; } + for (int i = 0; i < times; i++) Do(r + 4); + } + void DoRowLeft(int r, int times) + { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(r + 4); return; } + for (int i = 0; i < times; i++) Do(r); + } + void DoColUp(int c, int times) + { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(c + 12); return; } + for (int i = 0; i < times; i++) Do(c + 8); + } + void DoColDown(int c, int times) + { + times = ((times % 4) + 4) % 4; + if (times == 3) { Do(c + 8); return; } + for (int i = 0; i < times; i++) Do(c + 12); + } + + int C0 = tgtRows[0]; + for (int c = 0; c < 4; c++) + { + if (g[0, c] == C0) continue; + + int foundRow = -1; + for (int r = 1; r <= 3; r++) + if (g[r, c] == C0) { foundRow = r; break; } + + if (foundRow >= 0) + { + DoColUp(c, foundRow); + } + else + { + bool found = false; + for (int r = 1; r <= 3 && !found; r++) + for (int c2 = 0; c2 < 4 && !found; c2++) + if (c2 != c && g[r, c2] == C0) + { + DoRowRight(r, (c - c2 + 4) % 4); + DoColUp(c, r); + found = true; + } + if (!found) return null; + } + } + + int C1 = tgtRows[1]; + for (int pass = 0; pass < 8; pass++) + { + int colW = -1; + for (int c = 0; c < 4; c++) if (g[1, c] != C1) { colW = c; break; } + if (colW < 0) break; + + int srcR = -1, srcC = -1; + for (int r = 2; r <= 3 && srcR < 0; r++) + for (int c = 0; c < 4; c++) + if (g[r, c] == C1) { srcR = r; srcC = c; break; } + if (srcR < 0) return null; + + if (srcC != colW) DoRowRight(srcR, (colW - srcC + 4) % 4); + + int k2 = srcR - 1; + DoRowLeft(1, 1); + DoColUp(colW, k2); + DoRowRight(1, 1); + DoColDown(colW, k2); + } + + int C2 = tgtRows[2]; + for (int pass = 0; pass < 8; pass++) + { + int colW = -1; + for (int c = 0; c < 4; c++) if (g[2, c] != C2) { colW = c; break; } + if (colW < 0) break; + + int srcC = -1; + for (int c = 0; c < 4; c++) if (g[3, c] == C2) { srcC = c; break; } + if (srcC < 0) return null; + + if (srcC != colW) DoRowRight(3, (colW - srcC + 4) % 4); + + DoRowLeft(2, 1); + DoColUp(colW, 1); + DoRowRight(2, 1); + DoColDown(colW, 1); + } + + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (g[r, c] != tgtRows[r]) return null; + + bool changed = true; + while (changed) + { + changed = false; + for (int i = 0; i < moves.Count - 1; i++) + { + int a = moves[i], b = moves[i + 1]; + bool cancel = false; + if (a < 4 && b == a + 4) cancel = true; + if (a >= 4 && a < 8 && b == a - 4) cancel = true; + if (a >= 8 && a < 12 && b == a + 4) cancel = true; + if (a >= 12 && b == a - 4) cancel = true; + if (cancel) + { + moves.RemoveAt(i + 1); + moves.RemoveAt(i); + changed = true; + break; + } + } + } + + return moves; +} + +bool IsSolvedForTarget(int[,] g, int[] targetRows) +{ + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (g[r, c] != targetRows[r]) return false; + return true; +} + +bool TryReadTargetRows(out int[] targetRows) +{ + targetRows = null; + + var byX = new Dictionary(); + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + + int x = item.Location.X; + int y = item.Location.Y; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (x >= GRID_X_MIN && x <= GRID_X_MAX) continue; + + if (!byX.ContainsKey(x)) + byX[x] = (new int[4], new bool[4], 0); + + var entry = byX[x]; + int r = y - GRID_Y_MIN; + if (!entry.found[r]) + { + entry.states[r] = GetState(item); + entry.found[r] = true; + entry.count++; + byX[x] = entry; + } + } + + if (byX.Count == 0) return false; + + var best = byX + .Select(kvp => new + { + X = kvp.Key, + States = kvp.Value.states, + Count = kvp.Value.count, + Dist = kvp.Key < GRID_X_MIN ? (GRID_X_MIN - kvp.Key) : (kvp.Key - GRID_X_MAX) + }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Dist) + .First(); + + if (best.Count < 4) return false; + + targetRows = new int[4]; + for (int r = 0; r < 4; r++) targetRows[r] = best.States[r]; + return true; +} + +uint EncodeGrid(int[,] g) +{ + uint s = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + s |= ((uint)(g[r, c] & 3)) << (2 * (r * 4 + c)); + return s; +} + +uint ApplyMoveBits(uint s, int m) +{ + if (m < 4) + { + int sh = m * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row >> 2) | (row << 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); + } + if (m < 8) + { + int sh = (m - 4) * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row << 2) | (row >> 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); + } + if (m < 12) + { + int b = (m - 8) * 2; + uint v0 = (s >> b) & 3u, v1 = (s >> (b + 8)) & 3u, v2 = (s >> (b + 16)) & 3u, v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v1 << b) | (v2 << (b + 8)) | (v3 << (b + 16)) | (v0 << (b + 24)); + } + { + int b = (m - 12) * 2; + uint v0 = (s >> b) & 3u, v1 = (s >> (b + 8)) & 3u, v2 = (s >> (b + 16)) & 3u, v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v3 << b) | (v0 << (b + 8)) | (v1 << (b + 16)) | (v2 << (b + 24)); + } +} + +int InverseMove(int m) +{ + if (m < 4) return m + 4; + if (m < 8) return m - 4; + if (m < 12) return m + 4; + return m - 4; +} + +string MoveName(int m) +{ + if (m < 4) return $"Row{m} LEFT"; + if (m < 8) return $"Row{m - 4} RIGHT"; + if (m < 12) return $"Col{m - 8} UP"; + return $"Col{m - 12} DOWN"; +} + +int[,] ApplyMovesToCopy(int[,] src, List moves) +{ + int[,] g = new int[4, 4]; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + g[r, c] = src[r, c]; + foreach (int m in moves) SimMove(g, m); + return g; +} + +Log("=== Color Puzzle Solver (fix all rows) ==="); + +var grid = ReadGridFromRoom(); +if (grid == null) +{ + Log("ERROR: Could not read full 4x4 grid."); + return; +} +Log($"Start: {GridDump(grid)}"); + +var arrowIds = new Dictionary(); +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != ARROW_KIND) continue; + + int x = item.Location.X, y = item.Location.Y; + if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id; + else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id; + else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id; + else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id; +} + +if (arrowIds.Count < 16) +{ + Log($"ERROR: Missing arrows ({arrowIds.Count}/16)."); + return; +} + +int[] detectedTarget; +if (!TryReadTargetRows(out detectedTarget)) +{ + detectedTarget = new[] { 1, 2, 3, 0 }; + Log("WARN: Target tiles not fully detected, using fallback target rows 1,2,3,0."); +} + +var candidateTargets = new List(); +void AddTargetCandidate(int[] t) +{ + if (t == null || t.Length != 4) return; + if (!candidateTargets.Any(x => x[0] == t[0] && x[1] == t[1] && x[2] == t[2] && x[3] == t[3])) + candidateTargets.Add(new[] { t[0], t[1], t[2], t[3] }); +} + +AddTargetCandidate(detectedTarget); +AddTargetCandidate(new[] { detectedTarget[3], detectedTarget[2], detectedTarget[1], detectedTarget[0] }); +AddTargetCandidate(new[] { 1, 2, 3, 0 }); +AddTargetCandidate(new[] { 0, 3, 2, 1 }); + +List solution = null; +int[] targetRows = null; + +foreach (var candidate in candidateTargets) +{ + var s = SolveLayerByLayer(grid, candidate); + if (s == null || s.Count == 0) continue; + + var check = ApplyMovesToCopy(grid, s); + if (!IsSolvedForTarget(check, candidate)) continue; + + if (solution == null || s.Count < solution.Count) + { + solution = s; + targetRows = candidate; + } +} + +if (solution == null || targetRows == null) +{ + Log("ERROR: Could not build a valid full 4-row plan."); + return; +} + +Log($"Target rows chosen: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}"); +Log($"Plan length: {solution.Count} moves"); + +bool[] rowFlip = new bool[4]; +bool[] colFlip = new bool[4]; + +string KeyForMove(int m) +{ + if (m < 4) { int r = m; return rowFlip[r] ? $"right_{r}" : $"left_{r}"; } + if (m < 8) { int r = m - 4; return rowFlip[r] ? $"left_{r}" : $"right_{r}"; } + if (m < 12) { int c = m - 8; return colFlip[c] ? $"down_{c}" : $"up_{c}"; } + int cc = m - 12; return colFlip[cc] ? $"up_{cc}" : $"down_{cc}"; +} + +int[,] tgtGrid = new int[4, 4]; +for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + tgtGrid[r, c] = targetRows[r]; + +uint goalState = EncodeGrid(tgtGrid); +uint currentState = EncodeGrid(grid); + +int moveIdx = 0; +int retries = 0; +const int MAX_RETRIES = 3; + +while (moveIdx < solution.Count) +{ + if (currentState == goalState) + { + Log("=== Solved: all 4 rows complete ==="); + return; + } + + int move = solution[moveIdx]; + uint expected = ApplyMoveBits(currentState, move); + string key = KeyForMove(move); + + if (!arrowIds.ContainsKey(key)) + { + Log($"ERROR: Arrow '{key}' not found."); + return; + } + + long id = arrowIds[key]; + Log($"[{moveIdx + 1}/{solution.Count}] {MoveName(move)} via {key}"); + Send(Out["ClickFurni"], (int)id, 0); + Delay(CLICK_DELAY_MS); + + var newGrid = ReadGridFromRoom(); + if (newGrid == null) + { + Log("WARN: Grid read failed, retry..."); + Delay(400); + continue; + } + + uint afterState = EncodeGrid(newGrid); + + if (afterState == expected) + { + currentState = afterState; + moveIdx++; + retries = 0; + continue; + } + + uint invExpected = ApplyMoveBits(currentState, InverseMove(move)); + if (afterState == invExpected) + { + if (move < 8) + { + int r = move < 4 ? move : move - 4; + rowFlip[r] = !rowFlip[r]; + Log($"Auto-fix: Row {r} direction flipped."); + } + else + { + int c = move < 12 ? move - 8 : move - 12; + colFlip[c] = !colFlip[c]; + Log($"Auto-fix: Col {c} direction flipped."); + } + + grid = newGrid; + currentState = afterState; + + var replan = SolveLayerByLayer(grid, targetRows); + if (replan == null) + { + Log("ERROR: Replan failed after direction flip."); + return; + } + + solution = replan; + moveIdx = 0; + retries = 0; + Log($"Replan: {solution.Count} moves"); + continue; + } + + if (afterState == currentState) + { + retries++; + if (retries >= MAX_RETRIES) + { + Log("WARN: Click had no effect multiple times, replan."); + grid = newGrid; + var replan = SolveLayerByLayer(grid, targetRows); + if (replan == null) + { + Log("ERROR: Replan failed after no-effect clicks."); + return; + } + solution = replan; + moveIdx = 0; + retries = 0; + } + else + { + Log("Click no effect, retrying..."); + Delay(300); + } + continue; + } + + Log($"Desync detected. New grid: {GridDump(newGrid)}"); + grid = newGrid; + currentState = afterState; + + if (IsSolvedForTarget(grid, targetRows)) + { + Log("=== Solved: all 4 rows complete ==="); + return; + } + + var desyncReplan = SolveLayerByLayer(grid, targetRows); + if (desyncReplan == null) + { + Log("ERROR: Replan failed after desync."); + return; + } + + solution = desyncReplan; + moveIdx = 0; + retries = 0; + Log($"Replan after desync: {solution.Count} moves"); +} + +if (currentState == goalState) + Log("=== Solved: all 4 rows complete ==="); +else + Log("Moves done. Please check if room state changed externally."); diff --git a/Color Puzzle Solver Simple.csx b/Color Puzzle Solver Simple.csx new file mode 100644 index 0000000..ccb9f37 --- /dev/null +++ b/Color Puzzle Solver Simple.csx @@ -0,0 +1,477 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// Color Puzzle Solver v2 +// - Auto calibration of arrow -> move mapping +// - Waits for real state change after every click + +const int TILE_KIND = 3696; +const int ARROW_KIND = 17851; +const int GRID_X_MIN = 36; +const int GRID_X_MAX = 39; +const int GRID_Y_MIN = 27; +const int GRID_Y_MAX = 30; + +const int CLICK_SETTLE_DELAY_MS = 250; +const int WAIT_CHANGE_TIMEOUT_MS = 6000; +const int WAIT_CHANGE_POLL_MS = 120; +const int MAX_STEPS = 140; +const int BFS_MAX_NODES = 4_000_000; +const int IDA_MAX_SEC = 12; + +const bool AUTO_QUEUE_START = true; +const long TRANSPORTER_ID = 759030883; +const int QUEUE_CLICK_INTERVAL_MS = 5000; +const int WAIT_PUZZLE_POLL_MS = 250; +const int WAIT_PUZZLE_LOG_MS = 5000; +const bool REQUIRE_SELF_IN_PLAYZONE = true; +const int PLAY_X_MIN = 34; +const int PLAY_X_MAX = 41; +const int PLAY_Y_MIN = 26; +const int PLAY_Y_MAX = 31; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +uint EncodeGrid(int[,] g) +{ + uint s = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + s |= ((uint)(g[r, c] & 3)) << (2 * (r * 4 + c)); + return s; +} + +bool TryReadGrid(out uint state, out string dump) +{ + int[,] grid = new int[4, 4]; + bool[,] found = new bool[4, 4]; + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + + int row = y - GRID_Y_MIN; + int col = x - GRID_X_MIN; + grid[row, col] = GetState(item); + found[row, col] = true; + } + + int cnt = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (found[r, c]) cnt++; + + if (cnt < 16) + { + state = 0; + dump = ""; + return false; + } + + state = EncodeGrid(grid); + dump = string.Join(" | ", Enumerable.Range(0, 4).Select(r => + $"R{r}[{grid[r,0]},{grid[r,1]},{grid[r,2]},{grid[r,3]}]")); + return true; +} + +uint RowLeft(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row >> 2) | (row << 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint RowRight(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row << 2) | (row >> 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint ColUp(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v1 << b) | (v2 << (b + 8)) | (v3 << (b + 16)) | (v0 << (b + 24)); +} + +uint ColDown(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v3 << b) | (v0 << (b + 8)) | (v1 << (b + 16)) | (v2 << (b + 24)); +} + +uint ApplyMove(uint s, int m) +{ + if (m < 4) return RowLeft(s, m); + if (m < 8) return RowRight(s, m - 4); + if (m < 12) return ColUp(s, m - 8); + return ColDown(s, m - 12); +} + +int InverseMove(int m) +{ + if (m < 4) return m + 4; + if (m < 8) return m - 4; + if (m < 12) return m + 4; + return m - 4; +} + +string MoveName(int m) +{ + if (m < 4) return $"Row{m} LEFT"; + if (m < 8) return $"Row{m - 4} RIGHT"; + if (m < 12) return $"Col{m - 8} UP"; + return $"Col{m - 12} DOWN"; +} + +int DetectMove(uint before, uint after) +{ + int hit = -1; + for (int m = 0; m < 16; m++) + { + if (ApplyMove(before, m) != after) continue; + if (hit != -1) return -2; + hit = m; + } + return hit; +} + +List SolveBfs(uint start, uint goal) +{ + if (start == goal) return new List(); + + var visited = new Dictionary(); + var queue = new Queue(); + visited[start] = (start, -1); + queue.Enqueue(start); + int nodes = 0; + bool found = false; + + while (queue.Count > 0 && nodes < BFS_MAX_NODES) + { + uint cur = queue.Dequeue(); + nodes++; + + for (int m = 0; m < 16; m++) + { + uint nxt = ApplyMove(cur, m); + if (visited.ContainsKey(nxt)) continue; + visited[nxt] = (cur, m); + if (nxt == goal) + { + found = true; + queue.Clear(); + break; + } + queue.Enqueue(nxt); + } + } + + if (!found) return null; + + var sol = new List(); + uint s = goal; + while (s != start) + { + var p = visited[s]; + sol.Add(p.move); + s = p.parent; + } + sol.Reverse(); + return sol; +} + +List SolveIda(uint start, uint goal) +{ + if (start == goal) return new List(); + var t0 = DateTime.Now; + + int H(uint st) + { + int mis = 0; + for (int i = 0; i < 16; i++) + { + int a = (int)((st >> (i * 2)) & 3u); + int b = (int)((goal >> (i * 2)) & 3u); + if (a != b) mis++; + } + return (mis + 3) / 4; + } + + List best = null; + bool timeout = false; + + bool Dfs(uint st, List path, int maxDepth) + { + if (timeout) return false; + if ((DateTime.Now - t0).TotalSeconds > IDA_MAX_SEC) + { + timeout = true; + return false; + } + + if (st == goal) + { + best = new List(path); + return true; + } + + int h = H(st); + if (path.Count + h > maxDepth) return false; + + int block = path.Count > 0 ? InverseMove(path[path.Count - 1]) : -1; + for (int m = 0; m < 16; m++) + { + if (m == block) continue; + path.Add(m); + if (Dfs(ApplyMove(st, m), path, maxDepth)) return true; + path.RemoveAt(path.Count - 1); + if (timeout) return false; + } + return false; + } + + int d0 = H(start); + for (int d = d0; d <= 22 && !timeout; d++) + { + if (Dfs(start, new List(), d)) break; + } + return best; +} + +List Solve(uint start, uint goal) +{ + var bfs = SolveBfs(start, goal); + if (bfs != null) return bfs; + return SolveIda(start, goal); +} + +bool ClickAndWaitChange(long furniId, uint before, out uint after, out string dumpAfter) +{ + Send(Out["ClickFurni"], (int)furniId, 0); + Delay(CLICK_SETTLE_DELAY_MS); + + int waited = 0; + while (waited < WAIT_CHANGE_TIMEOUT_MS) + { + if (TryReadGrid(out after, out dumpAfter) && after != before) + return true; + Delay(WAIT_CHANGE_POLL_MS); + waited += WAIT_CHANGE_POLL_MS; + } + + after = before; + dumpAfter = ""; + return false; +} + +Dictionary ReadArrowIds() +{ + var arrowIds = new Dictionary(); + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != ARROW_KIND) continue; + int x = item.Location.X; + int y = item.Location.Y; + + if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id; + else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id; + else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id; + else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id; + } + + return arrowIds; +} + +bool IsSelfInPlayZone() +{ + try + { + int x = Self.Location.X; + int y = Self.Location.Y; + return x >= PLAY_X_MIN && x <= PLAY_X_MAX && y >= PLAY_Y_MIN && y <= PLAY_Y_MAX; + } + catch + { + return false; + } +} + +Log("=== Color Puzzle Auto-Solver (AutoCalib + WaitChange) ==="); + +Dictionary arrowIds = null; +uint current; +string dumpNow; + +int sinceQueueClick = QUEUE_CLICK_INTERVAL_MS; +int sinceLog = WAIT_PUZZLE_LOG_MS; + +while (true) +{ + bool hasGrid = TryReadGrid(out current, out dumpNow); + var probeArrows = ReadArrowIds(); + bool hasArrows = probeArrows.Count == 16; + bool inPlayZone = !REQUIRE_SELF_IN_PLAYZONE || IsSelfInPlayZone(); + + if (hasGrid && hasArrows && inPlayZone) + { + arrowIds = probeArrows; + break; + } + + if (AUTO_QUEUE_START && sinceQueueClick >= QUEUE_CLICK_INTERVAL_MS) + { + Send(Out["ClickFurni"], (int)TRANSPORTER_ID, 0); + Log($"Queue: Klick Transporter {TRANSPORTER_ID}..."); + sinceQueueClick = 0; + } + + if (sinceLog >= WAIT_PUZZLE_LOG_MS) + { + string selfPos = "?"; + try { selfPos = $"{Self.Location.X},{Self.Location.Y}"; } catch { } + Log($"Warte auf Spielstart... Grid={(hasGrid ? "ok" : "no")}, Pfeile={probeArrows.Count}/16, InZone={(inPlayZone ? "yes" : "no")}, Self={selfPos}"); + sinceLog = 0; + } + + Delay(WAIT_PUZZLE_POLL_MS); + sinceQueueClick += WAIT_PUZZLE_POLL_MS; + sinceLog += WAIT_PUZZLE_POLL_MS; +} + +Log("Puzzle erkannt. Starte Solver..."); +Log($"Pfeile: {arrowIds.Count}/16"); + +int[] targetRows = new int[4]; +bool targetFound = false; +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + if (item.Location.X != 41) continue; + int y = item.Location.Y; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + + targetRows[y - GRID_Y_MIN] = GetState(item); + targetFound = true; +} +if (!targetFound) targetRows = new[] { 1, 2, 3, 0 }; + +int[,] tgt = new int[4, 4]; +for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + tgt[r, c] = targetRows[r]; + +uint goal = EncodeGrid(tgt); +Log($"Ziel: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}"); + +Log($"Start: {dumpNow}"); + +var moveToKey = new Dictionary(); +var keyToMove = new Dictionary(); +var allKeys = arrowIds.Keys.OrderBy(k => k).ToList(); + +for (int step = 1; step <= MAX_STEPS; step++) +{ + if (current == goal) + { + Log("=== Geloest: alle 4 Reihen korrekt ==="); + return; + } + + var plan = Solve(current, goal); + if (plan == null || plan.Count == 0) + { + Log("ERROR: Kein Plan vom aktuellen Zustand."); + return; + } + + int wanted = plan[0]; + string key; + bool probing = false; + + if (moveToKey.ContainsKey(wanted)) + { + key = moveToKey[wanted]; + } + else + { + key = allKeys.FirstOrDefault(k => !keyToMove.ContainsKey(k)); + if (key == null) + { + key = allKeys[0]; + } + probing = true; + } + + long id = arrowIds[key]; + Log($"[{step}] want {MoveName(wanted)} | click {key}" + (probing ? " (probe)" : "")); + + if (!ClickAndWaitChange(id, current, out uint after, out string dumpAfter)) + { + Log(" Kein Move erkannt (Timeout), gleicher Schritt nochmal."); + continue; + } + + int actual = DetectMove(current, after); + if (actual >= 0) + { + moveToKey[actual] = key; + keyToMove[key] = actual; + if (actual != wanted) + Log($" AutoCalib: {key} == {MoveName(actual)} (nicht {MoveName(wanted)})"); + } + else if (actual == -1) + { + Log($" Unbekannter Transition-Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + else + { + Log($" Mehrdeutiger Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + + current = after; + + if (step % 10 == 0) + Log($" Calib: {moveToKey.Count}/16 Moves gemappt"); +} + +Log("Nicht fertig in MAX_STEPS. Script einfach nochmal starten."); diff --git a/Color Puzzle State Logger.csx b/Color Puzzle State Logger.csx new file mode 100644 index 0000000..0621669 --- /dev/null +++ b/Color Puzzle State Logger.csx @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +class ScanCell +{ + public long Id; + public int X; + public int Y; + public double Z; + public int Kind; + public int State; + public string Name; +} + +const int SOL_MIN_X = 4; +const int SOL_MAX_X = 9; +const int SOL_MIN_Y = 1; +const int SOL_MAX_Y = 8; + +const int PLAY_MIN_X = 8; +const int PLAY_MAX_X = 13; +const int PLAY_MIN_Y = 14; +const int PLAY_MAX_Y = 21; + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +string GetNameSafe(dynamic item) +{ + try + { + string n = item.GetName(); + return string.IsNullOrWhiteSpace(n) ? "" : n; + } + catch { return ""; } +} + +bool InRect(int x, int y, int minX, int maxX, int minY, int maxY) +{ + return x >= minX && x <= maxX && y >= minY && y <= maxY; +} + +Log("=== Color State Logger ==="); + +var all = new List(); +foreach (var it in FloorItems) +{ + if (it == null) continue; + all.Add(it); +} + +if (all.Count == 0) +{ + Log("ERROR: No floor items."); + return; +} + +var solutionRaw = new List(); +var playRaw = new List(); + +foreach (var it in all) +{ + int x = (int)it.Location.X; + int y = (int)it.Location.Y; + + var cell = new ScanCell { + Id = (long)it.Id, + X = x, + Y = y, + Z = (double)it.Location.Z, + Kind = GetKind(it), + State = GetState(it), + Name = GetNameSafe(it) + }; + + if (InRect(x, y, SOL_MIN_X, SOL_MAX_X, SOL_MIN_Y, SOL_MAX_Y)) + solutionRaw.Add(cell); + + if (InRect(x, y, PLAY_MIN_X, PLAY_MAX_X, PLAY_MIN_Y, PLAY_MAX_Y)) + playRaw.Add(cell); +} + +if (solutionRaw.Count == 0 || playRaw.Count == 0) +{ + Log($"ERROR: Missing board items. Solution={solutionRaw.Count}, Play={playRaw.Count}"); + return; +} + +var solKind = solutionRaw.GroupBy(x => x.Kind) + .Select(g => new { Kind = g.Key, CoordCount = g.Select(x => x.X + "," + x.Y).Distinct().Count(), Count = g.Count() }) + .OrderByDescending(x => x.CoordCount) + .ThenByDescending(x => x.Count) + .First().Kind; + +var playKind = playRaw.GroupBy(x => x.Kind) + .Select(g => new { Kind = g.Key, CoordCount = g.Select(x => x.X + "," + x.Y).Distinct().Count(), Count = g.Count() }) + .OrderByDescending(x => x.CoordCount) + .ThenByDescending(x => x.Count) + .First().Kind; + +var solCells = solutionRaw.Where(x => x.Kind == solKind) + .GroupBy(x => x.X + "," + x.Y) + .Select(g => g.OrderByDescending(x => x.Z).First()) + .OrderBy(x => x.Y) + .ThenBy(x => x.X) + .ToList(); + +var playCells = playRaw.Where(x => x.Kind == playKind) + .GroupBy(x => x.X + "," + x.Y) + .Select(g => g.OrderByDescending(x => x.Z).First()) + .OrderBy(x => x.Y) + .ThenBy(x => x.X) + .ToList(); + +Log($"Solution board kind={solKind} name={solCells.Select(x => x.Name).FirstOrDefault()} cells={solCells.Count}"); +Log($"Play board kind={playKind} name={playCells.Select(x => x.Name).FirstOrDefault()} cells={playCells.Count}"); + +var solStates = solCells.GroupBy(x => x.State).OrderBy(x => x.Key).ToList(); +var playStates = playCells.GroupBy(x => x.State).OrderBy(x => x.Key).ToList(); + +Log("\nStates in solution board:"); +foreach (var s in solStates) + Log($" state {s.Key}: {s.Count()} tiles"); + +Log("\nStates in play board:"); +foreach (var s in playStates) + Log($" state {s.Key}: {s.Count()} tiles"); + +Log("\nCoordinate -> state (solution board):"); +foreach (var c in solCells) + Log($" ({c.X},{c.Y}) = {c.State}"); + +Log("\nCoordinate -> state (play board):"); +foreach (var c in playCells) + Log($" ({c.X},{c.Y}) = {c.State}"); + +Log("\nHint: colors are client visuals; script logs exact numeric states."); +Log("Use this once, then map state->color manually by looking at one tile per state."); diff --git a/Color Run Path Recorder.csx b/Color Run Path Recorder.csx new file mode 100644 index 0000000..68c1d8f --- /dev/null +++ b/Color Run Path Recorder.csx @@ -0,0 +1,536 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +public struct Step +{ + public int X; + public int Y; + public int DelayMs; +} + +const int START_X = 13; +const int START_Y = 13; +const int FINISH_X = 14; +const int FINISH_Y = 8; + +const int PLAY_MIN_X = 8; +const int PLAY_MAX_X = 13; +const int PLAY_MIN_Y = 14; +const int PLAY_MAX_Y = 21; +const bool RECORD_ONLY_PLAYFIELD = true; + +const int MAX_RECORD_MS = 130000; +const int MIN_STEP_DELAY_MS = 40; +const int REPLAY_MOVE_INTERVAL_MS = 80; +const int FAST_PLAY_INTERVAL_MS = 70; +const bool AUTO_DUMP_ON_FINISH = true; +const bool LIVE_LOG_EACH_STEP = true; + +Regex mvRegex = new Regex(@"/mv (\d+),(\d+),([\d\.]+)", RegexOptions.Compiled); + +bool armed = true; +bool recording = false; +bool replaying = false; + +int targetIndex = -1; +string targetName = ""; +DateTime recordStart = DateTime.MinValue; +DateTime lastStepAt = DateTime.MinValue; +DateTime lastReplayMove = DateTime.MinValue; +string lastTrackedPos = ""; + +List steps = new List(); +List lastCompletedSteps = new List(); +string lastCompletedReason = ""; +int lastCompletedDurationMs = 0; +Dictionary> savedPaths = new Dictionary>(); +string pendingSymbol = ""; +string currentRunSymbol = ""; +DateTime lastStatusLog = DateTime.MinValue; + +string MissingSymbol() +{ + bool haveRose = HasSaved("rose"); + bool haveHeart = HasSaved("heart"); + if (haveRose && haveHeart) return ""; + return haveRose ? "heart" : "rose"; +} + +bool HasSaved(string symbol) +{ + string s = NormalizeSymbol(symbol); + return !string.IsNullOrEmpty(s) && savedPaths.ContainsKey(s) && savedPaths[s].Count > 0; +} + +string P(int x, int y) => x + "," + y; + +bool InPlay(int x, int y) +{ + return x >= PLAY_MIN_X && x <= PLAY_MAX_X && y >= PLAY_MIN_Y && y <= PLAY_MAX_Y; +} + +bool IsFinish(int x, int y) +{ + return x == FINISH_X && y == FINISH_Y; +} + +string NormalizeSymbol(string s) +{ + if (string.IsNullOrWhiteSpace(s)) return ""; + string t = s.Trim().ToLowerInvariant(); + if (t.Contains("rose")) return "rose"; + if (t.Contains("heart")) return "heart"; + return ""; +} + +void ResetRecorder(bool keepArmed) +{ + recording = false; + targetIndex = -1; + targetName = ""; + recordStart = DateTime.MinValue; + lastStepAt = DateTime.MinValue; + lastTrackedPos = ""; + if (!keepArmed) armed = false; +} + +dynamic FindUserOnStartTile() +{ + foreach (var u in Users) + { + if (u == null || u.Location == null) continue; + if (u.Location.X == START_X && u.Location.Y == START_Y) + return u; + } + return null; +} + +void ArmIfNeeded() +{ + if (!armed || recording || replaying) return; + + string missing = MissingSymbol(); + if (!string.IsNullOrEmpty(missing)) + { + if (string.IsNullOrEmpty(pendingSymbol)) + { + if ((DateTime.UtcNow - lastStatusLog).TotalSeconds >= 4) + { + Log($"Waiting for symbol call: {missing}."); + lastStatusLog = DateTime.UtcNow; + } + return; + } + + if (pendingSymbol != missing) + { + if ((DateTime.UtcNow - lastStatusLog).TotalSeconds >= 4) + { + Log($"Ignoring round symbol '{pendingSymbol}', waiting for missing '{missing}'."); + lastStatusLog = DateTime.UtcNow; + } + return; + } + } + + var u = FindUserOnStartTile(); + if (u == null) return; + + StartRecordingForUser(u); +} + +void StartRecordingForUser(dynamic u) +{ + if (u == null) return; + + targetIndex = u.Index; + targetName = u.Name; + recording = true; + recordStart = DateTime.UtcNow; + lastStepAt = recordStart; + steps.Clear(); + lastTrackedPos = P(START_X, START_Y); + currentRunSymbol = pendingSymbol; + Log($"REC START: {targetName} (index {targetIndex}) from {START_X}:{START_Y}"); + if (!string.IsNullOrEmpty(currentRunSymbol)) + Log($"REC symbol: {currentRunSymbol}"); +} + +void AddStep(int x, int y) +{ + if (RECORD_ONLY_PLAYFIELD && !InPlay(x, y)) return; + + int delay = (int)(DateTime.UtcNow - lastStepAt).TotalMilliseconds; + if (delay < MIN_STEP_DELAY_MS) delay = MIN_STEP_DELAY_MS; + + if (steps.Count > 0) + { + var prev = steps[steps.Count - 1]; + if (prev.X == x && prev.Y == y) return; + } + + steps.Add(new Step { X = x, Y = y, DelayMs = delay }); + lastStepAt = DateTime.UtcNow; + + if (LIVE_LOG_EACH_STEP) + Log($"REC step {steps.Count}: ({x},{y}) +{delay}ms"); + + if (steps.Count % 10 == 0) + Log($"REC progress: {steps.Count} steps..."); +} + +void StopRecording(string reason) +{ + if (!recording) return; + recording = false; + int dur = (int)(DateTime.UtcNow - recordStart).TotalMilliseconds; + lastCompletedSteps = new List(steps); + lastCompletedReason = reason; + lastCompletedDurationMs = dur; + + Log($"REC STOP ({reason}) steps={steps.Count}, duration={dur}ms"); + if (steps.Count > 0) + { + Log("Use .path replay to replay on your avatar."); + Log("Use .path dump to print recorded path."); + + if (AUTO_DUMP_ON_FINISH && reason.StartsWith("finish@")) + { + Log("Auto dump on finish:"); + DumpPath(); + } + + if (reason.StartsWith("finish@") && !string.IsNullOrEmpty(currentRunSymbol)) + { + if (!savedPaths.ContainsKey(currentRunSymbol)) + { + savedPaths[currentRunSymbol] = new List(steps); + Log($"Saved path for symbol '{currentRunSymbol}' ({steps.Count} steps)."); + } + else + { + int oldCount = savedPaths[currentRunSymbol].Count; + if (steps.Count < oldCount) + { + savedPaths[currentRunSymbol] = new List(steps); + Log($"Updated '{currentRunSymbol}' path: {oldCount} -> {steps.Count} steps (better)."); + } + else + { + Log($"Kept existing '{currentRunSymbol}' path ({oldCount} steps), new run had {steps.Count}."); + } + } + } + } + + if (reason.StartsWith("finish@")) + { + bool haveRose = HasSaved("rose"); + bool haveHeart = HasSaved("heart"); + + if (haveRose && haveHeart) + { + armed = false; + Log("Both symbols saved (rose + heart). Recorder auto-disarmed."); + } + else + { + armed = true; + string missing = !haveRose ? "rose" : "heart"; + Log($"Saved run complete. Waiting for missing symbol: {missing}."); + } + } + + currentRunSymbol = ""; + pendingSymbol = ""; +} + +dynamic FindTargetByIndex(int idx) +{ + foreach (var u in Users) + { + if (u == null) continue; + if (u.Index == idx) return u; + } + return null; +} + +void DumpPath(string symbol = "") +{ + var src = steps; + string which = "current"; + + string norm = NormalizeSymbol(symbol); + if (!string.IsNullOrEmpty(norm) && savedPaths.ContainsKey(norm)) + { + src = savedPaths[norm]; + which = norm; + } + else if (src.Count == 0 && lastCompletedSteps.Count > 0) + { + src = lastCompletedSteps; + which = "last"; + } + + if (src.Count == 0) + { + Log("No recorded steps."); + return; + } + + if (which == "last") + Log($"PATH DUMP (last complete, {lastCompletedReason}, {lastCompletedDurationMs}ms) steps={src.Count}"); + else if (which == "rose" || which == "heart") + Log($"PATH DUMP ({which}) steps={src.Count}"); + else + Log($"PATH DUMP steps={src.Count}"); + + for (int i = 0; i < src.Count; i++) + { + var s = src[i]; + Log($" {i + 1}. ({s.X},{s.Y}) after {s.DelayMs}ms"); + } +} + +void ReplayPath(string symbol = "", bool useRecordedDelays = true) +{ + if (replaying) return; + + var src = steps; + string norm = NormalizeSymbol(symbol); + if (!string.IsNullOrEmpty(norm) && savedPaths.ContainsKey(norm)) + src = savedPaths[norm]; + else if (src.Count == 0 && lastCompletedSteps.Count > 0) + src = lastCompletedSteps; + + if (src.Count == 0) + { + Log("No recorded path to replay."); + return; + } + + replaying = true; + Log($"REPLAY START: {src.Count} steps" + (string.IsNullOrEmpty(norm) ? "" : $" ({norm})") + (useRecordedDelays ? " [timed]" : " [fast]")); + + for (int i = 0; i < src.Count; i++) + { + var s = src[i]; + int wait = useRecordedDelays ? s.DelayMs : FAST_PLAY_INTERVAL_MS; + if (wait < REPLAY_MOVE_INTERVAL_MS) wait = REPLAY_MOVE_INTERVAL_MS; + Delay(wait); + Move(s.X, s.Y); + lastReplayMove = DateTime.UtcNow; + } + + replaying = false; + Log("REPLAY DONE"); +} + +OnIntercept(In["UserUpdate"], e => +{ + if (!recording) return; + + var packet = e.Packet; + int numUpdates = packet.ReadInt(); + for (int i = 0; i < numUpdates; i++) + { + int entityIndex = packet.ReadInt(); + packet.ReadInt(); + packet.ReadInt(); + packet.ReadString(); + packet.ReadInt(); + packet.ReadInt(); + string action = packet.ReadString(); + + if (entityIndex != targetIndex) continue; + Match m = mvRegex.Match(action ?? ""); + if (!m.Success) continue; + + int tx = int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture); + int ty = int.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture); + AddStep(tx, ty); + } +}); + +OnChat(e => +{ + try + { + string msg = (e.Message ?? "").ToLowerInvariant(); + string sym = NormalizeSymbol(msg); + if (!string.IsNullOrEmpty(sym) && msg.Contains("paint me")) + { + pendingSymbol = sym; + Log($"Detected round symbol: {pendingSymbol}"); + + if (recording && string.IsNullOrEmpty(currentRunSymbol)) + { + currentRunSymbol = pendingSymbol; + Log($"Bound current run to symbol: {currentRunSymbol}"); + + string missing = MissingSymbol(); + if (!string.IsNullOrEmpty(missing) && currentRunSymbol != missing) + { + StopRecording($"wrong_symbol_{currentRunSymbol}"); + ResetRecorder(true); + Log($"Discarded run: needed '{missing}', got '{currentRunSymbol}'."); + } + } + } + } + catch { } +}); + +OnIntercept(Out.Chat, e => +{ + string msg = e.Packet.ReadString(); + if (string.IsNullOrWhiteSpace(msg)) return; + string c = msg.Trim().ToLowerInvariant(); + var parts = c.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (c == ".path arm") + { + e.Block(); + armed = true; + if (recording) StopRecording("re-armed"); + ResetRecorder(true); + Log("Recorder armed. Waiting for someone on 13:13."); + return; + } + + if (parts.Length >= 3 && parts[0] == ".path" && parts[1] == "symbol") + { + e.Block(); + string sym = NormalizeSymbol(parts[2]); + if (string.IsNullOrEmpty(sym)) + Log("Unknown symbol. Use .path symbol rose|heart"); + else + { + pendingSymbol = sym; + Log($"Manual symbol set: {pendingSymbol}"); + } + return; + } + + if (parts.Length >= 1 && parts[0] == "!play") + { + e.Block(); + string sym = parts.Length >= 2 ? NormalizeSymbol(parts[1]) : ""; + if (parts.Length >= 2 && string.IsNullOrEmpty(sym)) + { + Log("Unknown play symbol. Use !play rose or !play heart"); + return; + } + ReplayPath(sym, false); + return; + } + + if (parts.Length >= 1 && parts[0] == "!playtimed") + { + e.Block(); + string sym = parts.Length >= 2 ? NormalizeSymbol(parts[1]) : ""; + if (parts.Length >= 2 && string.IsNullOrEmpty(sym)) + { + Log("Unknown play symbol. Use !playtimed rose or !playtimed heart"); + return; + } + ReplayPath(sym, true); + return; + } + + if (c == ".path stop") + { + e.Block(); + StopRecording("manual"); + ResetRecorder(true); + return; + } + + if (parts.Length >= 2 && parts[0] == ".path" && parts[1] == "dump") + { + e.Block(); + string sym = parts.Length >= 3 ? parts[2] : ""; + DumpPath(sym); + return; + } + + if (parts.Length >= 2 && parts[0] == ".path" && parts[1] == "replay") + { + e.Block(); + string sym = parts.Length >= 3 ? parts[2] : ""; + ReplayPath(sym); + return; + } + + if (c == ".path clear") + { + e.Block(); + steps.Clear(); + Log("Path cleared."); + return; + } +}); + +Log("=== Color Run Recorder (13:13) ==="); +Log("Commands: .path arm | .path stop | .path dump [rose|heart] | .path replay [rose|heart] | .path clear"); +Log("Optional: .path symbol rose|heart"); +Log("Quick play: !play rose | !play heart"); +Log("Timed play: !playtimed rose | !playtimed heart"); +Log("Auto-start records when a user is on 13:13."); +Log($"Auto-stop when target reaches finish tile {FINISH_X}:{FINISH_Y}."); + +while (Run) +{ + try + { + ArmIfNeeded(); + + if (recording) + { + // If a different user newly starts on 13:13, switch immediately to new run. + var starter = FindUserOnStartTile(); + if (starter != null && starter.Index != targetIndex) + { + StopRecording("replaced_by_new_start"); + ResetRecorder(true); + StartRecordingForUser(starter); + } + + var t = FindTargetByIndex(targetIndex); + if (t != null && t.Location != null) + { + int ux = t.Location.X; + int uy = t.Location.Y; + + // Fallback tracker by live position (works even if /mv parse misses packets). + string pk = P(ux, uy); + if (pk != lastTrackedPos) + { + AddStep(ux, uy); + lastTrackedPos = pk; + } + + if (IsFinish(ux, uy)) + { + StopRecording($"finish@{FINISH_X}:{FINISH_Y}"); + ResetRecorder(true); + Delay(200); + continue; + } + } + + int ms = (int)(DateTime.UtcNow - recordStart).TotalMilliseconds; + if (ms > MAX_RECORD_MS) + { + StopRecording("timeout"); + ResetRecorder(true); + } + } + } + catch { } + + Delay(100); +} diff --git a/Dance Switcher.csx b/Dance Switcher.csx new file mode 100644 index 0000000..ce0cd8a --- /dev/null +++ b/Dance Switcher.csx @@ -0,0 +1,13 @@ +// Dance Switcher - Wechselt jede Sekunde die Tanzart + +Log("=== DANCE SWITCHER ==="); + +int dance = 1; + +while (Run) +{ + Dance(dance); + dance++; + if (dance > 4) dance = 1; + Delay(100); +} diff --git a/Dave 2.csx b/Dave 2.csx new file mode 100644 index 0000000..6180121 --- /dev/null +++ b/Dave 2.csx @@ -0,0 +1,30 @@ +/// @name Catalog Item Finder + +var targetIdentifier = "clothing_r25_kittybag"; +var catalog = GetCatalog(); +var nodes = catalog.Where(x => x.Id > 0).ToArray(); + +for (int i = 0; i < nodes.Length; i++) { + var node = nodes[i]; + Status($"Searching {i+1}/{nodes.Length}..."); + + var page = GetCatalogPage(node); + + foreach (var offer in page.Offers) { + foreach (var product in offer.Products) { + if (product.Type != ItemType.Floor && product.Type != ItemType.Wall) continue; + if (product.GetIdentifier() != targetIdentifier) continue; + + var pointsLabel = offer.ActivityPointType.ToString() == "Diamond" + ? "PriceInDiamonds" + : "PriceInActivityPoints"; + + Log($"Found: {targetIdentifier} | id: {offer.Id} | pageId: {node.Id} | pageName: {node.Name} | furniLine: {offer.FurniLine} | priceInCredits: {offer.PriceInCredits} | {pointsLabel}: {offer.PriceInActivityPoints} | canPurchaseMultiple: {offer.CanPurchaseMultiple} | canPurchaseAsGift: {offer.CanPurchaseAsGift} | type: {product.Type} | isLimited: {product.IsLimited}"); + return; + } + } + + await Task.Delay(150); +} + +Log("Item not found in catalog"); \ No newline at end of file diff --git a/Duck Dodging Pathfinder AI.csx b/Duck Dodging Pathfinder AI.csx new file mode 100644 index 0000000..35c6250 --- /dev/null +++ b/Duck Dodging Pathfinder AI.csx @@ -0,0 +1,491 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; + +public struct Point : IEquatable +{ + public int X { get; } + public int Y { get; } + public Point(int x, int y) { X = x; Y = y; } + public static implicit operator Point((int x, int y) tuple) => new Point(tuple.x, tuple.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 static bool operator ==(Point left, Point right) => left.Equals(right); + public static bool operator !=(Point left, Point right) => !(left == right); + public override string ToString() => $"({X},{Y})"; +} + +public class Tile +{ + public int X { get; set; } + public int Y { get; set; } + public double Z { get; set; } + public Point XY => new Point(X, Y); + public Tile(int x, int y, double z = 0.0) { X = x; Y = y; Z = z; } +} + +public class Duck +{ + public long id { get; set; } + public Point pos { get; set; } + public Point lastpos { get; set; } + public Point vel { get; set; } + public DateTime lastseen { get; set; } + public Queue trail { get; set; } = new Queue(10); + public double spd { get; set; } +} + +HashSet tiles = new HashSet { + (13,13),(14,13),(15,13),(16,13),(17,13),(18,13),(19,13),(20,13),(21,13),(22,13),(23,13),(24,13),(25,13), + (13,14),(17,14),(21,14),(25,14), + (13,15),(14,15),(15,15),(17,15),(18,15),(20,15),(21,15),(23,15),(24,15),(25,15), + (13,16),(15,16),(18,16),(20,16),(23,16),(25,16), + (13,17),(15,17),(16,17),(17,17),(18,17),(19,17),(20,17),(21,17),(22,17),(23,17),(25,17), + (13,18),(15,18),(19,18),(23,18),(25,18), + (13,19),(14,19),(15,19),(17,19),(18,19),(19,19),(20,19),(21,19),(23,19),(24,19),(25,19), + (13,20),(15,20),(16,20),(17,20),(21,20),(22,20),(23,20),(25,20), + (12,21),(13,21),(17,21),(18,21),(19,21),(20,21),(21,21),(25,21),(26,21), + (13,22),(15,22),(16,22),(17,22),(21,22),(22,22),(23,22),(25,22), + (13,23),(14,23),(15,23),(17,23),(18,23),(19,23),(20,23),(21,23),(23,23),(24,23),(25,23), + (13,24),(15,24),(19,24),(23,24),(25,24), + (13,25),(15,25),(16,25),(17,25),(18,25),(19,25),(20,25),(21,25),(22,25),(23,25),(25,25), + (13,26),(15,26),(18,26),(20,26),(23,26),(25,26), + (13,27),(14,27),(15,27),(17,27),(18,27),(20,27),(21,27),(23,27),(24,27),(25,27), + (13,28),(17,28),(21,28),(25,28), + (13,29),(14,29),(15,29),(16,29),(17,29),(18,29),(19,29),(20,29),(21,29),(22,29),(23,29),(24,29),(25,29) +}; + +Dictionary> adj = new Dictionary>(); +Point[] dirs = { (0,1), (0,-1), (1,0), (-1,0), (1,1), (1,-1), (-1,1), (-1,-1) }; + +foreach(var t in tiles) +{ + var n = new List(); + foreach(var d in dirs) + { + Point p = new Point(t.X + d.X, t.Y + d.Y); + if(tiles.Contains(p)) n.Add(p); + } + adj[t] = n; +} + +Dictionary ducks = new Dictionary(); +Tile tgt = null; +Point lastcmd = default(Point); +DateTime cmdtime = DateTime.MinValue; +Point prev = default(Point); +Point curr = default(Point); +Queue hist = new Queue(5); +int stuck = 0; +HashSet danger = new HashSet(); + +Point dest = default(Point); +bool forcedest = false; +DateTime desttime = DateTime.MinValue; + +Point getpos() +{ + if (Self == null) return default(Point); + if (tgt != null) return tgt.XY; + if (!lastcmd.Equals(default(Point)) && (DateTime.UtcNow - cmdtime).TotalMilliseconds < 250) + return lastcmd; + if (Self.Location != null) return new Point(Self.Location.X, Self.Location.Y); + return default(Point); +} + +void go(int x, int y) +{ + Move(x, y); + lastcmd = new Point(x, y); + cmdtime = DateTime.UtcNow; + tgt = null; +} + +HashSet predict(int frames) +{ + var zones = new HashSet(); + + foreach(var d in ducks.Values) + { + zones.Add(d.pos); + + if(!d.vel.Equals(default(Point))) + { + for(int i = 1; i <= frames; i++) + { + Point pred = new Point( + d.pos.X + d.vel.X * i, + d.pos.Y + d.vel.Y * i + ); + if(tiles.Contains(pred)) + zones.Add(pred); + } + } + + if(frames >= 1 && adj.ContainsKey(d.pos)) + { + foreach(var n in adj[d.pos]) + zones.Add(n); + } + + if(frames >= 2) + { + foreach(var d1 in dirs) + { + Point p1 = new Point(d.pos.X + d1.X, d.pos.Y + d1.Y); + if(tiles.Contains(p1)) + { + zones.Add(p1); + foreach(var d2 in dirs) + { + Point p2 = new Point(p1.X + d2.X, p1.Y + d2.Y); + if(tiles.Contains(p2)) + zones.Add(p2); + } + } + } + } + } + + return zones; +} + +Point findsafe(Point me, HashSet bad) +{ + if(!bad.Any()) return tiles.First(); + + double best = double.MinValue; + Point spot = me; + + foreach(var t in tiles) + { + if(bad.Contains(t)) continue; + + double score = 0; + + double mindist = double.MaxValue; + foreach(var b in bad) + { + double d = Math.Abs(t.X - b.X) + Math.Abs(t.Y - b.Y); + mindist = Math.Min(mindist, d); + score += d; + } + + score += mindist * 100; + + if(adj.ContainsKey(t)) + { + int exits = adj[t].Count(n => !bad.Contains(n)); + score += exits * 10; + } + + double dist = Math.Abs(t.X - me.X) + Math.Abs(t.Y - me.Y); + score -= dist * 0.5; + + if(score > best) + { + best = score; + spot = t; + } + } + + return spot; +} + +Point pathto(Point me, Point goal, HashSet d1, HashSet d2, HashSet d3) +{ + if(!adj.ContainsKey(me)) return me; + + if(hist.Count >= 3) + { + var last3 = hist.TakeLast(3).ToArray(); + if(last3[0] == last3[2] && last3[0] != last3[1]) + { + stuck++; + if(stuck > 2) + { + var esc = adj[me] + .Where(n => !d1.Contains(n)) + .OrderBy(n => Guid.NewGuid()) + .FirstOrDefault(); + if(!esc.Equals(default(Point))) + { + stuck = 0; + return esc; + } + } + } + else stuck = 0; + } + + double best = double.MinValue; + Point move = me; + + foreach(var n in adj[me]) + { + if(d1.Contains(n)) continue; + + double score = 0; + + if(d2.Contains(n)) score -= 800; + if(d3.Contains(n)) score -= 400; + + double dist = Math.Abs(n.X - goal.X) + Math.Abs(n.Y - goal.Y); + score -= dist * 100; + + foreach(var duck in ducks.Values) + { + double dd = Math.Abs(n.X - duck.pos.X) + Math.Abs(n.Y - duck.pos.Y); + score += dd * 10; + } + + if(adj.ContainsKey(n)) + { + int safe = adj[n].Count(x => !d1.Contains(x)); + score += safe * 20; + + if(safe == 0 && d2.Contains(n)) + score -= 2000; + } + + if(!prev.Equals(default(Point)) && n.Equals(prev)) + score -= 50; + + if(score > best) + { + best = score; + move = n; + } + } + + return move; +} + +Point getmove(Point me, Point goal, HashSet d1, HashSet d2, HashSet d3) +{ + if(!adj.ContainsKey(me)) return me; + + if(hist.Count >= 3) + { + var last3 = hist.TakeLast(3).ToArray(); + if(last3[0] == last3[2] && last3[0] != last3[1]) + { + stuck++; + if(stuck > 1) + { + var any = adj[me] + .Where(n => !d1.Contains(n)) + .OrderBy(n => d2.Contains(n) ? 1 : 0) + .FirstOrDefault(); + if(!any.Equals(default(Point))) + { + stuck = 0; + return any; + } + } + } + else stuck = 0; + } + + double best = double.MinValue; + Point move = me; + + foreach(var n in adj[me]) + { + if(d1.Contains(n)) continue; + + if(!prev.Equals(default(Point)) && n.Equals(prev)) + continue; + + double score = 0; + + if(d2.Contains(n)) score -= 1000; + if(d3.Contains(n)) score -= 500; + + double dist = Math.Abs(n.X - goal.X) + Math.Abs(n.Y - goal.Y); + score -= dist * 10; + + foreach(var duck in ducks.Values) + { + double dd = Math.Abs(n.X - duck.pos.X) + Math.Abs(n.Y - duck.pos.Y); + score += dd * 20; + } + + if(adj.ContainsKey(n)) + { + int exits = adj[n].Count(x => !d1.Contains(x) && !d2.Contains(x)); + score += exits * 50; + } + + if(score > best) + { + best = score; + move = n; + } + } + + return move; +} + +OnIntercept(Out["MoveAvatar"], e => { + var pkt = e.Packet; + int x = pkt.ReadInt(); + int y = pkt.ReadInt(); + + dest = new Point(x, y); + forcedest = true; + desttime = DateTime.UtcNow; +}); + +OnEnteredRoom(e => { + ducks.Clear(); + hist.Clear(); + prev = default(Point); + stuck = 0; + forcedest = false; + dest = default(Point); +}); + +OnIntercept(In["WiredMovements"], e => { + var pkt = e.Packet; + int cnt = pkt.ReadInt(); + + for(int i = 0; i < cnt; i++) + { + pkt.ReadInt(); + int fx = pkt.ReadInt(); + int fy = pkt.ReadInt(); + int tx = pkt.ReadInt(); + int ty = pkt.ReadInt(); + pkt.ReadString(); + pkt.ReadString(); + int id = pkt.ReadInt(); + pkt.ReadInt(); + pkt.ReadInt(); + + long fid = id; + Point newp = new Point(tx, ty); + Point oldp = new Point(fx, fy); + + if(!ducks.ContainsKey(fid)) + { + ducks[fid] = new Duck { id = fid }; + } + + var d = ducks[fid]; + d.lastpos = d.pos; + d.pos = newp; + d.vel = new Point(tx - fx, ty - fy); + d.lastseen = DateTime.UtcNow; + + d.trail.Enqueue(newp); + if(d.trail.Count > 10) d.trail.Dequeue(); + + if((d.lastseen - DateTime.UtcNow).TotalSeconds < 1) + { + d.spd = Math.Sqrt(Math.Pow(d.vel.X, 2) + Math.Pow(d.vel.Y, 2)); + } + } +}); + +OnIntercept(In["UserUpdate"], e => { + if(Self == null) return; + + var pkt = e.Packet; + int num = pkt.ReadInt(); + + for(int i = 0; i < num; i++) + { + int idx = pkt.ReadInt(); + int x = pkt.ReadInt(); + int y = pkt.ReadInt(); + string z = pkt.ReadString(); + pkt.ReadInt(); + pkt.ReadInt(); + string act = pkt.ReadString(); + + if(idx == Self.Index) + { + prev = curr; + curr = new Point(x, y); + + hist.Enqueue(curr); + if(hist.Count > 5) hist.Dequeue(); + + if(forcedest && curr.Equals(dest)) + { + forcedest = false; + } + + if(act.Contains("/mv")) + { + var parts = act.Split(new[] {' ', '/', ','}, StringSplitOptions.RemoveEmptyEntries); + if(parts.Length >= 4 && parts[0] == "mv") + { + if(int.TryParse(parts[1], out int mx) && + int.TryParse(parts[2], out int my) && + double.TryParse(parts[3], NumberStyles.Any, CultureInfo.InvariantCulture, out double mz)) + { + tgt = new Tile(mx, my, mz); + lastcmd = default(Point); + } + } + } + else if(act.EndsWith("//") && !act.Contains("/mv")) + { + tgt = null; + } + } + } +}); + +while(Run) +{ + try + { + Point me = getpos(); + if(me.Equals(default(Point))) { Delay(20); continue; } + + if(ducks.Any() || forcedest) + { + var d1 = predict(1); + var d2 = predict(2); + var d3 = predict(3); + + Point goal; + Point next = me; + + if(forcedest && tiles.Contains(dest)) + { + if((DateTime.UtcNow - desttime).TotalSeconds > 30) + { + forcedest = false; + goal = findsafe(me, d1); + next = getmove(me, goal, d1, d2, d3); + } + else + { + goal = dest; + next = pathto(me, goal, d1, d2, d3); + } + } + else + { + goal = findsafe(me, d1); + next = getmove(me, goal, d1, d2, d3); + } + + if(!next.Equals(me)) + { + go(next.X, next.Y); + } + } + } + catch(Exception ex) + { + } + + Delay(20); +} \ No newline at end of file diff --git a/Enter-One-Way-Door #1.csx b/Enter-One-Way-Door #1.csx new file mode 100644 index 0000000..d834b4c --- /dev/null +++ b/Enter-One-Way-Door #1.csx @@ -0,0 +1,59 @@ +/// @group Bots + +public class TileAction +{ + private readonly Action MainAction; + private readonly Action ElseAction; + private readonly Func Condition; + + public TileAction(Action mainAction) => MainAction = mainAction; + + public TileAction(Action mainAction, Action elseAction, Func condition) : this(mainAction) + { + ElseAction = elseAction; + Condition = condition; + } + + public void Execute() + { + if (Condition == null || Condition()) + { + MainAction(); + } + else + { + ElseAction(); + } + } +} + +public Point RealPos => (Self.CurrentUpdate.MovingTo ?? Self.Location).XY; + +Dictionary Actions = new() +{ + // HIER DEINE ACTIONS + { ( 24, 15), new (() => Send(Out["EnterOneWayDoor"],2147418205)) }, + { ( 24, 16), new (() => Send(Out["EnterOneWayDoor"],2147418204)) }, + { ( 25, 16), new (() => Send(Out["EnterOneWayDoor"],2147418203)) }, + { ( 25, 17), new (() => Send(Out["EnterOneWayDoor"],2147418547)) }, + + { ( 26, 19), new (() => Send(Out["EnterOneWayDoor"],2147418490)) }, + { ( 25, 19), new (() => Send(Out["EnterOneWayDoor"],2147418211)) }, + { ( 25, 20), new (() => Send(Out["EnterOneWayDoor"],2147418212)) }, + { ( 24, 20), new (() => Send(Out["EnterOneWayDoor"],2147418551)) }, + + +}; + +while (Run) +{ + try + { + if (Actions.TryGetValue(RealPos, out var result)) + { + result.Execute(); + } + } + catch { } + Delay(100); +} \ No newline at end of file diff --git a/FURNIMATIC.csx b/FURNIMATIC.csx new file mode 100644 index 0000000..4ee408a --- /dev/null +++ b/FURNIMATIC.csx @@ -0,0 +1,578 @@ +using System.Net; +using System.Text; +using System.Diagnostics; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Threading.Tasks; + +var port = 8230; +var queue = new List(); +var progress = 0; +var total = 0; +var delay = 850; +var lastrun = DateTime.Now; +HttpListener server = null; + +try { + EnsureInventory(); + + var items = Inventory + .Where(x => x.IsRecyclable) + .GroupBy(x => x.GetDescriptor()) + .Where(g => g.Count() >= 8) + .Select(g => new { + name = g.Key.GetName(), + id = g.Key.GetInfo().Identifier, + rev = g.Key.GetInfo().Revision, + count = g.Count(), + list = g.Select(i => (int)i.Id).ToList() + }) + .OrderByDescending(x => x.count) + .ToList(); + + Log($"Found {items.Count} recyclable types (8+ items)"); + items.ForEach(x => Log($" {x.name}: {x.count}x")); + + var json = "[" + string.Join(",", items.Select((item, i) => + $"{{\"i\":{i},\"n\":\"{item.name.Replace("\"", "\\\"")}\",\"id\":\"{item.id}\",\"r\":{item.rev},\"c\":{item.count}}}" + )) + "]"; + + var html = @" + + + +Recycler + + + +
+

Recycler

+
+ +
+ + + + +
+
+
+
+
+
Ready
+
+
+
+
Select items to recycle
+
+
+
+
+ + +"; + + server = new HttpListener(); + server.Prefixes.Add($"http://localhost:{port}/"); + server.Start(); + + Process.Start(new ProcessStartInfo { + FileName = $"http://localhost:{port}/", + UseShellExecute = true + }); + + Log($"Server on port {port}"); + + Task.Run(async () => { + while (Run && server.IsListening) { + try { + var ctx = await server.GetContextAsync(); + Task.Run(() => handle(ctx)); + } + catch { break; } + } + }); + + while (Run) { + if (queue.Count >= 8 && (DateTime.Now - lastrun).TotalMilliseconds >= delay) { + var batch = queue.Take(8).ToList(); + queue.RemoveRange(0, 8); + + Send(Out["RecycleItems"], 8, batch[0], batch[1], batch[2], batch[3], batch[4], batch[5], batch[6], batch[7]); + + progress += 8; + lastrun = DateTime.Now; + + if (queue.Count < 8) { + Log($"Done - recycled {progress} items"); + queue.Clear(); + progress = 0; + total = 0; + } + } + Delay(10); + } + + void handle(HttpListenerContext ctx) { + try { + var req = ctx.Request; + var res = ctx.Response; + var path = req.RawUrl; + + if (path == "/") { + var b = Encoding.UTF8.GetBytes(html); + res.ContentType = "text/html"; + res.ContentLength64 = b.Length; + res.OutputStream.Write(b, 0, b.Length); + } + else if (path == "/recycle" && req.HttpMethod == "POST") { + using (var r = new StreamReader(req.InputStream)) { + var data = System.Text.Json.JsonSerializer.Deserialize>(r.ReadToEnd()); + + queue.Clear(); + progress = 0; + + if (data.ContainsKey("delay")) + delay = System.Text.Json.JsonSerializer.Deserialize(data["delay"].ToString()); + + if (data.ContainsKey("items")) { + var selected = System.Text.Json.JsonSerializer.Deserialize>>(data["items"].ToString()); + foreach (var s in selected) { + if (s["i"] < items.Count) { + var item = items[s["i"]]; + var amt = Math.Min(s["a"], item.count); + for (int i = 0; i < amt && i < item.list.Count; i++) + queue.Add(item.list[i]); + } + } + } + + total = queue.Count; + Log($"Queued {total} items @ {delay}ms"); + } + res.StatusCode = 200; + } + else if (path == "/stop") { + queue.Clear(); + progress = total = 0; + Log("Stopped"); + res.StatusCode = 200; + } + else if (path == "/status") { + var json = $"{{\"done\":{progress},\"total\":{total}}}"; + var b = Encoding.UTF8.GetBytes(json); + res.ContentType = "application/json"; + res.ContentLength64 = b.Length; + res.OutputStream.Write(b, 0, b.Length); + } + else { + res.StatusCode = 404; + } + res.Close(); + } + catch { } + } +} +finally { + server?.Stop(); + server?.Close(); + Log("Shutdown"); +} \ No newline at end of file diff --git a/Find Item by ID.csx b/Find Item by ID.csx new file mode 100644 index 0000000..9e4945f --- /dev/null +++ b/Find Item by ID.csx @@ -0,0 +1,21 @@ +using System; + +int searchId = 796960578; + +Log("Suche Item..."); + +foreach (var item in FloorItems) +{ + if (item == null) continue; + + if (item.Id == searchId) + { + Log("=== ITEM GEFUNDEN ==="); + Log("ID: " + item.Id); + Log("Position: X=" + item.Location.X + ", Y=" + item.Location.Y); + Log("Kind: " + item.Kind); + break; + } +} + +Log("Suche beendet."); \ No newline at end of file diff --git a/Flappy Bird [28.12.25] V1.0.csx b/Flappy Bird [28.12.25] V1.0.csx new file mode 100644 index 0000000..76bb128 --- /dev/null +++ b/Flappy Bird [28.12.25] V1.0.csx @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +const int BIRD_X = 17; +const int BIRD_KIND = 9039; +const int PIPE_KIND = 5986; +const int CONTROL_ID = 630985637; + +int GetKind(dynamic item) { try { return (int)item.Kind; } catch { return -1; } } +void Flap() { Send(Out["UseFurniture"], CONTROL_ID, 0); } + +int GetBirdY() +{ + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != BIRD_KIND) continue; + if (item.Location.X == BIRD_X) return item.Location.Y; + } + return -1; +} + +(int gapMin, int gapMax, int pipeX) FindNextPipe() +{ + var pipesByX = new Dictionary>(); + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != PIPE_KIND) continue; + int x = item.Location.X, y = item.Location.Y; + if (!pipesByX.ContainsKey(x)) pipesByX[x] = new List(); + pipesByX[x].Add(y); + } + + var ahead = pipesByX.Keys.Where(x => x >= BIRD_X).OrderBy(x => x).ToList(); + if (ahead.Count == 0) return (-1, -1, 99); + + int closestX = ahead.First(); + var wallYs = new HashSet(pipesByX[closestX]); + + int bestStart = -1, bestEnd = -1, bestSize = 0, gapStart = -1; + for (int y = 7; y <= 24; y++) + { + if (!wallYs.Contains(y)) { if (gapStart < 0) gapStart = y; } + else + { + if (gapStart >= 0) + { + int size = (y - 1) - gapStart + 1; + if (size > bestSize) { bestSize = size; bestStart = gapStart; bestEnd = y - 1; } + } + gapStart = -1; + } + } + if (gapStart >= 0) + { + int size = 24 - gapStart + 1; + if (size > bestSize) { bestStart = gapStart; bestEnd = 24; } + } + return (bestStart, bestEnd, closestX); +} + +Log("FLAPPY BOT V19 - SILENT MODE"); // Output spam reduced + +long lastFlap = 0; +int tick = 0; +int lastY = -1; + +while (Run) +{ + tick++; + int birdY = GetBirdY(); + if (birdY < 0) { Delay(25); continue; } + + var (gapMin, gapMax, pipeX) = FindNextPipe(); + int dist = pipeX - BIRD_X; + + long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + bool shouldFlap = false; + string reason = ""; + + // MINIMUM COOLDOWN = 120ms + int cooldown = 120; + + if (gapMin > 0 && dist <= 15) + { + bool aboveGap = birdY < gapMin; + bool belowGap = birdY > gapMax; + int gapCenter = (gapMin + gapMax) / 2; + int afterFlap = birdY - 3; + bool wouldOvershoot = afterFlap <= gapMin; + + if (belowGap) + { + shouldFlap = true; + reason = "BELOW"; + } + else if (aboveGap) + { + shouldFlap = false; + reason = "ABOVE"; + } + else + { + if (dist <= 4) + { + if (birdY >= gapMax && !wouldOvershoot) + { + shouldFlap = true; + reason = "CLOSE-SAVE"; + } + else { reason = "CLOSE-OK"; } + } + else if (dist <= 10) + { + if (birdY > gapMax && !wouldOvershoot) + { + shouldFlap = true; + reason = "MED-LOW"; + } + else if (birdY > gapCenter + 2 && !wouldOvershoot) + { + shouldFlap = true; + reason = "MED-ADJ"; + } + else { reason = "MED-OK"; } + } + else + { + if (birdY > gapCenter + 3 && !wouldOvershoot) + { + shouldFlap = true; + reason = "FAR-LOW"; + } + else { reason = "FAR-OK"; } + } + } + } + else + { + if (birdY > 16) { shouldFlap = true; reason = "HOVER"; } + } + + if (birdY >= 22) { shouldFlap = true; reason = "EMERG"; } + if (birdY <= 8) shouldFlap = false; + + int dy = lastY > 0 ? birdY - lastY : 0; + string gapStr = gapMin > 0 ? $"gap={gapMin}-{gapMax}" : "NO_GAP"; + + if (shouldFlap && (now - lastFlap) >= cooldown) + { + // Nur noch Loggen wenn er wirklich springt + Log($">>> FLAP! Y={birdY} | d={dist} {gapStr} | {reason}"); + Flap(); + lastFlap = now; + } + + // Der Code-Block, der sonst hier war (else if dist <= 3...), wurde entfernt. + // Dadurch hΓΆrt der Spam auf. + + lastY = birdY; + Delay(25); +} \ No newline at end of file diff --git a/Flood-IT [28.12.25] V1.0.csx b/Flood-IT [28.12.25] V1.0.csx new file mode 100644 index 0000000..046224b --- /dev/null +++ b/Flood-IT [28.12.25] V1.0.csx @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +const int CLICK_DELAY = 2500; +const int GAME_MIN_X = 9; +const int GAME_MAX_X = 26; +const int GAME_MIN_Y = 12; +const int GAME_MAX_Y = 29; +const int GRID_W = 18; +const int GRID_H = 18; +const int TOTAL = 324; +const int KIND_TILE = 3666; + +Dictionary tileIdByColor = new Dictionary(); +int[,] grid = new int[GRID_W, GRID_H]; + +int GetState(dynamic item) { try { return int.Parse(item.State?.ToString() ?? "0"); } catch { return 0; } } +int GetId(dynamic item) { try { return (int)item.Id; } catch { return 0; } } +int GetKind(dynamic item) { try { return (int)item.Kind; } catch { return -1; } } + +void ReadGrid() +{ + Array.Clear(grid, 0, grid.Length); + tileIdByColor.Clear(); + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != KIND_TILE) continue; + int x = item.Location.X, y = item.Location.Y; + if (x < GAME_MIN_X || x > GAME_MAX_X || y < GAME_MIN_Y || y > GAME_MAX_Y) continue; + int gx = x - GAME_MIN_X, gy = GAME_MAX_Y - y; + int state = GetState(item), id = GetId(item); + if (gx >= 0 && gx < GRID_W && gy >= 0 && gy < GRID_H) + { + grid[gx, gy] = state; + if (!tileIdByColor.ContainsKey(state)) tileIdByColor[state] = id; + } + } +} + +HashSet<(int,int)> Flood(HashSet<(int,int)> area, int color) +{ + var result = new HashSet<(int,int)>(area); + var q = new Queue<(int,int)>(); + foreach (var p in area) q.Enqueue(p); + while (q.Count > 0) + { + var (x, y) = q.Dequeue(); + foreach (var (nx, ny) in new[]{(x-1,y),(x+1,y),(x,y-1),(x,y+1)}) + { + if (nx < 0 || nx >= GRID_W || ny < 0 || ny >= GRID_H) continue; + if (result.Contains((nx,ny))) continue; + if (grid[nx,ny] == color) { result.Add((nx,ny)); q.Enqueue((nx,ny)); } + } + } + return result; +} + +HashSet<(int,int)> InitArea() +{ + var area = new HashSet<(int,int)>{(0,0)}; + return Flood(area, grid[0,0]); +} + +HashSet BorderColors(HashSet<(int,int)> area) +{ + var c = new HashSet(); + foreach (var (x,y) in area) + { + if (x > 0 && !area.Contains((x-1,y))) c.Add(grid[x-1,y]); + if (x < GRID_W-1 && !area.Contains((x+1,y))) c.Add(grid[x+1,y]); + if (y > 0 && !area.Contains((x,y-1))) c.Add(grid[x,y-1]); + if (y < GRID_H-1 && !area.Contains((x,y+1))) c.Add(grid[x,y+1]); + } + return c; +} + +HashSet RemainingColors(HashSet<(int,int)> area) +{ + var colors = new HashSet(); + for (int x = 0; x < GRID_W; x++) + for (int y = 0; y < GRID_H; y++) + if (!area.Contains((x,y))) colors.Add(grid[x,y]); + return colors; +} + +int CountRegions(HashSet<(int,int)> area) +{ + var visited = new bool[GRID_W, GRID_H]; + foreach (var (x,y) in area) visited[x,y] = true; + int regions = 0; + + for (int sx = 0; sx < GRID_W; sx++) + { + for (int sy = 0; sy < GRID_H; sy++) + { + if (visited[sx,sy]) continue; + regions++; + var q = new Queue<(int,int)>(); + q.Enqueue((sx,sy)); + visited[sx,sy] = true; + int c = grid[sx,sy]; + while (q.Count > 0) + { + var (x,y) = q.Dequeue(); + foreach (var (nx,ny) in new[]{(x-1,y),(x+1,y),(x,y-1),(x,y+1)}) + { + if (nx < 0 || nx >= GRID_W || ny < 0 || ny >= GRID_H) continue; + if (visited[nx,ny]) continue; + if (grid[nx,ny] == c) { visited[nx,ny] = true; q.Enqueue((nx,ny)); } + } + } + } + } + return regions; +} + +int Heuristic(HashSet<(int,int)> area) +{ + var remaining = RemainingColors(area); + int regions = CountRegions(area); + return Math.Max(remaining.Count, (regions + 1) / 2); +} + +bool EliminatesColor(HashSet<(int,int)> area, int color) +{ + var newArea = Flood(area, color); + for (int x = 0; x < GRID_W; x++) + for (int y = 0; y < GRID_H; y++) + if (!newArea.Contains((x,y)) && grid[x,y] == color) return false; + return true; +} + +int Eval(HashSet<(int,int)> area, int depth, int alpha) +{ + if (area.Count == TOTAL) return 1000 - depth; + if (depth <= 0) return area.Count - Heuristic(area) * 10; + + var borders = BorderColors(area); + int best = -999; + + var moves = borders.Select(c => { + var next = Flood(area, c); + bool elim = EliminatesColor(area, c); + return (c, next.Count - area.Count, elim, next); + }).OrderByDescending(m => m.Item3 ? 1000 : 0) + .ThenByDescending(m => m.Item2).ToList(); + + foreach (var (c, gain, elim, next) in moves) + { + int score = Eval(next, depth - 1, best); + if (score > best) best = score; + if (best >= alpha) break; + } + return best; +} + +(int color, int score) BestMove(HashSet<(int,int)> area, int depth) +{ + var borders = BorderColors(area); + int bestC = 0, bestS = -9999; + + var moves = borders.Select(c => { + var next = Flood(area, c); + bool elim = EliminatesColor(area, c); + return (c, next.Count - area.Count, elim, next); + }).OrderByDescending(m => m.Item3 ? 1000 : 0) + .ThenByDescending(m => m.Item2).ToList(); + + foreach (var (c, gain, elim, next) in moves) + { + int score; + if (next.Count == TOTAL) score = 10000; + else score = Eval(next, depth - 1, bestS) + (elim ? 50 : 0); + + if (score > bestS) { bestS = score; bestC = c; } + } + return (bestC, bestS); +} + +List Solve() +{ + var moves = new List(); + var area = InitArea(); + + while (area.Count < TOTAL && moves.Count < 32) + { + int remaining = TOTAL - area.Count; + int depth = remaining > 200 ? 3 : remaining > 100 ? 4 : remaining > 50 ? 5 : 6; + + var (c, s) = BestMove(area, depth); + if (c == 0) break; + + moves.Add(c); + area = Flood(area, c); + } + return moves; +} + +void Click(int color) +{ + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != KIND_TILE) continue; + int x = item.Location.X, y = item.Location.Y; + if (x < GAME_MIN_X || x > GAME_MAX_X || y < GAME_MIN_Y || y > GAME_MAX_Y) continue; + int state = GetState(item); + if (state == color) + { + Send(Out["ClickFurni"], (int)item.Id, 0); + return; + } + } +} + +Log("═══════════════════════════════════════════"); +Log(" FLOOD-IT BOT - RESEARCH BASED"); +Log("═══════════════════════════════════════════"); + +while (Run) +{ + ReadGrid(); + var area = InitArea(); + Log($"Start: {area.Count}/{TOTAL}"); + + if (area.Count == TOTAL) { Log("COMPLETE!"); Delay(2000); continue; } + + Log("Solving..."); + var sw = System.Diagnostics.Stopwatch.StartNew(); + var solution = Solve(); + sw.Stop(); + Log($"Solution: {string.Join(",", solution)} ({solution.Count} moves) in {sw.ElapsedMilliseconds}ms"); + + foreach (var c in solution) + { + Click(c); + Log($"Click {c}"); + Delay(CLICK_DELAY); + } + + Delay(1000); +} diff --git a/Floor Items CSV Exporter.csx b/Floor Items CSV Exporter.csx new file mode 100644 index 0000000..66a539e --- /dev/null +++ b/Floor Items CSV Exporter.csx @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +string Safe(string s) +{ + return string.IsNullOrWhiteSpace(s) ? "" : s.Replace("\r", " ").Replace("\n", " ").Trim(); +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0", CultureInfo.InvariantCulture); } + catch { return 0; } +} + +int GetDir(dynamic item) +{ + try { return (int)item.Direction; } + catch { return 0; } +} + +string GetNameSafe(dynamic item) +{ + try { return Safe(item.GetName()); } + catch { return ""; } +} + +string EscCsv(string s) +{ + s = s ?? ""; + if (s.Contains(",") || s.Contains("\"") || s.Contains("\n")) + return "\"" + s.Replace("\"", "\"\"") + "\""; + return s; +} + +var rows = new List(); +rows.Add("id,kind,name,x,y,z,state,dir"); + +int count = 0; +foreach (var item in FloorItems) +{ + if (item == null) continue; + count++; + + long id = item.Id; + int kind = GetKind(item); + string name = GetNameSafe(item); + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + int state = GetState(item); + int dir = GetDir(item); + + rows.Add(string.Join(",", new[] { + id.ToString(CultureInfo.InvariantCulture), + kind.ToString(CultureInfo.InvariantCulture), + EscCsv(name), + x.ToString(CultureInfo.InvariantCulture), + y.ToString(CultureInfo.InvariantCulture), + z.ToString("0.###", CultureInfo.InvariantCulture), + state.ToString(CultureInfo.InvariantCulture), + dir.ToString(CultureInfo.InvariantCulture) + })); +} + +if (count == 0) +{ + Log("ERROR: No floor items found."); + return; +} + +string roomName = "room"; +try { roomName = Safe(Room?.Name ?? "room"); } catch { roomName = "room"; } +if (string.IsNullOrWhiteSpace(roomName)) roomName = "room"; + +var invalid = Path.GetInvalidFileNameChars(); +foreach (char c in invalid) roomName = roomName.Replace(c, '_'); + +string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture); +string exportDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "HabboFloorExports"); +Directory.CreateDirectory(exportDir); + +string filePath = Path.Combine(exportDir, $"{roomName}_floor_{stamp}.csv"); +File.WriteAllText(filePath, string.Join(Environment.NewLine, rows), Encoding.UTF8); + +Log("=== Floor Export Complete ==="); +Log($"Items exported: {count}"); +Log($"File: {filePath}"); diff --git a/Furni-Matic Auto-Eco.csx b/Furni-Matic Auto-Eco.csx new file mode 100644 index 0000000..0242733 --- /dev/null +++ b/Furni-Matic Auto-Eco.csx @@ -0,0 +1,367 @@ +using System; +using System.Net; +using System.Text; +using System.Diagnostics; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using Xabbo.Messages; + +#nullable enable + +public class ItemView { + [JsonPropertyName("h")] public string HabboId { get; set; } = ""; + [JsonPropertyName("n")] public string Name { get; set; } = ""; + [JsonPropertyName("r")] public int Revision { get; set; } + [JsonPropertyName("c")] public int Count { get; set; } +} + +public class LiveState { + [JsonPropertyName("items")] public List Items { get; set; } = new(); + [JsonPropertyName("status")] public StatusUpdate Status { get; set; } = new(); +} + +public class RecycleRequest { + [JsonPropertyName("items")] public List Items { get; set; } = new(); + [JsonPropertyName("delay")] public int Delay { get; set; } +} + +public class RecycleItem { + [JsonPropertyName("h")] public string HabboId { get; set; } = ""; + [JsonPropertyName("a")] public int Amount { get; set; } +} + +public class StatusUpdate { + [JsonPropertyName("done")] public int Done { get; set; } + [JsonPropertyName("total")] public int Total { get; set; } +} + +var port = 8226; +var queue = new List(); +var progress = 0; +var total = 0; +var delay = 12000; +var lastRun = DateTime.Now; +HttpListener? server = null; + +void TryRefreshInventory() { + try { Send(Out["RequestFurniInventory"]); return; } catch { } +} + +OnIntercept(In["RecyclerFinished"], e => { + TryRefreshInventory(); +}); + +LiveState GetCurrentState() { + EnsureInventory(10000); + var currentItems = Inventory.Where(x => x.IsRecyclable) + .GroupBy(x => x.GetDescriptor()) + .Where(g => g.Count() >= 8) + .Select(g => new ItemView { + HabboId = g.Key.GetInfo().Identifier, + Name = g.Key.GetName(), + Revision = g.Key.GetInfo().Revision, + Count = g.Count() + }) + .OrderByDescending(x => x.Count) + .ToList(); + + if (progress >= total && total > 0) { + progress = total = 0; + } + + return new LiveState { + Items = currentItems, + Status = new StatusUpdate { Done = progress, Total = total } + }; +} + +try { + log("starting server..."); + var html = @" + + +Recycler + + + + + +
+
recycler
+
idle
+
+
+ + + +
+
+
+
+ +
select items
+
+
+
+

queue

+
+
+
+
+ + +"; + + server = new HttpListener(); + server.Prefixes.Add($"http://localhost:{port}/"); + server.Start(); + + Process.Start(new ProcessStartInfo { FileName = $"http://localhost:{port}/", UseShellExecute = true }); + log($"server listening on http://localhost:{port}"); + + _ = Task.Run(async () => { + while (Run && server.IsListening) { + try { + var ctx = await server.GetContextAsync(); + _ = Task.Run(() => HandleContextAsync(ctx, html)); + } catch { break; } + } + }); + + while (Run) { + if (queue.Count >= 8 && (DateTime.Now - lastRun).TotalMilliseconds >= delay) { + var batch = queue.Take(8).ToList(); + queue.RemoveRange(0, 8); + Send(Out["RecycleItems"], 8, batch[0], batch[1], batch[2], batch[3], batch[4], batch[5], batch[6], batch[7]); + progress += 8; + lastRun = DateTime.Now; + } + await Task.Delay(10); + } +} +catch (TaskCanceledException) { + log("error: inventory load timed out. ensure you are fully in-game."); +} +catch (Exception ex) { + log($"unhandled exception: {ex.Message}"); +} +finally { + server?.Stop(); + server?.Close(); + log("server shut down."); +} + +async Task HandleContextAsync(HttpListenerContext ctx, string html) { + var req = ctx.Request; + var res = ctx.Response; + try { + var endpoint = (req.HttpMethod, req.Url?.AbsolutePath); + switch (endpoint) { + case ("GET", "/"): + await WriteResponseAsync(res, html, "text/html"); + break; + case ("GET", "/state"): + var stateJson = JsonSerializer.Serialize(GetCurrentState()); + await WriteResponseAsync(res, stateJson, "application/json"); + break; + case ("POST", "/recycle"): + TryRefreshInventory(); + EnsureInventory(10000); + using (var r = new StreamReader(req.InputStream)) { + var payload = JsonSerializer.Deserialize(await r.ReadToEndAsync()); + if (payload != null) { + queue.Clear(); + delay = Math.Clamp(payload.Delay, 500, 60000); + foreach (var pItem in payload.Items) { + var itemInstances = Inventory + .Where(x => x.IsRecyclable && x.GetInfo().Identifier == pItem.HabboId) + .Select(i => -(int)i.Id) + .ToList(); + int amount = Math.Min(pItem.Amount, itemInstances.Count); + queue.AddRange(itemInstances.Take(amount)); + } + total = queue.Count; + progress = 0; + log($"queueing {total} items, {delay}ms delay."); + } + } + res.StatusCode = 200; + break; + case ("POST", "/stop"): + queue.Clear(); + progress = total = 0; + log("recycling stopped."); + res.StatusCode = 200; + break; + default: + res.StatusCode = 404; + break; + } + } + catch (Exception ex) { + log($"request error: {ex.Message}"); + if(!res.OutputStream.CanWrite) return; + res.StatusCode = 500; + } + finally { + res.Close(); + } +} + +async Task WriteResponseAsync(HttpListenerResponse res, string content, string type) { + var buffer = Encoding.UTF8.GetBytes(content); + res.ContentType = type; + res.ContentLength64 = buffer.Length; + await res.OutputStream.WriteAsync(buffer, 0, buffer.Length); +} + +void log(string message) => Log(message); \ No newline at end of file diff --git a/Furni-Matic Unboxing.csx b/Furni-Matic Unboxing.csx new file mode 100644 index 0000000..67fefb7 --- /dev/null +++ b/Furni-Matic Unboxing.csx @@ -0,0 +1,20 @@ +Send(Out["RequestFurniInventory"]); +Delay(200); +EnsureInventory(5000); + +var name = "Mystery Box"; +var boxes = Inventory.Where(x => x.GetDescriptor().GetName() == name); +var freeTiles = Heightmap.Where(x => x.IsFree); + +foreach (var box in boxes) { + Place(box, Rand(freeTiles).Location); + Delay(50); +} + +while (Run) { + foreach (var box in FloorItems.Where(x => x.GetName() == name)) + Send(Out["PresentOpen"], (int)box.Id); + Delay(100); +} + +Wait(); \ No newline at end of file diff --git a/Furni.csx b/Furni.csx new file mode 100644 index 0000000..5db0ad2 --- /dev/null +++ b/Furni.csx @@ -0,0 +1,366 @@ +using System; +using System.Net; +using System.Text; +using System.Diagnostics; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using Xabbo.Messages; + +#nullable enable + +public class ItemView { + [JsonPropertyName("h")] public string HabboId { get; set; } = ""; + [JsonPropertyName("n")] public string Name { get; set; } = ""; + [JsonPropertyName("r")] public int Revision { get; set; } + [JsonPropertyName("c")] public int Count { get; set; } +} + +public class LiveState { + [JsonPropertyName("items")] public List Items { get; set; } = new(); + [JsonPropertyName("status")] public StatusUpdate Status { get; set; } = new(); +} + +public class RecycleRequest { + [JsonPropertyName("items")] public List Items { get; set; } = new(); + [JsonPropertyName("delay")] public int Delay { get; set; } +} + +public class RecycleItem { + [JsonPropertyName("h")] public string HabboId { get; set; } = ""; + [JsonPropertyName("a")] public int Amount { get; set; } +} + +public class StatusUpdate { + [JsonPropertyName("done")] public int Done { get; set; } + [JsonPropertyName("total")] public int Total { get; set; } +} + +var port = 8226; +var queue = new List(); +var progress = 0; +var total = 0; +var delay = 12000; +var lastRun = DateTime.Now; +HttpListener? server = null; + +void TryRefreshInventory() { + try { Send(Out["RequestFurniInventory"]); return; } catch { } +} + +OnIntercept(In["RecyclerFinished"], e => { + TryRefreshInventory(); +}); + +LiveState GetCurrentState() { + EnsureInventory(10000); + var currentItems = Inventory.Where(x => x.IsRecyclable) + .GroupBy(x => x.GetDescriptor()) + .Where(g => g.Count() >= 8) + .Select(g => new ItemView { + HabboId = g.Key.GetInfo().Identifier, + Name = g.Key.GetName(), + Revision = g.Key.GetInfo().Revision, + Count = g.Count() + }) + .OrderByDescending(x => x.Count) + .ToList(); + + if (progress >= total && total > 0) { + progress = total = 0; + } + + return new LiveState { + Items = currentItems, + Status = new StatusUpdate { Done = progress, Total = total } + }; +} + +try { + log("starting server..."); + var html = @" + + +Recycler + + + + + +
+
recycler
+
idle
+
+
+ + + +
+
+
+
+ +
select items
+
+
+
+

queue

+
+
+
+
+ + +"; + + server = new HttpListener(); + server.Prefixes.Add($"http://localhost:{port}/"); + server.Start(); + + Process.Start(new ProcessStartInfo { FileName = $"http://localhost:{port}/", UseShellExecute = true }); + log($"server listening on http://localhost:{port}"); + + _ = Task.Run(async () => { + while (Run && server.IsListening) { + try { + var ctx = await server.GetContextAsync(); + _ = Task.Run(() => HandleContextAsync(ctx, html)); + } catch { break; } + } + }); + + while (Run) { + if (queue.Count >= 8 && (DateTime.Now - lastRun).TotalMilliseconds >= delay) { + var batch = queue.Take(8).ToList(); + queue.RemoveRange(0, 8); + Send(Out["RecycleItems"], 8, batch[0], batch[1], batch[2], batch[3], batch[4], batch[5], batch[6], batch[7]); + progress += 8; + lastRun = DateTime.Now; + } + await Task.Delay(10); + } +} +catch (TaskCanceledException) { + log("error: inventory load timed out. ensure you are fully in-game."); +} +catch (Exception ex) { + log($"unhandled exception: {ex.Message}"); +} +finally { + server?.Stop(); + server?.Close(); + log("server shut down."); +} + +async Task HandleContextAsync(HttpListenerContext ctx, string html) { + var req = ctx.Request; + var res = ctx.Response; + try { + var endpoint = (req.HttpMethod, req.Url?.AbsolutePath); + switch (endpoint) { + case ("GET", "/"): + await WriteResponseAsync(res, html, "text/html"); + break; + case ("GET", "/state"): + var stateJson = JsonSerializer.Serialize(GetCurrentState()); + await WriteResponseAsync(res, stateJson, "application/json"); + break; + case ("POST", "/recycle"): + EnsureInventory(10000); + using (var r = new StreamReader(req.InputStream)) { + var payload = JsonSerializer.Deserialize(await r.ReadToEndAsync()); + if (payload != null) { + queue.Clear(); + delay = Math.Clamp(payload.Delay, 500, 60000); + foreach (var pItem in payload.Items) { + var itemInstances = Inventory + .Where(x => x.IsRecyclable && x.GetInfo().Identifier == pItem.HabboId) + .Select(i => -(int)i.Id) + .ToList(); + int amount = Math.Min(pItem.Amount, itemInstances.Count); + queue.AddRange(itemInstances.Take(amount)); + } + total = queue.Count; + progress = 0; + log($"queueing {total} items, {delay}ms delay."); + } + } + res.StatusCode = 200; + break; + case ("POST", "/stop"): + queue.Clear(); + progress = total = 0; + log("recycling stopped."); + res.StatusCode = 200; + break; + default: + res.StatusCode = 404; + break; + } + } + catch (Exception ex) { + log($"request error: {ex.Message}"); + if(!res.OutputStream.CanWrite) return; + res.StatusCode = 500; + } + finally { + res.Close(); + } +} + +async Task WriteResponseAsync(HttpListenerResponse res, string content, string type) { + var buffer = Encoding.UTF8.GetBytes(content); + res.ContentType = type; + res.ContentLength64 = buffer.Length; + await res.OutputStream.WriteAsync(buffer, 0, buffer.Length); +} + +void log(string message) => Log(message); \ No newline at end of file diff --git a/Furniture Position Debug.csx b/Furniture Position Debug.csx new file mode 100644 index 0000000..35037ab --- /dev/null +++ b/Furniture Position Debug.csx @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// ═══════════════════════════════════════════════════════════════════════════════ +// DEBUG - ZEIGT ALLE POSITIONEN +// ═══════════════════════════════════════════════════════════════════════════════ + +const int PILLOW_ID = 893412986; +const int GATE_ID = 2147418143; + +HashSet greenRollers = new HashSet { + 2147418115, 2147418119, 2147418133, + 2147418134, 2147418135, 2147418136 +}; + +int GetId(dynamic item) { try { return (int)item.Id; } catch { return 0; } } + +Log("═══════════════════════════════════════"); +Log(" DEBUG - POSITIONEN"); +Log("═══════════════════════════════════════"); + +while (Run) +{ + Delay(1000); + + Log("--- SCAN ---"); + + // Meine Position + try + { + Log($"ICH: ({Self.Location.X}, {Self.Location.Y})"); + } + catch { Log("ICH: nicht gefunden"); } + + // Kissen + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetId(item) == PILLOW_ID) + { + Log($"KISSEN: ({item.Location.X}, {item.Location.Y})"); + } + } + + // Alle grΓΌnen Roller + foreach (var item in FloorItems) + { + if (item == null) continue; + int id = GetId(item); + if (greenRollers.Contains(id)) + { + Log($"ROLLER {id}: ({item.Location.X}, {item.Location.Y})"); + } + if (id == GATE_ID) + { + Log($"GATE: ({item.Location.X}, {item.Location.Y})"); + } + } +} \ No newline at end of file diff --git a/Heal Bot.csx b/Heal Bot.csx new file mode 100644 index 0000000..4ab926f --- /dev/null +++ b/Heal Bot.csx @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +// Globale Variablen fΓΌr den State +bool isBusy = false; +Dictionary userMap = new Dictionary(); + +Log("Heal-Script gestartet. Bitte einmal den Raum neu betreten!"); + +// 1. Reset beim Raumwechsel +OnIntercept(In.RoomReady, e => { + userMap.Clear(); + isBusy = false; + Log("Raum geladen. Tracking aktiv."); +}); + +// 2. User-Liste aufbauen (Index zu ID Zuordnung) +OnIntercept(In.Users, e => { + try { + var p = e.Packet; + int count = p.ReadInt(); + + for (int i = 0; i < count; i++) { + // Standard Habbo User-Parsing + int id = p.ReadInt(); // Entity ID (Wichtig fΓΌr den Klick) + string name = p.ReadString(); + string motto = p.ReadString(); + string look = p.ReadString(); + int index = p.ReadInt(); // Index (Wichtig fΓΌr den Chat) + + // Restliche Daten ΓΌberspringen (Position etc.) + p.ReadInt(); // x + p.ReadInt(); // y + p.ReadString(); // z + p.ReadInt(); // dir + int type = p.ReadInt(); // type + + // In Map speichern + userMap[index] = id; + } + } catch { + // Parsing Fehler ignorieren (falls Server-Struktur abweicht) + } +}); + +// 3. Auf Chat reagieren +OnIntercept(In.Chat, async e => { + // Wenn wir gerade beschΓ€ftigt sind, ignorieren wir alles + if (isBusy) return; + + var p = e.Packet; + int index = p.ReadInt(); + string message = p.ReadString(); + + // Trigger-Wort prΓΌfen + if (message.ToLower().Contains("heal")) { + + // PrΓΌfen ob wir die ID zum User kennen + if (userMap.ContainsKey(index)) { + int targetId = userMap[index]; + + // Ablauf starten (in eigenem Task, damit der Chat nicht laggt) + _ = RunHealRoutine(targetId); + } else { + Log($"[Bot] User Index {index} unbekannt. Bitte Raum neu laden!"); + } + } +}); + +// 4. Die Logik zum Senden der Befehle +async Task RunHealRoutine(int targetId) { + if (isBusy) return; + isBusy = true; // Sperren + + Log($"[Bot] Heile User ID: {targetId}"); + + try { + // Schritt 1: :offer senden + Send(Out.Chat, ":offer", 0, -1); + await Task.Delay(600); // Wartezeit fΓΌr das MenΓΌ (anpassen bei Lag) + + // Schritt 2: 1 senden + Send(Out.Chat, "1", 0, -1); + await Task.Delay(600); // Wartezeit fΓΌr die Zielauswahl + + // Schritt 3: User anklicken + // Header "GetSelectedBadges" ist der Standard-Klick auf einen User + Send(Out.GetSelectedBadges, targetId); + + } catch (Exception ex) { + Log($"Fehler: {ex.Message}"); + } finally { + // Sperre aufheben + await Task.Delay(200); + isBusy = false; + } +} \ No newline at end of file diff --git a/Heart.csx b/Heart.csx new file mode 100644 index 0000000..8b3c961 --- /dev/null +++ b/Heart.csx @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +public struct Step +{ + public int X; + public int Y; + public int DelayMs; +} + +const int START_X = 13; +const int START_Y = 13; + +string rawDump = @" +1. (13,14) after 971ms +2. (13,15) after 499ms +3. (13,14) after 61ms +4. (13,16) after 438ms +5. (13,15) after 40ms +6. (13,17) after 499ms +7. (13,16) after 47ms +8. (13,18) after 452ms +9. (13,17) after 89ms +10. (13,19) after 410ms +11. (13,18) after 40ms +12. (13,20) after 467ms +13. (13,19) after 75ms +14. (13,21) after 424ms +15. (13,20) after 40ms +16. (12,21) after 489ms +17. (13,21) after 61ms +18. (12,20) after 438ms +19. (12,21) after 104ms +20. (12,19) after 395ms +21. (12,20) after 47ms +22. (12,18) after 452ms +23. (12,19) after 91ms +24. (12,17) after 407ms +25. (12,18) after 40ms +26. (12,16) after 474ms +27. (12,17) after 71ms +28. (12,15) after 424ms +29. (12,16) after 40ms +30. (12,14) after 486ms +31. (12,15) after 60ms +32. (11,14) after 438ms +33. (12,14) after 108ms +34. (11,15) after 391ms +35. (11,14) after 46ms +36. (11,15) after 544ms +37. (11,16) after 408ms +38. (11,17) after 499ms +39. (11,16) after 80ms +40. (11,18) after 419ms +41. (11,17) after 40ms +42. (11,19) after 484ms +43. (11,18) after 58ms +44. (11,20) after 442ms +45. (11,19) after 110ms +46. (11,21) after 389ms +47. (11,20) after 47ms +48. (10,21) after 452ms +49. (11,21) after 91ms +50. (10,20) after 409ms +51. (10,21) after 40ms +52. (10,19) after 473ms +53. (10,20) after 79ms +54. (10,18) after 420ms +55. (10,19) after 40ms +56. (10,17) after 485ms +57. (10,18) after 62ms +58. (10,16) after 437ms +59. (10,17) after 109ms +60. (10,15) after 390ms +61. (10,16) after 44ms +62. (10,14) after 455ms +63. (10,15) after 93ms +64. (9,14) after 406ms +65. (10,14) after 40ms +66. (9,15) after 476ms +67. (9,14) after 68ms +68. (9,16) after 424ms +69. (9,15) after 40ms +70. (9,17) after 485ms +71. (9,16) after 62ms +72. (9,18) after 437ms +73. (9,17) after 94ms +74. (9,19) after 405ms +75. (9,18) after 40ms +76. (9,20) after 481ms +77. (9,19) after 62ms +78. (9,21) after 437ms +79. (9,20) after 40ms +80. (8,20) after 500ms +81. (9,21) after 44ms +82. (8,20) after 546ms +83. (8,21) after 1407ms +84. (8,20) after 500ms +85. (8,21) after 62ms +86. (8,19) after 437ms +87. (8,20) after 104ms +88. (8,18) after 394ms +89. (8,19) after 42ms +90. (8,17) after 457ms +91. (8,18) after 94ms +92. (8,16) after 406ms +93. (8,17) after 40ms +94. (8,15) after 469ms +95. (8,16) after 77ms +96. (8,14) after 422ms +97. (8,15) after 40ms +98. (9,14) after 492ms +99. (8,14) after 55ms +100. (9,14) after 441ms +101. (8,14) after 499ms +102. (8,15) after 1000ms +103. (8,16) after 499ms +104. (8,15) after 75ms +105. (8,17) after 424ms +106. (8,16) after 40ms +107. (8,18) after 484ms +108. (8,17) after 61ms +109. (8,19) after 438ms +110. (8,18) after 106ms +111. (8,19) after 440ms +112. (9,20) after 453ms +113. (8,21) after 500ms +114. (9,20) after 40ms +115. (8,21) after 548ms +116. (9,21) after 423ms +117. (10,21) after 499ms +118. (9,21) after 62ms +119. (10,20) after 438ms +120. (10,21) after 104ms +121. (9,19) after 394ms +122. (10,20) after 47ms +123. (9,18) after 452ms +124. (9,19) after 89ms +125. (9,17) after 410ms +126. (9,18) after 40ms +127. (9,16) after 472ms +128. (9,17) after 80ms +129. (9,15) after 419ms +130. (9,16) after 40ms +131. (10,14) after 487ms +132. (9,15) after 59ms +133. (10,14) after 546ms +134. (10,15) after 393ms +135. (10,16) after 499ms +136. (10,15) after 91ms +137. (10,17) after 408ms +138. (10,16) after 40ms +139. (10,18) after 467ms +140. (10,17) after 75ms +141. (10,19) after 423ms +142. (10,18) after 40ms +143. (11,20) after 486ms +144. (10,19) after 61ms +145. (11,21) after 437ms +146. (11,20) after 109ms +147. (11,21) after 436ms +148. (12,21) after 954ms +149. (13,21) after 499ms +150. (12,21) after 75ms +151. (12,20) after 424ms +152. (13,21) after 40ms +153. (13,20) after 485ms +154. (12,20) after 64ms +155. (13,19) after 436ms +156. (13,20) after 40ms +157. (12,19) after 497ms +158. (13,19) after 44ms +159. (11,19) after 455ms +160. (12,19) after 94ms +161. (11,18) after 405ms +162. (11,19) after 40ms +163. (12,18) after 471ms +164. (11,18) after 74ms +165. (13,18) after 425ms +166. (12,18) after 40ms +167. (13,17) after 484ms +168. (13,18) after 64ms +169. (13,17) after 543ms +170. (12,17) after 391ms +171. (11,17) after 499ms +172. (12,17) after 90ms +173. (11,17) after 443ms +174. (11,16) after 466ms +175. (11,15) after 499ms +176. (11,16) after 40ms +177. (11,14) after 485ms +178. (11,15) after 63ms +179. (12,14) after 436ms +180. (11,14) after 106ms +181. (12,14) after 438ms +182. (12,15) after 455ms +183. (12,16) after 499ms +184. (12,15) after 40ms +185. (13,16) after 487ms +186. (12,16) after 63ms +187. (13,15) after 436ms +188. (13,16) after 107ms +189. (13,14) after 393ms +190. (13,15) after 45ms +191. (13,14) after 545ms +192. (12,14) after 409ms +193. (11,14) after 500ms +194. (12,14) after 74ms +195. (12,15) after 424ms +196. (11,14) after 40ms +197. (13,16) after 488ms +198. (12,15) after 64ms +199. (13,15) after 435ms +200. (13,16) after 106ms +201. (13,14) after 394ms +202. (13,15) after 46ms +203. (13,14) after 548ms +204. (12,14) after 403ms +205. (11,14) after 499ms +206. (12,14) after 74ms +207. (11,14) after 437ms +208. (12,15) after 1487ms +209. (13,14) after 500ms +210. (12,15) after 95ms +211. (13,14) after 435ms +212. (13,15) after 468ms +213. (13,16) after 500ms +214. (13,15) after 40ms +215. (13,17) after 487ms +216. (13,16) after 60ms +217. (13,18) after 440ms +218. (13,17) after 105ms +219. (13,19) after 392ms +220. (13,18) after 48ms +221. (13,20) after 451ms +222. (13,19) after 92ms +223. (13,21) after 407ms +224. (13,20) after 40ms +225. (12,21) after 471ms +226. (13,21) after 80ms +227. (11,21) after 420ms +228. (12,21) after 40ms +229. (12,20) after 487ms +230. (11,21) after 61ms +231. (13,19) after 439ms +232. (12,20) after 108ms +233. (13,20) after 390ms +234. (13,19) after 47ms +235. (13,21) after 452ms +236. (13,20) after 95ms +237. (13,21) after 438ms +238. (12,21) after 2966ms +239. (12,20) after 999ms +240. (11,21) after 500ms +241. (12,20) after 40ms +242. (11,21) after 553ms +243. (10,21) after 934ms +244. (9,21) after 500ms +245. (10,21) after 92ms +246. (8,21) after 407ms +247. (9,21) after 40ms +248. (8,21) after 545ms +249. (9,20) after 1924ms +250. (8,21) after 999ms +251. (8,20) after 999ms +252. (8,19) after 500ms +253. (8,20) after 64ms +254. (8,18) after 435ms +255. (8,19) after 109ms +256. (8,18) after 435ms +257. (8,17) after 454ms +258. (8,18) after 999ms +259. (8,17) after 1000ms +260. (8,16) after 1999ms +261. (8,15) after 500ms +262. (8,16) after 78ms +263. (8,14) after 421ms +264. (8,15) after 40ms +265. (9,15) after 486ms +266. (8,14) after 63ms +267. (9,15) after 546ms +268. (8,14) after 891ms +269. (9,14) after 997ms +270. (10,14) after 500ms +271. (9,14) after 40ms +272. (10,15) after 484ms +273. (10,14) after 58ms +274. (11,15) after 440ms +275. (10,15) after 111ms +276. (12,16) after 387ms +277. (11,15) after 49ms +278. (12,16) after 546ms +279. (11,16) after 404ms +280. (10,16) after 500ms +281. (11,16) after 75ms +282. (9,16) after 424ms +283. (10,16) after 40ms +284. (9,17) after 483ms +285. (9,16) after 60ms +286. (10,17) after 439ms +287. (9,17) after 40ms +288. (11,17) after 498ms +289. (10,17) after 45ms +290. (12,17) after 453ms +291. (11,17) after 96ms +292. (12,18) after 403ms +293. (12,17) after 40ms +294. (11,18) after 467ms +295. (12,18) after 82ms +296. (10,18) after 417ms +297. (11,18) after 40ms +298. (9,18) after 482ms +299. (10,18) after 65ms +300. (9,19) after 435ms +301. (9,18) after 107ms +302. (10,19) after 392ms +303. (9,19) after 47ms +304. (11,19) after 452ms +305. (10,19) after 89ms +306. (12,19) after 409ms +307. (11,19) after 40ms +308. (11,20) after 471ms +309. (12,19) after 81ms +310. (10,20) after 418ms +311. (11,20) after 40ms +312. (10,20) after 545ms +"; + +List ParseSteps(string text) +{ + var steps = new List(); + var rx = new Regex(@"\((\d+),(\d+)\)\s+after\s+(\d+)ms", RegexOptions.Compiled); + foreach (Match m in rx.Matches(text)) + { + steps.Add(new Step { + X = int.Parse(m.Groups[1].Value), + Y = int.Parse(m.Groups[2].Value), + DelayMs = int.Parse(m.Groups[3].Value) + }); + } + return steps; +} + +Log("=== Play Path From Dump ==="); +var steps = ParseSteps(rawDump); +if (steps.Count == 0) +{ + Log("ERROR: No steps parsed."); + return; +} + +Log($"Parsed {steps.Count} steps."); +Log($"Waiting until you are on {START_X}:{START_Y}..."); + +while (Run) +{ + if (Self != null && Self.Location != null && Self.Location.X == START_X && Self.Location.Y == START_Y) + break; + Delay(100); +} + +if (!Run) return; + +Log("Start reached. Replaying exact timed path..."); +for (int i = 0; i < steps.Count; i++) +{ + var s = steps[i]; + if (s.DelayMs > 0) Delay(s.DelayMs); + Move(s.X, s.Y); +} + +Log("Replay complete."); diff --git a/One-Way Door Auto Enter.csx b/One-Way Door Auto Enter.csx new file mode 100644 index 0000000..4c527f9 --- /dev/null +++ b/One-Way Door Auto Enter.csx @@ -0,0 +1,512 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// Color Puzzle Solver v2 +// - Auto calibration of arrow -> move mapping +// - Waits for real state change after every click + +const int TILE_KIND = 3696; +const int ARROW_KIND = 17851; +const int GRID_X_MIN = 36; +const int GRID_X_MAX = 39; +const int GRID_Y_MIN = 27; +const int GRID_Y_MAX = 30; + +const int CLICK_SETTLE_DELAY_MS = 250; +const int WAIT_CHANGE_TIMEOUT_MS = 6000; +const int WAIT_CHANGE_POLL_MS = 120; +const int MAX_STEPS = 140; +const int BFS_MAX_NODES = 4_000_000; +const int IDA_MAX_SEC = 12; + +const bool AUTO_QUEUE_START = true; +const long TRANSPORTER_ID = 759030883; +const int QUEUE_CLICK_INTERVAL_MS = 5000; +const int WAIT_PUZZLE_POLL_MS = 250; +const int WAIT_PUZZLE_LOG_MS = 5000; +const bool REQUIRE_SELF_IN_PLAYZONE = true; +const int PLAY_X_MIN = 34; +const int PLAY_X_MAX = 41; +const int PLAY_Y_MIN = 26; +const int PLAY_Y_MAX = 31; +const bool REQUIRE_SELF_MIN_Z = true; +const double SELF_MIN_Z = 17.0; +const bool REQUIRE_PLAYER_QUEUE_CLEAR = true; +const string WATCH_PLAYER_NAME = "gracie"; +const int WATCH_QUEUE_X = 33; +const int WATCH_QUEUE_Y = 19; +const double WATCH_QUEUE_Z = 4.5; +const double WATCH_QUEUE_Z_TOL = 1.0; + +int GetState(dynamic item) +{ + try { return int.Parse(item.State?.ToString() ?? "0"); } + catch { return 0; } +} + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } + catch { return -1; } +} + +uint EncodeGrid(int[,] g) +{ + uint s = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + s |= ((uint)(g[r, c] & 3)) << (2 * (r * 4 + c)); + return s; +} + +bool TryReadGrid(out uint state, out string dump) +{ + int[,] grid = new int[4, 4]; + bool[,] found = new bool[4, 4]; + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + + if (x < GRID_X_MIN || x > GRID_X_MAX) continue; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + if (z < 18.4) continue; + + int row = y - GRID_Y_MIN; + int col = x - GRID_X_MIN; + grid[row, col] = GetState(item); + found[row, col] = true; + } + + int cnt = 0; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (found[r, c]) cnt++; + + if (cnt < 16) + { + state = 0; + dump = ""; + return false; + } + + state = EncodeGrid(grid); + dump = string.Join(" | ", Enumerable.Range(0, 4).Select(r => + $"R{r}[{grid[r,0]},{grid[r,1]},{grid[r,2]},{grid[r,3]}]")); + return true; +} + +uint RowLeft(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row >> 2) | (row << 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint RowRight(uint s, int r) +{ + int sh = r * 8; + uint row = (s >> sh) & 0xFFu; + uint rot = ((row << 2) | (row >> 6)) & 0xFFu; + return (s & ~(0xFFu << sh)) | (rot << sh); +} + +uint ColUp(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v1 << b) | (v2 << (b + 8)) | (v3 << (b + 16)) | (v0 << (b + 24)); +} + +uint ColDown(uint s, int c) +{ + int b = c * 2; + uint v0 = (s >> b) & 3u; + uint v1 = (s >> (b + 8)) & 3u; + uint v2 = (s >> (b + 16)) & 3u; + uint v3 = (s >> (b + 24)) & 3u; + uint mask = ~(3u << b | 3u << (b + 8) | 3u << (b + 16) | 3u << (b + 24)); + return (s & mask) | (v3 << b) | (v0 << (b + 8)) | (v1 << (b + 16)) | (v2 << (b + 24)); +} + +uint ApplyMove(uint s, int m) +{ + if (m < 4) return RowLeft(s, m); + if (m < 8) return RowRight(s, m - 4); + if (m < 12) return ColUp(s, m - 8); + return ColDown(s, m - 12); +} + +int InverseMove(int m) +{ + if (m < 4) return m + 4; + if (m < 8) return m - 4; + if (m < 12) return m + 4; + return m - 4; +} + +string MoveName(int m) +{ + if (m < 4) return $"Row{m} LEFT"; + if (m < 8) return $"Row{m - 4} RIGHT"; + if (m < 12) return $"Col{m - 8} UP"; + return $"Col{m - 12} DOWN"; +} + +int DetectMove(uint before, uint after) +{ + int hit = -1; + for (int m = 0; m < 16; m++) + { + if (ApplyMove(before, m) != after) continue; + if (hit != -1) return -2; + hit = m; + } + return hit; +} + +List SolveBfs(uint start, uint goal) +{ + if (start == goal) return new List(); + + var visited = new Dictionary(); + var queue = new Queue(); + visited[start] = (start, -1); + queue.Enqueue(start); + int nodes = 0; + bool found = false; + + while (queue.Count > 0 && nodes < BFS_MAX_NODES) + { + uint cur = queue.Dequeue(); + nodes++; + + for (int m = 0; m < 16; m++) + { + uint nxt = ApplyMove(cur, m); + if (visited.ContainsKey(nxt)) continue; + visited[nxt] = (cur, m); + if (nxt == goal) + { + found = true; + queue.Clear(); + break; + } + queue.Enqueue(nxt); + } + } + + if (!found) return null; + + var sol = new List(); + uint s = goal; + while (s != start) + { + var p = visited[s]; + sol.Add(p.move); + s = p.parent; + } + sol.Reverse(); + return sol; +} + +List SolveIda(uint start, uint goal) +{ + if (start == goal) return new List(); + var t0 = DateTime.Now; + + int H(uint st) + { + int mis = 0; + for (int i = 0; i < 16; i++) + { + int a = (int)((st >> (i * 2)) & 3u); + int b = (int)((goal >> (i * 2)) & 3u); + if (a != b) mis++; + } + return (mis + 3) / 4; + } + + List best = null; + bool timeout = false; + + bool Dfs(uint st, List path, int maxDepth) + { + if (timeout) return false; + if ((DateTime.Now - t0).TotalSeconds > IDA_MAX_SEC) + { + timeout = true; + return false; + } + + if (st == goal) + { + best = new List(path); + return true; + } + + int h = H(st); + if (path.Count + h > maxDepth) return false; + + int block = path.Count > 0 ? InverseMove(path[path.Count - 1]) : -1; + for (int m = 0; m < 16; m++) + { + if (m == block) continue; + path.Add(m); + if (Dfs(ApplyMove(st, m), path, maxDepth)) return true; + path.RemoveAt(path.Count - 1); + if (timeout) return false; + } + return false; + } + + int d0 = H(start); + for (int d = d0; d <= 22 && !timeout; d++) + { + if (Dfs(start, new List(), d)) break; + } + return best; +} + +List Solve(uint start, uint goal) +{ + var bfs = SolveBfs(start, goal); + if (bfs != null) return bfs; + return SolveIda(start, goal); +} + +bool ClickAndWaitChange(long furniId, uint before, out uint after, out string dumpAfter) +{ + Send(Out["ClickFurni"], (int)furniId, 0); + Delay(CLICK_SETTLE_DELAY_MS); + + int waited = 0; + while (waited < WAIT_CHANGE_TIMEOUT_MS) + { + if (TryReadGrid(out after, out dumpAfter) && after != before) + return true; + Delay(WAIT_CHANGE_POLL_MS); + waited += WAIT_CHANGE_POLL_MS; + } + + after = before; + dumpAfter = ""; + return false; +} + +Dictionary ReadArrowIds() +{ + var arrowIds = new Dictionary(); + + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetKind(item) != ARROW_KIND) continue; + int x = item.Location.X; + int y = item.Location.Y; + + if (y == GRID_Y_MIN - 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"up_{x - GRID_X_MIN}"] = item.Id; + else if (y == GRID_Y_MAX + 1 && x >= GRID_X_MIN && x <= GRID_X_MAX) + arrowIds[$"down_{x - GRID_X_MIN}"] = item.Id; + else if (x == GRID_X_MIN - 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"left_{y - GRID_Y_MIN}"] = item.Id; + else if (x == GRID_X_MAX + 1 && y >= GRID_Y_MIN && y <= GRID_Y_MAX) + arrowIds[$"right_{y - GRID_Y_MIN}"] = item.Id; + } + + return arrowIds; +} + +bool IsSelfInPlayZone() +{ + try + { + int x = Self.Location.X; + int y = Self.Location.Y; + double z = Self.Location.Z; + bool inRect = x >= PLAY_X_MIN && x <= PLAY_X_MAX && y >= PLAY_Y_MIN && y <= PLAY_Y_MAX; + bool inZ = !REQUIRE_SELF_MIN_Z || z >= SELF_MIN_Z; + return inRect && inZ; + } + catch + { + return false; + } +} + +bool IsWatchedPlayerAtQueueSpot() +{ + try + { + var u = Users.FirstOrDefault(x => + x != null && + x.Name != null && + x.Name.Equals(WATCH_PLAYER_NAME, StringComparison.OrdinalIgnoreCase)); + + if (u == null || u.Location == null) return false; + + int x = u.Location.X; + int y = u.Location.Y; + double z = u.Location.Z; + + return x == WATCH_QUEUE_X && y == WATCH_QUEUE_Y && Math.Abs(z - WATCH_QUEUE_Z) <= WATCH_QUEUE_Z_TOL; + } + catch + { + return false; + } +} + +Log("=== Color Puzzle Auto-Solver (AutoCalib + WaitChange) ==="); + +Dictionary arrowIds = null; +uint current; +string dumpNow; + +int sinceQueueClick = QUEUE_CLICK_INTERVAL_MS; +int sinceLog = WAIT_PUZZLE_LOG_MS; + +while (true) +{ + bool hasGrid = TryReadGrid(out current, out dumpNow); + var probeArrows = ReadArrowIds(); + bool hasArrows = probeArrows.Count == 16; + bool inPlayZone = !REQUIRE_SELF_IN_PLAYZONE || IsSelfInPlayZone(); + bool queueClear = !REQUIRE_PLAYER_QUEUE_CLEAR || !IsWatchedPlayerAtQueueSpot(); + + if (hasGrid && hasArrows && inPlayZone && queueClear) + { + arrowIds = probeArrows; + break; + } + + if (AUTO_QUEUE_START && sinceQueueClick >= QUEUE_CLICK_INTERVAL_MS) + { + Send(Out["ClickFurni"], (int)TRANSPORTER_ID, 0); + Log($"Queue: Klick Transporter {TRANSPORTER_ID}..."); + sinceQueueClick = 0; + } + + if (sinceLog >= WAIT_PUZZLE_LOG_MS) + { + string selfPos = "?"; + try { selfPos = $"{Self.Location.X},{Self.Location.Y},{Self.Location.Z:F2}"; } catch { } + Log($"Warte auf Spielstart... Grid={(hasGrid ? "ok" : "no")}, Pfeile={probeArrows.Count}/16, InZone={(inPlayZone ? "yes" : "no")}, QueueClear={(queueClear ? "yes" : "no")}, Self={selfPos}"); + sinceLog = 0; + } + + Delay(WAIT_PUZZLE_POLL_MS); + sinceQueueClick += WAIT_PUZZLE_POLL_MS; + sinceLog += WAIT_PUZZLE_POLL_MS; +} + +Log("Puzzle erkannt. Starte Solver..."); +Log($"Pfeile: {arrowIds.Count}/16"); + +int[] targetRows = new int[4]; +bool targetFound = false; +foreach (var item in FloorItems) +{ + if (item == null) continue; + if (GetKind(item) != TILE_KIND) continue; + if (item.Location.X != 41) continue; + int y = item.Location.Y; + if (y < GRID_Y_MIN || y > GRID_Y_MAX) continue; + + targetRows[y - GRID_Y_MIN] = GetState(item); + targetFound = true; +} +if (!targetFound) targetRows = new[] { 1, 2, 3, 0 }; + +int[,] tgt = new int[4, 4]; +for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + tgt[r, c] = targetRows[r]; + +uint goal = EncodeGrid(tgt); +Log($"Ziel: R0={targetRows[0]}, R1={targetRows[1]}, R2={targetRows[2]}, R3={targetRows[3]}"); + +Log($"Start: {dumpNow}"); + +var moveToKey = new Dictionary(); +var keyToMove = new Dictionary(); +var allKeys = arrowIds.Keys.OrderBy(k => k).ToList(); + +for (int step = 1; step <= MAX_STEPS; step++) +{ + if (current == goal) + { + Log("=== Geloest: alle 4 Reihen korrekt ==="); + return; + } + + var plan = Solve(current, goal); + if (plan == null || plan.Count == 0) + { + Log("ERROR: Kein Plan vom aktuellen Zustand."); + return; + } + + int wanted = plan[0]; + string key; + bool probing = false; + + if (moveToKey.ContainsKey(wanted)) + { + key = moveToKey[wanted]; + } + else + { + key = allKeys.FirstOrDefault(k => !keyToMove.ContainsKey(k)); + if (key == null) + { + key = allKeys[0]; + } + probing = true; + } + + long id = arrowIds[key]; + Log($"[{step}] want {MoveName(wanted)} | click {key}" + (probing ? " (probe)" : "")); + + if (!ClickAndWaitChange(id, current, out uint after, out string dumpAfter)) + { + Log(" Kein Move erkannt (Timeout), gleicher Schritt nochmal."); + continue; + } + + int actual = DetectMove(current, after); + if (actual >= 0) + { + moveToKey[actual] = key; + keyToMove[key] = actual; + if (actual != wanted) + Log($" AutoCalib: {key} == {MoveName(actual)} (nicht {MoveName(wanted)})"); + } + else if (actual == -1) + { + Log($" Unbekannter Transition-Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + else + { + Log($" Mehrdeutiger Delta, weiter mit Re-Plan. State: {dumpAfter}"); + } + + current = after; + + if (step % 10 == 0) + Log($" Calib: {moveToKey.Count}/16 Moves gemappt"); +} + +Log("Nicht fertig in MAX_STEPS. Script einfach nochmal starten."); diff --git a/Pet Level Scanner.csx b/Pet Level Scanner.csx new file mode 100644 index 0000000..65a17db --- /dev/null +++ b/Pet Level Scanner.csx @@ -0,0 +1,33 @@ +string suchName = "Goki"; +int tempLevel = 0; +bool gotPetInfo = false; + +OnIntercept(In["PetInfo"], e => { + e.Packet.ReadInt(); + e.Packet.ReadString(); + tempLevel = e.Packet.ReadInt(); + gotPetInfo = true; +}); + +Log("Suche: " + suchName); + +int gefunden = 0; + +foreach (var entity in Entities) { + if (entity.Name == suchName) { + gefunden++; + + gotPetInfo = false; + Send(Out["GetPetInfo"], entity.Id); + + int waited = 0; + while (!gotPetInfo && waited < 20 && Run) { + Delay(100); + waited++; + } + + Log("[" + gefunden + "] Level: " + tempLevel); + } +} + +Log("Ende"); diff --git a/Pet Trainer Auto v84.csx b/Pet Trainer Auto v84.csx new file mode 100644 index 0000000..4c35628 --- /dev/null +++ b/Pet Trainer Auto v84.csx @@ -0,0 +1,2177 @@ +// Auto Pet Training Script V84 +// FIX: Zeigt echte XP-Menge (+10, +20, +35 etc.) +// FIX: Energy kann > MaxEnergy sein (durch Items) + +using System.Linq; // Fuer OrderBy (zufaellige Sortierung) + +// ============ CONFIGURATION ============ +string petName = ""; // Wird automatisch erkannt beim Anklicken! +int trainDelay = 4000; +int energyThreshold = 40; +int hungerThreshold = 40; + +// ============ PET-TYP FOOD FILTER ============ +// Essen das NUR fΓΌr bestimmte Tiere ist und fΓΌr andere ausgeschlossen werden soll +// HORSE ONLY (Pferde-Essen - Hunde/Katzen etc. essen das NICHT!) +string[] horseOnlyFood = { + "hay", "hay bucket", "heu", "heuballen", + "horse trough", "pferdetrog" +}; + +// CAT ONLY (Katzen-Essen) +string[] catOnlyFood = { + // Die meisten Fische kΓΆnnen alle essen, aber falls nΓΆtig hier eintragen +}; + +// DOG ONLY (Hunde-Essen) +string[] dogOnlyFood = { + // Knochen kΓΆnnen die meisten essen +}; + +// Habbo Pet Types: +// 0=Dog, 1=Cat, 2=Crocodile, 3=Terrier, 4=Bear, 5=Pig, 6=Lion, 7=Horse, 8=Spider, 9=Turtle, 10=Chicken +const int PET_TYPE_HORSE = 7; + +// PrΓΌft ob ein Essen fΓΌr den aktuellen Pet-Typ geeignet ist +bool IsFoodForPetType(string foodName, int currentPetType) { + if (string.IsNullOrEmpty(foodName)) return false; + string lower = foodName.ToLower(); + + // HORSE-ONLY FOOD: Hay, Hay Bucket, Horse Trough + // Diese dΓΌrfen NUR an Pferde (type 7) verfΓΌttert werden! + foreach (var horseFood in horseOnlyFood) { + if (lower.Contains(horseFood)) { + if (currentPetType != PET_TYPE_HORSE) { + Log($"[FOOD-FILTER] '{foodName}' ist Pferde-Essen - Pet ist Typ {currentPetType} (kein Pferd!)"); + return false; // Kein Pferd -> kann dieses Essen nicht fressen! + } + return true; // Pferd kann es fressen + } + } + + // CAT-ONLY FOOD (falls nΓΆtig) + foreach (var catFood in catOnlyFood) { + if (lower.Contains(catFood)) { + if (currentPetType != 1) { // 1 = Cat + return false; + } + return true; + } + } + + // DOG-ONLY FOOD (falls nΓΆtig) + foreach (var dogFood in dogOnlyFood) { + if (lower.Contains(dogFood)) { + if (currentPetType != 0 && currentPetType != 3) { // 0=Dog, 3=Terrier + return false; + } + return true; + } + } + + // Alles andere ist universelles Essen - ALLE Pets kΓΆnnen es fressen! + return true; +} + +// ============ COMMAND ID MAPPING ============ +// Habbo Pet Command IDs -> Command Names +Dictionary commandIdToName = new Dictionary() { + { 0, "free" }, + { 1, "sit" }, + { 2, "down" }, + { 3, "here" }, + { 4, "beg" }, + { 5, "play dead" }, + { 6, "stay" }, + { 7, "follow" }, + { 8, "stand" }, + { 9, "jump" }, + { 10, "speak" }, + { 11, "play" }, + { 12, "silent" }, + { 13, "nest" }, + { 14, "drink" }, + { 15, "follow left" }, + { 16, "follow right" }, + { 17, "play football" }, + { 18, "come here" }, + { 24, "move forward" }, + { 25, "turn left" }, + { 26, "turn right" }, + { 43, "eat" }, + // Horse/Special Pet Commands + { 46, "wag tail" }, // Pferde: Wag Tail + { 47, "count" }, // Pferde: Count (zaehlen) + { 48, "breed" } // Breed +}; + +// Commands die NICHT fuer Training verwendet werden sollen +// Diese geben KEINE XP oder sind spezielle Befehle! +string[] excludeFromTraining = { + // Keine XP: + "free", // Laesst Pet frei laufen - KEINE XP! + // Spezielle Befehle: + "eat", // Fuer Care-Modus reserviert + "drink", // Fuer Care-Modus reserviert + "breed", // Zuechten + "wag tail", // Spezial-Animation (Pferd) + "count" // Spezial-Trick (Pferd) +}; + +// ============ PET FOOD NAMES ============ +// NUR echte Pet Food Items aus dem Katalog "Toys and Accessories"! +// KEINE MΓΆbel wie "Delicious Street Food"! +string[] petFoodClassNames = { + "petfood", "petgoodie", "pet_food", + "a0 petfood", "a0 petgoodie" +}; + +// NUR EXAKTE Namen von echtem Pet Food! +// WICHTIG: "Carrot Bite" ist ein KLEIDUNGSSTUECK, kein Essen! +// KOMPLETTE LISTE aller 33 Pet Food Items aus dem Katalog! +string[] petFoodExactNames = { + // === STANDARD FOOD === + "Cabbage", + "Carrot", + "Hay", + "Hay Bucket", + + // === FLEISCH & FISCH === + "Meat Bone", + "Doggy Bones", + "T-Bones", + "Salmon", + "Sardines", + "Chicken", + "Shrimps", + + // === OBST & GEMUESE === + "Green Apples", + "Red Apples", + "Ice Apples", + "Kale", + "Grapes", + "Webbed Grapes", + "Banana", + "Red Pepper", + "Yellow Chili", + "Corn", + "Leaves", + + // === SUESSIGKEITEN & SPEZIAL === + "Holiday Cake", + "Popsicles", + "Chocolate Gazelle", + "Choco-Bunny", + "Chocolate Mouse", + "Marzipan Man", + + // === SONSTIGES === + "Pile of Letters", + "Books", + "TV Remote", + "The Fly", + "Grass Duck", + + // === DEUTSCHE NAMEN === + "Kohl", + "Karotte", + "Heu", + "Heuballen", + "Knochen", + "Lachs", + "Sardinen", + "Haehnchen", + "Garnelen", + "Gruene Aepfel", + "Rote Aepfel", + "Eis Aepfel", + "Gruenkohl", + "Trauben", + "Banane", + "Paprika", + "Chili", + "Mais", + "Blaetter", + "Kuchen", + "Eis am Stiel", + "Schokoladen Gazelle", + "Schoko-Hase", + "Schokoladen Maus", + "Marzipan Mann", + "Briefstapel", + "Buecher", + "Fernbedienung", + "Die Fliege", + "Gras Ente" +}; + +// ============ PET WATER BOWL NAMES (from catalog) ============ +string[] petWaterClassNames = { + "waterbowl", "water_bowl", "milkbowl", "pond", "trough", + "grail_water", "grail_nectar", "lm_pond", "pet_waterbottle", + "baby_waterbottle", "babybottle" +}; + +string[] petWaterDisplayNames = { + // From catalog "Toys and Accessories" + "Milk Bowl", "Water Bowl", "Basic Grey Water Bowl", + "Basic Blue Water Bowl", "Basic Green Water Bowl", + "Basic Pink Water Bowl", "Basic Yellow Water Bowl", + "Basic Red Water Bowl", + "Horse Trough", "Pond", "Water Bottle", + "Grail Water", "Grail Nectar", "Leprechaun Pond", + "Baby Waterbottle", "Baby Water Bottle", "Baby Bottle", + // German names + "Wassernapf", "Wasserschale", "Milchnapf", + "Pferdetrog", "Teich", "Wasserflasche", + "Baby Wasserflasche", "Babyflasche" +}; + +// ============ SICHERE BASIS-BEFEHLE ============ +// Level 1 Pets haben NUR diese 2 Befehle freigeschaltet! +// (eat und drink sind auch freigeschaltet aber nicht fuer Training) +string[] safeCommands = { + "free", "sit" +}; + +// Befehle die Essen/Wasser benoetigen +string[] careCommands = { "eat", "drink" }; + +// ============ STATE ============ +List unlockedCommands = new List(); +string currentCmd = ""; +bool waitingForResponse = false; +bool gotXP = false; +int lastXPGained = 0; // Echte XP-Menge aus dem Packet! +bool gotRefusal = false; + +int petLevel = 0; +int petXP = 0; +int petMaxXP = 0; +int petEnergy = 100; +int petMaxEnergy = 100; // DYNAMISCH! Wird aus PetInfo gelesen! +int petHunger = 100; + +long selectedPetId = -1; +int petType = -1; // Pet-Typ: 0=Dog, 1=Cat, 2=Croc, 3=Terrier, 7=Horse, etc. +bool petSwitchRequested = false; // Flag: Neues Pet wurde angeklickt! +long newPetId = -1; // ID des neuen Pets + +// PetCommands state - WICHTIG: vor dem Handler deklarieren! +bool petCommandsReceived = false; +List petCommandIds = new List(); +int cmdIndex = 0; // GLOBAL damit PetCommands Handler es resetten kann + +// Pet movement tracking +bool petIsMoving = false; +bool petIsBusy = false; +DateTime lastPetUpdate = DateTime.Now; + +// ============ AUTO-SWITCH: RegelmÀßig zum niedrigsten Level wechseln! ============ +DateTime lastLevelScan = DateTime.MinValue; +int levelScanIntervalSeconds = 30; // Alle 30 Sekunden scannen! +string targetPetName = ""; // Name der Pets die trainiert werden sollen + +// TemporΓ€re Variablen fΓΌr Level-Scan +int scanTempLevel = -1; +bool scanGotResponse = false; + +// Care mode state - DYNAMISCH basierend auf petMaxEnergy! +bool inCareMode = false; +int careModeThresholdPercent = 20; // Unter 20% = Care-Modus STARTEN +int careModeExitPercent = 95; // Bei 95%+ = Care-Modus BEENDEN +int careModeCmdCount = 0; // Zaehler fuer Care-Modus Befehle +int careModeCmdLimit = 50; // Nach 50 eat/drink -> kurze Pause + +// Room data +string roomHeightMap = ""; +List<(int x, int y)> occupiedTiles = new List<(int x, int y)>(); + +// ============ BOT MESSAGE (Bubble 31) ============ +void BotMessage(string msg) { + Send(In["Chat"], 0, $"[PET TRAINER] {msg}", 0, 31, 0, -1); + Log($"[BOT] {msg}"); +} + +// ============ PACKET HANDLERS ============ + +// Capture FloorHeightMap +OnIntercept(In["FloorHeightMap"], e => { + try { + e.Packet.ReadBool(); + e.Packet.ReadInt(); + roomHeightMap = e.Packet.ReadString(); + } catch { } +}); + +// Chat responses +OnIntercept(In["Chat"], e => { + if (!waitingForResponse) return; + + int idx = e.Packet.ReadInt(); + string msg = e.Packet.ReadString(); + + if (msg.Contains("I'd rather not") || + msg.Contains("Yes, yes, I'll do it later") || + msg.Contains("Well, I'm not going to do it") || + msg.Contains("Maybe I'll do it later") || + msg.Contains("Maybe later") || + msg.Contains("Not now") || + msg.Contains("I don't want to") || + msg.Contains("Food, please") || + msg.Contains("My stomach is growling")) { + gotRefusal = true; + } +}); + +// XP gain - lese echte XP-Menge aus dem Packet! +// PetExperience Packet Format: {i:PetId}{i:PetRoomIndex}{i:XPGained} +OnIntercept(In["PetExperience"], e => { + try { + int petId = e.Packet.ReadInt(); // Pet ID + int roomIndex = e.Packet.ReadInt(); // Pet Room Index + int xpGained = e.Packet.ReadInt(); // XP gewonnen! + + lastXPGained = xpGained; + if (waitingForResponse) gotXP = true; + + Log($"[XP] +{xpGained} XP erhalten!"); + } catch { + // Fallback wenn Packet-Format anders ist + lastXPGained = 10; // Annahme: 10 XP + if (waitingForResponse) gotXP = true; + } +}); + +// Pet info - KORRIGIERTES Paket-Format! +// {i:ID}{s:Name}{i:Level}{i:MaxLevel}{i:XP}{i:MaxXP}{i:ENERGY}{i:MaxEnergy}{i:Hunger}{i:MaxHunger}... +string lastPetInfoName = ""; // Speichere Pet-Name aus PetInfo +int lastPetInfoId = -1; // Speichere Pet-ID aus PetInfo + +OnIntercept(In["PetInfo"], e => { + try { + int id = e.Packet.ReadInt(); // 0: Pet ID + string name = e.Packet.ReadString(); // 1: Name + lastPetInfoName = name; // Speichern fΓΌr Fallback! + lastPetInfoId = id; // ID auch speichern + + int receivedLevel = e.Packet.ReadInt(); // 2: Level + petLevel = receivedLevel; + + // FÜR LEVEL-SCAN: Speichere Level in temporΓ€rer Variable! + scanTempLevel = receivedLevel; + scanGotResponse = true; + + int maxLevel = e.Packet.ReadInt(); // 3: Max Level (20) + petXP = e.Packet.ReadInt(); // 4: XP + petMaxXP = e.Packet.ReadInt(); // 5: Max XP + + // Energy und MaxEnergy - NICHT TAUSCHEN! + // In Habbo kann Energy HΓ–HER als MaxEnergy sein (durch Items/Buffs)! + // Das Packet-Format ist IMMER: Energy zuerst, dann MaxEnergy + petEnergy = e.Packet.ReadInt(); // 6: Energy (aktuell) + petMaxEnergy = e.Packet.ReadInt(); // 7: MaxEnergy (Limit) + + petHunger = e.Packet.ReadInt(); // 8: Hunger + int maxHunger = e.Packet.ReadInt(); // 9: Max Hunger (100) + + // PetInfo Format (nach Hunger): + // 10: Respect (int) + // 11: OwnerId (int) + // 12: Age (int) + // 13: OwnerName (string) + // 14: PetType (int) <-- DAS BRAUCHEN WIR! + int respect = e.Packet.ReadInt(); // 10: Respect + int ownerId = e.Packet.ReadInt(); // 11: Owner ID + int age = e.Packet.ReadInt(); // 12: Age (Tage) + string ownerName = e.Packet.ReadString(); // 13: Owner Name + petType = e.Packet.ReadInt(); // 14: PET TYPE! + + // Berechne Prozent + int energyPercent = petMaxEnergy > 0 ? (petEnergy * 100 / petMaxEnergy) : 0; + + // Pet-Typ Namen fuer Debug + string[] petTypeNames = { "Dog", "Cat", "Crocodile", "Terrier", "Bear", "Pig", "Lion", "Horse", "Spider", "Turtle", "Chicken" }; + string petTypeName = petType >= 0 && petType < petTypeNames.Length ? petTypeNames[petType] : $"Unknown({petType})"; + + Log($"[PetInfo] {name} Lv{petLevel} | Type: {petTypeName} | Energy: {petEnergy}/{petMaxEnergy} ({energyPercent}%)"); + + // Wenn petName noch leer ist, setze ihn hier! + if (string.IsNullOrEmpty(petName)) { + petName = name; + Log($"[PetInfo] Pet-Name gesetzt: {petName}"); + } + + // SOFORT Care-Modus pruefen wenn Energie kritisch niedrig! + if (energyPercent <= 5 && !inCareMode) { + Log($"[PetInfo] !!! KRITISCH NIEDRIGE ENERGIE: {energyPercent}% !!!"); + } + } catch (Exception ex) { + Log($"[PetInfo] FEHLER: {ex.Message}"); + } +}); + +// Pet selection - Pet wurde angeklickt +OnIntercept(Out["GetPetInfo"], e => { + int clickedPetId = e.Packet.ReadInt(); + Log($"[SELECT] Pet angeklickt mit ID: {clickedPetId}"); + + // Wenn bereits ein Pet trainiert wird und ein ANDERES angeklickt wurde + if (selectedPetId != -1 && clickedPetId != selectedPetId) { + Log($"[SWITCH] Neues Pet angeklickt! Wechsel von ID {selectedPetId} zu {clickedPetId}"); + petSwitchRequested = true; + newPetId = clickedPetId; + } else { + selectedPetId = clickedPetId; + } + + // PetCommands Packet sollte kurz danach kommen! + petCommandsReceived = false; // Reset fuer neues Pet +}); + +// ============ PET COMMANDS PACKET ============ +// Laedt die freigeschalteten Commands vom Server +// PACKET FORMAT: +// {i:PetID} +// {i:AnzahlSichtbareCommands}{i:cmd1}{i:cmd2}... <- IGNORIEREN (alle sichtbaren) +// {i:AnzahlGelernteCommands}{i:cmd1}{i:cmd2}... <- NUR DIESE LESEN! + +OnIntercept(In["PetCommands"], e => { + try { + int petId = e.Packet.ReadInt(); + Log($"[PetCommands] Empfangen fuer Pet ID: {petId}"); + + petCommandIds.Clear(); + unlockedCommands.Clear(); + + // ERSTE LISTE = Alle sichtbaren Commands (IGNORIEREN!) + int allCommandsCount = e.Packet.ReadInt(); + Log($"[PetCommands] Sichtbare Befehle: {allCommandsCount} (ueberspringe...)"); + + // Ueberspringe die erste Liste komplett + for (int i = 0; i < allCommandsCount; i++) { + e.Packet.ReadInt(); // Lesen und wegwerfen + } + + // ZWEITE LISTE = Gelernte Commands (NUR DIESE NUTZEN!) + int learnedCount = e.Packet.ReadInt(); + Log($"[PetCommands] GELERNTE Befehle: {learnedCount}"); + + // Lese die gelernten Commands + for (int i = 0; i < learnedCount; i++) { + int cmdId = e.Packet.ReadInt(); + + if (commandIdToName.ContainsKey(cmdId) && !petCommandIds.Contains(cmdId)) { + petCommandIds.Add(cmdId); + string cmdName = commandIdToName[cmdId]; + + // Nur Training-Commands hinzufuegen (nicht eat/drink/nest/silent) + bool excluded = false; + foreach (var ex in excludeFromTraining) { + if (cmdName == ex) { + excluded = true; + break; + } + } + + if (!excluded) { + unlockedCommands.Add(cmdName); + } + + Log($"[PetCommands] + {cmdId} = {cmdName}" + (excluded ? " (excluded)" : "")); + } else if (!commandIdToName.ContainsKey(cmdId)) { + Log($"[PetCommands] ? {cmdId} = UNBEKANNT (ignoriert)"); + } + } + + Log($"[PetCommands] {unlockedCommands.Count} Training-Commands geladen!"); + Log($"[PetCommands] Befehle: {string.Join(", ", unlockedCommands)}"); + petCommandsReceived = true; + cmdIndex = 0; + BotMessage($"Neue Commands geladen! {unlockedCommands.Count} Befehle"); + + } catch (Exception ex) { + Log($"[PetCommands] FEHLER: {ex.Message}"); + } +}); + +// Track pet movement +OnIntercept(In["UserUpdate"], e => { + try { + string data = e.Packet.ToString(); + + if (data.Contains("mv ") || data.Contains("/mv ")) { + petIsMoving = true; + petIsBusy = true; + lastPetUpdate = DateTime.Now; + } else if (data.Contains("//eat") || data.Contains("//scr") || + data.Contains("//snf") || data.Contains("//lay") || + data.Contains("//sit") || data.Contains("//beg") || + data.Contains("//ded") || data.Contains("//gst")) { + petIsMoving = false; + petIsBusy = true; + lastPetUpdate = DateTime.Now; + } else if (data.Contains("\"//\"")) { + petIsMoving = false; + petIsBusy = false; + lastPetUpdate = DateTime.Now; + } + } catch { } +}); + +// ============ HELPERS ============ +void SendCmd(string cmd) { + Send(Out["Chat"], $"{petName} {cmd}", 0, -1); +} + +void ResetFlags() { + gotXP = false; + gotRefusal = false; +} + +void GetPetInfo() { + // DIREKT selectedPetId benutzen - nicht nach Name suchen! + // WICHTIG: Send erwartet int, also casten! + if (selectedPetId > 0) { + Send(Out["GetPetInfo"], (int)selectedPetId); + Delay(300); // Kurz warten auf Antwort + } +} + +void WaitForPetFinish() { + Delay(2500); +} + +// ============ ZUM PET HINLAUFEN ============ +// WICHTIG: Bei mehreren Pets mit gleichem Namen reagiert das NΓ„CHSTE Pet! +// Diese Funktion lΓ€uft zum Pet damit es das nΓ€chste ist. +void WalkToPet(long petId) { + foreach (var pet in Pets) { + if (pet.Id == petId) { + int petX = pet.Location.X; + int petY = pet.Location.Y; + int myX = Self.Location.X; + int myY = Self.Location.Y; + + int dist = Math.Abs(petX - myX) + Math.Abs(petY - myY); + + if (dist <= 1) { + Log($"[WALK] Bin schon neben {pet.Name} (Distanz: {dist})"); + return; + } + + // Finde Tile neben dem Pet + int targetX = petX; + int targetY = petY; + + if (myX < petX) targetX = petX - 1; + else if (myX > petX) targetX = petX + 1; + else if (myY < petY) targetY = petY - 1; + else if (myY > petY) targetY = petY + 1; + + Log($"[WALK] Laufe zu {pet.Name} auf ({targetX}, {targetY})..."); + Log($"[WALK] Pet: ({petX},{petY}), Ich: ({myX},{myY})"); + + try { + Send(Out["Walk"], targetX, targetY); + } catch { + try { + Send(Out["MoveAvatar"], targetX, targetY); + } catch { + Log("[WALK] Walk-Packet fehlgeschlagen!"); + return; + } + } + + // Warte bis angekommen (max 4 Sek) + for (int i = 0; i < 40 && Run; i++) { + Delay(100); + int newDist = Math.Abs(petX - Self.Location.X) + Math.Abs(petY - Self.Location.Y); + if (newDist <= 1) { + Log($"[WALK] Angekommen neben {pet.Name}!"); + return; + } + } + Log("[WALK] Timeout - hoffentlich nah genug!"); + return; + } + } + Log($"[WALK] Pet mit ID {petId} nicht gefunden!"); +} + +// EINFACHE, ZUVERLΓ„SSIGE Wartezeit nach eat/drink +// Das Pet braucht ca. 6-8 Sekunden um zum Essen zu laufen und zu essen +void WaitForPetToEat() { + Log("[WAIT] Warte 8 Sekunden bis Pet fertig..."); + + // Feste Wartezeit - das ist ZUVERLΓ„SSIGER als komplizierte Logik! + for (int i = 0; i < 8 && Run; i++) { + Delay(1000); + } + + Log("[WAIT] Pet sollte jetzt fertig sein!"); +} + +// Tile scanning - KOMPLETT NEU! +// Sammle ALLE belegten Tiles (Items + Users) +void ScanOccupiedTiles() { + occupiedTiles.Clear(); + foreach (var item in FloorItems) { + var pos = (item.Location.X, item.Location.Y); + if (!occupiedTiles.Contains(pos)) occupiedTiles.Add(pos); + } + foreach (var user in Users) { + var pos = (user.Location.X, user.Location.Y); + if (!occupiedTiles.Contains(pos)) occupiedTiles.Add(pos); + } + Log($"[TILES] {occupiedTiles.Count} belegte Tiles gescannt"); +} + +// NEUE METHODE: Finde ALLE bekannten Tiles im Raum +// Basierend auf FloorItems, Users, und der Heightmap +List<(int x, int y)> GetAllKnownTiles() { + var knownTiles = new HashSet<(int x, int y)>(); + + // 1. Alle FloorItem-Positionen sind gueltige Tiles + foreach (var item in FloorItems) { + knownTiles.Add((item.Location.X, item.Location.Y)); + } + + // 2. Alle User-Positionen sind gueltige Tiles + foreach (var user in Users) { + knownTiles.Add((user.Location.X, user.Location.Y)); + } + + // 3. Self Position + knownTiles.Add((Self.Location.X, Self.Location.Y)); + + Log($"[TILES] {knownTiles.Count} bekannte Tiles aus FloorItems/Users"); + + // 4. Erweitere um Nachbar-Tiles (1 Tile Radius um bekannte Tiles) + // Diese sind sehr wahrscheinlich auch gueltig! + var expandedTiles = new HashSet<(int x, int y)>(knownTiles); + foreach (var tile in knownTiles) { + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + int nx = tile.x + dx; + int ny = tile.y + dy; + if (nx >= 0 && ny >= 0) { + expandedTiles.Add((nx, ny)); + } + } + } + } + + Log($"[TILES] {expandedTiles.Count} Tiles nach Expansion (1 Tile Radius)"); + + // 5. Filter mit Heightmap wenn vorhanden + if (!string.IsNullOrEmpty(roomHeightMap)) { + var validTiles = new List<(int x, int y)>(); + var rows = roomHeightMap.Split('\r'); + + foreach (var tile in expandedTiles) { + if (tile.y < rows.Length && tile.x < rows[tile.y].Length) { + char c = rows[tile.y][tile.x]; + // Gueltig: 0-9 (Hoehen), a-z (hohe Hoehen), NICHT 'x' (blockiert) + if (c != 'x' && c != 'X') { + validTiles.Add(tile); + } + } + } + + Log($"[TILES] {validTiles.Count} gueltige Tiles nach Heightmap-Filter"); + return validTiles; + } + + return expandedTiles.ToList(); +} + +// Finde freie Tiles (bekannte Tiles minus belegte) +List<(int x, int y)> FindAllFreeTiles() { + ScanOccupiedTiles(); // Update occupiedTiles + + var allTiles = GetAllKnownTiles(); + var freeTiles = new List<(int x, int y)>(); + + foreach (var tile in allTiles) { + if (!occupiedTiles.Contains(tile)) { + freeTiles.Add(tile); + } + } + + Log($"[TILES] {freeTiles.Count} freie Tiles gefunden (von {allTiles.Count} bekannten)"); + return freeTiles; +} + +// ============ STACK MAGIC TILE FUNKTIONEN ============ + +// Stack Magic Tile Identifier (BC Item) +string[] stackMagicIdentifiers = { "bc_tile", "stack_magic", "tile_stackmagic", "stackmagic" }; +string[] stackMagicNames = { "Stack Magic Tile", "BC Tile", "Magic Tile", "Builders Club Tile" }; + +// Finde Stack Magic Tile im Inventar +(long id, string name, bool found) FindStackMagicInInventory() { + EnsureInventory(); + + foreach (var item in Inventory) { + string name = item.GetName(); + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { } + + string lowerName = name.ToLower(); + string lowerIdent = identifier.ToLower(); + + // Check identifier + foreach (var id in stackMagicIdentifiers) { + if (lowerIdent.Contains(id)) { + Log($"[STACK] Gefunden im Inventar: '{name}' [ID: {identifier}]"); + return (item.Id, name, true); + } + } + + // Check name + foreach (var stackName in stackMagicNames) { + if (lowerName.Contains(stackName.ToLower())) { + Log($"[STACK] Gefunden im Inventar: '{name}'"); + return (item.Id, name, true); + } + } + } + + return (0, "", false); +} + +// Finde Stack Magic Tile im Raum (um es spaeter aufzuheben) +(long id, int x, int y, bool found) FindStackMagicInRoom() { + foreach (var item in FloorItems) { + string name = item.GetName(); + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { } + + string lowerName = name.ToLower(); + string lowerIdent = identifier.ToLower(); + + // Check identifier + foreach (var id in stackMagicIdentifiers) { + if (lowerIdent.Contains(id)) { + return (item.Id, item.Location.X, item.Location.Y, true); + } + } + + // Check name + foreach (var stackName in stackMagicNames) { + if (lowerName.Contains(stackName.ToLower())) { + return (item.Id, item.Location.X, item.Location.Y, true); + } + } + } + + return (0, 0, 0, false); +} + +// Platziere Stack Magic Tile aus Inventar +bool PlaceStackMagicTile(int x, int y) { + var stackMagic = FindStackMagicInInventory(); + + if (!stackMagic.found) { + Log("[STACK] Kein Stack Magic Tile im Inventar!"); + BotMessage("Kein Stack Magic Tile im Inventar!"); + // TODO: Aus BC Katalog kaufen + return false; + } + + Log($"[STACK] Platziere '{stackMagic.name}' auf ({x}, {y})..."); + Send(Out["PlaceObject"], $"-{stackMagic.id} {x} {y} 0"); + Delay(800); + + // Verifiziere + var placed = FindStackMagicInRoom(); + if (placed.found && placed.x == x && placed.y == y) { + Log($"[STACK] Stack Magic Tile erfolgreich platziert auf ({x}, {y})!"); + return true; + } + + // Vielleicht woanders platziert? + if (placed.found) { + Log($"[STACK] Stack Magic Tile platziert auf ({placed.x}, {placed.y}) (nicht exakt)"); + return true; + } + + Log("[STACK] FEHLER: Stack Magic Tile konnte nicht platziert werden!"); + return false; +} + +// Nimm Stack Magic Tile wieder auf (Pickup) +bool PickupStackMagicTile() { + var stackMagic = FindStackMagicInRoom(); + + if (!stackMagic.found) { + Log("[STACK] Kein Stack Magic Tile im Raum zum Aufnehmen!"); + return false; + } + + Log($"[STACK] Nehme Stack Magic Tile auf (ID: {stackMagic.id})..."); + Send(Out["PickupObject"], 2, (int)stackMagic.id); // 2 = Floor item + Delay(500); + + // Verifiziere + var stillThere = FindStackMagicInRoom(); + if (!stillThere.found) { + Log("[STACK] Stack Magic Tile erfolgreich aufgenommen!"); + return true; + } + + Log("[STACK] WARNUNG: Stack Magic Tile ist noch im Raum!"); + return false; +} + +// Food detection - Sucht nach bekannten Pet Food Namen UND Class Names! +// WICHTIG: Katalog zeigt class names wie "petfood5_dt", "A0 petfood1_dt" +bool IsPetFood(string itemName) { + if (string.IsNullOrEmpty(itemName)) return false; + + // Normalisiere den Namen: lowercase, trim, remove extra spaces + string lower = itemName.ToLower().Trim(); + + // AUSSCHLUSS ZUERST: Moebel und Kleidung die KEIN Pet Food sind! + if (lower.Contains("street food") || lower.Contains("streetfood") || + lower.Contains("bite") || // "Carrot Bite" ist Kleidung! + lower.Contains("costume") || + lower.Contains("outfit") || + lower.Contains("clothing") || + lower.Contains("cabinet")) { // "Cabinet" matched sonst wegen "cab" + return false; + } + + // CHECK 1: Class name patterns aus dem Katalog! + // Diese sind ZUVERLAESSIG weil sie direkt vom Server kommen + if (lower.Contains("petfood") || lower.Contains("pet_food") || + lower.Contains("petgoodie") || lower.Contains("pet_goodie")) { + return true; + } + + // CHECK 2: EXAKTER Match fuer bekannte Food-Namen + // KOMPLETTE LISTE aller 33 Pet Food Items! + foreach (var foodName in petFoodExactNames) { + if (lower == foodName.ToLower()) { + return true; + } + } + + // CHECK 3: Class names (partial match) - fuer Identifier + foreach (var className in petFoodClassNames) { + if (lower.Contains(className)) { + return true; + } + } + + return false; +} + +// Prueft ob ein Identifier Pet Food ist +bool IsPetFoodByIdentifier(string identifier) { + if (string.IsNullOrEmpty(identifier)) return false; + string lower = identifier.ToLower(); + return lower.Contains("petfood") || lower.Contains("pet_food") || + lower.Contains("petgoodie") || lower.Contains("pet_goodie"); +} + +// Hilfsfunktion: Prueft Identifier und Name fuer Inventory Items +bool CheckInventoryItemIsFood(string identifier, string name) { + if (!string.IsNullOrEmpty(identifier)) { + if (IsPetFoodByIdentifier(identifier)) { + return true; + } + } + return IsPetFood(name); +} + +// Water detection - uses water arrays from catalog +bool IsPetWater(string itemName) { + string lower = itemName.ToLower(); + + // Check display names (partial match) + foreach (var waterName in petWaterDisplayNames) { + if (lower.Contains(waterName.ToLower())) { + return true; + } + } + + // Check class names + foreach (var className in petWaterClassNames) { + if (lower.Contains(className)) { + return true; + } + } + + return false; +} + +// Find water bowl using the IsPetWater function +(long id, bool empty, bool found) FindWaterBowl() { + foreach (var item in FloorItems) { + if (IsPetWater(item.GetName())) { + return (item.Id, item.State == 0, true); + } + } + return (0, false, false); +} + +// Find ALL water bowls in the room +List<(long id, string name, bool empty, int state)> FindAllWaterBowls() { + var bowls = new List<(long id, string name, bool empty, int state)>(); + foreach (var item in FloorItems) { + if (IsPetWater(item.GetName())) { + // DEBUG: Zeige den State-Wert + Log($"[WATER-DEBUG] '{item.GetName()}' ID:{item.Id} State:{item.State}"); + bowls.Add((item.Id, item.GetName(), item.State == 0, item.State)); + } + } + return bowls; +} + +// Refill a single water bowl by ID +// WICHTIG: Habbo muss zum Napf LAUFEN - das dauert! +void RefillWaterBowlById(long id, string name) { + Log($"[WATER] Fuelle '{name}' nach (ID: {id})..."); + Log("[WATER] Warte bis Habbo zum Napf gelaufen ist..."); + Send(Out["UseFurniture"], (int)id, 0); + + // LANGE WARTEN! Habbo muss hinlaufen + Animation abspielen + // Bei weit entfernten NΓ€pfen kann das 5-8 Sekunden dauern! + for (int i = 0; i < 6 && Run; i++) { + Delay(1000); // 6 Sekunden total pro Napf + } + Log($"[WATER] '{name}' sollte jetzt voll sein!"); +} + +// Refill ALL water bowls in the room +// WICHTIG: FΓΌllt ALLE NΓ€pfe auf weil State-Erkennung unzuverlΓ€ssig ist! +int RefillAllEmptyWaterBowls() { + var bowls = FindAllWaterBowls(); + int refilled = 0; + + // DEBUG: Zeige alle gefundenen NΓ€pfe + Log($"[WATER] === WASSERNAPF-SCAN ==="); + foreach (var bowl in bowls) { + Log($"[WATER] Napf: '{bowl.name}' | State: {bowl.state}"); + } + Log($"[WATER] Total: {bowls.Count} NΓ€pfe - ALLE werden aufgefΓΌllt!"); + Log($"[WATER] ======================="); + + if (bowls.Count == 0) { + Log("[WATER] Keine Wassernaepfe gefunden!"); + return 0; + } + + BotMessage($"Fuelle ALLE {bowls.Count} Wassernapf/e auf..."); + + // ALLE NΓ€pfe auffΓΌllen, nicht nur die mit State 0! + foreach (var bowl in bowls) { + if (!Run) break; + + refilled++; + Log($"[WATER] === Napf {refilled}/{bowls.Count}: {bowl.name} ==="); + + // Sende Befehl zum AuffΓΌllen + Send(Out["UseFurniture"], (int)bowl.id, 0); + + // WARTE bis Habbo hingelaufen ist und aufgefΓΌllt hat! + Log("[WATER] Warte 4 Sekunden..."); + for (int i = 0; i < 4 && Run; i++) { + Delay(1000); + } + + Log($"[WATER] Napf {refilled}/{bowls.Count} erledigt!"); + + // Kurze Extra-Pause zwischen den NΓ€pfen + if (refilled < bowls.Count) { + Delay(1000); + } + } + + if (refilled > 0) { + Log($"[WATER] === ALLE {refilled} NAEPFE AUFGEFUELLT! ==="); + BotMessage($"Fertig! {refilled} Wassernapf/e aufgefuellt!"); + + // Habbo soll weggehen vom Wassernapf - finde freies Tile + var freeTiles = FindAllFreeTiles(); + Log($"[WATER] Freie Tiles gefunden: {freeTiles.Count}"); + + if (freeTiles.Count > 0) { + // Nimm ein zufΓ€lliges freies Tile + var random = new Random(); + var targetTile = freeTiles[random.Next(freeTiles.Count)]; + + Log($"[WATER] Gehe weg zu freiem Tile ({targetTile.x}, {targetTile.y})..."); + + // xabbo: Walk Packet senden + try { + Send(Out["Walk"], targetTile.x, targetTile.y); + } catch { + // Fallback: MoveAvatar probieren + try { + Send(Out["MoveAvatar"], targetTile.x, targetTile.y); + } catch { + Log("[WATER] WARNUNG: Konnte nicht weggehen!"); + } + } + + Delay(2000); // Warte bis Habbo angekommen ist + } else { + Log("[WATER] Keine freien Tiles gefunden!"); + } + + Log("[WATER] Fertig - weiter geht's!"); + } + + return refilled; +} + +// Check if any water bowl is empty +bool HasEmptyWaterBowl() { + var bowls = FindAllWaterBowls(); + foreach (var bowl in bowls) { + if (bowl.empty) return true; + } + return false; +} + +void RefillWaterBowl() { + // Legacy function - now uses RefillAllEmptyWaterBowls + RefillAllEmptyWaterBowls(); +} + +// List all pet food in room +void ListFoodInRoom() { + Log(""); + Log("=== PET FOOD IM RAUM ==="); + int count = 0; + foreach (var item in FloorItems) { + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { } + if (CheckFloorItemIsFood(identifier, item.GetName())) { + count++; + Log($" [{count}] {item.GetName()} [ID: {identifier}] (Position: {item.Location.X},{item.Location.Y})"); + } + } + if (count == 0) { + Log(" Kein Pet Food im Raum gefunden!"); + } else { + Log($" TOTAL: {count} Pet Food Items"); + } + Log("========================"); + Log(""); +} + +// List all pet food in inventory +void ListFoodInInventory() { + EnsureInventory(); + Log(""); + Log("=== PET FOOD IM INVENTAR ==="); + int count = 0; + foreach (var item in Inventory) { + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { } + if (CheckInventoryItemIsFood(identifier, item.GetName())) { + count++; + Log($" [{count}] {item.GetName()} [ID: {identifier}]"); + } + } + if (count == 0) { + Log(" Kein Pet Food im Inventar gefunden!"); + } else { + Log($" TOTAL: {count} Pet Food Items"); + } + Log("============================"); + Log(""); +} + +// DEBUG: Liste ALLE Inventar-Items (erste 50) +void DebugListAllInventoryItems() { + EnsureInventory(); + Delay(1500); // Extra warten! + + Log(""); + Log("###############################################"); + Log("### DEBUG: ALLE INVENTAR-ITEMS (erste 50) ###"); + Log("###############################################"); + + int count = 0; + int foodCount = 0; + foreach (var item in Inventory) { + count++; + string name = item.GetName(); + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { identifier = "?"; } + + bool isFood = CheckInventoryItemIsFood(identifier, name); + + if (isFood) { + foodCount++; + Log($" [{count}] >>> {name} <<< [ID: {identifier}] [FOOD!]"); + } else if (count <= 50) { + Log($" [{count}] {name} [ID: {identifier}]"); + } + + if (count == 50 && !isFood) { + Log(" ... (mehr Items vorhanden, zeige nur erste 50)"); + } + } + + Log(""); + Log($" TOTAL: {count} Items im Inventar"); + Log($" DAVON FOOD: {foodCount} Items"); + Log("###############################################"); + Log(""); +} + +// List all pet water in room +void ListWaterInRoom() { + Log(""); + Log("=== PET WATER IM RAUM ==="); + int count = 0; + foreach (var item in FloorItems) { + if (IsPetWater(item.GetName())) { + count++; + string state = item.State == 0 ? " [LEER!]" : " [voll]"; + Log($" [{count}] {item.GetName()} (Position: {item.Location.X},{item.Location.Y}){state}"); + } + } + if (count == 0) { + Log(" Kein Water Bowl im Raum gefunden!"); + } else { + Log($" TOTAL: {count} Water Items"); + } + Log("========================="); + Log(""); +} + +// List all pet water in inventory +void ListWaterInInventory() { + EnsureInventory(); + Log(""); + Log("=== PET WATER IM INVENTAR ==="); + int count = 0; + foreach (var item in Inventory) { + if (IsPetWater(item.GetName())) { + count++; + Log($" [{count}] {item.GetName()}"); + } + } + if (count == 0) { + Log(" Kein Water Bowl im Inventar gefunden!"); + } else { + Log($" TOTAL: {count} Water Items"); + } + Log("============================="); + Log(""); +} + +// List all pet food (room + inventory) +void ListAllFood() { + ListFoodInRoom(); + ListFoodInInventory(); +} + +// List all pet water (room + inventory) +void ListAllWater() { + ListWaterInRoom(); + ListWaterInInventory(); +} + +// List EVERYTHING (food + water, room + inventory) +void ListAllPetItems() { + Log(""); + Log("###############################################"); + Log("### PET ITEMS UEBERSICHT ###"); + Log("###############################################"); + ListFoodInRoom(); + ListWaterInRoom(); + ListFoodInInventory(); + ListWaterInInventory(); + Log("###############################################"); + Log(""); +} + +// Prueft ob ein FloorItem Pet Food ist - mit Identifier! +bool CheckFloorItemIsFood(string identifier, string name) { + // Pruefe zuerst Identifier + if (!string.IsNullOrEmpty(identifier)) { + if (IsPetFoodByIdentifier(identifier)) { + return true; + } + } + // Fallback: Name pruefen + return IsPetFood(name); +} + +bool HasFoodInRoom() { + foreach (var item in FloorItems) { + string identifier = ""; + string name = item.GetName(); + try { identifier = item.GetInfo().Identifier; } catch { } + + if (CheckFloorItemIsFood(identifier, name)) { + // DEBUG: Zeige gefundenes Essen + Log($"[FOOD-CHECK] Gefunden: '{name}' [ID: {identifier}]"); + return true; + } + } + return false; +} + +// Zaehle Essen im Raum (fuer Debug) +int CountFoodInRoom() { + int count = 0; + foreach (var item in FloorItems) { + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { } + if (CheckFloorItemIsFood(identifier, item.GetName())) { + count++; + } + } + return count; +} + +// Zaehle Essen im Inventar (fuer Debug) +int CountFoodInInventory() { + EnsureInventory(); + int count = 0; + foreach (var item in Inventory) { + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { } + if (CheckInventoryItemIsFood(identifier, item.GetName())) { + count++; + } + } + return count; +} + +bool PlaceFoodFromInventory() { + Log(""); + Log("###############################################"); + Log("### STACK MAGIC TILE - ALLE ESSEN PLATZIEREN ###"); + Log("###############################################"); + + // Erst Inventar laden + Log("[FOOD] Lade Inventar..."); + EnsureInventory(); + Delay(2000); + + // 1. Sammle ALLE Food-Items aus dem Inventar + var foodItems = new List<(long id, string name)>(); + + foreach (var item in Inventory) { + string itemName = item.GetName(); + string lowerName = itemName.ToLower(); + + // AUSSCHLUSS: Kleidungsstuecke und Stack Magic Tile! + if (lowerName.Contains("bite") || + lowerName.Contains("costume") || + lowerName.Contains("outfit") || + lowerName.Contains("clothing") || + lowerName.Contains("cabinet") || + lowerName.Contains("stack magic") || + lowerName.Contains("bc tile")) { + continue; + } + + // Pruefe ob es Food ist + string identifier = ""; + try { identifier = item.GetInfo().Identifier; } catch { } + string lowerIdent = identifier.ToLower(); + + // Skip Stack Magic Tile by identifier + bool isStackMagic = false; + foreach (var stackId in stackMagicIdentifiers) { + if (lowerIdent.Contains(stackId)) { + isStackMagic = true; + break; + } + } + if (isStackMagic) continue; + + bool isFood = false; + if (!string.IsNullOrEmpty(identifier)) { + if (lowerIdent.Contains("petfood") || lowerIdent.Contains("petgoodie")) { + isFood = true; + } + } + + if (!isFood) { + foreach (var foodName in petFoodExactNames) { + if (lowerName == foodName.ToLower()) { + isFood = true; + break; + } + } + } + + // V74: Filter nach Pet-Typ! + // Überspringe Essen das nicht fΓΌr dieses Pet geeignet ist + if (isFood && petType >= 0) { + if (!IsFoodForPetType(itemName, petType)) { + Log($"[FOOD] SKIP: '{itemName}' nicht fΓΌr Pet-Typ {petType} geeignet!"); + isFood = false; + } + } + + if (isFood) { + foodItems.Add((item.Id, itemName)); + } + } + + Log($"[FOOD] {foodItems.Count} Food-Items im Inventar gefunden (gefiltert fΓΌr Pet-Typ {petType})!"); + + if (foodItems.Count == 0) { + Log("[FOOD] KEIN Pet Food im Inventar!"); + BotMessage("KEIN ESSEN IM INVENTAR! Bitte im Katalog kaufen!"); + return false; + } + + // 2. Finde ein freies, zugaengliches Tile + var freeTiles = FindAllFreeTiles(); + + if (freeTiles.Count == 0) { + Log("[FOOD] FEHLER: Keine freien Tiles im Raum!"); + BotMessage("Keine freien Tiles im Raum!"); + return false; + } + + // Waehle ein zufaelliges freies Tile (nah am Spieler bevorzugt) + int playerX = Self.Location.X; + int playerY = Self.Location.Y; + + // Sortiere nach Distanz zum Spieler + freeTiles = freeTiles.OrderBy(t => Math.Abs(t.x - playerX) + Math.Abs(t.y - playerY)).ToList(); + + // Nimm eins der naechsten 5 Tiles + var random = new Random(); + int tileChoice = Math.Min(random.Next(5), freeTiles.Count - 1); + var targetTile = freeTiles[tileChoice]; + + Log($"[FOOD] Ziel-Tile: ({targetTile.x}, {targetTile.y}) - nah am Spieler"); + + // 3. Pruefe ob Stack Magic Tile bereits im Raum ist + var existingStackMagic = FindStackMagicInRoom(); + bool needToPlaceStackMagic = !existingStackMagic.found; + bool needToPickupStackMagic = false; + + if (existingStackMagic.found) { + Log($"[STACK] Stack Magic Tile bereits im Raum auf ({existingStackMagic.x}, {existingStackMagic.y})"); + targetTile = (existingStackMagic.x, existingStackMagic.y); + } else { + // 4. Platziere Stack Magic Tile + Log("[STACK] Platziere Stack Magic Tile..."); + + var stackMagic = FindStackMagicInInventory(); + if (!stackMagic.found) { + Log("[STACK] KEIN Stack Magic Tile im Inventar!"); + BotMessage("Kein Stack Magic Tile! Bitte aus BC Katalog holen!"); + // Fallback: Normale Platzierung ohne Stack Magic + Log("[FOOD] Fallback: Platziere ohne Stack Magic Tile..."); + return PlaceFoodWithoutStackMagic(foodItems); + } + + // Platziere Stack Magic Tile + Log($"[STACK] Platziere '{stackMagic.name}' auf ({targetTile.x}, {targetTile.y})..."); + Send(Out["PlaceObject"], $"-{stackMagic.id} {targetTile.x} {targetTile.y} 0"); + Delay(800); + + // Verifiziere + var placedStack = FindStackMagicInRoom(); + if (!placedStack.found) { + Log("[STACK] FEHLER: Stack Magic Tile konnte nicht platziert werden!"); + return PlaceFoodWithoutStackMagic(foodItems); + } + + // Update target tile to where the stack magic actually was placed + targetTile = (placedStack.x, placedStack.y); + needToPickupStackMagic = true; + Log($"[STACK] Stack Magic Tile platziert auf ({targetTile.x}, {targetTile.y})!"); + } + + // 5. Platziere ALLE Food-Items auf das gleiche Tile! + Log(""); + Log($"[FOOD] === PLATZIERE {foodItems.Count} ESSEN AUF ({targetTile.x}, {targetTile.y}) ==="); + + int placed = 0; + foreach (var foodItem in foodItems) { + if (!Run) break; + + Log($"[FOOD] {placed + 1}/{foodItems.Count}: '{foodItem.name}'"); + Send(Out["PlaceObject"], $"-{foodItem.id} {targetTile.x} {targetTile.y} 0"); + Delay(300); // Schnell da Stack Magic Tile alles erlaubt! + placed++; + } + + Log($"[FOOD] === {placed} ESSEN PLATZIERT! ==="); + BotMessage($"{placed}x Essen auf einem Tile gestapelt!"); + + // 6. Stack Magic Tile wieder aufnehmen + if (needToPickupStackMagic) { + Log("[STACK] Nehme Stack Magic Tile wieder auf..."); + Delay(500); + + var stackToPickup = FindStackMagicInRoom(); + if (stackToPickup.found) { + Send(Out["PickupObject"], 2, (int)stackToPickup.id); + Delay(500); + + var stillThere = FindStackMagicInRoom(); + if (!stillThere.found) { + Log("[STACK] Stack Magic Tile aufgenommen!"); + } else { + Log("[STACK] WARNUNG: Stack Magic Tile konnte nicht aufgenommen werden!"); + } + } + } + + Log("###############################################"); + Log(""); + + return placed > 0; +} + +// Fallback-Funktion ohne Stack Magic Tile (alte Methode) +bool PlaceFoodWithoutStackMagic(List<(long id, string name)> foodItems) { + Log("[FOOD] Fallback: Platziere auf verschiedenen Tiles..."); + + int targetAmount = Math.Min(20, foodItems.Count); + var freeTiles = FindAllFreeTiles(); + var random = new Random(); + + int placed = 0; + int tileIndex = 0; + + freeTiles = freeTiles.OrderBy(x => random.Next()).ToList(); + + foreach (var foodItem in foodItems) { + if (placed >= targetAmount) break; + if (tileIndex >= freeTiles.Count) break; + + var tile = freeTiles[tileIndex]; + tileIndex++; + + Send(Out["PlaceObject"], $"-{foodItem.id} {tile.x} {tile.y} 0"); + Delay(400); + placed++; + } + + if (placed > 0) { + BotMessage($"{placed}x Essen platziert (verteilt)!"); + return true; + } + return false; +} + +// Quick scan - pruefen ob Essen/Wasser vorhanden und fixen +void QuickScan() { + // DEAKTIVIERT: Water refill + // if (HasEmptyWaterBowl()) { + // Log("[SCAN] Leerer Wassernapf gefunden!"); + // RefillAllEmptyWaterBowls(); + // } + + // Check food - WICHTIG! + bool hasFood = HasFoodInRoom(); + if (!hasFood) { + Log("[SCAN] KEIN ESSEN IM RAUM! Platziere aus Inventar..."); + BotMessage("Kein Essen im Raum - platziere..."); + bool placed = PlaceFoodFromInventory(); + if (!placed) { + Log("[SCAN] WARNUNG: Konnte kein Essen platzieren!"); + } + } +} + +// Full room scan at startup - fix everything +void StartupRoomScan() { + Log(""); + Log("###############################################"); + Log("### RAUM-SCAN BEIM START ###"); + Log("###############################################"); + + // WICHTIG: Kurz warten damit FloorItems geladen sind! + Log("[STARTUP] Warte auf Raum-Daten..."); + Delay(1500); + + // DEAKTIVIERT: Water bowl check + // Log("[STARTUP] Pruefe Wassernaepfe..."); + // var bowls = FindAllWaterBowls(); + // ... + + // 2. Check food + Log("[STARTUP] Pruefe Essen..."); + int foodInRoom = CountFoodInRoom(); + int foodInInventory = CountFoodInInventory(); + + Log($"[STARTUP] Essen im Raum: {foodInRoom}"); + Log($"[STARTUP] Essen im Inventar: {foodInInventory}"); + + if (foodInRoom == 0) { + Log("[STARTUP] KEIN ESSEN IM RAUM - PLATZIERE JETZT!"); + BotMessage("Platziere Essen..."); + + if (foodInInventory > 0) { + bool success = PlaceFoodFromInventory(); + if (success) { + Log("[STARTUP] Essen erfolgreich platziert!"); + BotMessage("Essen platziert!"); + } else { + Log("[STARTUP] FEHLER: Konnte Essen nicht platzieren!"); + BotMessage("FEHLER: Konnte Essen nicht platzieren!"); + } + } else { + Log("[STARTUP] AUCH KEIN ESSEN IM INVENTAR!"); + BotMessage("KEIN ESSEN! Bitte im Katalog kaufen!"); + } + + Delay(1000); + } else { + Log($"[STARTUP] OK - {foodInRoom} Essen im Raum vorhanden"); + } + + Log("###############################################"); + Log("[STARTUP] Raum-Scan abgeschlossen!"); + Log("###############################################"); + Log(""); + + // Kurze Pause bevor Training beginnt + Delay(2000); +} + +// Care pet - used during training +void CarePet() { + GetPetInfo(); + Delay(300); + + // DEAKTIVIERT: Water refill + // if (HasEmptyWaterBowl()) { + // BotMessage("Leere Wassernaepfe gefunden!"); + // RefillAllEmptyWaterBowls(); + // } + + // Check food + if (!HasFoodInRoom()) { + BotMessage("Kein Essen im Raum!"); + PlaceFoodFromInventory(); + } +} + +// Care mode - feed and water pet until energy is restored +void EnterCareMode() { + if (inCareMode) return; + + inCareMode = true; + careModeCmdCount = 0; // Reset counter + + int energyPercent = petMaxEnergy > 0 ? (petEnergy * 100 / petMaxEnergy) : 0; + +Log(""); + Log("!!! CARE-MODUS AKTIVIERT !!!"); + Log($"!!! Energie: {petEnergy}/{petMaxEnergy} ({energyPercent}%) - Ziel: {careModeExitPercent}% !!!"); + BotMessage($"CARE-MODUS! Energie: {energyPercent}%"); + + // DEAKTIVIERT: Water refill + // Log("[CARE] Pruefe und fuelle ALLE Wassernaepfe..."); + // RefillAllEmptyWaterBowls(); + + // Dann Essen checken + QuickScan(); +} + +// ============ LEVEL-SCAN: Finde Pet mit niedrigstem Level ============ +// Scannt alle Pets mit dem gleichen Namen und wechselt zum niedrigsten Level! +long ScanAndSwitchToLowestLevel() { + if (string.IsNullOrEmpty(targetPetName)) { + Log("[LEVEL-SCAN] Kein Target-Name gesetzt!"); + return -1; + } + + Log(""); + Log("###############################################"); + Log($"### LEVEL-SCAN: Suche alle '{targetPetName}' ###"); + Log("###############################################"); + + // Sammle alle Pet IDs mit dem gleichen Namen + List petIds = new List(); + + foreach (var pet in Pets) { + if (pet.Name == targetPetName) { + petIds.Add(pet.Id); + Log($"[LEVEL-SCAN] Gefunden: {pet.Name} (ID: {pet.Id})"); + } + } + + if (petIds.Count == 0) { + Log($"[LEVEL-SCAN] Keine Pets mit Namen '{targetPetName}' gefunden!"); + return -1L; + } + + if (petIds.Count == 1) { + Log($"[LEVEL-SCAN] Nur 1 Pet gefunden - kein Wechsel nΓΆtig"); + return selectedPetId; + } + + Log($"[LEVEL-SCAN] {petIds.Count} Pets gefunden - scanne Level..."); + Log(""); + + // Scanne jedes Pet und speichere Level + List levels = new List(); + + for (int i = 0; i < petIds.Count; i++) { + long petId = petIds[i]; + + // Request PetInfo + scanGotResponse = false; + scanTempLevel = -1; + + Send(Out["GetPetInfo"], (int)petId); + + // Warte auf Antwort - LΓ„NGER WARTEN! (max 2 Sekunden) + int waitCount = 0; + while (!scanGotResponse && waitCount < 20 && Run) { + Delay(100); + waitCount++; + } + + if (scanGotResponse && scanTempLevel > 0) { + levels.Add(scanTempLevel); + Log($"[LEVEL-SCAN] Pet {i+1}/{petIds.Count}: ID {petId} = Level {scanTempLevel}"); + } else { + levels.Add(99); + Log($"[LEVEL-SCAN] Pet {i+1}/{petIds.Count}: ID {petId} = Level UNBEKANNT (timeout)"); + } + + Delay(300); // LΓ€ngere Pause zwischen Anfragen + } + + // ZEIGE ALLE LEVELS + Log(""); + Log("[LEVEL-SCAN] === ALLE LEVELS ==="); + for (int i = 0; i < petIds.Count; i++) { + string marker = ""; + if (petIds[i] == selectedPetId) marker = " <-- AKTUELL"; + Log($" Pet ID {petIds[i]}: Level {levels[i]}{marker}"); + } + + // Finde das Pet mit dem niedrigsten Level + int lowestLevel = 99; + long lowestLevelPetId = -1; + + for (int i = 0; i < petIds.Count; i++) { + if (levels[i] < lowestLevel) { + lowestLevel = levels[i]; + lowestLevelPetId = petIds[i]; + } + } + + Log(""); + Log($"[LEVEL-SCAN] === ERGEBNIS ==="); + Log($"[LEVEL-SCAN] Niedrigstes Level: {lowestLevel} (Pet ID: {lowestLevelPetId})"); + Log($"[LEVEL-SCAN] Aktuelles Pet: ID {selectedPetId}, Level {petLevel}"); + + // Wechsle nur wenn ein anderes Pet niedriger ist! + if (lowestLevelPetId != selectedPetId && lowestLevelPetId > 0) { + Log($"[LEVEL-SCAN] >>> WECHSEL ZU PET MIT LEVEL {lowestLevel}! <<<"); + BotMessage($"Wechsel zu Level {lowestLevel} Pet!"); + + // Setze Wechsel-Flag + petSwitchRequested = true; + newPetId = lowestLevelPetId; // KEINE CAST! Beide sind long! + + return lowestLevelPetId; + } else { + Log($"[LEVEL-SCAN] Kein Wechsel nΓΆtig - aktuelles Pet ist bereits das niedrigste"); + } + + Log("###############################################"); + Log(""); + + return selectedPetId; +} + +// ============ MAIN ============ +Log("==========================================="); +Log("=== Auto Pet Training V82 ==="); +Log("=== SIMPEL: Ein Pet anklicken + trainieren ==="); +Log("==========================================="); +Log($"Care-Modus startet bei: {careModeThresholdPercent}% Energie"); +Log($"Care-Modus endet bei: {careModeExitPercent}% Energie"); +Log(""); +Log("Klicke auf ein Pet um zu starten!"); + +while (selectedPetId == -1 && Run) { + Delay(100); +} + +// Pet-Name kommt aus dem PetInfo Packet (wird automatisch empfangen beim Klicken) +// Warte kurz bis PetInfo angekommen ist +Log("[INIT] Warte auf PetInfo Packet..."); +Delay(500); + +// Der Name wurde bereits im PetInfo Handler gesetzt +if (string.IsNullOrEmpty(petName) && !string.IsNullOrEmpty(lastPetInfoName)) { + petName = lastPetInfoName; +} + +// Falls immer noch leer, warte nochmal +if (string.IsNullOrEmpty(petName)) { + Delay(500); + if (!string.IsNullOrEmpty(lastPetInfoName)) { + petName = lastPetInfoName; + } +} + +if (string.IsNullOrEmpty(petName)) { + BotMessage("FEHLER: Pet-Name nicht erkannt!"); + BotMessage("Bitte Script neu starten und Pet anklicken."); +} + +Log($">>> Pet ausgewaehlt: {petName} (ID: {selectedPetId}) <<<"); + +// WICHTIG: Setze targetPetName fΓΌr Level-Scan! +targetPetName = petName; +Log($"[INIT] Target-Name fΓΌr Level-Scan: '{targetPetName}'"); + +// Falls petLevel noch 0 ist, nochmal anfragen +if (petLevel == 0) { + Log("[INIT] Lade Pet-Informationen..."); + GetPetInfo(); + + // Warte bis petLevel > 0 (max 3 Sekunden) + int waitCount = 0; + while (petLevel == 0 && waitCount < 30 && Run) { + Delay(100); + waitCount++; + } +} + +if (petLevel == 0) { + Log("[INIT] WARNUNG: Pet Level konnte nicht geladen werden!"); + Log("[INIT] Setze Level auf 1 als Fallback..."); + petLevel = 1; // Fallback +} + +Log($"[INIT] Pet Level: {petLevel}"); +Log($"[INIT] Pet Energie: {petEnergy}/{petMaxEnergy} ({(petMaxEnergy > 0 ? petEnergy * 100 / petMaxEnergy : 0)}%)"); +Log($"[INIT] Pet Hunger: {petHunger}/100"); + +// Erster Level-Scan wird nach dem Start durchgefΓΌhrt +Log(""); +Log("[INIT] Level-Scan startet automatisch nach dem Start!"); +Log(""); + +// ============ QUICK INVENTORY CHECK ============ +Log(""); +Log("=== INVENTAR-CHECK ==="); + +EnsureInventory(); +Delay(2000); + +int totalInv = 0; +int foodInv = 0; + +foreach (var item in Inventory) { + totalInv++; + string name = item.GetName(); + string lowerName = name.ToLower(); + + // AUSSCHLUSS: Kleidungsstuecke! + if (lowerName.Contains("bite") || lowerName.Contains("costume") || lowerName.Contains("outfit")) { + continue; // Kein Essen! + } + + // Zaehle Food - NUR echtes Pet Food! + if (lowerName == "cabbage" || lowerName == "carrot" || lowerName == "hay" || + lowerName == "bone" || lowerName == "dog food" || lowerName == "cat food" || + lowerName == "apple" || lowerName == "piggy pudding" || lowerName == "t-bone") { + foodInv++; + if (foodInv <= 5) { // Zeige nur erste 5 Food Items + Log($"[INV] FOOD: '{name}'"); + } + } +} + +Log($"[INV] Total: {totalInv} Items, davon {foodInv} Food Items"); +Log("======================"); +Log(""); + +// Skip detailed pet items listing - script is working now! +// ListAllPetItems(); +// DebugListAllInventoryItems(); + +// ============ STARTUP ROOM SCAN - FIX EVERYTHING ============ +StartupRoomScan(); + +// ============ FORDERE PET COMMANDS AN ============ +Log(""); +Log("==========================================="); +Log("Fordere Pet-Commands vom Server an..."); +Log("==========================================="); + +// WICHTIG: Wir SENDEN GetPetCommands um die verfuegbaren Befehle zu erhalten! +// Das Packet wird NICHT automatisch gesendet - wir muessen es anfordern! +Log($"[CMD] Sende GetPetCommands fuer Pet ID: {selectedPetId}"); +Send(Out["GetPetCommands"], (int)selectedPetId); + +// Warte bis PetCommands empfangen wurde (max 3 Sekunden) +int cmdWaitCount = 0; +while (!petCommandsReceived && cmdWaitCount < 30 && Run) { + Delay(100); + cmdWaitCount++; +} + +if (!petCommandsReceived) { + Log(""); + Log("[WARNUNG] ============================================="); + Log("[WARNUNG] PetCommands Packet nicht empfangen!"); + Log("[WARNUNG] Nutze Fallback mit Basis-Befehlen."); + Log("[WARNUNG] ============================================="); + Log(""); + // Fallback: Nutze sichere Basis-Befehle + unlockedCommands.Clear(); + unlockedCommands.AddRange(safeCommands); + // Fallback: Assume eat/drink sind verfuegbar + petCommandIds.Add(43); // eat + petCommandIds.Add(14); // drink +} else { + Log("[OK] PetCommands erfolgreich geladen!"); +} + +// Zeige geladene Commands +Log(""); +Log($"=== {unlockedCommands.Count} TRAINING-BEFEHLE GELADEN ==="); +foreach (var cmd in unlockedCommands) { + Log($" + {cmd}"); +} +Log("==========================================="); + +// Check ob eat/drink verfuegbar sind (fuer Care-Modus) +// WICHTIG: Ausserhalb des Loops damit Pet-Wechsel sie aktualisieren kann! +bool hasEat = petCommandIds.Contains(43); // 43 = eat +bool hasDrink = petCommandIds.Contains(14); // 14 = drink +Log($"[INFO] eat verfuegbar: {hasEat}"); +Log($"[INFO] drink verfuegbar: {hasDrink}"); + +// 8 Sekunden Wartezeit NUR fΓΌr eat/drink! +// Andere Befehle brauchen nur kurze Wartezeit + +// ============ START TRAINING IMMEDIATELY ============ +Log(""); +Log("Starte Training SOFORT!"); +BotMessage($"Training gestartet! Level {petLevel}, {unlockedCommands.Count} Befehle"); + +// DEAKTIVIERT: Level-Scan - trainiere nur das angeklickte Pet! +// Log(""); +// Log("=== INITIALER LEVEL-SCAN ==="); +// lastLevelScan = DateTime.Now; +// long initialScanResult = ScanAndSwitchToLowestLevel(); +// Log("=== LEVEL-SCAN ABGESCHLOSSEN ==="); +// Log(""); +// Log("=== LAUFE ZUM PET ==="); +// WalkToPet(selectedPetId); +// Log("=== BEREIT ZUM TRAINING ==="); +// Log(""); + +// cmdIndex ist jetzt GLOBAL definiert (oben) - damit PetCommands es resetten kann +int totalCmds = 0; +int totalXP = 0; +DateTime lastCareCheck = DateTime.MinValue; +DateTime lastRoomScan = DateTime.Now; // Fuer 1-Minuten Scan + +while (Run) { + // ============ PET WECHSEL CHECK ============ + // Wenn ein anderes Pet angeklickt wurde, wechsle zu diesem! + if (petSwitchRequested) { + Log(""); + Log("###############################################"); + Log("### PET WECHSEL! ###"); + Log("###############################################"); + + // Übernehme neue Pet ID + selectedPetId = newPetId; + petSwitchRequested = false; + newPetId = -1; + + // Reset States + petName = ""; // Wird aus PetInfo neu geladen + petType = -1; + inCareMode = false; + careModeCmdCount = 0; + cmdIndex = 0; + + // Warte auf PetInfo (wurde schon beim Klicken angefordert) + Log("[SWITCH] Warte auf Pet-Daten..."); + Delay(800); + + // Pet-Name aus lastPetInfoName ΓΌbernehmen + if (!string.IsNullOrEmpty(lastPetInfoName)) { + petName = lastPetInfoName; + } + + // Falls Name noch leer, nochmal PetInfo anfragen + if (string.IsNullOrEmpty(petName)) { + GetPetInfo(); + Delay(500); + if (!string.IsNullOrEmpty(lastPetInfoName)) { + petName = lastPetInfoName; + } + } + + Log($"[SWITCH] Neues Pet: {petName} (ID: {selectedPetId}, Typ: {petType})"); + + // Lade Commands fΓΌr neues Pet + Log("[SWITCH] Lade Commands..."); + Send(Out["GetPetCommands"], (int)selectedPetId); + + // Warte auf PetCommands + int switchWait = 0; + while (!petCommandsReceived && switchWait < 30 && Run) { + Delay(100); + switchWait++; + } + + if (petCommandsReceived) { + Log($"[SWITCH] {unlockedCommands.Count} Commands geladen!"); + } else { + Log("[SWITCH] WARNUNG: Commands nicht empfangen - nutze Fallback"); + unlockedCommands.Clear(); + unlockedCommands.AddRange(safeCommands); + } + + // Update hasEat/hasDrink + hasEat = petCommandIds.Contains(43); + hasDrink = petCommandIds.Contains(14); + + // WalkToPet entfernt - nicht nΓΆtig fΓΌr manuelles Switching + + BotMessage($"Wechsel zu {petName}! Level {petLevel}, {unlockedCommands.Count} Befehle"); + + Log("###############################################"); + Log(""); + + // Starte Training mit neuem Pet + continue; + } + + // WICHTIG: Pet Info bei JEDER Iteration aktualisieren! + GetPetInfo(); + + // Berechne Energie-Prozent fuer Anzeige + int currentEnergyPercent = petMaxEnergy > 0 ? (petEnergy * 100 / petMaxEnergy) : 0; + + // Status-Anzeige alle 10 Sekunden + if ((DateTime.Now - lastCareCheck).TotalSeconds > 10) { + lastCareCheck = DateTime.Now; + Log($"[Status] Level: {petLevel} | Energy: {petEnergy}/{petMaxEnergy} ({currentEnergyPercent}%) | Hunger: {petHunger}"); + } + +// RAUM-SCAN alle 15 SEKUNDEN (haeufiger um Essen zu checken!) + if ((DateTime.Now - lastRoomScan).TotalSeconds > 15) { + lastRoomScan = DateTime.Now; + +// DEBUG: Zeige aktuellen Food-Status + int foodCount = CountFoodInRoom(); + Log($"[SCAN] Essen im Raum: {foodCount}"); + + // DEAKTIVIERT: Wasser pruefen und nachfuellen + // if (HasEmptyWaterBowl()) { + // Log("[SCAN] Leere Wassernaepfe gefunden - fuelle nach!"); + // RefillAllEmptyWaterBowls(); + // } + + // Essen pruefen und nachfuellen + if (foodCount == 0) { + Log("[SCAN] KEIN ESSEN IM RAUM - platziere!"); + BotMessage("Kein Essen - platziere..."); + PlaceFoodFromInventory(); + } + + Log("[SCAN] Scan abgeschlossen."); + } + + // DEAKTIVIERT: Level-Scan - trainiere nur das angeklickte Pet! + // if ((DateTime.Now - lastLevelScan).TotalSeconds > levelScanIntervalSeconds) { + // lastLevelScan = DateTime.Now; + // Log($"[LEVEL-SCAN] Zeit fΓΌr Level-Scan! (alle {levelScanIntervalSeconds} Sek.)"); + // long result = ScanAndSwitchToLowestLevel(); + // if (petSwitchRequested) { + // Log("[LEVEL-SCAN] Wechsel wird durchgefΓΌhrt..."); + // continue; + // } + // } + + // ============ DYNAMISCHE CARE-MODUS LOGIK ============ + // Berechne aktuelle Energie in Prozent! + // WICHTIG: Energy kann > MaxEnergy sein (durch Items) - dann ist Prozent > 100! + int energyPercent = petMaxEnergy > 0 ? (petEnergy * 100 / petMaxEnergy) : 100; + + // Wenn Energy >= MaxEnergy, brauchen wir KEINEN Care-Modus! + if (petEnergy >= petMaxEnergy) { + energyPercent = 100; // Überschreibe auf 100% wenn voll oder ΓΌbervoll + } + + int careModeThreshold = (petMaxEnergy * careModeThresholdPercent) / 100; // z.B. 20% von 280 = 56 + int careModeExitThreshold = (petMaxEnergy * careModeExitPercent) / 100; // z.B. 95% von 280 = 266 + + // Check if we need to enter or exit care mode + // WICHTIG: Nicht starten wenn Energy >= MaxEnergy! + if (energyPercent <= careModeThresholdPercent && !inCareMode && petEnergy < petMaxEnergy) { + Log($"[CARE] Energie {petEnergy}/{petMaxEnergy} ({energyPercent}%) unter {careModeThresholdPercent}%! STARTE CARE-MODUS!"); + EnterCareMode(); + } else if (inCareMode) { + // Exit care mode if: energy restored OR too many commands sent + if (energyPercent >= careModeExitPercent) { + inCareMode = false; + Log($"[CARE] Care-Modus beendet - Energie {petEnergy}/{petMaxEnergy} ({energyPercent}%) >= {careModeExitPercent}%!"); + BotMessage($"Care-Modus beendet! Energie: {energyPercent}%"); + } else if (careModeCmdCount >= careModeCmdLimit) { + // Nur kurze Pause, NICHT komplett beenden wenn Energie noch niedrig! + if (energyPercent < 50) { + Log($"[CARE] {careModeCmdLimit} Befehle aber Energie noch {energyPercent}% - weitermachen!"); + careModeCmdCount = 0; // Reset und weitermachen + } else { + inCareMode = false; + Log($"[CARE] Care-Modus Pause nach {careModeCmdLimit} Befehlen - Energie: {energyPercent}%"); + BotMessage($"Care-Pause - Energie: {energyPercent}%"); + } + } + } + + // DEBUG: Zeige Care-Modus Status + if (inCareMode) { + Log($"[CARE] AKTIV! Energy={petEnergy}/{petMaxEnergy} ({energyPercent}%), Ziel={careModeExitPercent}%, Befehle={careModeCmdCount}/{careModeCmdLimit}"); + } + + // Select command based on pet needs and care mode + string cmd; + bool skipCommand = false; + + if (inCareMode) { + careModeCmdCount++; // Zaehle Care-Modus Befehle + Log($"[CARE] Befehl {careModeCmdCount}/{careModeCmdLimit}"); + + // In care mode: alternate between eat and drink + // PRE-CHECK passiert spaeter - hier nur Befehl waehlen! + if (hasDrink && totalCmds % 3 == 0) { + cmd = "drink"; + } else if (hasEat) { + cmd = "eat"; + } else { + cmd = "drink"; + } + Status($"[CARE-MODUS] Energy: {energyPercent}% -> Ziel: {careModeExitPercent}%"); + + } else { + // NORMALES TRAINING - keine separaten eat-Checks mehr! + cmd = unlockedCommands[cmdIndex]; + cmdIndex = (cmdIndex + 1) % unlockedCommands.Count; + + Log($"[TRAIN] Befehl: {cmd} (Index: {cmdIndex-1})"); + Status($"[{totalCmds}] {cmd} | +{totalXP} XP"); + } + + if (skipCommand) { + continue; + } + + // ============ WICHTIG: VOR eat/drink IMMER pruefen! ============ + if (cmd == "eat") { + // IMMER pruefen ob Essen im Raum ist BEVOR der Befehl gesendet wird! + if (!HasFoodInRoom()) { + Log("[PRE-CHECK] KEIN ESSEN IM RAUM - platziere zuerst!"); + bool placed = PlaceFoodFromInventory(); + if (!placed) { + Log("[PRE-CHECK] Konnte kein Essen platzieren - ueberspringe eat!"); + continue; // Skip this command entirely + } + Delay(1000); // Kurz warten nach Platzierung + } +} else if (cmd == "drink") { + // DEAKTIVIERT: Water refill + // if (HasEmptyWaterBowl()) { + // Log("[PRE-CHECK] Wassernapf leer - fuelle nach!"); + // RefillAllEmptyWaterBowls(); + // } + } + + // ============ BEFEHL SENDEN MIT RETRY ============ + int maxRetries = 5; // Maximal 5 Versuche bei Verweigerung + int retryCount = 0; + bool commandSucceeded = false; + + while (retryCount < maxRetries && !commandSucceeded && Run) { + ResetFlags(); + waitingForResponse = true; + + if (retryCount > 0) { + Log($"[RETRY] Versuch {retryCount + 1}/{maxRetries}: {petName} {cmd}"); + } else { + Log($"[CMD] Sende: {petName} {cmd}"); + } + + SendCmd(cmd); + if (retryCount == 0) totalCmds++; + + int waited = 0; + while (waited < trainDelay && !gotXP && !gotRefusal && Run) { + Delay(100); + waited += 100; + } + + waitingForResponse = false; + + if (gotXP) { + totalXP += lastXPGained; // Echte XP addieren! + commandSucceeded = true; + Log($"[OK] {cmd} erfolgreich! (+{lastXPGained} XP)"); + } else if (gotRefusal) { + retryCount++; + Log($"[!] Pet verweigert '{cmd}' - Retry {retryCount}/{maxRetries}"); + + // Bei Verweigerung: Problem beheben und SOFORT wieder versuchen! + if (cmd == "eat") { + Log("[RETRY] Pruefe/platziere Essen..."); + if (!HasFoodInRoom()) { + PlaceFoodFromInventory(); + } + Delay(1500); +} else if (cmd == "drink") { + Log("[RETRY] Drink retry - skip water refill"); + // DEAKTIVIERT: RefillAllEmptyWaterBowls(); + Delay(1000); + } else { + // Andere Befehle: kurz warten und nochmal + Delay(1000); + } + } else { + // Keine Antwort - wahrscheinlich OK + commandSucceeded = true; + } + } + + if (!commandSucceeded && retryCount >= maxRetries) { + Log($"[FAIL] {cmd} nach {maxRetries} Versuchen fehlgeschlagen - ueberspringe"); + BotMessage($"{cmd} fehlgeschlagen nach {maxRetries} Versuchen!"); + } + + // Wartezeit nach Befehl: + // - eat/drink: 8 Sekunden (Pet muss laufen + essen) + // - Alle anderen: Nur 2 Sekunden (kurze Animation) + if (cmd == "eat" || cmd == "drink") { + Log($"[WAIT] Warte 8 Sek. bis Pet fertig mit {cmd}..."); + WaitForPetToEat(); // 8 Sekunden + GetPetInfo(); // Update energy nach dem Essen + int newEnergyPercent = petMaxEnergy > 0 ? (petEnergy * 100 / petMaxEnergy) : 0; + Log($"[WAIT] Fertig! Energie jetzt: {petEnergy}/{petMaxEnergy} ({newEnergyPercent}%)"); + } else { + // Normale Befehle: Nur kurz warten (2 Sekunden) + Delay(2000); + } +} + +Log($"Training beendet! {totalCmds} Befehle, +{totalXP} XP"); diff --git a/Random Walker Bot.csx b/Random Walker Bot.csx new file mode 100644 index 0000000..a2a76bf --- /dev/null +++ b/Random Walker Bot.csx @@ -0,0 +1,12 @@ +Random rnd = new Random(); + +while(Run) +{ + if(Self?.Location == null) { Delay(100); continue; } + + int x = Self.Location.X + rnd.Next(-1, 2); + int y = Self.Location.Y + rnd.Next(-1, 2); + + Move(x, y); + Delay(100); +} \ No newline at end of file diff --git a/Roller Gate Bot.csx b/Roller Gate Bot.csx new file mode 100644 index 0000000..c13c817 --- /dev/null +++ b/Roller Gate Bot.csx @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +// ════════════════════════════════════════════════════ +// FIXED ROLLER BOT (Ohne Schnickschnack) +// ════════════════════════════════════════════════════ + +// IDs +int pillowId = 893412986; +int gateId = 2147418143; +List rollers = new List { + 2147418115, 2147418119, 2147418133, + 2147418134, 2147418135, 2147418136 +}; + +// Einstellungen +int delay = 130; // Wartezeit auf dem Roller (ms) + +// Speicher fΓΌr Koordinaten: Key=ItemID, Value=[x, y] +Dictionary coords = new Dictionary(); +bool active = false; + +Log("Bot gestartet. BITTE RAUM NEU LADEN!"); + +// 1. Alles scannen beim Raum betreten +OnIntercept(In.RoomReady, e => { coords.Clear(); active = false; }); +OnIntercept(In.Users, e => { coords.Clear(); active = false; }); + +OnIntercept(In.FloorItems, e => { + var p = e.Packet; + int n = p.ReadInt(); + + for(int i=0; i { + if (!active) return; + + var p = e.Packet; + int oldX = p.ReadInt(); + int oldY = p.ReadInt(); + int newX = p.ReadInt(); // Ziel X + int newY = p.ReadInt(); // Ziel Y + int count = p.ReadInt(); + + bool pillowMoved = false; + + for(int i=0; i Hin und ZurΓΌck + Send(Out.MoveAvatar, cx, cy); + await Task.Delay(delay); + Send(Out.MoveAvatar, px, py); + return; + } + } + } +} \ No newline at end of file diff --git a/Roller Surf Game Bot.csx b/Roller Surf Game Bot.csx new file mode 100644 index 0000000..ca1fdad --- /dev/null +++ b/Roller Surf Game Bot.csx @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// ═══════════════════════════════════════════════════════════════════════════════ +// ROLLER SURF BOT +// ═══════════════════════════════════════════════════════════════════════════════ + +const int PILLOW_ID = 893412986; +const int GATE_ID = 2147418143; +const int MARBLE_TILE_ID = 2147418142; +const int TICK_DELAY = 50; +const int MOVE_COOLDOWN = 150; + +HashSet greenRollers = new HashSet { + 2147418115, 2147418119, 2147418133, + 2147418134, 2147418135, 2147418136 +}; + +bool gameStarted = false; +DateTime lastMoveCmd = DateTime.MinValue; + +int GetId(dynamic item) { try { return (int)item.Id; } catch { return 0; } } + +(int x, int y) GetPillowPos() +{ + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetId(item) == PILLOW_ID) + return (item.Location.X, item.Location.Y); + } + return (-1, -1); +} + +(int x, int y) GetMarbleTilePos() +{ + foreach (var item in FloorItems) + { + if (item == null) continue; + if (GetId(item) == MARBLE_TILE_ID) + return (item.Location.X, item.Location.Y); + } + return (-1, -1); +} + +(int x, int y) GetMyPos() +{ + try { return (Self.Location.X, Self.Location.Y); } + catch { return (-1, -1); } +} + +bool IsAdjacent(int x1, int y1, int x2, int y2) +{ + int dx = Math.Abs(x1 - x2); + int dy = Math.Abs(y1 - y2); + return (dx == 1 && dy == 0) || (dx == 0 && dy == 1); +} + +int GetTargetIdAt(int x, int y) +{ + foreach (var item in FloorItems) + { + if (item == null) continue; + if (item.Location.X != x || item.Location.Y != y) continue; + + int id = GetId(item); + if (id == GATE_ID || greenRollers.Contains(id)) + { + return id; + } + } + return -1; +} + +(int x, int y, int id) FindAdjacentTarget(int px, int py) +{ + int[] dX = { 1, -1, 0, 0 }; + int[] dY = { 0, 0, 1, -1 }; + + for (int i = 0; i < 4; i++) + { + int checkX = px + dX[i]; + int checkY = py + dY[i]; + + int targetId = GetTargetIdAt(checkX, checkY); + if (targetId != -1) + { + return (checkX, checkY, targetId); + } + } + return (-1, -1, -1); +} + +Log("═══════════════════════════════════════"); +Log(" ROLLER SURF BOT"); +Log("═══════════════════════════════════════"); + +while (Run) +{ + Delay(TICK_DELAY); + + var pillow = GetPillowPos(); + var myPos = GetMyPos(); + var marbleTile = GetMarbleTilePos(); + + if (pillow.x == -1 || myPos.x == -1) continue; + + bool onPillow = (myPos.x == pillow.x && myPos.y == pillow.y); + bool onMarbleTile = (marbleTile.x != -1 && myPos.x == marbleTile.x && myPos.y == marbleTile.y); + + if (!gameStarted) + { + if (onMarbleTile) + { + Log("Start!"); + Send(Out["MoveAvatar"], pillow.x, pillow.y); + gameStarted = true; + lastMoveCmd = DateTime.Now; + continue; + } + + if (onPillow) + { + gameStarted = true; + Log("GO!"); + continue; + } + + if (IsAdjacent(myPos.x, myPos.y, pillow.x, pillow.y)) + { + Send(Out["MoveAvatar"], pillow.x, pillow.y); + gameStarted = true; + lastMoveCmd = DateTime.Now; + continue; + } + continue; + } + + if (!onPillow) + { + if ((DateTime.Now - lastMoveCmd).TotalMilliseconds > MOVE_COOLDOWN) + { + Send(Out["MoveAvatar"], pillow.x, pillow.y); + lastMoveCmd = DateTime.Now; + } + continue; + } + + var target = FindAdjacentTarget(pillow.x, pillow.y); + + if (target.id == -1) continue; + + if (target.id == GATE_ID) + { + Log("GATE!"); + Send(Out["UseFurniture"], GATE_ID, 0); + gameStarted = false; + Delay(300); + continue; + } + + Send(Out["MoveAvatar"], target.x, target.y); + Delay(TICK_DELAY); + + var newPillow = GetPillowPos(); + if (newPillow.x != -1) + { + Send(Out["MoveAvatar"], newPillow.x, newPillow.y); + lastMoveCmd = DateTime.Now; + } +} \ No newline at end of file diff --git a/Room Furni Scanner.csx b/Room Furni Scanner.csx new file mode 100644 index 0000000..8177765 --- /dev/null +++ b/Room Furni Scanner.csx @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xabbo.Core; + +Log("=== Color Puzzle Scanner ==="); +Log("Scanning all floor items in the room..."); + +var furniByName = new Dictionary>(); + +foreach (IFloorItem item in FloorItems) +{ + if (item == null) continue; + string name = item.GetName(); + if (!furniByName.ContainsKey(name)) + furniByName[name] = new List<(long, int, int, double, int, int)>(); + furniByName[name].Add((item.Id, item.Location.X, item.Location.Y, item.Location.Z, item.State, item.Direction)); +} + +Log($"\nFound {furniByName.Count} unique furni types:\n"); + +foreach (var kvp in furniByName.OrderByDescending(x => x.Value.Count)) +{ + Log($"--- {kvp.Key} (x{kvp.Value.Count}) ---"); + foreach (var f in kvp.Value.OrderBy(x => x.Y).ThenBy(x => x.X)) + { + Log($" ID={f.Id} Pos=({f.X},{f.Y}) Z={f.Z:F2} State={f.State} Dir={f.Dir}"); + } +} + +Log("\n=== Scan Complete ==="); diff --git a/Room Queue Auto Joiner.csx b/Room Queue Auto Joiner.csx new file mode 100644 index 0000000..4fc230f --- /dev/null +++ b/Room Queue Auto Joiner.csx @@ -0,0 +1,50 @@ +// Room Queue Joiner - Versucht solange bis man in der Queue landet +int roomId = 62909254; +int delayMs = 500; // Wartezeit zwischen Versuchen + +bool inQueue = false; +bool success = false; +int attempts = 0; + +// Wenn wir in die Queue kommen oder den Raum betreten +OnIntercept(In["RoomQueueStatus"], e => { + Log(">>> IN DER QUEUE!"); + inQueue = true; + success = true; +}); + +// Wenn wir den Raum betreten haben +OnIntercept(In["RoomReady"], e => { + Log(">>> IM RAUM!"); + success = true; +}); + +// Error abfangen (Raum voll, Queue voll, etc.) +OnIntercept(In["CantConnect"], e => { + // Raum/Queue voll - weiter versuchen +}); + +Log("=== ROOM QUEUE JOINER ==="); +Log("Room ID: " + roomId); +Log("Versuche in den Raum zu kommen..."); + +while (Run && !success) +{ + attempts++; + Log("Versuch #" + attempts + "..."); + + // GetGuestRoom senden + Send(Out["GetGuestRoom"], roomId, 0, 1); + + Delay(delayMs); + + if (success) + break; +} + +if (inQueue) + Log("ERFOLG! Du bist jetzt in der Room Queue."); +else if (success) + Log("ERFOLG! Du bist im Raum."); +else + Log("Script gestoppt."); \ No newline at end of file diff --git a/SOLVER.csx b/SOLVER.csx new file mode 100644 index 0000000..eb2d66f --- /dev/null +++ b/SOLVER.csx @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +const int MAIN_ROOM = 80262160; +const int SOURCE_MIN_X = 18; +const int SOURCE_MAX_X = 29; +const int SOURCE_MIN_Y = 25; +const int SOURCE_MAX_Y = 36; +const int FIELD_MIN_X = 18; +const int FIELD_MAX_X = 29; +const int FIELD_MIN_Y = 9; +const int FIELD_MAX_Y = 20; + +var gameRooms = new[] { 80262165, 80262159, 80262161, 80262162, 80262164, 80262166, 80262167, 80262163 }; +var completed = new HashSet(); + +int GetColorNumber(string name) +{ + var match = Regex.Match(name, @"\d+$"); + return match.Success ? int.Parse(match.Value) : -1; +} + +bool InArea(int x, int y, int minX, int maxX, int minY, int maxY) => x >= minX && x <= maxX && y >= minY && y <= maxY; +bool InField(int x, int y) => InArea(x, y, FIELD_MIN_X, FIELD_MAX_X, FIELD_MIN_Y, FIELD_MAX_Y); +bool InWaitingLine(int x, int y) => x == 14 && y >= 16 && y <= 33; +bool AtChair(int x, int y) => x == 14 && y == 15; + +int GetRoomId() +{ + try { return (int)Room.Id; } catch { return 0; } +} + +(int x, int y) GetSelfPos() +{ + try { return (Self.Location.X, Self.Location.Y); } catch { return (-1, -1); } +} + +void WalkToAndWait(int tx, int ty) +{ + for (int retry = 0; retry < 10; retry++) + { + Send(Out["MoveAvatar"], tx, ty); + for (int i = 0; i < 30; i++) + { + Delay(100); + var pos = GetSelfPos(); + if (pos.x == tx && pos.y == ty) + { + Delay(200); + return; + } + } + } +} + +void WalkToAreaAndWait(int minX, int maxX, int minY, int maxY) +{ + for (int retry = 0; retry < 10; retry++) + { + var pos = GetSelfPos(); + if (pos.x >= 0 && InArea(pos.x, pos.y, minX, maxX, minY, maxY)) + { + Delay(200); + return; + } + Send(Out["MoveAvatar"], minX, minY); + for (int i = 0; i < 30; i++) + { + Delay(100); + pos = GetSelfPos(); + if (pos.x >= 0 && InArea(pos.x, pos.y, minX, maxX, minY, maxY)) + { + Delay(200); + return; + } + } + } +} + +void ClickUntilTeleport(int id, int targetX, int targetY) +{ + for (int retry = 0; retry < 20; retry++) + { + Send(Out["ClickFurni"], id, 0); + for (int i = 0; i < 20; i++) + { + Delay(100); + var pos = GetSelfPos(); + if (pos.x == targetX && pos.y == targetY) + { + Delay(200); + return; + } + } + } +} + +void UseUntilRoom(int id, int roomId) +{ + for (int retry = 0; retry < 20; retry++) + { + Send(Out["UseFurniture"], id, 0); + for (int i = 0; i < 30; i++) + { + Delay(100); + if (GetRoomId() == roomId) + { + Delay(2000); + return; + } + } + } +} + +void WaitForMainRoom() +{ + for (int i = 0; i < 200; i++) + { + Delay(100); + if (GetRoomId() == MAIN_ROOM) + { + Delay(2000); + return; + } + } +} + +void WaitUntilInField() +{ + Log("Waiting for field..."); + + while (true) + { + var pos = GetSelfPos(); + if (pos.x < 0) { Delay(100); continue; } + + if (InField(pos.x, pos.y)) + { + Log($"In field at ({pos.x},{pos.y})"); + Delay(300); + return; + } + + if (AtChair(pos.x, pos.y)) + { + Delay(100); + continue; + } + + if (InWaitingLine(pos.x, pos.y)) + { + Send(Out["MoveAvatar"], 14, 15); + Delay(200); + continue; + } + + Send(Out["MoveAvatar"], 14, 20); + Delay(300); + } +} + +void DoPixelArt() +{ + WaitUntilInField(); + + var selectors = new Dictionary(); + try + { + foreach (var item in FloorItems) + { + if (item == null) continue; + string name = item.GetName(); + if (name.Contains("Cylinder Block") || name.Contains("Hemisphere Block")) + { + int colorNum = GetColorNumber(name); + if (colorNum > 0 && !selectors.ContainsKey(colorNum)) + selectors[colorNum] = (int)item.Id; + } + } + } catch { } + + var sourcePixels = new Dictionary<(int x, int y), int>(); + try + { + foreach (var item in FloorItems) + { + if (item == null) continue; + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + if (x < SOURCE_MIN_X || x > SOURCE_MAX_X || y < SOURCE_MIN_Y || y > SOURCE_MAX_Y) continue; + if (z < 0.7 || z > 1.0) continue; + int colorNum = GetColorNumber(item.GetName()); + if (colorNum > 0) + sourcePixels[(x, y)] = colorNum; + } + } catch { } + + int offsetY = SOURCE_MIN_Y - FIELD_MIN_Y; + int changed = 0; + + for (int attempt = 1; attempt <= 3; attempt++) + { + var fieldTiles = new Dictionary<(int x, int y), (int id, int colorNum)>(); + try + { + foreach (var item in FloorItems) + { + if (item == null) continue; + int x = item.Location.X; + int y = item.Location.Y; + double z = item.Location.Z; + if (x < FIELD_MIN_X || x > FIELD_MAX_X || y < FIELD_MIN_Y || y > FIELD_MAX_Y) continue; + + int colorNum = GetColorNumber(item.GetName()); + + if (z > 0.5) + fieldTiles[(x, y)] = ((int)item.Id, colorNum); + else if (!fieldTiles.ContainsKey((x, y))) + fieldTiles[(x, y)] = ((int)item.Id, -1); + } + } catch { } + + var toChange = new List<(int fieldId, int srcColor)>(); + foreach (var src in sourcePixels) + { + int fieldX = src.Key.x; + int fieldY = src.Key.y - offsetY; + int srcColor = src.Value; + + if (!fieldTiles.TryGetValue((fieldX, fieldY), out var field)) continue; + if (field.colorNum == srcColor) continue; + if (!selectors.ContainsKey(srcColor)) continue; + + toChange.Add((field.id, srcColor)); + } + + if (toChange.Count == 0) break; + + var sorted = toChange.OrderBy(x => x.srcColor).ToList(); + int lastColor = -1; + + foreach (var item in sorted) + { + if (item.srcColor != lastColor) + { + Send(Out["ClickFurni"], selectors[item.srcColor], 0); + Delay(50); + lastColor = item.srcColor; + } + + Send(Out["ClickFurni"], item.fieldId, 0); + Delay(50); + changed++; + } + + Delay(500); + } + + try + { + var badge = FloorItems.FirstOrDefault(f => f != null && f.GetName() == "Badge Display Case"); + if (badge != null) + Send(Out["ClickFurni"], (int)badge.Id, 0); + } catch { } + + Log($"Done: {changed}"); + completed.Add(GetRoomId()); +} + +void GoToRoom1() +{ + if (completed.Contains(80262165)) return; + Log("Room 1"); + WalkToAreaAndWait(5, 6, 25, 27); + ClickUntilTeleport(902905683, 11, 27); + ClickUntilTeleport(902906203, 11, 24); + UseUntilRoom(902906203, 80262165); + DoPixelArt(); + WaitForMainRoom(); +} + +void GoToRoom2() +{ + if (completed.Contains(80262159)) return; + Log("Room 2"); + WalkToAreaAndWait(16, 20, 34, 35); + ClickUntilTeleport(902906461, 20, 34); + ClickUntilTeleport(902906426, 20, 32); + UseUntilRoom(902906426, 80262159); + DoPixelArt(); + WaitForMainRoom(); +} + +void GoToRoom3() +{ + if (completed.Contains(80262161)) return; + Log("Room 3"); + WalkToAreaAndWait(25, 25, 17, 21); + Send(Out["ClickFurni"], 902906152, 0); + Delay(500); + ClickUntilTeleport(902907470, 25, 19); + UseUntilRoom(902907470, 80262161); + DoPixelArt(); + WaitForMainRoom(); +} + +void GoToRoom4() +{ + if (completed.Contains(80262162)) return; + Log("Room 4"); + WalkToAreaAndWait(30, 31, 15, 16); + Send(Out["ClickFurni"], 902907718, 0); + Delay(500); + ClickUntilTeleport(902907623, 31, 15); + UseUntilRoom(902907623, 80262162); + DoPixelArt(); + WaitForMainRoom(); +} + +void GoToRoom5() +{ + if (completed.Contains(80262164)) return; + Log("Room 5"); + WalkToAreaAndWait(30, 31, 15, 16); + Send(Out["ClickFurni"], 902907718, 0); + Delay(500); + WalkToAndWait(36, 16); + ClickUntilTeleport(902905838, 36, 16); + UseUntilRoom(902905838, 80262164); + DoPixelArt(); + WaitForMainRoom(); +} + +void GoToRoom6() +{ + if (completed.Contains(80262166)) return; + Log("Room 6"); + WalkToAreaAndWait(30, 31, 15, 16); + Send(Out["ClickFurni"], 902907718, 0); + Delay(500); + WalkToAndWait(44, 14); + ClickUntilTeleport(902907457, 44, 14); + UseUntilRoom(902907457, 80262166); + DoPixelArt(); + WaitForMainRoom(); +} + +void GoToRoom7() +{ + if (completed.Contains(80262167)) return; + Log("Room 7"); + WalkToAreaAndWait(30, 31, 15, 16); + Send(Out["ClickFurni"], 902907718, 0); + Delay(500); + WalkToAndWait(50, 12); + ClickUntilTeleport(902906400, 50, 12); + UseUntilRoom(902906400, 80262167); + DoPixelArt(); + WaitForMainRoom(); +} + +void GoToRoom8() +{ + if (completed.Contains(80262163)) return; + Log("Room 8"); + WalkToAreaAndWait(50, 51, 21, 21); + Send(Out["ClickFurni"], 902906802, 0); + Delay(500); + ClickUntilTeleport(902906273, 50, 21); + UseUntilRoom(902906273, 80262163); + DoPixelArt(); + WaitForMainRoom(); +} + +Log("Starting"); + +int currentRoom = GetRoomId(); + +if (gameRooms.Contains(currentRoom)) +{ + Log($"In game room {currentRoom}"); + DoPixelArt(); + WaitForMainRoom(); +} + +GoToRoom1(); +GoToRoom2(); +GoToRoom3(); +GoToRoom4(); +GoToRoom5(); +GoToRoom6(); +GoToRoom7(); +GoToRoom8(); + +Log($"All done: {completed.Count}/8"); +WalkToAreaAndWait(41, 41, 32, 33); diff --git a/Seed trade (simple).csx b/Seed trade (simple).csx new file mode 100644 index 0000000..130ea31 --- /dev/null +++ b/Seed trade (simple).csx @@ -0,0 +1,3 @@ +EnsureInventory(); +var items = Inventory.GetFloorItems().Named("Plant Seed").Where(seed => (seed.Data as MapData)?["rarity"] == "7").Take(20); +Send(Out.TradeAddItems, items.Select(x => (int)x.ItemId).ToList()); \ No newline at end of file diff --git a/Seed trade (split).csx b/Seed trade (split).csx new file mode 100644 index 0000000..1e1f470 --- /dev/null +++ b/Seed trade (split).csx @@ -0,0 +1,9 @@ +EnsureInventory(); +var items = Inventory.GetFloorItems() + .Named("Plant Seed") + .GroupBy(seed => (seed.Data as MapData)?["rarity"] as string) // Gruppiert nach rarity + .Where(group => int.TryParse(group.Key, out int r) && r >= 0 && r <= 11) // PrΓΌft, ob rarity zwischen 0-11 liegt + .SelectMany(group => group.Take(150)) // Nimmt 10 aus jeder Gruppe + .ToList(); + +Send(Out.TradeAddItems, items.Select(x => (int)x.ItemId).ToList()); \ No newline at end of file diff --git a/Sit Spam.csx b/Sit Spam.csx new file mode 100644 index 0000000..1fbdd04 --- /dev/null +++ b/Sit Spam.csx @@ -0,0 +1,9 @@ +// Sit Spam - Setzt dich dauerhaft hin + +Log("=== Sit Spam gestartet ==="); + +while (Run) +{ + Sit(); + Delay(100); +} \ No newline at end of file diff --git a/Snake Auto (AutoCalib).csx b/Snake Auto (AutoCalib).csx new file mode 100644 index 0000000..1ffb1d3 --- /dev/null +++ b/Snake Auto (AutoCalib).csx @@ -0,0 +1,404 @@ +/// @name Snake Auto (AutoCalib) +using System; +using System.Linq; +using System.Collections.Generic; + +const int GAME_MIN_X = 9; +const int GAME_MAX_X = 28; +const int GAME_MIN_Y = 11; +const int GAME_MAX_Y = 30; + +const int BLOCKED_MIN_X = 17; +const int BLOCKED_MAX_X = 20; +const int BLOCKED_MIN_Y = 19; +const int BLOCKED_MAX_Y = 22; + +const int KIND_SNAKE = 7100; +const int KIND_FOOD = 5068; + +// Fallback controls for this room layout +const int FALLBACK_UP = 769407748; +const int FALLBACK_RIGHT = 769407103; +const int FALLBACK_DOWN = 769407216; +const int FALLBACK_LEFT = 769407068; + +int CTRL_UP = FALLBACK_UP; +int CTRL_RIGHT = FALLBACK_RIGHT; +int CTRL_DOWN = FALLBACK_DOWN; +int CTRL_LEFT = FALLBACK_LEFT; + +List<(int x, int y)> body = new List<(int x, int y)>(256); +HashSet<(int x, int y)> bodySet = new HashSet<(int x, int y)>(); +(int x, int y) food = (-1, -1); + +string dir = "DOWN"; +string lastSentDir = ""; +DateTime lastCmdTime = DateTime.MinValue; +bool ate = false; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +bool Wall(int x, int y) => x < GAME_MIN_X || x > GAME_MAX_X || y < GAME_MIN_Y || y > GAME_MAX_Y; +bool Blk(int x, int y) => x >= BLOCKED_MIN_X && x <= BLOCKED_MAX_X && y >= BLOCKED_MIN_Y && y <= BLOCKED_MAX_Y; + +void Step(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; +int DirToIdx(string d) => d == "UP" ? 0 : d == "DOWN" ? 1 : d == "LEFT" ? 2 : 3; + +bool Blocked(int x, int y, bool ignoreTail) +{ + if (Wall(x, y) || Blk(x, y)) return true; + if (body.Count == 0) return false; + + int checkLen = ignoreTail && !ate ? body.Count - 1 : body.Count; + for (int i = 0; i < checkLen; i++) + if (body[i].x == x && body[i].y == y) return true; + return false; +} + +void SendDir(int dIdx) +{ + string d = DIRS[dIdx]; + if (d == lastSentDir && (DateTime.Now - lastCmdTime).TotalMilliseconds < 80) return; + + int id = dIdx == 0 ? CTRL_UP : dIdx == 1 ? CTRL_DOWN : dIdx == 2 ? CTRL_LEFT : CTRL_RIGHT; + Send(Out["ClickFurni"], id, 0); + + lastCmdTime = DateTime.Now; + lastSentDir = d; + dir = d; +} + +int OpenSpace(int sx, int sy, int cap) +{ + if (Blocked(sx, sy, false)) return 0; + var seen = new HashSet<(int, int)>(); + var q = new Queue<(int, int)>(); + + q.Enqueue((sx, sy)); + seen.Add((sx, sy)); + int c = 0; + + while (q.Count > 0 && c < cap) + { + var p = q.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Step(p.Item1, p.Item2, d, out int nx, out int ny); + if (!Blocked(nx, ny, false) && seen.Add((nx, ny))) + q.Enqueue((nx, ny)); + } + } + return c; +} + +int DistTo(int sx, int sy, int tx, int ty, int cap = 180) +{ + if (sx == tx && sy == ty) return 0; + var seen = new HashSet<(int, int)>(); + var q = new Queue<((int x, int y) pos, int dist)>(); + + q.Enqueue(((sx, sy), 0)); + seen.Add((sx, sy)); + + while (q.Count > 0) + { + var cur = q.Dequeue(); + if (cur.dist >= cap) return 999; + + for (int d = 0; d < 4; d++) + { + Step(cur.pos.x, cur.pos.y, d, out int nx, out int ny); + if (nx == tx && ny == ty) return cur.dist + 1; + if (!Blocked(nx, ny, false) && seen.Add((nx, ny))) + q.Enqueue(((nx, ny), cur.dist + 1)); + } + } + return -1; +} + +int BestMove(int hx, int hy, int fx, int fy, int curDir) +{ + int best = curDir; + int bestScore = int.MinValue; + int minSpace = Math.Max(body.Count / 4, 8); + + for (int d = 0; d < 4; d++) + { + if (d == Opp(curDir)) continue; + Step(hx, hy, d, out int nx, out int ny); + if (Blocked(nx, ny, true)) continue; + + int space = OpenSpace(nx, ny, 180); + if (space < minSpace) continue; + + int score = 0; + if (nx == fx && ny == fy) score += 10000; + + int distFood = DistTo(nx, ny, fx, fy, 180); + if (distFood > 0 && distFood < 999) score += (220 - distFood) * 7; + else if (distFood == -1) score -= 150; + + score += Math.Min(space, 100) * 4; + + if (score > bestScore) + { + bestScore = score; + best = d; + } + } + + return best; +} + +int Emergency(int hx, int hy, int curDir) +{ + int best = curDir; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (d == Opp(curDir)) continue; + Step(hx, hy, d, out int nx, out int ny); + if (Blocked(nx, ny, true)) continue; + + int space = OpenSpace(nx, ny, 80); + if (space > bestSpace) + { + bestSpace = space; + best = d; + } + } + + return best; +} + +List> GetComponents(List<(long id, int kind, int x, int y)> points) +{ + var comps = new List>(); + var used = new HashSet(); + + foreach (var p in points) + { + if (!used.Add(p.id)) continue; + + var comp = new List<(long id, int x, int y)>(); + var q = new Queue<(long id, int x, int y)>(); + q.Enqueue((p.id, p.x, p.y)); + + while (q.Count > 0) + { + var cur = q.Dequeue(); + comp.Add(cur); + + foreach (var n in points) + { + if (used.Contains(n.id)) continue; + if (Math.Abs(cur.x - n.x) <= 1 && Math.Abs(cur.y - n.y) <= 1) + { + used.Add(n.id); + q.Enqueue((n.id, n.x, n.y)); + } + } + } + + comps.Add(comp); + } + + return comps; +} + +bool TryAutoCalibrateControls() +{ + if (Self == null) return false; + + int sx = Self.Location.X; + int sy = Self.Location.Y; + + var nearby = FloorItems + .Where(i => i != null) + .Where(i => Math.Abs(i.Location.X - sx) <= 14 && Math.Abs(i.Location.Y - sy) <= 14) + .Where(i => i.Kind != KIND_SNAKE && i.Kind != KIND_FOOD) + .Select(i => (id: (long)i.Id, kind: (int)i.Kind, x: i.Location.X, y: i.Location.Y)) + .ToList(); + + if (nearby.Count == 0) return false; + + List<(long id, int x, int y)> best = null; + double bestScore = double.MaxValue; + + foreach (var g in nearby.GroupBy(p => p.kind)) + { + if (g.Count() < 4) continue; + + var components = GetComponents(g.ToList()); + foreach (var comp in components) + { + if (comp.Count < 4) continue; + + int minX = comp.Min(p => p.x); + int maxX = comp.Max(p => p.x); + int minY = comp.Min(p => p.y); + int maxY = comp.Max(p => p.y); + + int w = maxX - minX; + int h = maxY - minY; + if (w > 6 || h > 6) continue; + + double cx = comp.Average(p => p.x); + double cy = comp.Average(p => p.y); + double dist = Math.Abs(cx - sx) + Math.Abs(cy - sy); + double score = dist + Math.Abs(comp.Count - 4) * 0.5; + + if (score < bestScore) + { + bestScore = score; + best = comp; + } + } + } + + if (best == null || best.Count < 4) return false; + + double centerX = best.Average(p => p.x); + double centerY = best.Average(p => p.y); + + var used = new HashSet(); + + long Pick(IEnumerable<(long id, int x, int y)> list) + { + foreach (var p in list) + { + if (used.Add(p.id)) return p.id; + } + return -1; + } + + var upList = best.OrderBy(p => p.y).ThenBy(p => Math.Abs(p.x - centerX)).ToList(); + var downList = best.OrderByDescending(p => p.y).ThenBy(p => Math.Abs(p.x - centerX)).ToList(); + var leftList = best.OrderBy(p => p.x).ThenBy(p => Math.Abs(p.y - centerY)).ToList(); + var rightList = best.OrderByDescending(p => p.x).ThenBy(p => Math.Abs(p.y - centerY)).ToList(); + + long up = Pick(upList); + long down = Pick(downList); + long left = Pick(leftList); + long right = Pick(rightList); + + if (up <= 0 || down <= 0 || left <= 0 || right <= 0) return false; + + CTRL_UP = (int)up; + CTRL_RIGHT = (int)right; + CTRL_DOWN = (int)down; + CTRL_LEFT = (int)left; + + return true; +} + +Log("Snake Auto (AutoCalib) started"); + +bool calibrated = TryAutoCalibrateControls(); +if (calibrated) + Log($"Auto-calibrated controls: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); +else + Log($"Auto-calibration failed, using fallback: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); + +Log("Waiting for snake + food spawn..."); + +while (Run) +{ + Delay(15); + + var curSnake = new HashSet<(int x, int y)>(); + food = (-1, -1); + + foreach (var item in FloorItems) + { + if (item == null) continue; + int k; + try { k = (int)item.Kind; } catch { continue; } + + if (k == KIND_SNAKE && !Wall(item.Location.X, item.Location.Y)) + curSnake.Add((item.Location.X, item.Location.Y)); + else if (k == KIND_FOOD) + food = (item.Location.X, item.Location.Y); + } + + int curLen = curSnake.Count; + if (curLen == 0) + { + body.Clear(); + bodySet.Clear(); + continue; + } + + if (body.Count == 0) + { + var sorted = curSnake.OrderBy(p => p.y).ThenBy(p => p.x).ToList(); + body = new List<(int x, int y)>(sorted); + bodySet = new HashSet<(int x, int y)>(sorted); + continue; + } + + var head = body[0]; + (int x, int y) newHead = (-1, -1); + + for (int d = 0; d < 4; d++) + { + Step(head.x, head.y, d, out int nx, out int ny); + if (curSnake.Contains((nx, ny)) && !bodySet.Contains((nx, ny))) + { + newHead = (nx, ny); + if ((DateTime.Now - lastCmdTime).TotalMilliseconds > 400) + dir = DIRS[d]; + break; + } + } + + if (newHead.x != -1) + { + int prevLen = body.Count; + body.Insert(0, newHead); + bodySet.Add(newHead); + ate = curLen > prevLen; + + while (body.Count > curLen) + { + var tail = body[body.Count - 1]; + bodySet.Remove(tail); + body.RemoveAt(body.Count - 1); + } + } + + if (body.Count == 0 || food.x == -1) continue; + + head = body[0]; + int curDir = DirToIdx(dir); + + Step(head.x, head.y, curDir, out int fx, out int fy); + if (Blocked(fx, fy, true)) + { + SendDir(Emergency(head.x, head.y, curDir)); + continue; + } + + for (int d = 0; d < 4; d++) + { + if (d == Opp(curDir)) continue; + Step(head.x, head.y, d, out int nx, out int ny); + if (nx == food.x && ny == food.y && !Blocked(nx, ny, true)) + { + SendDir(d); + goto nextLoop; + } + } + + SendDir(BestMove(head.x, head.y, food.x, food.y, curDir)); + +nextLoop:; +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v1.csx b/Snake Auto (Campaign) v1.csx new file mode 100644 index 0000000..efbfafe --- /dev/null +++ b/Snake Auto (Campaign) v1.csx @@ -0,0 +1,116 @@ +/// @name Snake Auto (Campaign) v1 +using System; +using System.Linq; + +// Fixed from your room +const long HEAD_ID = 769407982; +const int TARGET_KIND = 3666; // Color Tile kind + +// Pad button IDs (as confirmed by you) +const int PAD_TOP_LEFT = 769407085; +const int PAD_TOP_RIGHT = 769406820; +const int PAD_BOTTOM_LEFT = 769407385; +const int PAD_BOTTOM_RIGHT = 769407116; + +// Direction mapping (tuned for this room) +const int CTRL_UP = PAD_TOP_LEFT; +const int CTRL_RIGHT = PAD_TOP_RIGHT; +const int CTRL_DOWN = PAD_BOTTOM_LEFT; +const int CTRL_LEFT = PAD_BOTTOM_RIGHT; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastInfo = DateTime.MinValue; +int lastDir = -1; // 0=UP 1=DOWN 2=LEFT 3=RIGHT + +bool TryGetButton(int dir, out int id, out int x, out int y) +{ + id = dir == 0 ? CTRL_UP : dir == 1 ? CTRL_DOWN : dir == 2 ? CTRL_LEFT : CTRL_RIGHT; + var btn = GetFloorItem(id); + if (btn == null) + { + x = 0; + y = 0; + return false; + } + + x = btn.Location.X; + y = btn.Location.Y; + return true; +} + +void SendDir(int dir) +{ + if ((DateTime.Now - lastCmd).TotalMilliseconds < 140 && dir == lastDir) + return; + + if (TryGetButton(dir, out int id, out int x, out int y)) + { + // This game mode reacts when user steps on the plate. + Move(x, y); + } + else + { + int fallbackId = dir == 0 ? CTRL_UP : dir == 1 ? CTRL_DOWN : dir == 2 ? CTRL_LEFT : CTRL_RIGHT; + Send(Out["ClickFurni"], fallbackId, 0); + } + + lastCmd = DateTime.Now; + lastDir = dir; +} + +int ToDir(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) + return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +Log("Snake Auto (Campaign) v3 started"); +Log($"HeadId={HEAD_ID}, TargetKind={TARGET_KIND}"); +Log($"Buttons: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); + +while (Run) +{ + Delay(40); + + var head = GetFloorItem(HEAD_ID); + if (head == null) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: head not visible"); + lastInfo = DateTime.Now; + } + continue; + } + + var targets = FloorItems + .Where(i => i != null && i.Kind == TARGET_KIND) + .Where(i => Math.Abs(i.Location.X - head.Location.X) + Math.Abs(i.Location.Y - head.Location.Y) <= 45) + .ToList(); + + if (targets.Count == 0) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + int allTargets = FloorItems.Count(i => i != null && i.Kind == TARGET_KIND); + Log($"waiting: no nearby target tiles (all kind {TARGET_KIND}: {allTargets})"); + lastInfo = DateTime.Now; + } + continue; + } + + var target = targets + .OrderBy(t => Math.Abs(t.Location.X - head.Location.X) + Math.Abs(t.Location.Y - head.Location.Y)) + .ThenBy(t => t.State) + .First(); + + int dx = target.Location.X - head.Location.X; + int dy = target.Location.Y - head.Location.Y; + + if (dx == 0 && dy == 0) + continue; + + int dir = ToDir(dx, dy); + SendDir(dir); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v10.csx b/Snake Auto (Campaign) v10.csx new file mode 100644 index 0000000..6a23901 --- /dev/null +++ b/Snake Auto (Campaign) v10.csx @@ -0,0 +1,286 @@ +/// @name Snake Auto (Campaign) v10 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int SNAKE_KIND = 5478; +static readonly int[] TARGET_KINDS = { 6559, 3666 }; + +const int GAME_MIN_X = 20; +const int GAME_MAX_X = 65; +const int GAME_MIN_Y = 20; +const int GAME_MAX_Y = 65; + +// Direction -> plate ids (verified from logs) +const int CTRL_UP = 769406820; +const int CTRL_DOWN = 769407116; +const int CTRL_LEFT = 769407385; +const int CTRL_RIGHT = 769407085; + +// Known static obstacle (blue block cluster) +const int BLOCK_MIN_X = 20; +const int BLOCK_MAX_X = 22; +const int BLOCK_MIN_Y = 20; +const int BLOCK_MAX_Y = 21; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastDbg = DateTime.MinValue; +int curDir = -1; +string lastSentDir = ""; +(int x, int y) prevHead = (-1, -1); + +int lastAttemptDir = -1; +long lastTargetId = 0; +DateTime lastAttemptAt = DateTime.MinValue; + +Dictionary<(int x, int y), DateTime> tempBlocked = new Dictionary<(int, int), DateTime>(); +Dictionary targetBlacklist = new Dictionary(); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +bool IsStaticBlock(int x, int y) + => x >= BLOCK_MIN_X && x <= BLOCK_MAX_X && y >= BLOCK_MIN_Y && y <= BLOCK_MAX_Y; + +void Dbg(string m, bool force = false) +{ + if (!force && (DateTime.Now - lastDbg).TotalMilliseconds < 220) return; + Log($"dbg: {m}"); + lastDbg = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int BtnForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetBtnPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) { x = 0; y = 0; return false; } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void CleanupMaps() +{ + var now = DateTime.Now; + + foreach (var k in tempBlocked.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + tempBlocked.Remove(k); + + foreach (var k in targetBlacklist.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + targetBlacklist.Remove(k); +} + +bool IsTempBlocked(int x, int y) + => tempBlocked.TryGetValue((x, y), out var until) && until > DateTime.Now; + +bool Bad(int x, int y, HashSet<(int x, int y)> body) +{ + if (!InBoard(x, y)) return true; + if (IsStaticBlock(x, y)) return true; + if (IsTempBlocked(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, int cap) +{ + if (Bad(sx, sy, body)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 260); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + + return bestDir; +} + +int PickDir((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body) +{ + int desired = DirFromDelta(target.x - head.x, target.y - head.y); + var dirs = new List { desired, 0, 1, 2, 3 }; + + int bestDir = curDir >= 0 ? curDir : desired; + int bestScore = int.MinValue; + + foreach (int d in dirs.Distinct()) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int dist = Math.Abs(target.x - nx) + Math.Abs(target.y - ny); + int space = Fill(nx, ny, body, 260); + + int score = -dist * 6 + space * 2; + if (d == desired) score += 25; + if (curDir >= 0 && d == curDir) score += 4; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target, long targetId) +{ + string nd = DIRS[d]; + if (nd == lastSentDir && (DateTime.Now - lastCmd).TotalMilliseconds < 220) + return; + + int id = BtnForDir(d); + if (TryGetBtnPos(id, out int bx, out int by)) Move(bx, by); + Send(Out["ClickFurni"], id, 0); + + lastCmd = DateTime.Now; + lastSentDir = nd; + + lastAttemptDir = d; + lastAttemptAt = DateTime.Now; + lastTargetId = targetId; + + if (target.HasValue) + Dbg($"{reason} h=({head.x},{head.y}) t=({target.Value.x},{target.Value.y}) d={nd} b={id}", true); + else + Dbg($"{reason} h=({head.x},{head.y}) d={nd} b={id}", true); +} + +Log("Snake Auto (Campaign) v10 started"); +Log($"target kinds: {string.Join(",", TARGET_KINDS)}"); +Log($"buttons U={CTRL_UP} D={CTRL_DOWN} L={CTRL_LEFT} R={CTRL_RIGHT}"); + +while (Run) +{ + Delay(22); + CleanupMaps(); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + Dbg("head missing"); + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + // infer direction from movement + if (prevHead.x != -1) + { + int hdx = head.Item1 - prevHead.x; + int hdy = head.Item2 - prevHead.y; + if (hdx != 0 || hdy != 0) + curDir = DirFromDelta(hdx, hdy); + } + + // stuck detection: command sent but head did not move + if (prevHead.x == head.Item1 && prevHead.y == head.Item2 && + lastAttemptDir >= 0 && (DateTime.Now - lastAttemptAt).TotalMilliseconds > 420) + { + Next(head.Item1, head.Item2, lastAttemptDir, out int bx, out int by); + tempBlocked[(bx, by)] = DateTime.Now.AddMilliseconds(1800); + + if (lastTargetId != 0) + targetBlacklist[lastTargetId] = DateTime.Now.AddMilliseconds(3000); + + Dbg($"stuck: block ({bx},{by}) and skip target {lastTargetId}", true); + lastAttemptDir = -1; + } + + prevHead = head; + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targetsAll = FloorItems + .Where(i => i != null && TARGET_KINDS.Contains((int)i.Kind)) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (x: i.Location.X, y: i.Location.Y, id: (long)i.Id, kind: (int)i.Kind)) + .ToList(); + + if (targetsAll.Count == 0) + { + int safe = Emergency(head, body); + Cmd(safe, "no_target_safe", head, null, 0); + continue; + } + + var targets = targetsAll + .Where(t => !targetBlacklist.TryGetValue(t.id, out var until) || until <= DateTime.Now) + .ToList(); + + if (targets.Count == 0) + targets = targetsAll; + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + int dsel = PickDir(head, (target.x, target.y), body); + Cmd(dsel, $"seek k={target.kind} id={target.id}", head, (target.x, target.y), target.id); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v11.csx b/Snake Auto (Campaign) v11.csx new file mode 100644 index 0000000..f9480c3 --- /dev/null +++ b/Snake Auto (Campaign) v11.csx @@ -0,0 +1,293 @@ +/// @name Snake Auto (Campaign) v11 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int SNAKE_KIND = 5478; +static readonly int[] TARGET_KINDS = { 3666 }; + +const int GAME_MIN_X = 20; +const int GAME_MAX_X = 65; +const int GAME_MIN_Y = 20; +const int GAME_MAX_Y = 65; + +// Direction -> plate ids (verified from logs) +const int CTRL_UP = 769406820; +const int CTRL_DOWN = 769407116; +const int CTRL_LEFT = 769407385; +const int CTRL_RIGHT = 769407085; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastDbg = DateTime.MinValue; +int curDir = -1; +string lastSentDir = ""; +(int x, int y) prevHead = (-1, -1); + +int lastCmdDir = -1; +(int x, int y) lastCmdHead = (-999, -999); + +int lastAttemptDir = -1; +(int x, int y) lastAttemptHead = (-999, -999); +DateTime lastAttemptAt = DateTime.MinValue; +long lastTargetId = 0; + +Dictionary<(int x, int y), DateTime> tempBlocked = new Dictionary<(int, int), DateTime>(); +Dictionary targetBlacklist = new Dictionary(); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +void Dbg(string m, bool force = false) +{ + if (!force && (DateTime.Now - lastDbg).TotalMilliseconds < 220) return; + Log($"dbg: {m}"); + lastDbg = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int BtnForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetBtnPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) { x = 0; y = 0; return false; } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void CleanupMaps() +{ + var now = DateTime.Now; + + foreach (var k in tempBlocked.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + tempBlocked.Remove(k); + + foreach (var k in targetBlacklist.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + targetBlacklist.Remove(k); +} + +bool IsTempBlocked(int x, int y) + => tempBlocked.TryGetValue((x, y), out var until) && until > DateTime.Now; + +bool Bad(int x, int y, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + if (!InBoard(x, y)) return true; + if (blocks.Contains((x, y))) return true; + if (IsTempBlocked(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks, int cap) +{ + if (Bad(sx, sy, body, blocks)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body, blocks) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body, blocks)) continue; + + int space = Fill(nx, ny, body, blocks, 260); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + + return bestDir; +} + +int PickDir((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + int desired = DirFromDelta(target.x - head.x, target.y - head.y); + var dirs = new List { desired, 0, 1, 2, 3 }; + + int bestDir = curDir >= 0 ? curDir : desired; + int bestScore = int.MinValue; + + foreach (int d in dirs.Distinct()) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body, blocks)) continue; + + int dist = Math.Abs(target.x - nx) + Math.Abs(target.y - ny); + int space = Fill(nx, ny, body, blocks, 260); + + int score = -dist * 6 + space * 2; + if (d == desired) score += 25; + if (curDir >= 0 && d == curDir) score += 4; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target, long targetId) +{ + string nd = DIRS[d]; + + if (d == lastCmdDir && head == lastCmdHead && (DateTime.Now - lastCmd).TotalMilliseconds < 450) + return; + + if (nd == lastSentDir && (DateTime.Now - lastCmd).TotalMilliseconds < 180) + return; + + int id = BtnForDir(d); + if (TryGetBtnPos(id, out int bx, out int by)) Move(bx, by); + Send(Out["ClickFurni"], id, 0); + + lastCmd = DateTime.Now; + lastSentDir = nd; + lastCmdDir = d; + lastCmdHead = head; + + lastAttemptDir = d; + lastAttemptHead = head; + lastAttemptAt = DateTime.Now; + lastTargetId = targetId; + + if (target.HasValue) + Dbg($"{reason} h=({head.x},{head.y}) t=({target.Value.x},{target.Value.y}) d={nd} b={id}", true); + else + Dbg($"{reason} h=({head.x},{head.y}) d={nd} b={id}", true); +} + +Log("Snake Auto (Campaign) v11 started"); +Log($"target kinds: {string.Join(",", TARGET_KINDS)}"); +Log($"buttons U={CTRL_UP} D={CTRL_DOWN} L={CTRL_LEFT} R={CTRL_RIGHT}"); + +while (Run) +{ + Delay(24); + CleanupMaps(); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + Dbg("head missing"); + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + if (prevHead.x != -1) + { + int hdx = head.Item1 - prevHead.x; + int hdy = head.Item2 - prevHead.y; + if (hdx != 0 || hdy != 0) + curDir = DirFromDelta(hdx, hdy); + } + + // stuck detection + if (head == lastAttemptHead && lastAttemptDir >= 0 && (DateTime.Now - lastAttemptAt).TotalMilliseconds > 520) + { + Next(head.Item1, head.Item2, lastAttemptDir, out int bx, out int by); + tempBlocked[(bx, by)] = DateTime.Now.AddMilliseconds(2200); + + if (lastTargetId != 0) + targetBlacklist[lastTargetId] = DateTime.Now.AddMilliseconds(3200); + + Dbg($"stuck: block ({bx},{by}) skip target {lastTargetId}", true); + lastAttemptDir = -1; + } + + prevHead = head; + + var blocks = FloorItems + .Where(i => i != null && i.Kind == 9109) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .ToHashSet(); + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targetsAll = FloorItems + .Where(i => i != null && TARGET_KINDS.Contains((int)i.Kind)) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Where(i => !blocks.Contains((i.Location.X, i.Location.Y))) + .Select(i => (x: i.Location.X, y: i.Location.Y, id: (long)i.Id, kind: (int)i.Kind)) + .ToList(); + + if (targetsAll.Count == 0) + { + int safe = Emergency(head, body, blocks); + Cmd(safe, "no_target_safe", head, null, 0); + continue; + } + + var targets = targetsAll + .Where(t => !targetBlacklist.TryGetValue(t.id, out var until) || until <= DateTime.Now) + .ToList(); + + if (targets.Count == 0) + targets = targetsAll; + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + int dsel = PickDir(head, (target.x, target.y), body, blocks); + Cmd(dsel, $"seek k={target.kind} id={target.id}", head, (target.x, target.y), target.id); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v12.csx b/Snake Auto (Campaign) v12.csx new file mode 100644 index 0000000..8cd8c4c --- /dev/null +++ b/Snake Auto (Campaign) v12.csx @@ -0,0 +1,285 @@ +/// @name Snake Auto (Campaign) v12 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int SNAKE_KIND = 5478; +static readonly int[] TARGET_KINDS = { 6559, 3666 }; + +const int GAME_MIN_X = 20; +const int GAME_MAX_X = 65; +const int GAME_MIN_Y = 20; +const int GAME_MAX_Y = 65; + +// Direction -> plate ids +const int CTRL_UP = 769406820; +const int CTRL_DOWN = 769407116; +const int CTRL_LEFT = 769407385; +const int CTRL_RIGHT = 769407085; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastDbg = DateTime.MinValue; +string lastSentDir = ""; +int curDir = -1; +(int x, int y) prevHead = (-1, -1); +int lastCmdDir = -1; +(int x, int y) lastCmdHead = (-999, -999); + +int lastAttemptDir = -1; +(int x, int y) lastAttemptHead = (-999, -999); +DateTime lastAttemptAt = DateTime.MinValue; +long lastTargetId = 0; + +Dictionary<(int x, int y), DateTime> tempBlocked = new Dictionary<(int, int), DateTime>(); +Dictionary targetBlacklist = new Dictionary(); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +void Dbg(string m, bool force = false) +{ + if (!force && (DateTime.Now - lastDbg).TotalMilliseconds < 220) return; + Log($"dbg: {m}"); + lastDbg = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int BtnForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetBtnPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) { x = 0; y = 0; return false; } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void CleanupMaps() +{ + var now = DateTime.Now; + foreach (var k in tempBlocked.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + tempBlocked.Remove(k); + foreach (var k in targetBlacklist.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + targetBlacklist.Remove(k); +} + +bool IsTempBlocked(int x, int y) + => tempBlocked.TryGetValue((x, y), out var until) && until > DateTime.Now; + +bool Bad(int x, int y, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + if (!InBoard(x, y)) return true; + if (blocks.Contains((x, y))) return true; + if (IsTempBlocked(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks, int cap) +{ + if (Bad(sx, sy, body, blocks)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body, blocks) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body, blocks)) continue; + + int space = Fill(nx, ny, body, blocks, 260); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + return bestDir; +} + +int PickDir((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + int desired = DirFromDelta(target.x - head.x, target.y - head.y); + var dirs = new List { desired, 0, 1, 2, 3 }; + + int bestDir = curDir >= 0 ? curDir : desired; + int bestScore = int.MinValue; + + foreach (int d in dirs.Distinct()) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body, blocks)) continue; + + int dist = Math.Abs(target.x - nx) + Math.Abs(target.y - ny); + int space = Fill(nx, ny, body, blocks, 260); + + int score = -dist * 6 + space * 2; + if (d == desired) score += 25; + if (curDir >= 0 && d == curDir) score += 4; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target, long targetId) +{ + string nd = DIRS[d]; + + if (d == lastCmdDir && head == lastCmdHead && (DateTime.Now - lastCmd).TotalMilliseconds < 450) + return; + + if (nd == lastSentDir && (DateTime.Now - lastCmd).TotalMilliseconds < 180) + return; + + int id = BtnForDir(d); + if (TryGetBtnPos(id, out int bx, out int by)) Move(bx, by); + Send(Out["ClickFurni"], id, 0); + + lastCmd = DateTime.Now; + lastSentDir = nd; + lastCmdDir = d; + lastCmdHead = head; + + lastAttemptDir = d; + lastAttemptHead = head; + lastAttemptAt = DateTime.Now; + lastTargetId = targetId; + + if (target.HasValue) + Dbg($"{reason} h=({head.x},{head.y}) t=({target.Value.x},{target.Value.y}) d={nd} b={id}", true); + else + Dbg($"{reason} h=({head.x},{head.y}) d={nd} b={id}", true); +} + +Log("Snake Auto (Campaign) v12 started"); +Log($"target kinds: {string.Join(",", TARGET_KINDS)}"); +Log($"buttons U={CTRL_UP} D={CTRL_DOWN} L={CTRL_LEFT} R={CTRL_RIGHT}"); + +while (Run) +{ + Delay(24); + CleanupMaps(); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + Dbg("head missing"); + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + if (prevHead.x != -1) + { + int hdx = head.Item1 - prevHead.x; + int hdy = head.Item2 - prevHead.y; + if (hdx != 0 || hdy != 0) + curDir = DirFromDelta(hdx, hdy); + } + + if (head == lastAttemptHead && lastAttemptDir >= 0 && (DateTime.Now - lastAttemptAt).TotalMilliseconds > 520) + { + Next(head.Item1, head.Item2, lastAttemptDir, out int bx, out int by); + tempBlocked[(bx, by)] = DateTime.Now.AddMilliseconds(2200); + if (lastTargetId != 0) targetBlacklist[lastTargetId] = DateTime.Now.AddMilliseconds(3200); + Dbg($"stuck: block ({bx},{by}) skip target {lastTargetId}", true); + lastAttemptDir = -1; + } + + prevHead = head; + + var blocks = FloorItems + .Where(i => i != null && i.Kind == 9109) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .ToHashSet(); + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targetsAll = FloorItems + .Where(i => i != null && TARGET_KINDS.Contains((int)i.Kind)) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Where(i => !blocks.Contains((i.Location.X, i.Location.Y))) + .Select(i => (x: i.Location.X, y: i.Location.Y, id: (long)i.Id, kind: (int)i.Kind)) + .ToList(); + + if (targetsAll.Count == 0) + { + int safe = Emergency(head, body, blocks); + Cmd(safe, "no_target_safe", head, null, 0); + continue; + } + + var targets = targetsAll + .Where(t => !targetBlacklist.TryGetValue(t.id, out var until) || until <= DateTime.Now) + .ToList(); + + if (targets.Count == 0) + targets = targetsAll; + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + int dsel = PickDir(head, (target.x, target.y), body, blocks); + Cmd(dsel, $"seek k={target.kind} id={target.id}", head, (target.x, target.y), target.id); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v13.csx b/Snake Auto (Campaign) v13.csx new file mode 100644 index 0000000..14ce050 --- /dev/null +++ b/Snake Auto (Campaign) v13.csx @@ -0,0 +1,287 @@ +/// @name Snake Auto (Campaign) v13 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int SNAKE_KIND = 5478; +static readonly int[] TARGET_KINDS = { 6559, 3666 }; + +const int GAME_MIN_X = 20; +const int GAME_MAX_X = 65; +const int GAME_MIN_Y = 20; +const int GAME_MAX_Y = 65; + +const int CTRL_UP = 769406820; +const int CTRL_DOWN = 769407116; +const int CTRL_LEFT = 769407385; +const int CTRL_RIGHT = 769407085; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastDbg = DateTime.MinValue; +int curDir = -1; +string lastSentDir = ""; +(int x, int y) prevHead = (-1, -1); + +int lastCmdDir = -1; +(int x, int y) lastCmdHead = (-999, -999); + +int lastAttemptDir = -1; +(int x, int y) lastAttemptHead = (-999, -999); +DateTime lastAttemptAt = DateTime.MinValue; +long lastTargetId = 0; + +Dictionary<(int x, int y), DateTime> tempBlocked = new Dictionary<(int, int), DateTime>(); +Dictionary targetBlacklist = new Dictionary(); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +void Dbg(string m, bool force = false) +{ + if (!force && (DateTime.Now - lastDbg).TotalMilliseconds < 220) return; + Log($"dbg: {m}"); + lastDbg = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int BtnForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetBtnPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) { x = 0; y = 0; return false; } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void CleanupMaps() +{ + var now = DateTime.Now; + foreach (var k in tempBlocked.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + tempBlocked.Remove(k); + foreach (var k in targetBlacklist.Where(kv => kv.Value <= now).Select(kv => kv.Key).ToList()) + targetBlacklist.Remove(k); +} + +bool IsTempBlocked(int x, int y) + => tempBlocked.TryGetValue((x, y), out var until) && until > DateTime.Now; + +bool Bad(int x, int y, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + if (!InBoard(x, y)) return true; + if (blocks.Contains((x, y))) return true; + if (IsTempBlocked(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks, int cap) +{ + if (Bad(sx, sy, body, blocks)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body, blocks) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body, blocks)) continue; + + int space = Fill(nx, ny, body, blocks, 260); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + return bestDir; +} + +int PickDir((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body, HashSet<(int x, int y)> blocks) +{ + int desired = DirFromDelta(target.x - head.x, target.y - head.y); + var dirs = new List { desired, 0, 1, 2, 3 }; + + int bestDir = curDir >= 0 ? curDir : desired; + int bestScore = int.MinValue; + + foreach (int d in dirs.Distinct()) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body, blocks)) continue; + + int dist = Math.Abs(target.x - nx) + Math.Abs(target.y - ny); + int space = Fill(nx, ny, body, blocks, 260); + + int score = -dist * 6 + space * 2; + if (d == desired) score += 25; + if (curDir >= 0 && d == curDir) score += 4; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target, long targetId) +{ + string nd = DIRS[d]; + + if (d == lastCmdDir && head == lastCmdHead && (DateTime.Now - lastCmd).TotalMilliseconds < 500) + return; + + if (nd == lastSentDir && (DateTime.Now - lastCmd).TotalMilliseconds < 200) + return; + + int id = BtnForDir(d); + if (TryGetBtnPos(id, out int bx, out int by)) Move(bx, by); + Send(Out["ClickFurni"], id, 0); + + lastCmd = DateTime.Now; + lastSentDir = nd; + lastCmdDir = d; + lastCmdHead = head; + + // Do NOT reset attempt timer for repeated same head+dir. + if (lastAttemptDir != d || lastAttemptHead != head) + { + lastAttemptDir = d; + lastAttemptHead = head; + lastAttemptAt = DateTime.Now; + lastTargetId = targetId; + } + + if (target.HasValue) + Dbg($"{reason} h=({head.x},{head.y}) t=({target.Value.x},{target.Value.y}) d={nd} b={id}", true); + else + Dbg($"{reason} h=({head.x},{head.y}) d={nd} b={id}", true); +} + +Log("Snake Auto (Campaign) v13 started"); +Log($"target kinds: {string.Join(",", TARGET_KINDS)}"); +Log($"buttons U={CTRL_UP} D={CTRL_DOWN} L={CTRL_LEFT} R={CTRL_RIGHT}"); + +while (Run) +{ + Delay(24); + CleanupMaps(); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + Dbg("head missing"); + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + if (prevHead.x != -1) + { + int hdx = head.Item1 - prevHead.x; + int hdy = head.Item2 - prevHead.y; + if (hdx != 0 || hdy != 0) + curDir = DirFromDelta(hdx, hdy); + } + + // Stuck: head did not move after attempted direction. + if (head == lastAttemptHead && lastAttemptDir >= 0 && (DateTime.Now - lastAttemptAt).TotalMilliseconds > 560) + { + Next(head.Item1, head.Item2, lastAttemptDir, out int bx, out int by); + tempBlocked[(bx, by)] = DateTime.Now.AddMilliseconds(2500); + if (lastTargetId != 0) targetBlacklist[lastTargetId] = DateTime.Now.AddMilliseconds(3000); + Dbg($"stuck: blocked ({bx},{by}), blacklisted target {lastTargetId}", true); + + lastAttemptDir = -1; + } + + prevHead = head; + + var blocks = FloorItems + .Where(i => i != null && i.Kind == 9109) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .ToHashSet(); + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targetsAll = FloorItems + .Where(i => i != null && TARGET_KINDS.Contains((int)i.Kind)) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Where(i => !blocks.Contains((i.Location.X, i.Location.Y))) + .Select(i => (x: i.Location.X, y: i.Location.Y, id: (long)i.Id, kind: (int)i.Kind)) + .ToList(); + + if (targetsAll.Count == 0) + { + int safe = Emergency(head, body, blocks); + Cmd(safe, "no_target_safe", head, null, 0); + continue; + } + + var targets = targetsAll + .Where(t => !targetBlacklist.TryGetValue(t.id, out var until) || until <= DateTime.Now) + .ToList(); + if (targets.Count == 0) targets = targetsAll; + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + int dsel = PickDir(head, (target.x, target.y), body, blocks); + Cmd(dsel, $"seek k={target.kind} id={target.id}", head, (target.x, target.y), target.id); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v4.csx b/Snake Auto (Campaign) v4.csx new file mode 100644 index 0000000..2ff11d3 --- /dev/null +++ b/Snake Auto (Campaign) v4.csx @@ -0,0 +1,282 @@ +/// @name Snake Auto (Campaign) v4 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int TARGET_KIND = 3666; +const int SNAKE_KIND = 5478; + +const int GAME_MIN_X = 37; +const int GAME_MAX_X = 59; +const int GAME_MIN_Y = 29; +const int GAME_MAX_Y = 51; + +// Confirmed controls +const int CTRL_UP = 769407385; +const int CTRL_RIGHT = 769406820; +const int CTRL_DOWN = 769407085; +const int CTRL_LEFT = 769407116; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastInfo = DateTime.MinValue; +string dir = "UP"; +string lastSentDir = ""; +(int x, int y) prevHead = (-1, -1); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); +HashSet<(int x, int y)> pathVisited = new HashSet<(int, int)>(); +Queue<((int x, int y) pos, int dist)> pathQueue = new Queue<((int, int), int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +bool IsInQueueArea() +{ + if (Self == null) return false; + return Self.Location.X == 59 && Self.Location.Y >= 29 && Self.Location.Y <= 51; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; +int DirToIdx(string d) => d == "UP" ? 0 : d == "DOWN" ? 1 : d == "LEFT" ? 2 : 3; + +int IdForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetButtonPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) + { + x = 0; + y = 0; + return false; + } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void Cmd(int d) +{ + string nd = DIRS[d]; + if (nd == lastSentDir && (DateTime.Now - lastCmd).TotalMilliseconds < 120) return; + + int id = IdForDir(d); + if (TryGetButtonPos(id, out int bx, out int by)) + Move(bx, by); + + Send(Out["ClickFurni"], id, 0); + + lastCmd = DateTime.Now; + lastSentDir = nd; + dir = nd; +} + +bool Bad(int x, int y, HashSet<(int x, int y)> body) +{ + if (!InBoard(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, int cap) +{ + if (Bad(sx, sy, body)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + int c = 0; + + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Dist(int sx, int sy, int tx, int ty, HashSet<(int x, int y)> body, int cap) +{ + if (sx == tx && sy == ty) return 0; + + pathVisited.Clear(); + pathQueue.Clear(); + + pathVisited.Add((sx, sy)); + pathQueue.Enqueue(((sx, sy), 0)); + + while (pathQueue.Count > 0) + { + var cur = pathQueue.Dequeue(); + if (cur.dist >= cap) return 999; + + for (int d = 0; d < 4; d++) + { + Next(cur.pos.x, cur.pos.y, d, out int nx, out int ny); + if (nx == tx && ny == ty) return cur.dist + 1; + if (!Bad(nx, ny, body) && pathVisited.Add((nx, ny))) + pathQueue.Enqueue(((nx, ny), cur.dist + 1)); + } + } + return -1; +} + +int Decide((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body, int curDir) +{ + int bestDir = curDir; + int bestScore = int.MinValue; + + for (int d = 0; d < 4; d++) + { + if (d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 180); + if (space < 8) continue; + + int dist = Dist(nx, ny, target.x, target.y, body, 180); + + int score = 0; + if (nx == target.x && ny == target.y) score += 10000; + if (dist > 0 && dist < 999) score += (220 - dist) * 7; + else if (dist == -1) score -= 200; + + score += Math.Min(space, 100) * 4; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body, int curDir) +{ + int bestDir = curDir; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (d == Opp(curDir)) continue; + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 100); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + return bestDir; +} + +Log("Snake Auto (Campaign) v4 started"); +Log($"Buttons: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); + +while (Run) +{ + Delay(20); + + if (IsInQueueArea()) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: in queue"); + lastInfo = DateTime.Now; + } + continue; + } + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: head not visible"); + lastInfo = DateTime.Now; + } + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) continue; + + // Infer current movement direction from head delta. + if (prevHead.x != -1) + { + int dx = head.Item1 - prevHead.x; + int dy = head.Item2 - prevHead.y; + if (dx != 0 || dy != 0) + dir = Math.Abs(dx) >= Math.Abs(dy) + ? (dx < 0 ? "LEFT" : "RIGHT") + : (dy < 0 ? "UP" : "DOWN"); + } + prevHead = head; + + // Body = all snake tiles in board except current head position. + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targets = FloorItems + .Where(i => i != null && i.Kind == TARGET_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (x: i.Location.X, y: i.Location.Y, state: (int)i.State)) + .ToList(); + + if (targets.Count == 0) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: no color tile in board"); + lastInfo = DateTime.Now; + } + continue; + } + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .ThenBy(t => t.state) + .First(); + + int curDir = DirToIdx(dir); + + // Never force reverse into body. + Next(head.Item1, head.Item2, curDir, out int fx, out int fy); + if (Bad(fx, fy, body)) + { + int e = Emergency(head, body, curDir); + Cmd(e); + continue; + } + + int best = Decide(head, (target.x, target.y), body, curDir); + Cmd(best); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v5.csx b/Snake Auto (Campaign) v5.csx new file mode 100644 index 0000000..00d7176 --- /dev/null +++ b/Snake Auto (Campaign) v5.csx @@ -0,0 +1,269 @@ +/// @name Snake Auto (Campaign) v5 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int TARGET_KIND = 3666; +const int SNAKE_KIND = 5478; + +const int GAME_MIN_X = 37; +const int GAME_MAX_X = 59; +const int GAME_MIN_Y = 29; +const int GAME_MAX_Y = 51; + +// Confirmed controls +const int CTRL_UP = 769407385; +const int CTRL_RIGHT = 769406820; +const int CTRL_DOWN = 769407085; +const int CTRL_LEFT = 769407116; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmdTime = DateTime.MinValue; +DateTime lastInfo = DateTime.MinValue; +string lastSentDir = ""; +int curDir = -1; // 0=UP 1=DOWN 2=LEFT 3=RIGHT +(int x, int y) prevHead = (-1, -1); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); +HashSet<(int x, int y)> pathVisited = new HashSet<(int, int)>(); +Queue<((int x, int y) pos, int dist)> pathQueue = new Queue<((int, int), int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) + return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int IdForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetButtonPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) + { + x = 0; + y = 0; + return false; + } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void Cmd(int d) +{ + string nd = DIRS[d]; + if (nd == lastSentDir && (DateTime.Now - lastCmdTime).TotalMilliseconds < 110) + return; + + int id = IdForDir(d); + + if (TryGetButtonPos(id, out int bx, out int by)) + Move(bx, by); + + Send(Out["ClickFurni"], id, 0); + + lastCmdTime = DateTime.Now; + lastSentDir = nd; +} + +bool Bad(int x, int y, HashSet<(int x, int y)> body) +{ + if (!InBoard(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, int cap) +{ + if (Bad(sx, sy, body)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Dist(int sx, int sy, int tx, int ty, HashSet<(int x, int y)> body, int cap) +{ + if (sx == tx && sy == ty) return 0; + + pathVisited.Clear(); + pathQueue.Clear(); + + pathVisited.Add((sx, sy)); + pathQueue.Enqueue(((sx, sy), 0)); + + while (pathQueue.Count > 0) + { + var cur = pathQueue.Dequeue(); + if (cur.dist >= cap) return 999; + + for (int d = 0; d < 4; d++) + { + Next(cur.pos.x, cur.pos.y, d, out int nx, out int ny); + if (nx == tx && ny == ty) return cur.dist + 1; + if (!Bad(nx, ny, body) && pathVisited.Add((nx, ny))) + pathQueue.Enqueue(((nx, ny), cur.dist + 1)); + } + } + return -1; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 120); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + return bestDir; +} + +int Decide((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestScore = int.MinValue; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 200); + if (space < 10) continue; + + int dist = Dist(nx, ny, target.x, target.y, body, 200); + + int score = 0; + if (nx == target.x && ny == target.y) score += 15000; + if (dist > 0 && dist < 999) score += (240 - dist) * 8; + else if (dist == -1) score -= 300; + + score += Math.Min(space, 120) * 4; + if (curDir >= 0 && d == curDir) score += 2; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +Log("Snake Auto (Campaign) v5 started"); +Log($"Buttons: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); + +while (Run) +{ + Delay(18); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: head not visible"); + lastInfo = DateTime.Now; + } + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + if (prevHead.x != -1) + { + int dx = head.Item1 - prevHead.x; + int dy = head.Item2 - prevHead.y; + if (dx != 0 || dy != 0) + curDir = DirFromDelta(dx, dy); + } + prevHead = head; + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targets = FloorItems + .Where(i => i != null && i.Kind == TARGET_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (x: i.Location.X, y: i.Location.Y)) + .ToList(); + + if (targets.Count == 0) + { + // No tile visible: stay safe, do not random reverse. + int safe = Emergency(head, body); + Cmd(safe); + continue; + } + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + if (curDir >= 0) + { + Next(head.Item1, head.Item2, curDir, out int fx, out int fy); + if (Bad(fx, fy, body)) + { + int emergency = Emergency(head, body); + Cmd(emergency); + continue; + } + } + + int best = Decide(head, target, body); + Cmd(best); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v6.csx b/Snake Auto (Campaign) v6.csx new file mode 100644 index 0000000..66efd23 --- /dev/null +++ b/Snake Auto (Campaign) v6.csx @@ -0,0 +1,287 @@ +/// @name Snake Auto (Campaign) v6 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int TARGET_KIND = 3666; +const int SNAKE_KIND = 5478; + +const int GAME_MIN_X = 37; +const int GAME_MAX_X = 59; +const int GAME_MIN_Y = 29; +const int GAME_MAX_Y = 51; + +// Confirmed controls +const int CTRL_UP = 769407385; +const int CTRL_RIGHT = 769406820; +const int CTRL_DOWN = 769407085; +const int CTRL_LEFT = 769407116; + +const bool DEBUG = true; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmdTime = DateTime.MinValue; +DateTime lastInfo = DateTime.MinValue; +DateTime lastDebug = DateTime.MinValue; +string lastSentDir = ""; +int curDir = -1; // 0=UP 1=DOWN 2=LEFT 3=RIGHT +(int x, int y) prevHead = (-1, -1); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); +HashSet<(int x, int y)> pathVisited = new HashSet<(int, int)>(); +Queue<((int x, int y) pos, int dist)> pathQueue = new Queue<((int, int), int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +void Dbg(string msg, bool force = false) +{ + if (!DEBUG) return; + if (!force && (DateTime.Now - lastDebug).TotalMilliseconds < 120) return; + Log($"dbg: {msg}"); + lastDebug = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) + return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int IdForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetButtonPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) + { + x = 0; + y = 0; + return false; + } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target) +{ + string nd = DIRS[d]; + if (nd == lastSentDir && (DateTime.Now - lastCmdTime).TotalMilliseconds < 110) + return; + + int id = IdForDir(d); + + if (TryGetButtonPos(id, out int bx, out int by)) + Move(bx, by); + + Send(Out["ClickFurni"], id, 0); + + lastCmdTime = DateTime.Now; + lastSentDir = nd; + + if (target.HasValue) + Dbg($"reason={reason} head=({head.x},{head.y}) target=({target.Value.x},{target.Value.y}) dir={nd} btn={id}", true); + else + Dbg($"reason={reason} head=({head.x},{head.y}) dir={nd} btn={id}", true); +} + +bool Bad(int x, int y, HashSet<(int x, int y)> body) +{ + if (!InBoard(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, int cap) +{ + if (Bad(sx, sy, body)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Dist(int sx, int sy, int tx, int ty, HashSet<(int x, int y)> body, int cap) +{ + if (sx == tx && sy == ty) return 0; + + pathVisited.Clear(); + pathQueue.Clear(); + + pathVisited.Add((sx, sy)); + pathQueue.Enqueue(((sx, sy), 0)); + + while (pathQueue.Count > 0) + { + var cur = pathQueue.Dequeue(); + if (cur.dist >= cap) return 999; + + for (int d = 0; d < 4; d++) + { + Next(cur.pos.x, cur.pos.y, d, out int nx, out int ny); + if (nx == tx && ny == ty) return cur.dist + 1; + if (!Bad(nx, ny, body) && pathVisited.Add((nx, ny))) + pathQueue.Enqueue(((nx, ny), cur.dist + 1)); + } + } + return -1; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 120); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + return bestDir; +} + +int Decide((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestScore = int.MinValue; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 200); + if (space < 10) continue; + + int dist = Dist(nx, ny, target.x, target.y, body, 200); + + int score = 0; + if (nx == target.x && ny == target.y) score += 15000; + if (dist > 0 && dist < 999) score += (240 - dist) * 8; + else if (dist == -1) score -= 300; + + score += Math.Min(space, 120) * 4; + if (curDir >= 0 && d == curDir) score += 2; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +Log("Snake Auto (Campaign) v6 started"); +Log($"Buttons: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); + +while (Run) +{ + Delay(18); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: head not visible"); + lastInfo = DateTime.Now; + } + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + if (prevHead.x != -1) + { + int dx = head.Item1 - prevHead.x; + int dy = head.Item2 - prevHead.y; + if (dx != 0 || dy != 0) + curDir = DirFromDelta(dx, dy); + } + prevHead = head; + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targets = FloorItems + .Where(i => i != null && i.Kind == TARGET_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (x: i.Location.X, y: i.Location.Y)) + .ToList(); + + if (targets.Count == 0) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 500) + { + Dbg($"reason=no_target_hold head=({head.Item1},{head.Item2})", true); + lastInfo = DateTime.Now; + } + continue; + } + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + if (curDir >= 0) + { + Next(head.Item1, head.Item2, curDir, out int fx, out int fy); + if (Bad(fx, fy, body)) + { + int emergency = Emergency(head, body); + Cmd(emergency, "front_blocked", head, target); + continue; + } + } + + int best = Decide(head, target, body); + Cmd(best, "seek_target", head, target); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v7.csx b/Snake Auto (Campaign) v7.csx new file mode 100644 index 0000000..f3a798d --- /dev/null +++ b/Snake Auto (Campaign) v7.csx @@ -0,0 +1,336 @@ +/// @name Snake Auto (Campaign) v7 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int TARGET_KIND = 3666; +const int SNAKE_KIND = 5478; + +const int GAME_MIN_X = 37; +const int GAME_MAX_X = 59; +const int GAME_MIN_Y = 29; +const int GAME_MAX_Y = 51; + +// Initial guess; script auto-corrects while running. +const int BTN_TL = 769407085; +const int BTN_TR = 769406820; +const int BTN_BL = 769407385; +const int BTN_BR = 769407116; + +// dirs: 0=UP 1=DOWN 2=LEFT 3=RIGHT +Dictionary dirBtn = new Dictionary +{ + [0] = BTN_TR, + [1] = BTN_BR, + [2] = BTN_BL, + [3] = BTN_TL +}; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmdTime = DateTime.MinValue; +DateTime lastInfo = DateTime.MinValue; +DateTime lastDebug = DateTime.MinValue; +string lastSentDir = ""; +int curDir = -1; +(int x, int y) prevHead = (-1, -1); + +int pendingBtn = 0; +(int x, int y) pendingHead = (-1, -1); +DateTime pendingAt = DateTime.MinValue; + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); +HashSet<(int x, int y)> pathVisited = new HashSet<(int, int)>(); +Queue<((int x, int y) pos, int dist)> pathQueue = new Queue<((int, int), int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +void Dbg(string msg, bool force = false) +{ + if (!force && (DateTime.Now - lastDebug).TotalMilliseconds < 120) return; + Log($"dbg: {msg}"); + lastDebug = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) + return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +bool TryGetButtonPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) + { + x = 0; + y = 0; + return false; + } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void LearnMappingFromMovement((int x, int y) head) +{ + if (pendingBtn == 0) return; + if ((DateTime.Now - pendingAt).TotalMilliseconds > 1200) + { + pendingBtn = 0; + return; + } + + int dx = head.x - pendingHead.x; + int dy = head.y - pendingHead.y; + if (dx == 0 && dy == 0) return; + + int movedDir = DirFromDelta(dx, dy); + int oldBtn = dirBtn[movedDir]; + + if (oldBtn != pendingBtn) + { + dirBtn[movedDir] = pendingBtn; + + // keep mapping one-to-one by swapping previous owner + foreach (var kv in dirBtn.ToList()) + { + if (kv.Key != movedDir && kv.Value == pendingBtn) + { + dirBtn[kv.Key] = oldBtn; + break; + } + } + + Dbg($"calib: {DIRS[movedDir]} now={pendingBtn} swapped_old={oldBtn}", true); + Dbg($"map: U={dirBtn[0]} D={dirBtn[1]} L={dirBtn[2]} R={dirBtn[3]}", true); + } + + pendingBtn = 0; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target) +{ + string nd = DIRS[d]; + if (nd == lastSentDir && (DateTime.Now - lastCmdTime).TotalMilliseconds < 110) + return; + + int id = dirBtn[d]; + if (TryGetButtonPos(id, out int bx, out int by)) + Move(bx, by); + + Send(Out["ClickFurni"], id, 0); + + pendingBtn = id; + pendingHead = head; + pendingAt = DateTime.Now; + + lastCmdTime = DateTime.Now; + lastSentDir = nd; + + if (target.HasValue) + Dbg($"reason={reason} head=({head.x},{head.y}) target=({target.Value.x},{target.Value.y}) dir={nd} btn={id}", true); + else + Dbg($"reason={reason} head=({head.x},{head.y}) dir={nd} btn={id}", true); +} + +bool Bad(int x, int y, HashSet<(int x, int y)> body) +{ + if (!InBoard(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, int cap) +{ + if (Bad(sx, sy, body)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Dist(int sx, int sy, int tx, int ty, HashSet<(int x, int y)> body, int cap) +{ + if (sx == tx && sy == ty) return 0; + + pathVisited.Clear(); + pathQueue.Clear(); + + pathVisited.Add((sx, sy)); + pathQueue.Enqueue(((sx, sy), 0)); + + while (pathQueue.Count > 0) + { + var cur = pathQueue.Dequeue(); + if (cur.dist >= cap) return 999; + + for (int d = 0; d < 4; d++) + { + Next(cur.pos.x, cur.pos.y, d, out int nx, out int ny); + if (nx == tx && ny == ty) return cur.dist + 1; + if (!Bad(nx, ny, body) && pathVisited.Add((nx, ny))) + pathQueue.Enqueue(((nx, ny), cur.dist + 1)); + } + } + return -1; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 120); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + return bestDir; +} + +int Decide((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestScore = int.MinValue; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 200); + if (space < 10) continue; + + int dist = Dist(nx, ny, target.x, target.y, body, 200); + + int score = 0; + if (nx == target.x && ny == target.y) score += 15000; + if (dist > 0 && dist < 999) score += (240 - dist) * 8; + else if (dist == -1) score -= 300; + + score += Math.Min(space, 120) * 4; + if (curDir >= 0 && d == curDir) score += 2; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +Log("Snake Auto (Campaign) v7 started"); +Log($"init map: U={dirBtn[0]} D={dirBtn[1]} L={dirBtn[2]} R={dirBtn[3]}"); + +while (Run) +{ + Delay(18); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: head not visible"); + lastInfo = DateTime.Now; + } + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + LearnMappingFromMovement(head); + + if (prevHead.x != -1) + { + int dx = head.Item1 - prevHead.x; + int dy = head.Item2 - prevHead.y; + if (dx != 0 || dy != 0) + curDir = DirFromDelta(dx, dy); + } + prevHead = head; + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targets = FloorItems + .Where(i => i != null && i.Kind == TARGET_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (x: i.Location.X, y: i.Location.Y)) + .ToList(); + + if (targets.Count == 0) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 500) + { + Dbg($"reason=no_target_hold head=({head.Item1},{head.Item2})", true); + lastInfo = DateTime.Now; + } + continue; + } + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + if (curDir >= 0) + { + Next(head.Item1, head.Item2, curDir, out int fx, out int fy); + if (Bad(fx, fy, body)) + { + int emergency = Emergency(head, body); + Cmd(emergency, "front_blocked", head, target); + continue; + } + } + + int best = Decide(head, target, body); + Cmd(best, "seek_target", head, target); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v8.csx b/Snake Auto (Campaign) v8.csx new file mode 100644 index 0000000..003802d --- /dev/null +++ b/Snake Auto (Campaign) v8.csx @@ -0,0 +1,171 @@ +/// @name Snake Auto (Campaign) v8 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int SNAKE_KIND = 5478; +static readonly int[] TARGET_KINDS = { 6559, 3666 }; + +const int GAME_MIN_X = 20; +const int GAME_MAX_X = 65; +const int GAME_MIN_Y = 20; +const int GAME_MAX_Y = 65; + +// Direction -> plate ids (kept from your latest calibration) +const int CTRL_UP = 769406820; +const int CTRL_DOWN = 769407116; +const int CTRL_LEFT = 769407385; +const int CTRL_RIGHT = 769407085; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastDbg = DateTime.MinValue; +int curDir = -1; +(int x, int y) prevHead = (-1, -1); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +void Dbg(string m, bool force = false) +{ + if (!force && (DateTime.Now - lastDbg).TotalMilliseconds < 220) return; + Log($"dbg: {m}"); + lastDbg = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int BtnForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetBtnPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) { x = 0; y = 0; return false; } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target) +{ + if ((DateTime.Now - lastCmd).TotalMilliseconds < 120) return; + + int id = BtnForDir(d); + if (TryGetBtnPos(id, out int bx, out int by)) Move(bx, by); + Send(Out["ClickFurni"], id, 0); + + lastCmd = DateTime.Now; + + if (target.HasValue) + Dbg($"{reason} head=({head.x},{head.y}) target=({target.Value.x},{target.Value.y}) dir={DIRS[d]} btn={id}", true); + else + Dbg($"{reason} head=({head.x},{head.y}) dir={DIRS[d]} btn={id}", true); +} + +bool Bad(int x, int y, HashSet<(int x, int y)> body) +{ + if (!InBoard(x, y)) return true; + return body.Contains((x, y)); +} + +int PickDir((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body) +{ + var order = new List(); + + int dx = target.x - head.x; + int dy = target.y - head.y; + + if (Math.Abs(dx) >= Math.Abs(dy)) + { + order.Add(dx < 0 ? 2 : 3); + order.Add(dy < 0 ? 0 : 1); + } + else + { + order.Add(dy < 0 ? 0 : 1); + order.Add(dx < 0 ? 2 : 3); + } + + order.AddRange(new[] { 0, 1, 2, 3 }); + + foreach (int d in order.Distinct()) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + Next(head.x, head.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body)) return d; + } + + return curDir >= 0 ? curDir : 0; +} + +Log("Snake Auto (Campaign) v8 started"); +Log($"target kinds: {string.Join(",", TARGET_KINDS)}"); +Log($"buttons U={CTRL_UP} D={CTRL_DOWN} L={CTRL_LEFT} R={CTRL_RIGHT}"); + +while (Run) +{ + Delay(20); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + Dbg("head missing"); + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + { + Dbg($"head out of board ({head.Item1},{head.Item2})"); + continue; + } + + if (prevHead.x != -1) + { + int hdx = head.Item1 - prevHead.x; + int hdy = head.Item2 - prevHead.y; + if (hdx != 0 || hdy != 0) curDir = DirFromDelta(hdx, hdy); + } + prevHead = head; + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targets = FloorItems + .Where(i => i != null && TARGET_KINDS.Contains((int)i.Kind)) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (x: i.Location.X, y: i.Location.Y, kind: (int)i.Kind, id: (long)i.Id)) + .ToList(); + + if (targets.Count == 0) + { + Dbg("no_target_hold", true); + continue; + } + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + int dsel = PickDir(head, (target.x, target.y), body); + Cmd(dsel, $"seek k={target.kind} id={target.id}", head, (target.x, target.y)); +} \ No newline at end of file diff --git a/Snake Auto (Campaign) v9.csx b/Snake Auto (Campaign) v9.csx new file mode 100644 index 0000000..6f333f2 --- /dev/null +++ b/Snake Auto (Campaign) v9.csx @@ -0,0 +1,233 @@ +/// @name Snake Auto (Campaign) v9 +using System; +using System.Linq; +using System.Collections.Generic; + +const long HEAD_ID = 769407982; +const int SNAKE_KIND = 5478; +static readonly int[] TARGET_KINDS = { 6559, 3666 }; + +const int GAME_MIN_X = 20; +const int GAME_MAX_X = 65; +const int GAME_MIN_Y = 20; +const int GAME_MAX_Y = 65; + +// Hard obstacle (blue block area) +const int BLOCK_MIN_X = 20; +const int BLOCK_MAX_X = 22; +const int BLOCK_MIN_Y = 20; +const int BLOCK_MAX_Y = 21; + +// Direction -> plate ids +const int CTRL_UP = 769406820; +const int CTRL_DOWN = 769407116; +const int CTRL_LEFT = 769407385; +const int CTRL_RIGHT = 769407085; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastDbg = DateTime.MinValue; +int curDir = -1; +(int x, int y) prevHead = (-1, -1); + +HashSet<(int x, int y)> fillVisited = new HashSet<(int, int)>(); +Queue<(int x, int y)> fillQueue = new Queue<(int, int)>(); + +bool InBoard(int x, int y) + => x >= GAME_MIN_X && x <= GAME_MAX_X && y >= GAME_MIN_Y && y <= GAME_MAX_Y; + +bool IsBlock(int x, int y) + => x >= BLOCK_MIN_X && x <= BLOCK_MAX_X && y >= BLOCK_MIN_Y && y <= BLOCK_MAX_Y; + +void Dbg(string m, bool force = false) +{ + if (!force && (DateTime.Now - lastDbg).TotalMilliseconds < 220) return; + Log($"dbg: {m}"); + lastDbg = DateTime.Now; +} + +void Next(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int Opp(int d) => d ^ 1; + +int DirFromDelta(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +int BtnForDir(int d) + => d == 0 ? CTRL_UP : d == 1 ? CTRL_DOWN : d == 2 ? CTRL_LEFT : CTRL_RIGHT; + +bool TryGetBtnPos(int id, out int x, out int y) +{ + var b = GetFloorItem(id); + if (b == null) { x = 0; y = 0; return false; } + x = b.Location.X; + y = b.Location.Y; + return true; +} + +void Cmd(int d, string reason, (int x, int y) head, (int x, int y)? target) +{ + if ((DateTime.Now - lastCmd).TotalMilliseconds < 120) return; + + int id = BtnForDir(d); + if (TryGetBtnPos(id, out int bx, out int by)) Move(bx, by); + Send(Out["ClickFurni"], id, 0); + + lastCmd = DateTime.Now; + + if (target.HasValue) + Dbg($"{reason} h=({head.x},{head.y}) t=({target.Value.x},{target.Value.y}) d={DIRS[d]} b={id}", true); + else + Dbg($"{reason} h=({head.x},{head.y}) d={DIRS[d]} b={id}", true); +} + +bool Bad(int x, int y, HashSet<(int x, int y)> body) +{ + if (!InBoard(x, y)) return true; + if (IsBlock(x, y)) return true; + return body.Contains((x, y)); +} + +int Fill(int sx, int sy, HashSet<(int x, int y)> body, int cap) +{ + if (Bad(sx, sy, body)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + fillVisited.Add((sx, sy)); + fillQueue.Enqueue((sx, sy)); + + int c = 0; + while (fillQueue.Count > 0 && c < cap) + { + var p = fillQueue.Dequeue(); + c++; + for (int d = 0; d < 4; d++) + { + Next(p.x, p.y, d, out int nx, out int ny); + if (!Bad(nx, ny, body) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +int Emergency((int x, int y) head, HashSet<(int x, int y)> body) +{ + int bestDir = curDir >= 0 ? curDir : 0; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int space = Fill(nx, ny, body, 260); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + + return bestDir; +} + +int PickDir((int x, int y) head, (int x, int y) target, HashSet<(int x, int y)> body) +{ + int desired = DirFromDelta(target.x - head.x, target.y - head.y); + + var dirs = new List { desired, 0, 1, 2, 3 }; + int bestDir = curDir >= 0 ? curDir : desired; + int bestScore = int.MinValue; + + foreach (int d in dirs.Distinct()) + { + if (curDir >= 0 && d == Opp(curDir)) continue; + + Next(head.x, head.y, d, out int nx, out int ny); + if (Bad(nx, ny, body)) continue; + + int dist = Math.Abs(target.x - nx) + Math.Abs(target.y - ny); + int space = Fill(nx, ny, body, 260); + + int score = 0; + score += -dist * 6; + score += space * 2; + if (d == desired) score += 30; + if (curDir >= 0 && d == curDir) score += 5; + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +Log("Snake Auto (Campaign) v9 started"); +Log($"target kinds: {string.Join(",", TARGET_KINDS)}"); +Log($"buttons U={CTRL_UP} D={CTRL_DOWN} L={CTRL_LEFT} R={CTRL_RIGHT}"); + +while (Run) +{ + Delay(20); + + var headItem = GetFloorItem(HEAD_ID); + if (headItem == null) + { + Dbg("head missing"); + continue; + } + + var head = (headItem.Location.X, headItem.Location.Y); + if (!InBoard(head.Item1, head.Item2)) + continue; + + if (prevHead.x != -1) + { + int hdx = head.Item1 - prevHead.x; + int hdy = head.Item2 - prevHead.y; + if (hdx != 0 || hdy != 0) curDir = DirFromDelta(hdx, hdy); + } + prevHead = head; + + var body = FloorItems + .Where(i => i != null && i.Kind == SNAKE_KIND) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (i.Location.X, i.Location.Y)) + .Where(p => !(p.Item1 == head.Item1 && p.Item2 == head.Item2)) + .ToHashSet(); + + var targets = FloorItems + .Where(i => i != null && TARGET_KINDS.Contains((int)i.Kind)) + .Where(i => InBoard(i.Location.X, i.Location.Y)) + .Select(i => (x: i.Location.X, y: i.Location.Y, kind: (int)i.Kind, id: (long)i.Id)) + .ToList(); + + if (targets.Count == 0) + { + int safe = Emergency(head, body); + Cmd(safe, "no_target_safe", head, null); + continue; + } + + var target = targets + .OrderBy(t => Math.Abs(t.x - head.Item1) + Math.Abs(t.y - head.Item2)) + .First(); + + int dir = PickDir(head, (target.x, target.y), body); + Cmd(dir, $"seek k={target.kind} id={target.id}", head, (target.x, target.y)); +} \ No newline at end of file diff --git a/Snake Auto (Campaign).csx b/Snake Auto (Campaign).csx new file mode 100644 index 0000000..835a777 --- /dev/null +++ b/Snake Auto (Campaign).csx @@ -0,0 +1,115 @@ +/// @name Snake Auto (Campaign) +using System; +using System.Linq; + +// Fixed from your room +const long HEAD_ID = 769407982; +const int TARGET_KIND = 3666; // Color Tile kind + +// Pad button IDs (as confirmed by you) +const int PAD_TOP_LEFT = 769407085; +const int PAD_TOP_RIGHT = 769406820; +const int PAD_BOTTOM_LEFT = 769407385; +const int PAD_BOTTOM_RIGHT = 769407116; + +// Direction mapping (tuned for this room) +const int CTRL_UP = PAD_BOTTOM_LEFT; +const int CTRL_RIGHT = PAD_TOP_RIGHT; +const int CTRL_DOWN = PAD_TOP_LEFT; +const int CTRL_LEFT = PAD_BOTTOM_RIGHT; + +DateTime lastCmd = DateTime.MinValue; +DateTime lastInfo = DateTime.MinValue; +int lastDir = -1; // 0=UP 1=DOWN 2=LEFT 3=RIGHT + +bool TryGetButton(int dir, out int id, out int x, out int y) +{ + id = dir == 0 ? CTRL_UP : dir == 1 ? CTRL_DOWN : dir == 2 ? CTRL_LEFT : CTRL_RIGHT; + var btn = GetFloorItem(id); + if (btn == null) + { + x = 0; + y = 0; + return false; + } + + x = btn.Location.X; + y = btn.Location.Y; + return true; +} + +void SendDir(int dir) +{ + if ((DateTime.Now - lastCmd).TotalMilliseconds < 140 && dir == lastDir) + return; + + if (TryGetButton(dir, out int id, out int x, out int y)) + { + // This game mode reacts when user steps on the plate. + Move(x, y); + } + else + { + int fallbackId = dir == 0 ? CTRL_UP : dir == 1 ? CTRL_DOWN : dir == 2 ? CTRL_LEFT : CTRL_RIGHT; + Send(Out["ClickFurni"], fallbackId, 0); + } + + lastCmd = DateTime.Now; + lastDir = dir; +} + +int ToDir(int dx, int dy) +{ + if (Math.Abs(dx) >= Math.Abs(dy)) + return dx < 0 ? 2 : 3; + return dy < 0 ? 0 : 1; +} + +Log("Snake Auto (Campaign) v4 started"); +Log($"HeadId={HEAD_ID}, TargetKind={TARGET_KIND}"); +Log($"Buttons: U={CTRL_UP} R={CTRL_RIGHT} D={CTRL_DOWN} L={CTRL_LEFT}"); + +while (Run) +{ + Delay(40); + + var head = GetFloorItem(HEAD_ID); + if (head == null) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log("waiting: head not visible"); + lastInfo = DateTime.Now; + } + continue; + } + + var targets = FloorItems + .Where(i => i != null && i.Kind == TARGET_KIND) + .ToList(); + + if (targets.Count == 0) + { + if ((DateTime.Now - lastInfo).TotalMilliseconds > 1500) + { + Log($"waiting: no target tiles kind {TARGET_KIND}"); + lastInfo = DateTime.Now; + } + continue; + } + + + var target = targets + .OrderBy(t => Math.Abs(t.Location.X - head.Location.X) + Math.Abs(t.Location.Y - head.Location.Y)) + .ThenBy(t => t.State) + .First(); + + int dx = target.Location.X - head.Location.X; + int dy = target.Location.Y - head.Location.Y; + + if (dx == 0 && dy == 0) + continue; + + int dir = ToDir(dx, dy); + SendDir(dir); +} \ No newline at end of file diff --git a/Snake Debug Watch.csx b/Snake Debug Watch.csx new file mode 100644 index 0000000..c10e5a9 --- /dev/null +++ b/Snake Debug Watch.csx @@ -0,0 +1,37 @@ +/// @name Snake Debug Watch +long TARGET_ID = 769407982; +long[] BTN_IDS = { 769407085, 769407116, 769407385, 769406820 }; + +int tick = 0; +int lastX = int.MinValue, lastY = int.MinValue, lastState = int.MinValue; + +Log("Snake Debug Watch started"); + +while (Run) +{ + Delay(100); + tick++; + + var target = GetFloorItem(TARGET_ID); + if (target == null) + { + if (tick % 10 == 0) + Log("target missing"); + continue; + } + + if (target.Location.X != lastX || target.Location.Y != lastY || target.State != lastState) + { + Log($"target: ({target.Location.X},{target.Location.Y}) state={target.State} kind={target.Kind}"); + lastX = target.Location.X; + lastY = target.Location.Y; + lastState = target.State; + } + + if (tick % 20 == 0) + { + int k7100 = FloorItems.Count(i => i != null && i.Kind == 7100); + int k5068 = FloorItems.Count(i => i != null && i.Kind == 5068); + Log($"k7100={k7100}, k5068={k5068}"); + } +} \ No newline at end of file diff --git a/Snake Target Probe.csx b/Snake Target Probe.csx new file mode 100644 index 0000000..a61bd39 --- /dev/null +++ b/Snake Target Probe.csx @@ -0,0 +1,47 @@ +/// @name Snake Target Probe +using System; +using System.Linq; +using System.Collections.Generic; + +const int MIN_X = 37, MAX_X = 59, MIN_Y = 29, MAX_Y = 51; + +bool InBoard(dynamic i) + => i != null && i.Location.X >= MIN_X && i.Location.X <= MAX_X && i.Location.Y >= MIN_Y && i.Location.Y <= MAX_Y; + +var map = new Dictionary(); +foreach (var i in FloorItems) +{ + if (!InBoard(i)) continue; + map[i.Id] = ((int)i.Kind, i.Location.X, i.Location.Y, (int)i.State); +} + +Log("Snake Target Probe started"); +Log("Watching board item state/move changes..."); + +while (Run) +{ + Delay(100); + + foreach (var i in FloorItems) + { + if (!InBoard(i)) continue; + + int kind = (int)i.Kind; + int x = i.Location.X; + int y = i.Location.Y; + int state = (int)i.State; + + if (!map.TryGetValue(i.Id, out var prev)) + { + map[i.Id] = (kind, x, y, state); + Log($"add id={i.Id} kind={kind} pos=({x},{y}) state={state}"); + continue; + } + + if (prev.x != x || prev.y != y || prev.state != state) + { + Log($"chg id={i.Id} kind={kind} ({prev.x},{prev.y})/{prev.state} -> ({x},{y})/{state}"); + map[i.Id] = (kind, x, y, state); + } + } +} \ No newline at end of file diff --git a/Snake [28.12.25] V1.0.csx b/Snake [28.12.25] V1.0.csx new file mode 100644 index 0000000..c3fcb10 --- /dev/null +++ b/Snake [28.12.25] V1.0.csx @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// --- KONFIGURATION --- +const int GAME_MIN_X = 9; +const int GAME_MAX_X = 28; +const int GAME_MIN_Y = 11; +const int GAME_MAX_Y = 30; + +const int BLOCKED_MIN_X = 17; +const int BLOCKED_MAX_X = 20; +const int BLOCKED_MIN_Y = 19; +const int BLOCKED_MAX_Y = 22; + +const int KIND_SNAKE = 7100; +const int KIND_FOOD = 5068; + +const int CTRL_UP = 2147418172; +const int CTRL_RIGHT = 2147418173; +const int CTRL_DOWN = 2147418174; +const int CTRL_LEFT = 2147418175; + +// --- GLOBALE VARIABLEN --- +List<(int x, int y)> body = new List<(int x, int y)>(200); +HashSet<(int x, int y)> bodySet = new HashSet<(int x, int y)>(200); +(int x, int y) food = (-1, -1); + +string dir = "DOWN"; +string lastSentDir = ""; +DateTime lastCmdTime = DateTime.MinValue; + +int len = 0; +bool ate = false; + +static readonly string[] DIRS = { "UP", "DOWN", "LEFT", "RIGHT" }; +HashSet<(int, int)> fillVisited = new HashSet<(int, int)>(300); +Queue<(int, int)> fillQueue = new Queue<(int, int)>(300); +HashSet<(int, int)> pathVisited = new HashSet<(int, int)>(200); +Queue<((int x, int y) pos, int dist)> pathQueue = new Queue<((int, int), int)>(200); + +// --- HILFSFUNKTIONEN --- +bool Wall(int x, int y) => x < GAME_MIN_X || x > GAME_MAX_X || y < GAME_MIN_Y || y > GAME_MAX_Y; +bool Blk(int x, int y) => x >= BLOCKED_MIN_X && x <= BLOCKED_MAX_X && y >= BLOCKED_MIN_Y && y <= BLOCKED_MAX_Y; + +void Nxt(int x, int y, int d, out int nx, out int ny) +{ + nx = x + (d == 3 ? 1 : d == 2 ? -1 : 0); + ny = y + (d == 1 ? 1 : d == 0 ? -1 : 0); +} + +int OppIdx(int d) => d ^ 1; + +bool Bad(int x, int y, bool ignoreTail) +{ + if (Wall(x, y) || Blk(x, y)) return true; + if (body.Count == 0) return false; + + int checkLen = ignoreTail && !ate ? body.Count - 1 : body.Count; + for (int i = 0; i < checkLen; i++) + if (body[i].x == x && body[i].y == y) return true; + return false; +} + +void Cmd(int dIdx) +{ + string d = DIRS[dIdx]; + if (d == lastSentDir && (DateTime.Now - lastCmdTime).TotalMilliseconds < 80) return; + + int id = dIdx == 0 ? CTRL_UP : dIdx == 1 ? CTRL_DOWN : dIdx == 2 ? CTRL_LEFT : CTRL_RIGHT; + Send(Out["ClickFurni"], id, 0); + + lastCmdTime = DateTime.Now; + lastSentDir = d; + dir = d; +} + +int Fill(int sx, int sy, int limit) +{ + if (Bad(sx, sy, false)) return 0; + + fillVisited.Clear(); + fillQueue.Clear(); + + fillQueue.Enqueue((sx, sy)); + fillVisited.Add((sx, sy)); + int c = 0; + + while (fillQueue.Count > 0 && c < limit) + { + var (x, y) = fillQueue.Dequeue(); + c++; + + for (int d = 0; d < 4; d++) + { + Nxt(x, y, d, out int nx, out int ny); + if (!Bad(nx, ny, false) && fillVisited.Add((nx, ny))) + fillQueue.Enqueue((nx, ny)); + } + } + return c; +} + +// Pathfinding mit hΓΆherem Default-Limit +int GetPathDistance(int sx, int sy, int tx, int ty, int limit = 150) +{ + if (sx == tx && sy == ty) return 0; + + pathVisited.Clear(); + pathQueue.Clear(); + + pathQueue.Enqueue(((sx, sy), 0)); + pathVisited.Add((sx, sy)); + + while (pathQueue.Count > 0) + { + var curr = pathQueue.Dequeue(); + if (curr.dist >= limit) return 999; + + for (int d = 0; d < 4; d++) + { + Nxt(curr.pos.x, curr.pos.y, d, out int nx, out int ny); + + if (nx == tx && ny == ty) return curr.dist + 1; + if (!Bad(nx, ny, false) && pathVisited.Add((nx, ny))) + pathQueue.Enqueue(((nx, ny), curr.dist + 1)); + } + } + return -1; +} + +int DecideMove(int hx, int hy, int fx, int fy, int curDirIdx) +{ + int bestDir = curDirIdx; + int bestScore = int.MinValue; + + // HΓΆhere Limits fΓΌr besseres Pathfinding + int fillLimit = len > 100 ? 100 : len > 50 ? 150 : 200; + int pathLimit = len > 100 ? 80 : len > 50 ? 120 : 150; + int minSpace = Math.Max(len / 4, 10); // Noch toleranter + + var tail = body.Count > 0 ? body[body.Count - 1] : (-1, -1); + + // PrΓΌfe ob Food am Rand ist + bool foodAtEdge = fx <= GAME_MIN_X + 1 || fx >= GAME_MAX_X - 1 || + fy <= GAME_MIN_Y + 1 || fy >= GAME_MAX_Y - 1; + + for (int d = 0; d < 4; d++) + { + if (d == OppIdx(curDirIdx)) continue; + + Nxt(hx, hy, d, out int nx, out int ny); + if (Bad(nx, ny, true)) continue; + + int space = Fill(nx, ny, fillLimit); + if (space < minSpace) continue; + + int score = 0; + + // ESSEN DIREKT DANEBEN = HΓ–CHSTE PRIORITΓ„T! + if (nx == fx && ny == fy) + { + score += 10000; + } + else + { + int distFood = GetPathDistance(nx, ny, fx, fy, pathLimit); + if (distFood > 0 && distFood < 999) + { + // StΓ€rkerer Food-Bonus - Essen ist wichtiger! + score += (200 - distFood) * 8; + } + else if (distFood == -1) + { + // Kein Weg zum Essen gefunden - leichter Malus aber nicht zu hart + score -= 100; + } + } + + // Platz-Bonus (weniger gewichtet als vorher) + score += Math.Min(space, 100) * 5; + + // Safety: Schwanz erreichbar? + if (tail.Item1 != -1 && space < fillLimit) + { + int distTail = GetPathDistance(nx, ny, tail.Item1, tail.Item2, 30); + if (distTail > 0) score += 150; + else if (space < 40) score -= 300; + } + + // WandnΓ€he - NUR wenn Food NICHT am Rand ist! + // Wenn Food am Rand -> kein Penalty fΓΌr RandnΓ€he + if (!foodAtEdge && len > 50) + { + if (nx <= GAME_MIN_X + 1 || nx >= GAME_MAX_X - 1) score -= 20; + if (ny <= GAME_MIN_Y + 1 || ny >= GAME_MAX_Y - 1) score -= 20; + } + + if (score > bestScore) + { + bestScore = score; + bestDir = d; + } + } + + return bestDir; +} + +int EmergencyMove(int hx, int hy, int curDirIdx) +{ + int bestDir = curDirIdx; + int bestSpace = -1; + + for (int d = 0; d < 4; d++) + { + if (d == OppIdx(curDirIdx)) continue; + Nxt(hx, hy, d, out int nx, out int ny); + if (Bad(nx, ny, true)) continue; + + int space = Fill(nx, ny, 60); + if (space > bestSpace) + { + bestSpace = space; + bestDir = d; + } + } + return bestDir; +} + +int DirToIdx(string d) => d == "UP" ? 0 : d == "DOWN" ? 1 : d == "LEFT" ? 2 : 3; + +Log("SNAKE V16 - EDGE FOOD FIX"); + +DateTime lastFullCalc = DateTime.MinValue; +int cachedMove = -1; + +while (Run) +{ + Delay(15); + + var curSnake = new HashSet<(int x, int y)>(); + food = (-1, -1); + + foreach (var item in FloorItems) + { + if (item == null) continue; + int k; + try { k = (int)item.Kind; } catch { continue; } + + if (k == KIND_SNAKE && !Wall(item.Location.X, item.Location.Y)) + curSnake.Add((item.Location.X, item.Location.Y)); + else if (k == KIND_FOOD) + food = (item.Location.X, item.Location.Y); + } + + int curLen = curSnake.Count; + if (curLen == 0) { body.Clear(); bodySet.Clear(); len = 0; continue; } + + if (body.Count == 0) + { + var sorted = curSnake.OrderBy(p => p.y).ThenBy(p => p.x).ToList(); + body = new List<(int x, int y)>(sorted); + bodySet = new HashSet<(int x, int y)>(sorted); + len = curLen; + continue; + } + + var head = body[0]; + (int x, int y) newHead = (-1, -1); + + for (int d = 0; d < 4; d++) + { + Nxt(head.x, head.y, d, out int nx, out int ny); + if (curSnake.Contains((nx, ny)) && !bodySet.Contains((nx, ny))) + { + newHead = (nx, ny); + if ((DateTime.Now - lastCmdTime).TotalMilliseconds > 400) dir = DIRS[d]; + break; + } + } + + if (newHead.x != -1) + { + body.Insert(0, newHead); + bodySet.Add(newHead); + ate = curLen > len; + while (body.Count > curLen) + { + var tail = body[body.Count - 1]; + bodySet.Remove(tail); + body.RemoveAt(body.Count - 1); + } + len = curLen; + cachedMove = -1; + } + + if (body.Count == 0) continue; + head = body[0]; + int curDirIdx = DirToIdx(dir); + + // NOTFALL: Wand direkt vor uns? + Nxt(head.x, head.y, curDirIdx, out int frontX, out int frontY); + if (Bad(frontX, frontY, true)) + { + int emergency = EmergencyMove(head.x, head.y, curDirIdx); + Cmd(emergency); + continue; + } + + if (food.x == -1) continue; + + // SOFORT ESSEN wenn direkt daneben! + for (int d = 0; d < 4; d++) + { + if (d == OppIdx(curDirIdx)) continue; + Nxt(head.x, head.y, d, out int nx, out int ny); + + if (nx == food.x && ny == food.y && !Bad(nx, ny, true)) + { + int spaceAfter = Fill(nx, ny, 80); + if (spaceAfter >= Math.Max(len / 4, 8)) + { + Cmd(d); + goto nextLoop; + } + } + } + + // Normale KI - ΓΆfter neu berechnen fΓΌr bessere Reaktion + double msSinceCalc = (DateTime.Now - lastFullCalc).TotalMilliseconds; + int recalcInterval = len > 100 ? 100 : len > 50 ? 70 : 50; + + if (cachedMove == -1 || msSinceCalc > recalcInterval) + { + cachedMove = DecideMove(head.x, head.y, food.x, food.y, curDirIdx); + lastFullCalc = DateTime.Now; + } + + if (cachedMove != curDirIdx) + Cmd(cachedMove); + + nextLoop:; +} \ No newline at end of file diff --git a/Tetris [28.12.25] V1.0.csx b/Tetris [28.12.25] V1.0.csx new file mode 100644 index 0000000..b8c0da9 --- /dev/null +++ b/Tetris [28.12.25] V1.0.csx @@ -0,0 +1,1115 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// ═══════════════════════════════════════════════════════════════════════════════ +// TETRIS BOT V2 - Enhanced with Instant Drop, Hold/Swap, and Preview Look-ahead +// ═══════════════════════════════════════════════════════════════════════════════ + +// ───────────────────────────────────────────────────────────────────────────────── +// TIMING CONFIGURATION (tune these for optimal performance) +// ───────────────────────────────────────────────────────────────────────────────── +int sameCommandDelay = 60; // ms between same direction commands (increased) +int verifyTimeout = 230; // ms to wait for move confirmation +int downDelay = 80; // ms between down presses (if not using instant) +int landingTimeout = 300; // ms without movement = piece landed +int swapCooldown = 400; // ms between swap actions + +// ───────────────────────────────────────────────────────────────────────────────── +// FIELD COORDINATES +// ───────────────────────────────────────────────────────────────────────────────── +const int FIELD_MIN_X = 15; +const int FIELD_MAX_X = 24; +const int FIELD_MIN_Y = 12; +const int FIELD_MAX_Y = 32; + +// ───────────────────────────────────────────────────────────────────────────────── +// SPAWN AREA (where controlled blocks appear) +// ───────────────────────────────────────────────────────────────────────────────── +const int SPAWN_MIN_X = 18; +const int SPAWN_MAX_X = 21; +const int SPAWN_MIN_Y = 12; +const int SPAWN_MAX_Y = 13; + +// ───────────────────────────────────────────────────────────────────────────────── +// PREVIEW AREAS (next 3 pieces) +// ───────────────────────────────────────────────────────────────────────────────── +const int PREVIEW1_MIN_X = 9; +const int PREVIEW1_MAX_X = 13; +const int PREVIEW1_MIN_Y = 14; +const int PREVIEW1_MAX_Y = 16; + +const int PREVIEW2_MIN_X = 9; +const int PREVIEW2_MAX_X = 13; +const int PREVIEW2_MIN_Y = 18; +const int PREVIEW2_MAX_Y = 21; + +const int PREVIEW3_MIN_X = 9; +const int PREVIEW3_MAX_X = 13; +const int PREVIEW3_MIN_Y = 22; +const int PREVIEW3_MAX_Y = 25; + +// ───────────────────────────────────────────────────────────────────────────────── +// HOLD/SWAP AREA +// ───────────────────────────────────────────────────────────────────────────────── +const int HOLD_MIN_X = 24; +const int HOLD_MAX_X = 26; +const int HOLD_MIN_Y = 13; +const int HOLD_MAX_Y = 18; + +// ───────────────────────────────────────────────────────────────────────────────── +// CONTROL FURNI IDs +// ───────────────────────────────────────────────────────────────────────────────── +const int CTRL_LEFT = 2147418115; +const int CTRL_RIGHT = 2147418116; +const int CTRL_DOWN = 2147418113; +const int CTRL_INSTANT_DOWN = 2147418114; +const int CTRL_ROTATE = 2147418118; // TODO: UPDATE THIS WITH CORRECT ID! +const int CTRL_SWAP = 2147418117; + +// ───────────────────────────────────────────────────────────────────────────────── +// COMMAND TIMESTAMPS +// ───────────────────────────────────────────────────────────────────────────────── +DateTime lastLeftCmd = DateTime.MinValue; +DateTime lastRightCmd = DateTime.MinValue; +DateTime lastRotateCmd = DateTime.MinValue; +DateTime lastDownCmd = DateTime.MinValue; +DateTime lastSwapCmd = DateTime.MinValue; +DateTime lastMoveDetected = DateTime.MinValue; +DateTime spawnTime = DateTime.MinValue; + +// ───────────────────────────────────────────────────────────────────────────────── +// PENDING COMMAND TRACKING (for verification) +// ───────────────────────────────────────────────────────────────────────────────── +int pendingLeftFromX = -1; +int pendingRightFromX = -1; +int pendingRotateFromRot = -1; + +// ───────────────────────────────────────────────────────────────────────────────── +// CURRENT STATE +// ───────────────────────────────────────────────────────────────────────────────── +int targetX = -1; +int targetRot = -1; +int currentX = -1; +int currentRot = -1; +char currentPiece = '?'; + +bool pieceActive = false; +bool positioned = false; +bool useInstantDrop = true; // Set to false if instant drop causes issues +DateTime lastDropTime = DateTime.MinValue; +int dropCooldown = 350; // ms to wait after drop before detecting new piece + +HashSet pieceIds = new HashSet(); + +// ───────────────────────────────────────────────────────────────────────────────── +// HOLD/SWAP STATE +// ───────────────────────────────────────────────────────────────────────────────── +char heldPiece = '?'; +bool canSwap = true; +bool hasSwappedThisTurn = false; + +// ───────────────────────────────────────────────────────────────────────────────── +// PREVIEW STATE +// ───────────────────────────────────────────────────────────────────────────────── +char nextPiece1 = '?'; +char nextPiece2 = '?'; +char nextPiece3 = '?'; + +// ═══════════════════════════════════════════════════════════════════════════════ +// DATA STRUCTURES +// ═══════════════════════════════════════════════════════════════════════════════ + +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); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// TETROMINO SHAPES (normalized to top-left corner) +// ═══════════════════════════════════════════════════════════════════════════════ + +var shapes = new Dictionary { + // I-piece: 4 wide, 2 rotations + ["I0"] = new[] {0,0, 1,0, 2,0, 3,0}, + ["I1"] = new[] {0,0, 0,1, 0,2, 0,3}, + + // O-piece: 2x2, 1 rotation (no change) + ["O0"] = new[] {0,0, 1,0, 0,1, 1,1}, + + // T-piece: 4 rotations + ["T0"] = new[] {0,0, 1,0, 2,0, 1,1}, + ["T1"] = new[] {1,0, 0,1, 1,1, 1,2}, + ["T2"] = new[] {1,0, 0,1, 1,1, 2,1}, + ["T3"] = new[] {0,0, 0,1, 1,1, 0,2}, + + // S-piece: 2 rotations + ["S0"] = new[] {1,0, 2,0, 0,1, 1,1}, + ["S1"] = new[] {0,0, 0,1, 1,1, 1,2}, + + // Z-piece: 2 rotations + ["Z0"] = new[] {0,0, 1,0, 1,1, 2,1}, + ["Z1"] = new[] {1,0, 0,1, 1,1, 0,2}, + + // L-piece: 4 rotations + ["L0"] = new[] {0,0, 0,1, 0,2, 1,2}, + ["L1"] = new[] {0,0, 1,0, 2,0, 0,1}, + ["L2"] = new[] {0,0, 1,0, 1,1, 1,2}, + ["L3"] = new[] {2,0, 0,1, 1,1, 2,1}, + + // J-piece: 4 rotations + ["J0"] = new[] {1,0, 1,1, 0,2, 1,2}, + ["J1"] = new[] {0,0, 0,1, 1,1, 2,1}, + ["J2"] = new[] {0,0, 1,0, 0,1, 0,2}, + ["J3"] = new[] {0,0, 1,0, 2,0, 2,1} +}; + +// ═══════════════════════════════════════════════════════════════════════════════ +// BLOCK DETECTION - Using Kind property (TypeId) +// ═══════════════════════════════════════════════════════════════════════════════ + +// Piece Kind values (confirmed from debug) +HashSet pieceKinds = new HashSet { + 7100, // I piece + 7101, // J piece + 7108, // T piece + 7121, // Z piece + 7124, // L piece + 7138, // S piece + 5482, // O piece +}; + +// IGNORE these Kinds completely +HashSet ignoreKinds = new HashSet { + 4071, // Decoration/frame + 10469, // Shadow/overlay (always 5 blocks, follows active piece) +}; + +int GetKind(dynamic item) +{ + try { return (int)item.Kind; } catch { return -1; } +} + +bool IsPieceKind(int kind) => pieceKinds.Contains(kind); +bool IsIgnoredKind(int kind) => ignoreKinds.Contains(kind); + +// Wrapper for backward compatibility - checks Kind of item +bool IsBlockItem(dynamic item) +{ + int kind = GetKind(item); + if (kind < 0) return false; + if (IsIgnoredKind(kind)) return false; + return IsPieceKind(kind); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// COORDINATE HELPERS +// ═══════════════════════════════════════════════════════════════════════════════ + +bool IsInField(int x, int y) => x >= FIELD_MIN_X && x <= FIELD_MAX_X && y >= FIELD_MIN_Y && y <= FIELD_MAX_Y; +bool IsInSpawn(int x, int y) => x >= SPAWN_MIN_X && x <= SPAWN_MAX_X && y >= SPAWN_MIN_Y && y <= SPAWN_MAX_Y; +bool IsInPreview1(int x, int y) => x >= PREVIEW1_MIN_X && x <= PREVIEW1_MAX_X && y >= PREVIEW1_MIN_Y && y <= PREVIEW1_MAX_Y; +bool IsInPreview2(int x, int y) => x >= PREVIEW2_MIN_X && x <= PREVIEW2_MAX_X && y >= PREVIEW2_MIN_Y && y <= PREVIEW2_MAX_Y; +bool IsInPreview3(int x, int y) => x >= PREVIEW3_MIN_X && x <= PREVIEW3_MAX_X && y >= PREVIEW3_MIN_Y && y <= PREVIEW3_MAX_Y; +bool IsInHold(int x, int y) => x >= HOLD_MIN_X && x <= HOLD_MAX_X && y >= HOLD_MIN_Y && y <= HOLD_MAX_Y; + +// ═══════════════════════════════════════════════════════════════════════════════ +// COMMAND SENDERS +// ═══════════════════════════════════════════════════════════════════════════════ + +void SendLeft() +{ + pendingLeftFromX = currentX; + Send(Out["ClickFurni"], CTRL_LEFT, 0); + lastLeftCmd = DateTime.Now; +} + +void SendRight() +{ + pendingRightFromX = currentX; + Send(Out["ClickFurni"], CTRL_RIGHT, 0); + lastRightCmd = DateTime.Now; +} + +void SendRotate() +{ + pendingRotateFromRot = currentRot; + Send(Out["ClickFurni"], CTRL_ROTATE, 0); + lastRotateCmd = DateTime.Now; +} + +void SendDown() +{ + Send(Out["ClickFurni"], CTRL_DOWN, 0); + lastDownCmd = DateTime.Now; +} + +void SendInstantDown() +{ + Send(Out["ClickFurni"], CTRL_INSTANT_DOWN, 0); + lastDownCmd = DateTime.Now; +} + +void SendSwap() +{ + Send(Out["ClickFurni"], CTRL_SWAP, 0); + lastSwapCmd = DateTime.Now; + hasSwappedThisTurn = true; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// COMMAND TIMING CHECKS +// ═══════════════════════════════════════════════════════════════════════════════ + +bool CanLeft() => (DateTime.Now - lastLeftCmd).TotalMilliseconds >= sameCommandDelay; +bool CanRight() => (DateTime.Now - lastRightCmd).TotalMilliseconds >= sameCommandDelay; +bool CanRotate() => (DateTime.Now - lastRotateCmd).TotalMilliseconds >= sameCommandDelay; +bool CanDown() => (DateTime.Now - lastDownCmd).TotalMilliseconds >= downDelay; +bool CanSwap() => (DateTime.Now - lastSwapCmd).TotalMilliseconds >= swapCooldown && !hasSwappedThisTurn; + +// ═══════════════════════════════════════════════════════════════════════════════ +// SHAPE IDENTIFICATION +// ═══════════════════════════════════════════════════════════════════════════════ + +string IdentifyShape(List positions) +{ + if (positions.Count != 4) return "?"; + + int minX = positions.Min(p => p.X); + int minY = positions.Min(p => p.Y); + var norm = positions.Select(p => new Point(p.X - minX, p.Y - minY)) + .OrderBy(p => p.Y).ThenBy(p => p.X).ToList(); + + foreach (var kvp in shapes) + { + var pts = new List(); + for (int i = 0; i < 8; i += 2) + pts.Add(new Point(kvp.Value[i], kvp.Value[i+1])); + pts = pts.OrderBy(p => p.Y).ThenBy(p => p.X).ToList(); + + bool match = true; + for (int i = 0; i < 4; i++) + if (!norm[i].Equals(pts[i])) { match = false; break; } + if (match) return kvp.Key; + } + return "?"; +} + +char GetPiece(string s) => s != "?" && s.Length >= 1 ? s[0] : '?'; +int GetRot(string s) => s != "?" && s.Length >= 2 ? int.Parse(s.Substring(1)) : -1; + +List GetShapePoints(char piece, int rot) +{ + string key = $"{piece}{rot}"; + if (!shapes.ContainsKey(key)) return new List(); + var def = shapes[key]; + var pts = new List(); + for (int i = 0; i < 8; i += 2) pts.Add(new Point(def[i], def[i+1])); + return pts; +} + +int GetMaxRotations(char piece) +{ + if (piece == 'O') return 1; + if (piece == 'I' || piece == 'S' || piece == 'Z') return 2; + return 4; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PREVIEW READING +// ═══════════════════════════════════════════════════════════════════════════════ + +char ReadPreviewPiece(Func isInArea) +{ + var blocks = new List<(int kind, int x, int y)>(); + foreach (var item in FloorItems) + { + if (item == null) continue; + int kind = GetKind(item); + if (!IsPieceKind(kind)) continue; + int x = item.Location.X; + int y = item.Location.Y; + if (isInArea(x, y)) + blocks.Add((kind, x, y)); + } + + // Group by kind and find a group of exactly 4 + var byType = blocks.GroupBy(b => b.kind).ToList(); + foreach (var group in byType) + { + if (group.Count() != 4) continue; + var positions = group.Select(b => new Point(b.x, b.y)).ToList(); + string shape = IdentifyShape(positions); + if (shape != "?") return GetPiece(shape); + } + return '?'; +} + +void UpdatePreviews() +{ + nextPiece1 = ReadPreviewPiece(IsInPreview1); + nextPiece2 = ReadPreviewPiece(IsInPreview2); + nextPiece3 = ReadPreviewPiece(IsInPreview3); +} + +char ReadHeldPiece() +{ + // First check HOLD area (X24-26, Y13-18) + var blocks = new List<(int kind, int x, int y)>(); + foreach (var item in FloorItems) + { + if (item == null) continue; + int kind = GetKind(item); + if (!IsPieceKind(kind)) continue; + int x = item.Location.X; + int y = item.Location.Y; + if (IsInHold(x, y)) + blocks.Add((kind, x, y)); + } + + // Group by kind and find a group of exactly 4 + var byType = blocks.GroupBy(b => b.kind).ToList(); + foreach (var group in byType) + { + if (group.Count() != 4) continue; + var positions = group.Select(b => new Point(b.x, b.y)).ToList(); + string shape = IdentifyShape(positions); + if (shape != "?") return GetPiece(shape); + } + + // If not found in HOLD, check SWAP-AREA (X27-30, Y14-20) where held pieces go after swap + var swapBlocks = new List<(int kind, int x, int y)>(); + foreach (var item in FloorItems) + { + if (item == null) continue; + int kind = GetKind(item); + if (!IsPieceKind(kind)) continue; + int x = item.Location.X; + int y = item.Location.Y; + if (x >= 27 && x <= 30 && y >= 14 && y <= 20) + swapBlocks.Add((kind, x, y)); + } + + var byTypeSwap = swapBlocks.GroupBy(b => b.kind).ToList(); + foreach (var group in byTypeSwap) + { + if (group.Count() != 4) continue; + var positions = group.Select(b => new Point(b.x, b.y)).ToList(); + string shape = IdentifyShape(positions); + if (shape != "?") return GetPiece(shape); + } + + return '?'; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FIELD STATE +// ═══════════════════════════════════════════════════════════════════════════════ + +HashSet GetFieldBlocks() +{ + var result = new HashSet(); + foreach (var item in FloorItems) + { + if (item == null) continue; + int kind = GetKind(item); + if (!IsPieceKind(kind)) continue; + int x = item.Location.X; + int y = item.Location.Y; + if (IsInField(x, y) && !pieceIds.Contains((int)item.Id)) + result.Add(new Point(x, y)); + } + return result; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// SIMULATION & SCORING (El-Tetris inspired heuristics) +// ═══════════════════════════════════════════════════════════════════════════════ + +int SimulateDrop(char piece, int rot, int startX, HashSet field) +{ + var shape = GetShapePoints(piece, rot); + if (shape.Count != 4) return -1; + + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y + 1; y++) + { + foreach (var p in shape) + { + int px = startX + p.X; + int py = y + p.Y; + if (py > FIELD_MAX_Y || field.Contains(new Point(px, py))) + return y - 1; + } + } + return FIELD_MAX_Y; +} + +int CountHoles(HashSet field) +{ + int holes = 0; + for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) + { + bool blockFound = false; + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) + { + if (field.Contains(new Point(x, y))) blockFound = true; + else if (blockFound) holes++; + } + } + return holes; +} + +int CountHoleDepth(HashSet field) +{ + int depth = 0; + for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) + { + int above = 0; + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) + { + if (field.Contains(new Point(x, y))) above++; + else if (above > 0) depth += above; + } + } + return depth; +} + +int CountRowsWithHoles(HashSet field) +{ + int count = 0; + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) + { + for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) + { + if (!field.Contains(new Point(x, y))) + { + for (int yy = FIELD_MIN_Y; yy < y; yy++) + { + if (field.Contains(new Point(x, yy))) { count++; goto nextRow; } + } + } + } + nextRow:; + } + return count; +} + +int CountRowTransitions(HashSet field) +{ + int t = 0; + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) + { + bool last = true; + for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) + { + bool cur = field.Contains(new Point(x, y)); + if (cur != last) t++; + last = cur; + } + if (!last) t++; + } + return t; +} + +int CountColTransitions(HashSet field) +{ + int t = 0; + for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) + { + bool last = true; + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) + { + bool cur = field.Contains(new Point(x, y)); + if (cur != last) t++; + last = cur; + } + if (!last) t++; + } + return t; +} + +int CountWellSums(HashSet field) +{ + int wellSum = 0; + for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) + { + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) + { + if (field.Contains(new Point(x, y))) continue; + bool lb = (x == FIELD_MIN_X) || field.Contains(new Point(x - 1, y)); + bool rb = (x == FIELD_MAX_X) || field.Contains(new Point(x + 1, y)); + if (lb && rb) + { + int d = 1; + for (int yy = y + 1; yy <= FIELD_MAX_Y; yy++) + { + if (field.Contains(new Point(x, yy))) break; + bool lb2 = (x == FIELD_MIN_X) || field.Contains(new Point(x - 1, yy)); + bool rb2 = (x == FIELD_MAX_X) || field.Contains(new Point(x + 1, yy)); + if (lb2 && rb2) d++; + else break; + } + wellSum += (d * (d + 1)) / 2; + } + } + } + return wellSum; +} + +int[] GetHeights(HashSet field) +{ + int width = FIELD_MAX_X - FIELD_MIN_X + 1; + int[] h = new int[width]; + for (int x = FIELD_MIN_X; x <= FIELD_MAX_X; x++) + { + for (int y = FIELD_MIN_Y; y <= FIELD_MAX_Y; y++) + { + if (field.Contains(new Point(x, y))) + { + h[x - FIELD_MIN_X] = FIELD_MAX_Y - y + 1; + break; + } + } + } + return h; +} + +int GetBumpiness(int[] heights) +{ + int bump = 0; + for (int i = 0; i < heights.Length - 1; i++) + bump += Math.Abs(heights[i] - heights[i + 1]); + return bump; +} + +double ScorePlacement(char piece, int rot, int x, HashSet field) +{ + var shape = GetShapePoints(piece, rot); + if (shape.Count != 4) return double.MinValue; + + int landY = SimulateDrop(piece, rot, x, field); + if (landY < FIELD_MIN_Y) return double.MinValue; + + // Validate all blocks land within field + foreach (var p in shape) + { + int px = x + p.X; + int py = landY + p.Y; + if (px < FIELD_MIN_X || px > FIELD_MAX_X || py > FIELD_MAX_Y) + return double.MinValue; + } + + // Create new field state with piece placed + var newField = new HashSet(field); + var piecePoints = new HashSet(); + foreach (var p in shape) + { + var pt = new Point(x + p.X, landY + p.Y); + newField.Add(pt); + piecePoints.Add(pt); + } + + // Count lines cleared and piece cells involved + int linesCleared = 0; + int pieceCellsCleared = 0; + for (int row = FIELD_MIN_Y; row <= FIELD_MAX_Y; row++) + { + bool full = true; + for (int col = FIELD_MIN_X; col <= FIELD_MAX_X; col++) + if (!newField.Contains(new Point(col, row))) { full = false; break; } + if (full) + { + linesCleared++; + for (int col = FIELD_MIN_X; col <= FIELD_MAX_X; col++) + { + var pt = new Point(col, row); + if (piecePoints.Contains(pt)) pieceCellsCleared++; + newField.Remove(pt); + } + } + } + + // Simulate gravity after line clear + if (linesCleared > 0) + { + var dropped = new HashSet(); + for (int row = FIELD_MAX_Y; row >= FIELD_MIN_Y; row--) + { + for (int col = FIELD_MIN_X; col <= FIELD_MAX_X; col++) + { + var pt = new Point(col, row); + if (newField.Contains(pt)) + { + newField.Remove(pt); + int newRow = row; + while (newRow < FIELD_MAX_Y && !dropped.Contains(new Point(col, newRow + 1))) newRow++; + dropped.Add(new Point(col, newRow)); + } + } + } + newField = dropped; + } + + // Calculate features + int pieceH = shape.Max(p => p.Y) + 1; + double landingHeight = (FIELD_MAX_Y - landY + 1) + (pieceH - 1) / 2.0; + double erodedPieceCells = linesCleared * pieceCellsCleared; + int rowTrans = CountRowTransitions(newField); + int colTrans = CountColTransitions(newField); + int holes = CountHoles(newField); + int wellSums = CountWellSums(newField); + int holeDepth = CountHoleDepth(newField); + int rowsWithHoles = CountRowsWithHoles(newField); + var heights = GetHeights(newField); + int bumpiness = GetBumpiness(heights); + int maxH = heights.Length > 0 ? heights.Max() : 0; + int aggregateHeight = heights.Sum(); + + // El-Tetris inspired weights with additions + double score = 0; + score += -12.63 * landingHeight; + score += 6.60 * erodedPieceCells; + score += -9.22 * rowTrans; + score += -19.77 * colTrans; + score += -13.08 * holes; + score += -10.49 * wellSums; + score += -1.61 * holeDepth; + score += -24.04 * rowsWithHoles; + score += -0.51 * bumpiness; + score += -0.18 * aggregateHeight; + + // Bonus for clearing lines + score += linesCleared * 500; + + // Extra bonus for Tetris (4 lines) + if (linesCleared == 4) score += 2000; + + // Emergency mode when stack is high + if (maxH >= 15) + { + score += linesCleared * 3000; + score -= maxH * 100; + } + + return score; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// BEST PLACEMENT FINDER (with optional look-ahead) +// ═══════════════════════════════════════════════════════════════════════════════ + +(int x, int rot, double score) FindBestPlacementForPiece(char piece, HashSet field) +{ + int bestX = (FIELD_MIN_X + FIELD_MAX_X) / 2; + int bestRot = 0; + double bestScore = double.MinValue; + int maxRot = GetMaxRotations(piece); + + for (int rot = 0; rot < maxRot; rot++) + { + var shape = GetShapePoints(piece, rot); + if (shape.Count != 4) continue; + int w = shape.Max(p => p.X) + 1; + for (int px = FIELD_MIN_X; px <= FIELD_MAX_X - w + 1; px++) + { + double s = ScorePlacement(piece, rot, px, field); + if (s > bestScore) { bestScore = s; bestX = px; bestRot = rot; } + } + } + return (bestX, bestRot, bestScore); +} + +(int x, int rot) FindBestPlacement(char piece, HashSet field) +{ + var (bx, br, _) = FindBestPlacementForPiece(piece, field); + return (bx, br); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// SWAP DECISION LOGIC +// ═══════════════════════════════════════════════════════════════════════════════ + +bool ShouldSwap(char currentPiece, char heldOrNextPiece, HashSet field) +{ + if (heldOrNextPiece == '?') return false; + + var (_, _, currentScore) = FindBestPlacementForPiece(currentPiece, field); + var (_, _, alternateScore) = FindBestPlacementForPiece(heldOrNextPiece, field); + + // Swap if alternate piece gives significantly better score + return alternateScore > currentScore + 500; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PIECE STATE GETTER - Position-based for rooms that recreate items on move +// ═══════════════════════════════════════════════════════════════════════════════ + +(List positions, int minX, int minY, string shape, char piece, int rot) GetActivePieceInField() +{ + // Find blocks in the upper portion of the field (active piece area) + var fieldBlocks = new List<(int id, int kind, int x, int y)>(); + foreach (var item in FloorItems) + { + if (item == null) continue; + int kind = GetKind(item); + if (!IsPieceKind(kind)) continue; + int x = item.Location.X; + int y = item.Location.Y; + // Look in upper half of field for active piece + if (IsInField(x, y) && y <= FIELD_MIN_Y + 10) + fieldBlocks.Add(((int)item.Id, kind, x, y)); + } + + if (fieldBlocks.Count < 4) + return (new List(), -1, -1, "?", '?', -1); + + // Group by kind - pieces of same type have same kind + var byKind = fieldBlocks.GroupBy(b => b.kind) + .OrderBy(g => g.Min(b => b.y)) // Prioritize topmost groups + .ToList(); + + // For each kind, try the topmost 4 blocks + foreach (var group in byKind) + { + if (group.Count() < 4) continue; + + // Take the 4 blocks with lowest Y (topmost) + var top4 = group.OrderBy(b => b.y).ThenBy(b => b.x).Take(4).ToList(); + var positions = top4.Select(b => new Point(b.x, b.y)).ToList(); + string shape = IdentifyShape(positions); + + if (shape != "?") + { + int minX = positions.Min(p => p.X); + int minY = positions.Min(p => p.Y); + + // Store these IDs for movement tracking + pieceIds.Clear(); + foreach (var b in top4) + pieceIds.Add(b.id); + + return (positions, minX, minY, shape, GetPiece(shape), GetRot(shape)); + } + } + + return (new List(), -1, -1, "?", '?', -1); +} + +// Keep old method for ID-based tracking when we have valid IDs +(List positions, int minX, int minY, string shape, char piece, int rot) GetPieceState() +{ + var positions = new List(); + foreach (var item in FloorItems) + { + if (item == null) continue; + if (!pieceIds.Contains((int)item.Id)) continue; + positions.Add(new Point(item.Location.X, item.Location.Y)); + } + if (positions.Count != 4) + { + // IDs lost - try position-based detection + return GetActivePieceInField(); + } + int minX = positions.Min(p => p.X); + int minY = positions.Min(p => p.Y); + string shape = IdentifyShape(positions); + char piece = GetPiece(shape); + int rot = GetRot(shape); + return (positions, minX, minY, shape, piece, rot); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// MOVEMENT DETECTION INTERCEPTOR +// ═══════════════════════════════════════════════════════════════════════════════ + +OnIntercept(In["ObjectUpdate"], e => { + var p = e.Packet; + int id = p.ReadInt(); + if (pieceIds.Contains(id)) + lastMoveDetected = DateTime.Now; +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// MAIN LOOP +// ═══════════════════════════════════════════════════════════════════════════════ + +Log("═══════════════════════════════════════════"); +Log(" TETRIS BOT V2 - Enhanced Edition"); +Log(" Features: Instant Drop, Hold/Swap, Preview"); +Log("═══════════════════════════════════════════"); +Log($"Field: X{FIELD_MIN_X}-{FIELD_MAX_X}, Y{FIELD_MIN_Y}-{FIELD_MAX_Y}"); +Log($"Spawn: X{SPAWN_MIN_X}-{SPAWN_MAX_X}, Y{SPAWN_MIN_Y}-{SPAWN_MAX_Y}"); +Log(""); +Log("Block types: I=7100, J=7101, T=7108, Z=7121, L=7124, S=7138, O=5482"); +Log("Ghost (ignored): 10469"); +Log(""); + +// Initial preview scan +UpdatePreviews(); +heldPiece = ReadHeldPiece(); +Log($"Previews: Next1={nextPiece1} Next2={nextPiece2} Next3={nextPiece3} Hold={heldPiece}"); +Log(""); +Log("Press Ctrl+C to stop"); +Log(""); + +while (Run) +{ + Delay(30); // 30ms between iterations to prevent command spam + + // Update preview pieces periodically + UpdatePreviews(); + heldPiece = ReadHeldPiece(); + + // ───────────────────────────────────────────────────────────────────────── + // SPAWN DETECTION - Look for new piece at top of field + // ───────────────────────────────────────────────────────────────────────── + if (!pieceActive) + { + // Wait for cooldown after last drop + if ((DateTime.Now - lastDropTime).TotalMilliseconds < dropCooldown) + continue; + + // Look for blocks in the top area of field (Y12-18) + var topBlocks = new List<(int id, int kind, int x, int y)>(); + foreach (var item in FloorItems) + { + if (item == null) continue; + int kind = GetKind(item); + if (!IsPieceKind(kind)) continue; + int x = item.Location.X; + int y = item.Location.Y; + // Top of field area (expanded range) + if (x >= FIELD_MIN_X && x <= FIELD_MAX_X && y >= FIELD_MIN_Y && y <= FIELD_MIN_Y + 6) + topBlocks.Add(((int)item.Id, kind, x, y)); + } + + // Group by kind + var byKind = topBlocks.GroupBy(b => b.kind).ToList(); + + // For EACH kind, try the TOPMOST 4 blocks (lowest Y values) + foreach (var group in byKind.OrderBy(g => g.Min(b => b.y))) + { + if (group.Count() < 4) continue; + + // Take the 4 blocks with lowest Y (topmost), then by X + var top4 = group.OrderBy(b => b.y).ThenBy(b => b.x).Take(4).ToList(); + var positions = top4.Select(b => new Point(b.x, b.y)).ToList(); + string shape = IdentifyShape(positions); + + if (shape != "?") + { + pieceIds.Clear(); + foreach (var b in top4) + pieceIds.Add(b.id); + + currentPiece = GetPiece(shape); + currentRot = GetRot(shape); + currentX = positions.Min(p => p.X); + + // Log kind for debugging + int blockKind = top4[0].kind; + + var field = GetFieldBlocks(); + + // Check if we should swap with held piece + hasSwappedThisTurn = false; + char swapCandidate = heldPiece != '?' ? heldPiece : nextPiece1; + + if (CanSwap() && ShouldSwap(currentPiece, swapCandidate, field)) + { + Log($"SWAP: {currentPiece} -> {swapCandidate}"); + SendSwap(); + pieceIds.Clear(); + pieceActive = false; + Delay(150); + break; + } + + var (bx, br) = FindBestPlacement(currentPiece, field); + targetX = bx; + targetRot = br; + positioned = false; + pieceActive = true; + spawnTime = DateTime.Now; + lastMoveDetected = DateTime.Now; + pendingLeftFromX = -1; + pendingRightFromX = -1; + pendingRotateFromRot = -1; + + Log($"NEW: {currentPiece}{currentRot} [Kind:{blockKind}] @ X{currentX} β†’ X{targetX} r{targetRot} (Next:{nextPiece1},{nextPiece2},{nextPiece3})"); + break; + } + } + continue; + } + + // ───────────────────────────────────────────────────────────────────────── + // PIECE STATE UPDATE - Handle ID changes during movement + // ───────────────────────────────────────────────────────────────────────── + var state = GetPieceState(); + + // If we lost the piece but still have blocks in field, try to re-acquire + if (state.positions.Count != 4 || state.piece == '?') + { + // Piece might have changed IDs or moved - try position-based detection + state = GetActivePieceInField(); + } + + if (state.positions.Count != 4 || state.piece == '?') + { + // Still can't find piece - it may have landed or we lost it + double timeSinceSpawn = (DateTime.Now - spawnTime).TotalMilliseconds; + if (timeSinceSpawn > 200) // Give some time for piece to appear + { + pieceIds.Clear(); + pieceActive = false; + currentPiece = '?'; + targetX = -1; + targetRot = -1; + positioned = false; + } + continue; + } + + // Update piece if it changed (rotation wall kick or similar) + if (state.piece != '?' && state.piece != currentPiece) + { + Log($" Piece changed: {currentPiece} -> {state.piece}"); + currentPiece = state.piece; + // Recalculate target for new piece + var field = GetFieldBlocks(); + var (bx, br) = FindBestPlacement(currentPiece, field); + targetX = bx; + targetRot = br; + positioned = false; + } + + int newX = state.minX; + int newRot = state.rot != -1 ? state.rot : currentRot; + + // Verify pending commands completed + if (newX != currentX) + { + if (pendingLeftFromX != -1 && newX < pendingLeftFromX) + pendingLeftFromX = -1; + if (pendingRightFromX != -1 && newX > pendingRightFromX) + pendingRightFromX = -1; + currentX = newX; + } + + if (newRot != currentRot) + { + pendingRotateFromRot = -1; + currentRot = newRot; + } + + // ───────────────────────────────────────────────────────────────────────── + // LANDING DETECTION + // ───────────────────────────────────────────────────────────────────────── + double timeSinceMove = (DateTime.Now - lastMoveDetected).TotalMilliseconds; + if (timeSinceMove > landingTimeout) + { + pieceIds.Clear(); + pieceActive = false; + currentPiece = '?'; + targetX = -1; + targetRot = -1; + positioned = false; + continue; + } + + // ───────────────────────────────────────────────────────────────────────── + // POSITIONING LOGIC + // ───────────────────────────────────────────────────────────────────────── + int maxRot = GetMaxRotations(currentPiece); + int rotNeeded = (targetRot - currentRot + maxRot) % maxRot; + int xDiff = targetX - currentX; + + if (rotNeeded == 0 && xDiff == 0) + positioned = true; + + // ───────────────────────────────────────────────────────────────────────── + // DROP WHEN POSITIONED + // ───────────────────────────────────────────────────────────────────────── + if (positioned) + { + if (useInstantDrop) + { + // Use instant drop for maximum speed + SendInstantDown(); + Log($"DROP: {currentPiece} at X{currentX}"); + pieceIds.Clear(); + pieceActive = false; + currentPiece = '?'; + targetX = -1; + targetRot = -1; + positioned = false; + Delay(300); // Wait for piece to land and new piece to spawn + } + else if (CanDown()) + { + SendDown(); + } + continue; + } + + // ───────────────────────────────────────────────────────────────────────── + // MOVEMENT EXECUTION + // ───────────────────────────────────────────────────────────────────────── + double timeSinceLeft = (DateTime.Now - lastLeftCmd).TotalMilliseconds; + double timeSinceRight = (DateTime.Now - lastRightCmd).TotalMilliseconds; + double timeSinceRotate = (DateTime.Now - lastRotateCmd).TotalMilliseconds; + + bool leftPending = pendingLeftFromX != -1 && timeSinceLeft < verifyTimeout; + bool rightPending = pendingRightFromX != -1 && timeSinceRight < verifyTimeout; + bool rotatePending = pendingRotateFromRot != -1 && timeSinceRotate < verifyTimeout; + + bool leftTimeout = pendingLeftFromX != -1 && timeSinceLeft >= verifyTimeout; + bool rightTimeout = pendingRightFromX != -1 && timeSinceRight >= verifyTimeout; + bool rotateTimeout = pendingRotateFromRot != -1 && timeSinceRotate >= verifyTimeout; + + // Rotate first (higher priority) + if (rotNeeded > 0) + { + if (rotateTimeout && CanRotate()) + { + SendRotate(); + } + else if (!rotatePending && CanRotate()) + { + SendRotate(); + } + } + + // Horizontal movement + if (xDiff < 0) + { + if (leftTimeout && CanLeft()) + { + SendLeft(); + } + else if (!leftPending && CanLeft()) + { + SendLeft(); + } + } + else if (xDiff > 0) + { + if (rightTimeout && CanRight()) + { + SendRight(); + } + else if (!rightPending && CanRight()) + { + SendRight(); + } + } +} + +Log(""); +Log("Bot stopped."); \ No newline at end of file diff --git a/Trade Spam.csx b/Trade Spam.csx new file mode 100644 index 0000000..cd36f68 --- /dev/null +++ b/Trade Spam.csx @@ -0,0 +1,35 @@ +// Trade Spam - Klicke auf einen User um ihn zu spammen + +int targetIndex = 0; +string targetName = ""; + +Log("=== Trade Spam Script ==="); +Log("Klicke auf einen User um ihn zu spammen!"); + +// Erkennt wenn du auf einen User klickst +OnIntercept(Out["GetSelectedBadges"], e => { + var userId = e.Packet.ReadInt(); + var user = Users.FirstOrDefault(u => u.Id == userId); + if (user != null) + { + targetIndex = user.Index; + targetName = user.Name; + Log($">>> Ziel gesetzt: {targetName} <<<"); + } +}); + +while (Run) +{ + if (targetIndex > 0) + { + try + { + Send(Out["OpenTrading"], targetIndex); + Delay(80); + Send(Out["CloseTrading"]); + Delay(80); + } + catch { } + } + Delay(50); +} \ No newline at end of file diff --git a/User Collector FI Server.csx b/User Collector FI Server.csx new file mode 100644 index 0000000..8ab3022 --- /dev/null +++ b/User Collector FI Server.csx @@ -0,0 +1,95 @@ +// Automatisch durch RΓ€ume gehen und User in TXT speichern (nur Namen) + +using System.IO; + +var minUsers = 1; +var delayBetweenRooms = 3000; + +var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); +var filePath = Path.Combine(desktopPath, "HabboUserList_fi.txt"); + +var allUsers = new HashSet(); + +// Existierende User laden +if (File.Exists(filePath)) +{ + foreach (var line in File.ReadAllLines(filePath)) + { + if (!string.IsNullOrWhiteSpace(line)) + { + allUsers.Add(line.Trim()); + } + } + Log($"Bestehende Liste geladen: {allUsers.Count} User"); +} + +void SaveToFile() +{ + File.WriteAllLines(filePath, allUsers.OrderBy(x => x)); + Log($"[GESPEICHERT] {allUsers.Count} User"); +} + +var categories = new[] { "popular", "recommended" }; +var allRooms = new Dictionary(); + +Log("=== Suche RΓ€ume... ==="); + +foreach (var category in categories) +{ + try + { + var rooms = SearchNav(category).Where(x => x.Users >= minUsers); + foreach (var room in rooms) + { + if (!allRooms.ContainsKey(room.Id)) + allRooms[room.Id] = room; + } + Delay(1000); + } + catch { } +} + +Log($"Gefunden: {allRooms.Count} RΓ€ume"); + +int roomCount = 0; + +foreach (var room in allRooms.Values) +{ + if (!Run) break; + + roomCount++; + Log($"[{roomCount}/{allRooms.Count}] {room.Name}"); + + try + { + if (EnsureEnterRoom(room.Id) == RoomEntryResult.Success) + { + Delay(1500); + + int newUsers = 0; + foreach (var user in Users) + { + if (allUsers.Add(user.Name)) + newUsers++; + } + + if (newUsers > 0) + { + Log($" +{newUsers} neue User"); + SaveToFile(); + } + + LeaveRoom(); + } + } + catch { } + + Delay(delayBetweenRooms); +} + +SaveToFile(); + +Log($""); +Log($"=== FERTIG ==="); +Log($"Gesamt: {allUsers.Count} User"); +Log($"Datei: {filePath}"); \ No newline at end of file diff --git a/User Collector Hopper Advanced.csx b/User Collector Hopper Advanced.csx new file mode 100644 index 0000000..54011d8 --- /dev/null +++ b/User Collector Hopper Advanced.csx @@ -0,0 +1,245 @@ +/// @name User Collector + Auto Hopper +/// @desc Sammelt User-Namen und hoppt automatisch durch Raeume +/// @author OpenCode +/// @scripter 1.0.0-beta.136 + +// ============ EINSTELLUNGEN ============ +int minUsers = 5; // Minimum User im Raum +int waitInRoom = 6000; // Wie lange in jedem Raum bleiben (ms) +int delayBetweenRooms = 2000; // Pause zwischen Raumwechsel (ms) +// ======================================== + +// Pfad zur Ausgabedatei auf dem Desktop +var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); +var outputFile = Path.Combine(desktopPath, "HabboUserList.txt"); + +// HashSet um Duplikate zu vermeiden +var collectedUsers = new HashSet(); + +// Besuchte Raeume tracken +var visitedRooms = new HashSet(); + +// Raeume Queue - sortiert nach User-Anzahl +var roomQueue = new List<(long Id, string Name, int UserCount)>(); + +// Lade bereits gesammelte User aus der Datei (falls vorhanden) +if (File.Exists(outputFile)) +{ + var lines = File.ReadAllLines(outputFile); + foreach (var line in lines) + { + if (!string.IsNullOrWhiteSpace(line)) + collectedUsers.Add(line.Trim()); + } + Log($"Loaded: {collectedUsers.Count} users from file"); +} + +Log("================================="); +Log("User Collector + Auto Hopper"); +Log($"Min. users per room: {minUsers}"); +Log("================================="); +Log("HOW TO USE:"); +Log("1. Open Navigator in game"); +Log("2. Click 'Rooms' -> 'Popular'"); +Log("3. Script catches the rooms"); +Log("4. Type :start to begin hopping"); +Log("================================="); +Log("Commands: :start :stop :status :reset"); +Log("================================="); + +// Hopping aktiv? +bool isHopping = false; + +// Event: Wenn jemand den Raum betritt +OnEntityAdded(e => { + if (e.Entity is IRoomUser user) + { + var userName = user.Name; + if (collectedUsers.Add(userName)) + { + File.AppendAllText(outputFile, userName + "\n"); + Log($"+ {userName}"); + } + } +}); + +// Intercept Navigator Suchergebnisse (Navigator2) +OnIntercept(In.Navigator2SearchResultBlocks, e => { + try + { + var packet = e.Packet; + var searchCode = packet.ReadString(); + var filterText = packet.ReadString(); + var blockCount = packet.ReadInt(); + + int foundRooms = 0; + + for (int b = 0; b < blockCount; b++) + { + var blockCode = packet.ReadString(); + var blockText = packet.ReadString(); + var actionAllowed = packet.ReadInt(); + var forceClosed = packet.ReadBool(); + var roomCount = packet.ReadInt(); + + for (int r = 0; r < roomCount; r++) + { + var roomId = packet.ReadLong(); + var roomName = packet.ReadString(); + var ownerId = packet.ReadLong(); + var ownerName = packet.ReadString(); + var doorMode = packet.ReadInt(); + var userCount = packet.ReadInt(); + var maxUsers = packet.ReadInt(); + var description = packet.ReadString(); + var tradeMode = packet.ReadInt(); + var score = packet.ReadInt(); + var ranking = packet.ReadInt(); + var categoryId = packet.ReadInt(); + + // Tags + var tagCount = packet.ReadInt(); + for (int t = 0; t < tagCount; t++) + packet.ReadString(); + + // Flags + var flags = packet.ReadInt(); + if ((flags & 1) != 0) packet.ReadString(); + if ((flags & 2) != 0) packet.ReadString(); + if ((flags & 4) != 0) { packet.ReadString(); packet.ReadLong(); packet.ReadString(); } + + // Raum hinzufuegen + if (userCount >= minUsers && !visitedRooms.Contains(roomId)) + { + if (!roomQueue.Any(x => x.Id == roomId)) + { + roomQueue.Add((roomId, roomName, userCount)); + foundRooms++; + } + } + } + } + + if (foundRooms > 0) + { + // Sort by user count (most first) + roomQueue = roomQueue.OrderByDescending(x => x.UserCount).ToList(); + Log($"+ {foundRooms} new rooms found! (Total: {roomQueue.Count})"); + Status($"Queue: {roomQueue.Count} rooms | Users: {collectedUsers.Count}"); + } + } + catch (Exception ex) + { + Log($"Parse-Fehler: {ex.Message}"); + } +}); + +// Chat-Befehl zum Starten +OnIntercept(Out.Chat, e => { + var msg = e.Packet.ReadString(0); + if (msg == ":start") + { + e.Block(); + if (roomQueue.Count > 0) + { + isHopping = true; + Log($"Starting hopping through {roomQueue.Count} rooms..."); + } + else + { + Log("No rooms in queue! Open Navigator -> Popular"); + } + } + else if (msg == ":stop") + { + e.Block(); + isHopping = false; + Log("Hopping stopped."); + } + else if (msg == ":status") + { + e.Block(); + Log($"Queue: {roomQueue.Count} | Visited: {visitedRooms.Count} | Users: {collectedUsers.Count}"); + } + else if (msg == ":reset") + { + e.Block(); + visitedRooms.Clear(); + roomQueue.Clear(); + Log("Reset! Open Navigator again."); + } +}); + +// Hauptschleife +while (Run) +{ + // User im aktuellen Raum sammeln + if (IsInRoom) + { + foreach (var user in Users) + { + if (collectedUsers.Add(user.Name)) + { + File.AppendAllText(outputFile, user.Name + "\n"); + Log($"+ {user.Name}"); + } + } + } + + // Hopping wenn aktiv + if (isHopping && roomQueue.Count > 0) + { + var room = roomQueue[0]; + roomQueue.RemoveAt(0); + + if (!visitedRooms.Contains(room.Id)) + { + visitedRooms.Add(room.Id); + Log($">>> {room.Name} ({room.UserCount} User)"); + + Send(Out.GoToFlat, room.Id, "", -1); + Delay(3500); + + if (IsInRoom) + { + // User sammeln + foreach (var user in Users) + { + if (collectedUsers.Add(user.Name)) + { + File.AppendAllText(outputFile, user.Name + "\n"); + Log($"+ {user.Name}"); + } + } + + Status($"Users: {collectedUsers.Count} | Rooms: {visitedRooms.Count} | Queue: {roomQueue.Count}"); + + // Warten + Delay(waitInRoom); + + // Nochmal sammeln (fuer Joiner) + foreach (var user in Users) + { + if (collectedUsers.Add(user.Name)) + { + File.AppendAllText(outputFile, user.Name + "\n"); + Log($"+ {user.Name}"); + } + } + } + + Delay(delayBetweenRooms); + } + } + else if (isHopping && roomQueue.Count == 0) + { + isHopping = false; + Log($"=== DONE! ==="); + Log($"Visited: {visitedRooms.Count} rooms"); + Log($"Collected: {collectedUsers.Count} users"); + Log("Open Navigator for more rooms, then :start"); + } + + Status($"Users: {collectedUsers.Count} | Queue: {roomQueue.Count}"); + Delay(1000); +} diff --git a/User Collector NL Server.csx b/User Collector NL Server.csx new file mode 100644 index 0000000..7c4e2cb --- /dev/null +++ b/User Collector NL Server.csx @@ -0,0 +1,95 @@ +// Automatisch durch RΓ€ume gehen und User in TXT speichern (nur Namen) + +using System.IO; + +var minUsers = 1; +var delayBetweenRooms = 3000; + +var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); +var filePath = Path.Combine(desktopPath, "HabboUserList_nl.txt"); + +var allUsers = new HashSet(); + +// Existierende User laden +if (File.Exists(filePath)) +{ + foreach (var line in File.ReadAllLines(filePath)) + { + if (!string.IsNullOrWhiteSpace(line)) + { + allUsers.Add(line.Trim()); + } + } + Log($"Bestehende Liste geladen: {allUsers.Count} User"); +} + +void SaveToFile() +{ + File.WriteAllLines(filePath, allUsers.OrderBy(x => x)); + Log($"[GESPEICHERT] {allUsers.Count} User"); +} + +var categories = new[] { "popular", "recommended" }; +var allRooms = new Dictionary(); + +Log("=== Suche RΓ€ume... ==="); + +foreach (var category in categories) +{ + try + { + var rooms = SearchNav(category).Where(x => x.Users >= minUsers); + foreach (var room in rooms) + { + if (!allRooms.ContainsKey(room.Id)) + allRooms[room.Id] = room; + } + Delay(1000); + } + catch { } +} + +Log($"Gefunden: {allRooms.Count} RΓ€ume"); + +int roomCount = 0; + +foreach (var room in allRooms.Values) +{ + if (!Run) break; + + roomCount++; + Log($"[{roomCount}/{allRooms.Count}] {room.Name}"); + + try + { + if (EnsureEnterRoom(room.Id) == RoomEntryResult.Success) + { + Delay(1500); + + int newUsers = 0; + foreach (var user in Users) + { + if (allUsers.Add(user.Name)) + newUsers++; + } + + if (newUsers > 0) + { + Log($" +{newUsers} neue User"); + SaveToFile(); + } + + LeaveRoom(); + } + } + catch { } + + Delay(delayBetweenRooms); +} + +SaveToFile(); + +Log($""); +Log($"=== FERTIG ==="); +Log($"Gesamt: {allUsers.Count} User"); +Log($"Datei: {filePath}"); \ No newline at end of file diff --git a/Wired Message Blocker.csx b/Wired Message Blocker.csx new file mode 100644 index 0000000..e8d362d --- /dev/null +++ b/Wired Message Blocker.csx @@ -0,0 +1,28 @@ +using Xabbo.Interceptor; +using Xabbo.Messages; + +// Funktion zum PrΓΌfen der Nachricht +void BlockWired(Intercept e) +{ + try + { + e.Packet.Position = 0; + e.Packet.ReadInt(); // User Index + e.Packet.ReadString(); // Nachricht + e.Packet.ReadInt(); // Geste + + // Die Bubble ID lesen + int bubbleStyle = e.Packet.ReadInt(); + + // 34 ist die Standard-Wired-Bubble. + // Falls dein Retro eine andere ID fΓΌr Wired nutzt, Γ€ndere die 34. + if (bubbleStyle == 34) + { + e.Block(); + } + } + catch { } +} + +// Intercept nur auf FlΓΌstern (Whisper), da Wireds meistens flΓΌstern +Intercept(In.Whisper, BlockWired); \ No newline at end of file diff --git a/hello-once.csx b/hello-once.csx new file mode 100644 index 0000000..08cb984 --- /dev/null +++ b/hello-once.csx @@ -0,0 +1,2 @@ +/// @name hello-once +Log("hello once"); \ No newline at end of file diff --git a/room-scan-basic.csx b/room-scan-basic.csx new file mode 100644 index 0000000..16bd82a --- /dev/null +++ b/room-scan-basic.csx @@ -0,0 +1,12 @@ +/// @name room-scan-basic +Log("Room scan basic start"); + +if (!IsInRoom || Room == null) +{ + Log("Not in room."); +} +else +{ + Log($"Room: {Room.Name} ({Room.Id})"); + Log($"Users: {Users.Count()} | FloorItems: {FloorItems.Count()} | WallItems: {WallItems.Count()}"); +} diff --git a/room-scan-starter.csx b/room-scan-starter.csx new file mode 100644 index 0000000..682ed9b --- /dev/null +++ b/room-scan-starter.csx @@ -0,0 +1,50 @@ +using System.Linq; + +Log("=== Room Scan Starter ==="); + +if (!IsInRoom || Room == null) +{ + Log("Not in a room. Enter a room and run again."); +} +else +{ + Log($"Room: {Room.Name} (ID: {Room.Id})"); + Log($"Owner: {Room.OwnerName} ({Room.OwnerId})"); + Log($"Users: {Users.Count()} | FloorItems: {FloorItems.Count()} | WallItems: {WallItems.Count()}"); + + var topFloorKinds = FloorItems + .GroupBy(i => i.Kind) + .OrderByDescending(g => g.Count()) + .ThenBy(g => g.Key) + .Take(5) + .ToList(); + + Log("Top 5 floor kinds:"); + if (topFloorKinds.Count == 0) + { + Log("- none"); + } + else + { + foreach (var g in topFloorKinds) + Log($"- kind {g.Key}: {g.Count()}x"); + } + + var topWallKinds = WallItems + .GroupBy(i => i.Kind) + .OrderByDescending(g => g.Count()) + .ThenBy(g => g.Key) + .Take(5) + .ToList(); + + Log("Top 5 wall kinds:"); + if (topWallKinds.Count == 0) + { + Log("- none"); + } + else + { + foreach (var g in topWallKinds) + Log($"- kind {g.Key}: {g.Count()}x"); + } +} diff --git a/room-scan-topk.csx b/room-scan-topk.csx new file mode 100644 index 0000000..55c99c7 --- /dev/null +++ b/room-scan-topk.csx @@ -0,0 +1,20 @@ +/// @name room-scan-topk +using System.Linq; + +Log("Top kinds test start"); + +if (!IsInRoom || Room == null) +{ + Log("Not in room."); +} +else +{ + var topFloorKinds = FloorItems + .GroupBy(i => i.Kind) + .OrderByDescending(g => g.Count()) + .Take(5) + .ToList(); + + foreach (var g in topFloorKinds) + Log($"kind {g.Key}: {g.Count()}x"); +}