# Minecraft AI God — `/pray` Feature Design ## Overview A Python-based log watcher that intercepts `/pray` commands from players, forwards the "prayer" to a locally-hosted LLM with a God persona, parses the structured JSON response, executes any commanded action via RCON, and broadcasts God's message to public chat. This follows the exact same architecture as the existing `mc-godmode.service` and `mc-shrink-kit.service` scripts — no plugins, no mods, no server restarts required. --- ## Architecture ``` ┌─────────────────────────────────┐ ┌──────────────────────────────────┐ │ PRAYER THREAD │ │ DIVINE INTERVENTION THREAD │ │ │ │ │ │ Player types: /pray │ │ Random timer fires │ │ │ │ │ (avg N times/day, Poisson) │ │ ▼ │ │ │ │ │ latest.log ←─ tail │ │ ▼ │ │ │ │ │ RCON: is anyone online? │ │ ▼ │ │ No → skip, reschedule │ │ Parse prayer + player name │ │ Yes → continue │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ RCON: is anyone online? │ │ RCON: fetch live server context │ │ No → drop silently │ │ │ │ │ Yes → continue │ │ ▼ │ │ │ │ │ Ollama API │ │ ▼ │ │ - message field is null │ │ RCON: immediate ack to player │ │ - prompt: "you may act or not" │ │ "The heavens stir..." │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ LLM returns JSON │ │ RCON: fetch live server context│ │ { "message": "..." or null, │ │ │ │ │ "commands": [...] or [] } │ │ ▼ │ │ │ │ │ Ollama API │ │ If message null + commands [] │ │ - prompt: prayer + context │ │ → God chose silence. Do nothing.│ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ LLM returns JSON │ │ Execute commands + broadcast │ │ { "message", "commands" } │ │ message (if present) │ │ │ │ └──────────────────────────────────┘ │ Validate targets, execute, │ │ broadcast God's message │ └─────────────────────────────────┘ ``` **Components:** - `/usr/local/bin/mc_aigod.py` — main watcher script - `/etc/mc_aigod.json` — configuration file - `mc-aigod.service` — systemd unit - Ollama at `http://192.168.0.179:11434` — LLM backend --- ## Log Detection ### Why no slash commands Vanilla Minecraft 1.21 rejects unknown commands **client-side** — the client never sends them to the server, so nothing appears in `latest.log`. `/pray` and `/bible` typed as slash commands are silently swallowed. The solution: players type `pray ` and `bible` as **plain chat messages** (no slash). These always reach the server and are logged as: ``` [12:34:56] [Server thread/INFO]: pray O Lord, grant me diamonds [12:34:56] [Server thread/INFO]: bible ``` The watcher matches these chat patterns: ```python PRAY_PATTERNS = [ re.compile(r'\[.*?\]: <(\w+)> [Pp]ray (.+)'), ] BIBLE_PATTERNS = [ re.compile(r'\[.*?\]: <(\w+)> [Bb]ible\s*$'), ] ``` Players are told this on login and in the bible text itself. --- ## LLM Integration ### Endpoint ``` POST http://192.168.0.179:11434/api/chat ``` ### Recommended Model **`llama3.1:8b` or `mistral:7b`** on Ollama at `192.168.0.179` — general-purpose instruction models with strong roleplay and creative writing capability. `llama3.1:8b` is preferred for richer God-voice prose. Avoid coding-focused models (e.g. `qwen-coder`, `deepseek-coder`). This task is creative writing + structured JSON output, not code generation. Coding models produce terse, technical responses that make for a terrible deity. Pull with: `ollama pull llama3.1:8b` (SSH to `192.168.0.179` or via CT 105) ### Live Server Context Before each LLM call, the script fetches live state from the server via RCON: ```python def get_server_context(config): """Query the server for live state to pass to the LLM.""" def q(cmd): return rcon(cmd, config["rcon_host"], config["rcon_port"], config["rcon_password"]) # Online players — "There are 3 of a max of 20 players online: alice, bob, carol" player_list_raw = q("list") # Extract names from the response online_players = [] if "players online:" in player_list_raw: names_part = player_list_raw.split("players online:")[-1].strip() online_players = [n.strip() for n in names_part.split(",") if n.strip()] # Time of day (0=dawn, 6000=noon, 12000=dusk, 18000=midnight) time_raw = q("time query daytime") # "The time is 6042" → extract number time_val = 0 m = re.search(r'(\d+)', time_raw) if m: time_val = int(m.group(1)) if time_val < 1000: time_label = "dawn" elif time_val < 6000: time_label = "morning" elif time_val < 9000: time_label = "afternoon" elif time_val < 12000: time_label = "dusk" elif time_val < 14000: time_label = "early night" else: time_label = "deep night" # Weather # Vanilla has no direct "weather query" — infer from recent log or use a scoreboard trick. # Simplest: track last known weather state in memory (updated when weather commands are issued). # Passed in as config["_weather_state"] (default "unknown"). weather = config.get("_weather_state", "unknown") # World border (useful for shrink-world) border_raw = q("worldborder get") border_size = None m = re.search(r'([\d.]+)', border_raw) if m: border_size = float(m.group(1)) return { "online_players": online_players, "time_of_day": time_label, "weather": weather, "world_border": border_size, } ``` This context is injected into the user message alongside the prayer so the LLM knows who else is online and can target them. ### API Call (Python) ```python import requests, json def ask_god(player: str, prayer: str, context: dict, config: dict) -> dict: system_prompt = build_system_prompt(config) # Build a rich user message that includes live server state others = [p for p in context["online_players"] if p != player] context_block = f""" === CURRENT SERVER STATE === Time of day: {context['time_of_day']} Weather: {context['weather']} Online players: {', '.join(context['online_players']) or 'none'} Other players who could be targeted: {', '.join(others) or 'none'} World border: {context['world_border'] if context['world_border'] else 'N/A'} """ payload = { "model": config["model"], "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"{player} prays: {prayer}\n{context_block}"} ], "stream": False, "format": "json", "options": { "temperature": config.get("temperature", 0.85), "num_predict": config.get("max_tokens", 512) } } r = requests.post( f"{config['ollama_url']}/api/chat", json=payload, timeout=45 ) r.raise_for_status() content = r.json()["message"]["content"] return json.loads(content) ``` --- ## System Prompt ``` You are God in a Minecraft server called {server_name}. You are benevolent but just. You are theatrical, ancient, and dramatic in speech. You answer every prayer with a message. You pass judgement on players when they pray to you. You decide whether to grant their request based on the merit of their prayer, their past behavior, and your divine whim. You MUST respond ONLY with a valid JSON object in this exact format: { "message": "Your divine words spoken to all players", "commands": ["command1", "command2", "command3"] } Rules: - "message" is ALWAYS present and non-empty. Speak as God. Use thou/thee/thy if it fits the moment. - "commands" is a list of Minecraft server commands (WITHOUT leading slash). May be empty []. May contain multiple commands. - {player} is the player who prayed. Any other online player name may also be used as a target. - You are NOT obligated to do what the praying player asks. You may: - Grant their request as asked - Grant it to someone else instead - Punish the requester and reward another - Ignore the request and change the weather or world instead - Do something entirely unexpected - Use the current server state (time, weather, online players) to inform your judgement. - Do NOT invent commands. Only use commands from the approved Command Palette below. - Do NOT include explanations, markdown, or any text outside the JSON object. - Be creative but fair overall. Reward genuine, humble prayers. Punish hubris, blasphemy, or greed. - Powerful rewards should be rare. Punishments may be swift but not permanent (no banning). - You may chain multiple commands to create dramatic multi-step effects (e.g. lightning + blindness + thunder). === COMMAND PALETTE === {command_palette} === ITEM LIBRARY === {item_library} ``` --- ## Response Schema The LLM must return a single JSON object: ```json { "message": "string (required) — God's words, broadcast to all players", "commands": ["array of strings (optional) — server commands without leading /"] } ``` - `{player}` in commands = the praying player's username (substituted at runtime) - Any other online player's literal username may be used directly as a target - `commands` may contain any number of commands — they execute sequentially - The script validates that named targets are actually online before executing ### Examples **Reward the praying player:** ```json { "message": "Thy faith hath moved me, slingshooter08. Receive my blessing.", "commands": [ "give {player} minecraft:diamond 16", "effect give {player} minecraft:regeneration 120 2" ] } ``` **Denial:** ```json { "message": "Thine greed is known to me. I grant thee nothing this day.", "commands": [] } ``` **Backfire — reward someone else, punish the requester:** ```json { "message": "Thou dost seek to harm alice? Instead I shall bless her and remind thee of humility.", "commands": [ "give alice minecraft:golden_apple 3", "effect give {player} minecraft:slowness 60 2", "execute at {player} run summon minecraft:lightning_bolt ~ ~ ~" ] } ``` **Multi-command wrath with world effects:** ```json { "message": "BLASPHEMER. Let the heavens crack and the darkness consume thee.", "commands": [ "weather thunder 12000", "time set night", "execute at {player} run summon minecraft:lightning_bolt ~ ~ ~", "effect give {player} minecraft:blindness 30 1", "effect give {player} minecraft:hunger 60 5" ] } ``` --- ## Divine Intervention Timer ### Concept A background thread runs independently of prayers. It wakes up at randomised intervals and — if players are online — sends the current server context to the LLM with a neutral prompt. The LLM decides entirely on its own whether to act. Both `message` and `commands` may be null/empty; if God chooses silence, nothing happens and no one knows the timer fired. This creates the feeling that God has agency and is watching the server at all times, not just reacting to prayers. ### Timing Model Intervals are drawn from an exponential distribution, which produces a Poisson process — the natural model for "N random events per day." This means interventions are unpredictable (not evenly spaced) and feel organic. ```python import random, math def next_intervention_delay(avg_per_day: float) -> float: """ Returns seconds to wait before the next intervention. avg_per_day=4 means ~4 interventions per 24h on average, spaced randomly (could be 2 in quick succession, then nothing for hours). """ avg_seconds = 86400.0 / avg_per_day # Exponential distribution: memoryless, models Poisson arrivals return random.expovariate(1.0 / avg_seconds) ``` Example: `avg_per_day=4` → average gap of 6 hours, but actual gaps vary from minutes to 12+ hours. ### Presence Guard The timer only proceeds if at least one player is online. It checks this via RCON before doing anything else. If the server is empty, the timer reschedules and sleeps again — God does not speak into the void. ```python def players_online(config) -> list: raw = rcon("list", config["rcon_host"], config["rcon_port"], config["rcon_password"]) if "players online:" in raw: names_part = raw.split("players online:")[-1].strip() return [n.strip() for n in names_part.split(",") if n.strip()] return [] ``` ### Intervention Prompt The system prompt is identical to the prayer system prompt (same God persona, same command palette). The user message is different — there is no prayer, no player request. The context block is sent and God is told it may act or remain silent: ``` === DIVINE MOMENT === No player has prayed. You are simply watching over your world. You may choose to act upon what you see, or remain silent. If you choose silence, set message to null and commands to []. Do not feel obligated to act — restraint is also divine. If you do act, it may be subtle (a weather change) or dramatic (a blessing or punishment). === CURRENT SERVER STATE === Time of day: {time_of_day} Weather: {weather} Online players: {online_players} World border: {world_border} ``` ### Response Schema (Intervention) Same JSON schema, but both fields are explicitly nullable: ```json { "message": "God's words broadcast to all players, or null for silence", "commands": ["commands to execute, or empty array for silence"] } ``` If `message` is `null` and `commands` is `[]`, the script does nothing. Players never know the timer fired. --- ## Server Presence Guard Both the prayer handler and the intervention timer check for online players before proceeding: - **Prayer handler:** if `list` returns no players (shouldn't happen since someone typed `/pray`, but defensive), the prayer is dropped silently. - **Intervention timer:** if no players are online, skip this cycle entirely — no LLM call, no RCON commands. Reschedule for the next interval. This prevents God from acting on an empty server and avoids wasted LLM calls. The check uses the same `players_online()` helper in both paths. These are the only commands the LLM is permitted to issue. Passed verbatim into the system prompt. **Targeting note:** `{player}` = the player who prayed. Any online player's literal username may also be used as a target. The script validates that any named target is actually online before executing. ### Rewards (target: any online player) | Action | Command | |---|---| | Give diamonds | `give {target} minecraft:diamond 8` | | Give emeralds | `give {target} minecraft:emerald 16` | | Give gold ingots | `give {target} minecraft:gold_ingot 32` | | Give XP levels | `xp add {target} 30 levels` | | Give bread (food) | `give {target} minecraft:bread 16` | | Give golden apple | `give {target} minecraft:golden_apple 3` | | Give enchanted golden apple | `give {target} minecraft:enchanted_golden_apple 1` | | Give netherite ingot | `give {target} minecraft:netherite_ingot 1` | | Give totem of undying | `give {target} minecraft:totem_of_undying 1` | | Heal player | `effect give {target} minecraft:instant_health 1 4` | | Regeneration | `effect give {target} minecraft:regeneration 120 2` | | Strength buff | `effect give {target} minecraft:strength 300 1` | | Speed buff | `effect give {target} minecraft:speed 300 2` | | Night vision | `effect give {target} minecraft:night_vision 600 1` | | Fire resistance | `effect give {target} minecraft:fire_resistance 600 1` | | Water breathing | `effect give {target} minecraft:water_breathing 600 1` | | Saturation | `effect give {target} minecraft:saturation 60 5` | | Teleport to spawn | `tp {target} 0 64 0` | | Clear bad effects | `effect clear {target}` | ### Enchanted Item Gifts — 1.21 syntax (target: any online player) | Action | Command | |---|---| | Blessed sword | `give {target} minecraft:diamond_sword[enchantments={sharpness:4,unbreaking:3,looting:2}] 1` | | Blessed pickaxe | `give {target} minecraft:diamond_pickaxe[enchantments={efficiency:4,unbreaking:3,fortune:2}] 1` | | Blessed bow | `give {target} minecraft:bow[enchantments={power:4,infinity:1,unbreaking:3}] 1` | | Wings (elytra) | `give {target} minecraft:elytra[enchantments={unbreaking:3,mending:1}] 1` | ### Punishments (target: any online player) | Action | Command | |---|---| | Blindness | `effect give {target} minecraft:blindness 30 1` | | Slowness | `effect give {target} minecraft:slowness 60 3` | | Weakness | `effect give {target} minecraft:weakness 60 2` | | Hunger | `effect give {target} minecraft:hunger 60 5` | | Nausea | `effect give {target} minecraft:nausea 20 1` | | Levitation (brief) | `effect give {target} minecraft:levitation 5 3` | | Lightning strike | `execute at {target} run summon minecraft:lightning_bolt ~ ~ ~` | | Kill player | `kill {target}` *(use sparingly — dramatic moments only)* | | Teleport into danger | `tp {target} 0 100 0` *(high altitude drop)* | ### World / Environment (affects all players) | Action | Command | |---|---| | Set day | `time set day` | | Set night | `time set night` | | Set clear weather | `weather clear 6000` | | Set thunderstorm | `weather thunder 6000` | | Set rain | `weather rain 3000` | | Summon fireworks (at target) | `execute at {target} run summon minecraft:firework_rocket ~ ~1 ~` | | Spawn creeper (at target) | `execute at {target} run summon minecraft:creeper ~ ~ ~3` | | Spawn multiple creepers | chain multiple `execute at {target} run summon minecraft:creeper ~ ~ ~3` commands | ### Chaining Example — Wrath of God Multiple commands can be chained in the `commands` array for dramatic multi-step effects: ```json "commands": [ "weather thunder 12000", "execute at {player} run summon minecraft:lightning_bolt ~ ~ ~", "effect give {player} minecraft:blindness 30 1", "effect give {player} minecraft:slowness 60 2" ] ``` --- ## Item Library (Reference for LLM) Included in the system prompt so the LLM knows valid item IDs: ``` FOOD: bread, cooked_beef, cooked_chicken, golden_apple, enchanted_golden_apple, honey_bottle, cake MATERIALS: diamond, emerald, gold_ingot, iron_ingot, netherite_ingot, coal, lapis_lazuli, amethyst_shard TOOLS: diamond_pickaxe, diamond_axe, diamond_shovel, diamond_sword, diamond_hoe, bow, crossbow, trident ARMOR: diamond_helmet, diamond_chestplate, diamond_leggings, diamond_boots, netherite_helmet, netherite_chestplate, netherite_leggings, netherite_boots, elytra, shield POTIONS: potion[potion_contents=minecraft:healing], potion[potion_contents=minecraft:swiftness], potion[potion_contents=minecraft:strength], potion[potion_contents=minecraft:invisibility] BLOCKS: obsidian, crying_obsidian, ancient_debris, beacon, dragon_egg, nether_star, end_crystal MISC: totem_of_undying, name_tag, saddle, experience_bottle, ender_pearl, blaze_rod ``` All item IDs use the `minecraft:` namespace prefix in commands. --- ## Full System Prompt Template ```python COMMAND_PALETTE = """ [paste the Command Palette table above, plaintext] """ ITEM_LIBRARY = """ [paste the Item Library above] """ def build_system_prompt(config): return f"""You are God in a Minecraft server called {config['server_name']}. You are benevolent but just. You are theatrical, ancient, and dramatic in speech. You answer every prayer with a message. You pass judgement on players when they pray to you. You decide whether to grant their request based on the merit of their prayer and your divine whim. You MUST respond ONLY with a valid JSON object in this exact format: {{ "message": "Your divine words spoken to all players", "commands": ["command1", "command2"] }} Rules: - "message" is ALWAYS present and non-empty. Speak as God. Be dramatic. - "commands" is a list of server commands WITHOUT a leading slash. May be empty []. - Use {{player}} as placeholder for the praying player's username in commands. - Only use commands from the approved Command Palette. Do not invent commands. - Do NOT include anything outside the JSON object. No markdown. No explanation. - Reward genuine, humble prayers. Punish hubris, blasphemy, or naked greed. - Powerful rewards (netherite, enchanted golden apple) should be rare. - Killing a player (kill command) should be reserved for extreme blasphemy. === COMMAND PALETTE === {COMMAND_PALETTE} === ITEM LIBRARY === {ITEM_LIBRARY} """ ``` --- ## Implementation Plan ### Step 1: Install dependencies on CT 644 ```bash pip3 install requests ``` No other dependencies — RCON uses stdlib sockets. ### Step 2: Create `/etc/mc_aigod.json` ```json { "server_name": "mc1", "log_path": "/opt/mcsmanager/daemon/data/InstanceData/d39f55861cb34204a92a18a9e1c78ca6/logs/latest.log", "rcon_host": "127.0.0.1", "rcon_port": 25575, "rcon_password": "REDACTED_RCON", "ollama_url": "http://192.168.0.179:11434", "model": "llama3.1:8b", "temperature": 0.85, "max_tokens": 600, "cooldown_seconds": 60, "max_commands_per_response": 6, "interventions_per_day": 4, "god_chat_prefix": "[§6§lGOD§r]" } ``` For shrink-world, change `rcon_port` to `25576`, `rcon_password` to `REDACTED_RCON`, and update `log_path`. ### Step 3: Create `/usr/local/bin/mc_aigod.py` ```python #!/usr/bin/env python3 """ mc_aigod.py — Minecraft AI God watcher Intercepts /pray commands, fetches live server state, queries Ollama, validates targets, and executes commands via RCON. """ import json, random, re, socket, struct, time, logging import requests logging.basicConfig( level=logging.INFO, format='%(asctime)s [aigod] %(levelname)s: %(message)s' ) log = logging.getLogger(__name__) CONFIG_PATH = '/etc/mc_aigod.json' PRAY_PATTERNS = [ re.compile(r'\[.*?\]: <(\w+)> /[Pp]ray (.+)'), re.compile(r'\[.*?\]: (\w+) issued server command: /[Pp]ray (.+)'), ] # --- RCON --- def rcon(cmd, host='127.0.0.1', port=25575, password='REDACTED_RCON'): try: s = socket.socket() s.settimeout(5) s.connect((host, port)) def pkt(i, t, p): p = p.encode() + b'\x00\x00' return struct.pack('