// wrong assumption
The Architecture Was Backwards
The session started with a broken assumption baked into the architecture. The original setup
had Python acting as the TCP client and BizHawk as the server — which is
backwards. BizHawk's Lua API exposes comm.socketServerSend() and
comm.socketServerResponse(), meaning BizHawk is always the client.
It connects out to whatever is listening. Getting BizHawk to open the connection at all
required passing both URL flags at launch — get and post independently:
start "BizHawk" EmuHawk.exe --url-get=http://127.0.0.1:9001 --url-post=http://127.0.0.1:9001
// still broken
Empty Strings. Syntax Error. Session Stalls.
Even with the flags, comm.socketServerResponse() kept returning
empty strings on every call. A protocol fix was queued in Lua — but a
syntax error at line 154 in speedy_sender.lua blocked
testing before it could be validated. The raw TCP socket approach was fighting us at every layer:
timing races, two-port juggling, response polling that never quite landed.
It was the wrong tool for what we were actually doing.
// the real fix
Throw Out the Sockets. Use HTTP.
The breakthrough was stepping back and reading the BizHawk Lua API properly.
comm.httpPost(url, body) exists. It's synchronous. BizHawk POSTs the GSL payload,
blocks until Python responds, gets the action back in the response body. No socket timing.
No polling loop. No two-port juggling. Flask on port 9001 listening at /act
— that's the whole server.
// speedy_sender.lua
local URL = "http://127.0.0.1:9001/act"
-- synchronous: BizHawk blocks until Python responds
local ok, response = pcall(comm.httpPost, URL, gsl)
// speedy.py
@app.route("/act", methods=["POST"])
def act():
state = parse_gsl(request.get_data(as_text=True))
return process_state(state), 200
BizHawk doesn't need --socket_ip or --socket_port launch flags for this.
Load the ROM, run the script. comm.httpPost handles the rest.
Flask is a lightweight Python web framework — it lets you spin up an HTTP server
in a handful of lines, mapping URL routes to Python functions. Here it meant we could replace
the entire custom socket protocol with a single decorated function: BizHawk POSTs to
/act, Flask calls process_state(), returns the action string.
No handshake logic, no buffer management, no read/write timing to get right.
HTTP is a solved problem. Flask just exposes it. That's exactly why it helped —
we stopped writing plumbing and got back to writing the actual model.
// it works
Cecil Is Moving. The Model Is Learning.
With speedy.py and speedy_sender.lua running against each other over HTTP,
the loop closed. BizHawk reads WRAM every 30 frames, builds a GSL payload encoding
position, terrain, party state, and story flags, POSTs it to Flask, gets an ACT:DIRECTION
back, holds the button for 30 frames, repeats. SpeedyNet is training.
Epsilon decays from 1.0 as the replay buffer fills. Loss is stabilizing.
Cecil is on screen and moving under model control.
Architecture: HTTP POST/response — fully synchronous, no race conditions. ✓
Training: SpeedyNet live — replay buffer filling, loss stabilizing, epsilon decaying. ✓
Cecil: moving on screen under model control. ✓
Next session: wire in save state control for autonomous resets, uncap emulator speed for self-play at machine speed.
// proof
Cecil On Screen
This is what it looks like when it works. SpeedyNet pushing inputs,
BizHawk executing them, Cecil moving. First live run under model control.