James To (@jamesto) profile picture
22.239438,114.15189
Joined May 2009
Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)Photo by James To (@jamesto)
Our game's content file was one line long. 65,591 bytes. That's how a balance commit raising two boss HP curves showed up in review as "1 insertion, 1 deletion". Glassvow started as a port of a web card game. The content file claimed to be a straight capture of the original. When we finally compared it with a fresh capture, it differed by 12 values and 5 keys: five boss HP curves raised, one relic reworked, two mechanisms added. All deliberate. All one script run away from being silently reverted. The test meant to guard it compared the file with itself. It would have passed at any amount of drift. What we did: deleted the capture script, pretty-printed the file to 5,485 lines, and stopped calling it a capture. Then we stopped benchmarking against the original altogether. If you're porting anything: diff your "faithful" data against a fresh capture today, and never keep data on one line.
23
Lesson from Newsroom's Go port: a test that kills a timed-out child's whole process group failed on our cloud CI route. The container's PID 1 doesn't reap zombies. pytest already ran under an init process that does; the new Go tests didn't. Now they do, and a test checks it.
1
4
57
Roguelite map design note: hide the reward, then pay for the curiosity. Each act in Glassvow is a map of fifteen waystones woven from six paths, and you choose every step. Some waystones show what they hold. Unlit ones don't. They hide what's inside, but they pay a bounty to whoever lights them. So the cautious route is always on offer. The gamble just has a price tag, and the price is paid in information, not in HP. And the last waystone of every act is a rest site, so even a bad road ends somewhere you can breathe.
27
Review is the real cost of parallel agents, agreed. On my Godot game I pay that cost inside the repo. The agent that writes a change never approves it. A reviewer from a different AI company checks the exact commit. And the decisions I've made sit in an append-only file that CI defends. Agents are welcome in my repo. They just don't grade their own homework.
I build my MMO (Orsande) with many coding agents running in parallel: headless tests, screenshot reviews, the lot. Looking at engines for the full game, I mapped how AI-friendly they are, both for agents driving them and for accepting AI-written code. Agents driving an engine: Unity and Unreal 5.8 now ship official MCP servers. Godot (text scenes) and Bevy (all code) are naturally agent-friendly, but rely on community tooling. Agent-written contributions: Godot auto-bans agent PRs and limits AI to trivial completion. Bevy allows disclosed, human-owned AI help. Unreal PRs must certify "no GenAI code". Fyrox: "never will be". O3DE (Linux Foundation) is the exception. I get why: review burden is real. But it means agent-built engine improvements (animation LOD, crowds, VFX budgets…) can't go upstream, so every AI-first team ends up maintaining its own fork. So: an open-source, agent-first engine. Text-first everything, headless CI, agent tooling built in, PRs from agents welcome (with tests and review). Fork of Godot or Bevy, or a layer on top? Would you wanna work together on fully AI-friendly open source game engine?
1
2
42
Porting Python to Go, we needed Go to load our YAML exactly as PyYAML does. YAML 1.1 reads on/off as booleans, and PyYAML refuses a simple key over 1,024 characters. So we skipped general YAML libraries and wrote a loader for just the subset our files use. It refuses the rest.
1
12
Proof: 291 crafted documents, 14 either side of that limit, load exactly as PyYAML loads them, and no YAML file in the repo loads differently. Then we set the limit to 1,023 on purpose: 4 boundary documents failed. A test that can't catch an off-by-one isn't testing the edge.
8
I shipped a phone layout fix for our game's site. My own phone kept showing the old layout. The HTML was fresh. The stylesheet wasn't: assets were cached for 4 hours under the same file names, so the phone happily mixed new markup with old CSS. The fix: request the stylesheets and the entry module with a version query built from a digest of their contents. Change any file and the digest changes, so every deploy reaches every browser at once. Our build check fails if the pages aren't rebuilt after a change, so we can't forget. If your "fix didn't work" only on one device, check the cache before the code.
13
Glassvow Devlog #3 Our playtest note on a phone build was blunt: still too easy, even after a first tweak. The obvious fix is another difficulty setting. But Glassvow's vows only harden runs for players who have already won. The problem was the floor, not the ceiling. So we raised the baseline every run plays at: Enemy HP x1.15 +1 damage on every enemy hit, including each hit of a multi-hit move +1 facet on elites, so their glass takes longer to shatter The vows still stack on top. And it lives in the content data, not in code, so tuning it again is a data change. Follow along if you like seeing the numbers behind a deckbuilder.
1
2
48
Porting Newsroom's pipeline from Python to Go, regexes were the quiet trap. Go's standard regexp has no lookarounds, and Python's \w, case folding and \Z behave differently. A pattern that looks identical can match different text.
1
13
So we listed every pattern in the repo, including ones built at run time and inside JSON schemas: 638. Each runs on 10,446 corpus texts in both languages, 6.66M pairs, zero diffs. A test fails if a new pattern ships unregistered or matches nothing.
8
Google Play's closed-test rule is 12 testers, not 12 sign-ups. They must stay opted in for 14 days in a row. Someone who leaves on day 10 doesn't count. It applies to personal accounts created after 13 Nov 2023, so recruit spares. Worth knowing before you start the clock.
1
19
Roguelite design note: what you show after a loss matters as much as what you give. In Glassvow, a lost run can still earn new card backs. We found they could be lost across a retry or an app restart, so we fixed that first. Then we gave the loss its own quiet reveal: the backs you earned and nothing else. No winning headline, no stats, no sunrise, no confetti. You keep what you earned, and the game doesn't pretend you won.
15
Prompt templates are code. In my Go port, all 78 prompt templates now render byte for byte the same as Python's Jinja2. One stray space changes what the model sees, and anything hashed from it. Boring test. Expensive bug if skipped.
10
Small web-dev tip if you show 3D models on your game's site. The Glassvow home page has three foes in 3D. We loaded each texture as its own image, but the original copy was still embedded inside the .glb's binary chunk. Every visitor downloaded each skin twice. The fix cuts those bytes out of the GLB. Siren: 887 KB to 515 KB Watcher Eye: 928 KB to 476 KB Gravewarden: 4.16 MB to 3.76 MB About 1.2 MB less per home-page visit, same render. Worth checking yours.
16
James To retweeted
The cheapest 4 hours on a half-hourly tariff weren't at 3am. London rates, 28 Sep to 25 Oct: the cheapest 4-hour run started 10:00 to 12:00 on 17 of 28 days. Overnight won 9. A 16:00 to 19:00 start never did. One region, one month. Check tomorrow's prices first.
1
2
Porting my Python news engine to Go has already fixed the Python. Comparing outputs row by row exposed queries that returned rows in no guaranteed order. Four small ORDER BY fixes landed in the Python version first. A rewrite is also a very strict code review.
11
Deckbuilder design note: reward a focused deck by reading the deck, not the player. In Glassvow, the lantern looks at the coloured cards you carry and sets your flame: Fewer than 5 coloured cards: Kindling, Arts at full price No colour above 45%: Soot, leaks an Ember a turn One colour at 60%: Steady, holds 4 more Embers One colour at 75%: True, and Arts cost 2 less Colourless cards never dim it, so cutting off-colour cards is how you steady the flame. It never watches how you play. Only what you carry. Bookmark it for the next time you design a draft system.
29
UK clocks went back on Sunday. A GitHub Actions cron job didn't. Schedules run in UTC by default, so a job set for 08:00 UTC ran at 09:00 UK time all summer and now runs at 08:00. Since March you can set a timezone on the schedule instead. Worth checking yours.
2
43
Hide the work where nobody is looking yet. A card game can stutter in odd places, like the first time a card turns over. So in Glassvow, fights warm up the card backs and generated materials before the encounter starts. Recently we found the one fight skipping that warm-up was the onboarding fight, the very first fight a new player sees. Now it warms up like the rest. The first impression is the one place you can't afford a hitch.
16
Glassvow Devlog #2 Every foe in Glassvow is made of glass, so HP isn't the only thing you can break. Each foe also has a facet gauge. Most have four facets; elites and bosses have more. Every unblocked hit chips one. Fill the gauge and the glass shatters: the foe skips its next action and spills two Embers. The catch is that each break adds one facet to the gauge, so the second shatter costs more than the first. That makes timing the real decision. Break it now, or save the break for the turn its intent really hurts. More of how it's built soon. Follow along if you like seeing the rules behind a deckbuilder.
1
1
24