xabbo-scripts/Scripts/Snake [28.12.25] V1.0.csx
Administrator 7a548130a3 Move all scripts into Scripts/ subfolder
Keeps the repo root clean - only README.md visible on landing page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:49:37 +01:00

343 lines
9.4 KiB
C#

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:;
}