<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Python on echo3.co</title><link>https://echo3.co/tags/python/</link><description>Recent content in Python on echo3.co</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://echo3.co/tags/python/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a Real-Time AI Bridge for a Tabletop RPG</title><link>https://echo3.co/posts/fgu-claude-bridge/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://echo3.co/posts/fgu-claude-bridge/</guid><description>&lt;p>I play Dungeons and Dragons. Specifically, I run it: I&amp;rsquo;m the Dungeon Master, which means I&amp;rsquo;m responsible for the world, the story, the non-player characters, the rules, the pacing, and the improvised dialogue of every person in that world who isn&amp;rsquo;t one of my players. It&amp;rsquo;s a lot to carry, and preparation time is always under pressure.&lt;/p>
&lt;p>I&amp;rsquo;ve been using AI heavily for prep: developing characters, writing scene descriptions, planning encounters. But during sessions I kept running into the same problem. The AI was in a separate browser window. The game was happening somewhere else. Keeping the AI contextually aware of a live session, while simultaneously running that session, is genuinely difficult. I was either neglecting the AI because I was too busy, or breaking the flow of the game to type updates.&lt;/p></description><content>&lt;p>I play Dungeons and Dragons. Specifically, I run it: I&amp;rsquo;m the Dungeon Master, which means I&amp;rsquo;m responsible for the world, the story, the non-player characters, the rules, the pacing, and the improvised dialogue of every person in that world who isn&amp;rsquo;t one of my players. It&amp;rsquo;s a lot to carry, and preparation time is always under pressure.&lt;/p>
&lt;p>I&amp;rsquo;ve been using AI heavily for prep: developing characters, writing scene descriptions, planning encounters. But during sessions I kept running into the same problem. The AI was in a separate browser window. The game was happening somewhere else. Keeping the AI contextually aware of a live session, while simultaneously running that session, is genuinely difficult. I was either neglecting the AI because I was too busy, or breaking the flow of the game to type updates.&lt;/p>
&lt;p>The obvious solution was to connect them directly.&lt;/p>
&lt;h2 id="what-is-a-virtual-tabletop">What is a Virtual Tabletop?&lt;/h2>
&lt;p>A Virtual Tabletop (VTT) is software that replicates the tabletop RPG experience online: shared maps, character sheets, dice rolling, fog of war, initiative tracking, and a shared chat window where the game&amp;rsquo;s narrative plays out. It&amp;rsquo;s the online equivalent of sitting around a physical table.&lt;/p>
&lt;p>Fantasy Grounds Unity (FGU) is one of the leading VTTs. It&amp;rsquo;s a desktop application with a rich extension system, a large library of officially licensed content, and a dedicated community. I run all my games through the FGU chat window: we play online, and keep a Discord open purely for out of game chat and clarification.&lt;/p>
&lt;h2 id="the-problem-precisely-stated">The Problem, Precisely Stated&lt;/h2>
&lt;p>FGU has no native AI integration. To use AI during a session you have to:&lt;/p>
&lt;ol>
&lt;li>Watch what&amp;rsquo;s happening in FGU&lt;/li>
&lt;li>Manually summarise it in a separate AI chat window&lt;/li>
&lt;li>Ask your question&lt;/li>
&lt;li>Copy the response back into FGU&lt;/li>
&lt;/ol>
&lt;p>At a live table, with players waiting, that workflow falls apart quickly. What I needed was for the AI to be watching the session alongside me, building its own awareness of events as they happened, so that when I needed something I could ask with minimal friction and get an answer that was already grounded in the current situation.&lt;/p>
&lt;h2 id="the-architecture">The Architecture&lt;/h2>
&lt;p>The solution has three components.&lt;/p>
&lt;p>&lt;strong>A Lua extension inside FGU.&lt;/strong> FGU supports custom extensions written in Lua. This extension registers a set of GM-only slash commands in the chat window. When a GM types a command, the extension writes a small JSON request to FGU&amp;rsquo;s console log.&lt;/p>
&lt;p>&lt;strong>A Python server running locally.&lt;/strong> This is the middleware. It does two things simultaneously: it monitors FGU&amp;rsquo;s &lt;code>chatlog.html&lt;/code> file for new entries, and it monitors the console log for GM commands. The chatlog monitor feeds session events continuously into a conversation history buffer held in memory. When a GM command arrives, that buffer is sent to the Anthropic API along with the GM&amp;rsquo;s request and a system prompt containing the campaign notes.&lt;/p>
&lt;p>&lt;strong>The Anthropic API (Claude).&lt;/strong> Receives the full context, generates the response, returns it. The Python server copies the response to the system clipboard. The GM switches to FGU, pastes into the chat box, edits if needed, and sends.&lt;/p>
&lt;pre tabindex="0">&lt;code>FGU chatlog.html → Python monitors continuously
Session events accumulate in memory (no API call, no cost)
GM types /npctalk → console.log → Python detects it
Full context sent to Anthropic API
Response copied to clipboard
GM: Ctrl+V into FGU chat → Enter
&lt;/code>&lt;/pre>&lt;h2 id="how-the-live-context-works">How the Live Context Works&lt;/h2>
&lt;p>This is the part I find most interesting from a systems perspective.&lt;/p>
&lt;p>FGU writes everything that happens in a session to &lt;code>chatlog.html&lt;/code>: player dialogue, attack rolls, damage results, initiative order, turn markers. The Python server polls this file every 0.4 seconds. New lines are parsed, stripped of HTML, and appended to a Python list in memory. Mechanical lines (dice rolls, attack results) are prefixed with &lt;code>[ROLL]&lt;/code> so Claude understands they are game events rather than speech.&lt;/p>
&lt;p>Every few seconds, pending events are flushed into the conversation history as a &lt;code>[TABLE]&lt;/code> message with a silent acknowledgment response. This maintains the alternating user/assistant structure the Anthropic API requires, without making any API call. Claude never responds to &lt;code>[TABLE]&lt;/code> messages: the system prompt instructs it to absorb them silently and wait for a GM command.&lt;/p>
&lt;p>When a command arrives, the pending events are flushed, the GM&amp;rsquo;s request is appended, and the entire conversation history is sent to the API in a single call. Claude has seen everything that happened at the table and can respond accordingly.&lt;/p>
&lt;p>The conversation history persists in memory for the duration of the server session. Nothing is written to disk. The only persistent files are the campaign notes, which the GM maintains between sessions.&lt;/p>
&lt;h2 id="the-campaign-notes">The Campaign Notes&lt;/h2>
&lt;p>Two markdown files provide Claude&amp;rsquo;s background knowledge.&lt;/p>
&lt;p>&lt;strong>&lt;code>campaign_history.md&lt;/code>&lt;/strong> is the accumulated record of the campaign: the world, the player characters (race, class, personality, relationships, backstory), session recaps, established facts, and ongoing story threads. It grows over the course of a campaign and is updated after each session.&lt;/p>
&lt;p>&lt;strong>&lt;code>session_notes.md&lt;/code>&lt;/strong> is the preparation for a specific session: scene descriptions for every location the party might visit, full NPC profiles (personality, voice, manner, combat behaviour), GM notes, and secrets Claude should know but never reveal.&lt;/p>
&lt;p>Both files are loaded at server startup and injected into the system prompt of every API call. They are the static foundation. The chatlog is the live layer on top.&lt;/p>
&lt;h2 id="the-gm-commands">The GM Commands&lt;/h2>
&lt;p>All commands are slash commands typed in the FGU chat box. They are intercepted by the Lua extension before display: players never see them.&lt;/p>
&lt;p>&lt;code>/describe&lt;/code> — generates a sensory scene description and posts it to chat. Pure observation: what you see, hear, smell, feel. No inner states, no narrative closure.&lt;/p>
&lt;p>&lt;code>/npctalk (Name)&lt;/code> — generates the named NPC&amp;rsquo;s spoken dialogue only. No action, no mannerism. Ready to paste while talking as that character in FGU.&lt;/p>
&lt;p>&lt;code>/npcaction (Name)&lt;/code> — generates what the NPC physically does. No dialogue.&lt;/p>
&lt;p>&lt;code>/combatsummary&lt;/code> — reads the recent attack rolls and results from the chatlog and writes a narrative description of what just happened.&lt;/p>
&lt;p>&lt;code>/claude&lt;/code> — freeform private query, whispered to the GM only. Players never see the response.&lt;/p>
&lt;p>&lt;code>/setscene&lt;/code> — updates the current scene context in the config. Does not clear conversation history: NPCs travelling with the party retain their context across scene changes.&lt;/p>
&lt;p>&lt;code>/claudereset&lt;/code> — clears all conversation history. For use when you want a genuine fresh start.&lt;/p>
&lt;h2 id="cost">Cost&lt;/h2>
&lt;p>API calls are the only cost. The chatlog monitoring and context accumulation between commands is free.&lt;/p>
&lt;p>Each call sends the full system prompt (campaign notes) plus the accumulated conversation history. Cost per call increases gradually through a session as history grows. The model is configurable: Claude Haiku is the default, being the fastest and cheapest option. A full session will typically cost no more than a couple of dollars, often considerably less, depending on session length, note file size, and how many commands you send.&lt;/p>
&lt;h2 id="what-it-produces">What It Produces&lt;/h2>
&lt;p>The quality of output depends directly on the quality of the notes files. A well-written NPC profile produces consistent, specific, in-character dialogue. A vague placeholder produces generic responses.&lt;/p>
&lt;p>The system prompt enforces strict output rules: no inner states or emotional speculation, no concluding statements, no narrative padding. &lt;code>/npctalk&lt;/code> returns only spoken words. &lt;code>/npcaction&lt;/code> returns only physical behaviour. &lt;code>/describe&lt;/code> returns only sensory observation. The discipline is deliberate: at a live table you need responses, not prose.&lt;/p>
&lt;h2 id="the-repo">The Repo&lt;/h2>
&lt;p>The full project, including the Lua extension, Python server, and markdown templates for the notes files, is on GitHub:&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://github.com/heyjudeuk/fgu-claude-bridge">https://github.com/heyjudeuk/fgu-claude-bridge&lt;/a>&lt;/strong>&lt;/p>
&lt;p>The README covers installation step by step. You&amp;rsquo;ll need Python, an Anthropic API key, and Fantasy Grounds Unity. The extension was built and tested against FGU 5.1.8.&lt;/p>
&lt;p>One note for anyone who goes looking at the Lua code: FGU Unity 5.x uses &lt;code>Comm.registerSlashHandler&lt;/code> for custom slash commands. Older tutorials and documentation reference &lt;code>ChatManager.registerSlashCommand&lt;/code>, which no longer exists. That cost a few debugging rounds to establish.&lt;/p>
&lt;p>The broader pattern here is one I find myself returning to repeatedly: most useful AI integrations are not about replacing a workflow but about removing the friction in an existing one. The game was already happening in FGU. The AI was already capable of generating what I needed. The problem was the gap between them, and the gap turned out to be a 200-line Python file and a Lua extension.&lt;/p></content></item></channel></rss>