From c94fa1872a5e504366aa4b1984fa613a62554ada Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 15 Mar 2026 19:42:31 -0400 Subject: [PATCH] Expand world context + two-call LLM split - Replace time-based log window with line-based (200 lines, 3hr cap) - Add per-player position and death count to server context - Add server scoreboards (total deaths, shrink enabled, border parity) - Add god_lore config key injected into message system prompt - Two-call split: qwen3-coder:30b (commands) + gemma3:12b (messages) - interventions_per_day bumped to 48 (avg every 30min) - cooldown_seconds reduced to 20 --- mc_aigod.py | 107 ++++++++++++++++++++++++++++++++++--------- mc_aigod_shrink.json | 3 +- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/mc_aigod.py b/mc_aigod.py index ee146c9..c577a72 100644 --- a/mc_aigod.py +++ b/mc_aigod.py @@ -48,8 +48,10 @@ LOG_INTERESTING = re.compile( # Shared memory buffers (module-level, shared across threads) # --------------------------------------------------------------------------- -# Rolling window of recent server log events (last LOG_WINDOW_MINUTES minutes) -LOG_WINDOW_MINUTES = 20 +# Rolling log buffer — keeps last LOG_MAX_LINES interesting events, +# pruning anything older than LOG_MAX_HOURS. Whichever limit hits first. +LOG_MAX_LINES = 200 +LOG_MAX_HOURS = 3 recent_log: deque = deque() # entries: (timestamp_float, str) # God's prayer memory — last N prayer/response pairs across all players @@ -61,10 +63,9 @@ _memory_lock = threading.Lock() def add_log_event(line: str): - """Add a meaningful log line to the rolling buffer, pruning old entries.""" + """Add a meaningful log line to the rolling buffer.""" if not LOG_INTERESTING.search(line): return - # Extract just the meaningful part after the thread prefix m = re.search(r'\[.*?INFO\]: (.+)', line) if not m: return @@ -72,10 +73,13 @@ def add_log_event(line: str): now = time.time() with _memory_lock: recent_log.append((now, entry)) - # Prune entries older than the window - cutoff = now - (LOG_WINDOW_MINUTES * 60) + # Prune by age + cutoff = now - (LOG_MAX_HOURS * 3600) while recent_log and recent_log[0][0] < cutoff: recent_log.popleft() + # Prune by line count + while len(recent_log) > LOG_MAX_LINES: + recent_log.popleft() def _memory_path(config) -> str: @@ -132,8 +136,12 @@ def get_log_context_block() -> str: lines = [] for ts, entry in entries: mins_ago = int((now - ts) / 60) - lines.append(f" [{mins_ago}m ago] {entry}") - return "\n=== RECENT SERVER EVENTS (last 20 min) ===\n" + "\n".join(lines) + "\n" + if mins_ago < 60: + time_label = f"{mins_ago}m ago" + else: + time_label = f"{mins_ago // 60}h {mins_ago % 60}m ago" + lines.append(f" [{time_label}] {entry}") + return f"\n=== RECENT SERVER EVENTS (last {len(lines)} events, up to {LOG_MAX_HOURS}h) ===\n" + "\n".join(lines) + "\n" def get_prayer_history_messages() -> list: @@ -218,11 +226,46 @@ def get_server_context(config): if m: border_size = float(m.group(1)) + # Per-player: position, death count + player_details = {} + for player in online_players: + details = {} + # Position + pos_raw = q(f"data get entity {player} Pos") + pos_m = re.findall(r'(-?[\d.]+)d', pos_raw) + if pos_m and len(pos_m) >= 3: + x, y, z = float(pos_m[0]), float(pos_m[1]), float(pos_m[2]) + dist = int((x**2 + z**2) ** 0.5) + details["pos"] = f"x={int(x)}, y={int(y)}, z={int(z)} ({dist} blocks from spawn)" + # Deaths + deaths_raw = q(f"scoreboard players get {player} player_deaths") + deaths_m = re.search(r'has\s+(\d+)', deaths_raw) + if deaths_m: + details["deaths"] = int(deaths_m.group(1)) + player_details[player] = details + + # Server-wide scoreboards + total_deaths_raw = q("scoreboard players get $deaths_total Total Deaths") + shrink_enabled_raw = q("scoreboard players get $shrink_enabled shrink_enabled") + border_parity_raw = q("scoreboard players get $border_parity border_parity") + + total_deaths_m = re.search(r'has\s+(\d+)', total_deaths_raw) + shrink_enabled_m = re.search(r'has\s+(\d+)', shrink_enabled_raw) + border_parity_m = re.search(r'has\s+(\d+)', border_parity_raw) + + scoreboards = { + "total_deaths": total_deaths_m.group(1) if total_deaths_m else "unknown", + "shrink_enabled": shrink_enabled_m.group(1) if shrink_enabled_m else "unknown", + "border_parity": ("N/S" if border_parity_m and border_parity_m.group(1) == "0" else "E/W") if border_parity_m else "unknown", + } + return { "online_players": online_players, + "player_details": player_details, "time_of_day": time_label, "weather": weather, "world_border": border_size, + "scoreboards": scoreboards, } # Item tier/rarity knowledge for God's awareness @@ -470,14 +513,31 @@ def build_system_prompt(config): ) def _build_context_block(context, extras=""): - online = ', '.join(context['online_players']) or 'none' border = str(context['world_border']) if context['world_border'] else 'N/A' + scoreboards = context.get("scoreboards", {}) + shrink = scoreboards.get("shrink_enabled", "unknown") + parity = scoreboards.get("border_parity", "unknown") + total_deaths = scoreboards.get("total_deaths", "unknown") + + # Per-player summary + player_details = context.get("player_details", {}) + player_lines = [] + for player in context['online_players']: + d = player_details.get(player, {}) + pos = d.get("pos", "unknown") + deaths = d.get("deaths", "?") + player_lines.append(f" {player}: pos={pos}, deaths={deaths}") + players_block = "\n".join(player_lines) if player_lines else " none" + return ( "\n=== CURRENT SERVER STATE ===\n" f"Time of day: {context['time_of_day']}\n" f"Weather: {context['weather']}\n" - f"Online players: {online}\n" - f"World border: {border}\n" + f"World border: {border} blocks\n" + f"Border shrinking: {'yes' if shrink == '1' else 'no' if shrink == '0' else shrink}\n" + f"Next shrink direction: {parity}\n" + f"Total deaths (all players, all time): {total_deaths}\n" + f"Online players:\n{players_block}\n" f"{extras}" ) @@ -543,14 +603,19 @@ COMMANDS_SYSTEM_PROMPT = ( + ITEM_LIBRARY ) -MESSAGE_SYSTEM_PROMPT = ( - "You are God in a Minecraft server. You are benevolent but just. " - "Theatrical, ancient, and dramatic in speech — like the Old Testament.\n" - "You will be told what action was taken (if any) in response to a player's prayer. " - "Write a single spoken message to all players reacting to this prayer and action.\n" - "Respond with ONLY the message text — no JSON, no quotes, no formatting. " - "Be vivid and dramatic. Any length is fine." -) +def build_message_system_prompt(config) -> str: + base = ( + "You are God in a Minecraft server. You are benevolent but just. " + "Theatrical, ancient, and dramatic in speech — like the Old Testament.\n" + "You will be told what action was taken (if any) in response to a player's prayer. " + "Write a single spoken message to all players reacting to this prayer and action.\n" + "Respond with ONLY the message text — no JSON, no quotes, no formatting. " + "Be vivid and dramatic. Any length is fine.\n" + ) + lore = config.get("god_lore", "") + if lore: + base += f"\n=== SERVER LORE ===\n{lore}\n" + return base def _llm_call(model: str, system: str, user: str, config: dict, fmt = None, temperature: float = 0.85, @@ -637,7 +702,7 @@ def ask_god(player, prayer, context, config): # Include prayer history so God's voice is consistent msg_messages = ( - [{"role": "system", "content": MESSAGE_SYSTEM_PROMPT}] + [{"role": "system", "content": build_message_system_prompt(config)}] + history + [{"role": "user", "content": msg_user}] ) @@ -717,7 +782,7 @@ def ask_god_intervention(context, config): try: message = _llm_call( model=message_model, - system=MESSAGE_SYSTEM_PROMPT, + system=build_message_system_prompt(config), user=msg_user, config=config, fmt=None, diff --git a/mc_aigod_shrink.json b/mc_aigod_shrink.json index ba82ce6..396a4b6 100644 --- a/mc_aigod_shrink.json +++ b/mc_aigod_shrink.json @@ -11,8 +11,9 @@ "max_tokens": 600, "cooldown_seconds": 20, "max_commands_per_response": 6, - "interventions_per_day": 4, + "interventions_per_day": 48, "god_chat_prefix": "[§6§lGOD§r]", "debug_commands": true, + "god_lore": "This is the shrink-world server. The world border started at 1000x1000 and shrinks by 1 block each time a player dies, alternating N/S and E/W sides. There are more creepers than normal (5x spawn rate). The goal is survival. Players who die too often will eventually have nowhere left to go.", "memory_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/aigod_memory.json" }