commit 7bfc390ed63752635860873e9560249051ede686 Author: Administrator Date: Mon Mar 16 09:38:59 2026 +0100 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. 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"); +}