# Tom Greenaway — Full Published Corpus (llms-full.txt)

> Complete Markdown content of all published articles and pages from https://tomgreenaway.com

================================================================================
ARTICLE: Hello Agentic World: Making Your Website Agent-Friendly
Canonical URL: https://tomgreenaway.com/blog/hello-agentic-world
Markdown URL: https://tomgreenaway.com/blog/hello-agentic-world.md
Published: 9 Nov, 2026 (2026-11-09)
Tags: builders, vibe coding, AI, thoughts
================================================================================

---
date: 2026-11-09
title: "Hello Agentic World: Making Your Website Agent-Friendly"
status: published
tags: "builders", "vibe coding", "AI", "thoughts"
prompts: true
---

# Hello Agentic World: Making Your Website Agent-Friendly

A few days ago I read a great post by my colleague [Guillaume Laforge](https://glaforge.dev/posts/2026/10/30/agent-experience-in-practice-serving-this-hugo-blog-as-markdown/) about turning his blog into something AI agents can read cleanly. His blog was built with Hugo but mine is a from scratch static site generator I built myself.

I was curious how straightforward it would be to make my site 'agent friendly' like Guillaume's so I prompted Gemini 4 via Antigravity to first ask questions and then go ahead with the implementation. At the end of this article you can find a single compacted prompt I prepared that should assist with any website.

## Answering the Why

The first question we should always ask ourselves before taking on a task is to answer the "Why" – by that I mean, **why** are we about to do this task? Why would we care what an AI agent sees when it accesses one's website?

To answer this, let's start by thinking about what a person sees when opening my blog: a person sees a three-column layout with my bio on the left, a list of projects and articles in the middle, and the post itself on the right. That's a nice UX for humans but is it optimal for AI agents to digest and process?

When an AI agent fetches that exact same page, it has to wade through hundreds of lines of HTML tags, CSS classes, sidebar links, and scripts before it even reaches the first sentence of the page's actual content. **Roughly 80% of the page is just scaffolding.** That wastes the agent's context window, slows it down, and makes it more likely to misread what's actually on the page.

*In short: optimisation for AI agents reduces token consumption everywhere, speeds up processing time, and increases the accuracy and understanding of your page for the AI agent audience.*

Since I already write every page on this site in markdown, it felt silly to turn clean markdown into noisy HTML, only for visiting AI agents to try and scrape it back into text again.

## Step 1: Asking What "Agent-Friendly" Actually Means Today

Obviously I'd read Guillaume's article so I already knew the answer but I felt it was good to setup the discussion with Gemini first with the following prompt (who knows, maybe the best practices had evolved in the last 24 hours!):

> **Prompt:**  
> *What are the best practices for serving an agent-friendly version of a site nowadays? I've seen people rendering Markdown files directly.*

In general, I find grounding any major architectural task for a site, app or game is a great starting point with Antigravity. This is the exploratory phase.

The short answer was that you don't need to reinvent your website. If your site is built from markdown (as mine was), the simplest win is to **keep/serve a clean `.md` copy of every public page right next to the `.html` version**.

For example:
- People read: `https://tomgreenaway.com/blog/whats-a-programmer`
- AI Agents can read: `https://tomgreenaway.com/blog/whats-a-programmer.md`

And how does an AI agent know that the `.md` version exists? There are a few options but one of the simplest is to add a single line inside the `<head>` of your HTML template pointing to it:

```html
<link rel="alternate" type="text/markdown" href="https://tomgreenaway.com/blog/whats-a-programmer.md" title="Markdown" />
```

This same mechanism has been used for many purposes in website standards for decades including canonical URLs, RSS feeds, and even the Accelerated Mobile Pages format which I helped out on at Google.

## Step 2: `robots.txt` meet `llms.txt` and `agents.txt`

Giving every page a `.md` twin is great once an agent is already on a specific article. But what if an AI agent lands on your home page and wants to know what your site is about and what's worth reading?

> **Prompt:**  
> *What other steps does one need to take? Is there an agents.txt file or something like that?*

Right now, the web has settled on a defacto stanard of having a couple of simple text files that live at the root of your site – right alongside the classic `robots.txt`:

1. **[`/llms.txt`](/llms.txt) (the table of contents)**: This is the big one. It's a short, plain markdown file at the root of your domain that acts like a map of links for AI models. It has a one-paragraph summary of who you are, followed by a clean list of links to the `.md` versions of your published articles and pages.
2. **[`/llms-full.txt`](/llms-full.txt) (a single payload of everything)**: An optional companion file that combines all of your published markdown posts into one single document, for agents with huge context windows that want to read your whole site in one gulp.
3. **[`/agents.txt`](/agents.txt) (rules/guidance for the agents on how to behave)**: While `robots.txt` tells search crawlers where they aren't allowed to go, `agents.txt` is a newer convention for telling active AI agents *how* to use your site – pointing them to your `/llms.txt` file, letting them know they can request markdown directly, and stating whether you want attribution when they quote you.

Because my site has a build script that turns markdown into HTML, I didn't have to write these files by hand. I just asked the agent to generate `/llms.txt`, `/llms-full.txt`, `/robots.txt`, and `/agents.txt` automatically every time I run `npm run build`.

## Step 3: Teaching Cloudflare to know about both HTML and Markdown

Having `.md` URLs is helpful, but many modern coding agents (like Antigravity or Claude Code) don't even change the URL when they fetch a link you paste into chat. Instead, they request the normal URL (`https://tomgreenaway.com/blog/whats-a-programmer`) and quietly attach an HTTP header that says:

`Accept: text/markdown`

This means that when the AI agent is asking for the page, it is quietly checking if there's a markdown version available instead. 

> **Prompt:**  
> *Can Cloudflare support sending a different page based on an HTTP request header?*

I host this site on **Cloudflare** and deploy it from my terminal using **Wrangler** – Cloudflare's command-line tool.

Normally, a static site on Cloudflare is just a folder of pre-built files (`public/`). You run `npx wrangler deploy`, Wrangler uploads your folder to Cloudflare's servers around the world, and Cloudflare hands those files out for free. There's no server code running at all.

To support that `Accept: text/markdown` header, you only need to add two tiny pieces:

1. **A 20-line "server side" script (`worker.js`)**: In Cloudflare, a **Worker** is a tiny bit of JavaScript lets Cloudflare send a markdown file if the requestor (person or AI agent or whatever) requests it via the HTTP header I mentioned above.
2. **A pointer in `wrangler.jsonc`**: You tell Wrangler to use `worker.js` for page requests, while letting images, CSS, and fonts bypass the script completely.

```mermaid
flowchart LR
    V["Visitor requests<br/>/blog/whats-a-programmer"] --> W["Cloudflare Worker<br/>(worker.js checks header)"]
    W -- "Normal browser" --> H["Serves whats-a-programmer.html"]
    W -- "Accept: text/markdown" --> M["Serves whats-a-programmer.md"]
```

When a human being visits in Chrome, the Worker hands them the regular `.html` page. When an agent visits with `Accept: text/markdown`, the Worker swaps in the `.md` file from the exact same folder.

## Step 4: Watch Out for the "Unlisted" Trap

As builders, it's always really important after you've implemented anything to really test thoroughly and catch issues. So once everything was wired up and deployed, I opened my new `/llms.txt` file to check the list of pages – and immediately spotted a problem.

On my site, I have a few half-baked article drafts and experimental games/pages that aren't linked in the navigation menu. They're meant to be **unlisted**: live at their URL if I send the link to a friend, but hidden from search engines and indexes.

Because those pages still lived inside my `pages/` folder as Markdown files, the build script had dutifully listed every single one of them inside `/llms.txt` and `/llms-full.txt`!

> **Prompt:**  
> *Do the individual pages have the ability to be marked as 'unlisted' as well? If something is unlisted, it shouldn't have a public .md mirror and it shouldn't appear in llms.txt*

This is an easy trap to fall into when you're making a site agent-friendly. **An `llms.txt` file is a public megaphone.** If your build script blindly sweeps up every `.md` file in your repo, you'll end up advertising your unfinished drafts and private pages to every AI crawler who asks.

One follow-up prompt fixed it so that any article or page with `status: unlisted` in its front-matter:
- Still builds its `.html` page (with `<meta name="robots" content="noindex">`).
- **Does not** generate a `.md` file.
- **Does not** get included in `/llms.txt` or `/llms-full.txt`.

## Try It on Your Own Site

If you want to add an agentic view to your own blog or portfolio, you don't need a new framework. Just open your project in your favourite coding agent and paste this single prompt:

```wrap copy
I want to make this website agent-friendly by implementing the following:

1. Clean Markdown Mirrors: Update the build step so every published page and blog post outputs a clean `.md` copy alongside its `.html` file, and add `<link rel="alternate" type="text/markdown" href="...">` inside the HTML `<head>`.
2. Discovery Files: Auto-generate `/llms.txt` (a concise summary + links to every published `.md` page and article), `/llms-full.txt` (the full published Markdown corpus), `/agents.txt` (declaring Markdown support and attribution policy), and update `/robots.txt` to reference `Llms-Txt`.
3. Respect Unlisted & Draft Content: Ensure any draft, private, or `unlisted` pages/posts are strictly excluded from `.md` mirror generation, `/llms.txt`, and `/llms-full.txt` (and include `<meta name="robots" content="noindex">` on unlisted HTML pages).
4. HTTP Content Negotiation: Add a lightweight edge handler or Worker so requests sent with `Accept: text/markdown` receive the clean `.md` file (with `Content-Type: text/markdown; charset=utf-8` and `Vary: Accept`) while normal browsers continue receiving the HTML page. If the hosting provider supports this capability.

Before starting the implementation, walk me through the steps and be to the point with a low amount of tech jaron please.
```

And if you want to see what this exact article looks like to an agent right now, you can just run this on your terminal:

```bash
curl -H "Accept: text/markdown" https://tomgreenaway.com/blog/hello-agentic-world
```

================================================================================
ARTICLE: What's a Programmer?
Canonical URL: https://tomgreenaway.com/blog/whats-a-programmer
Markdown URL: https://tomgreenaway.com/blog/whats-a-programmer.md
Published: 29 Oct, 2026 (2026-10-29)
Tags: builders, vibe coding, AI, thoughts
================================================================================

---
date: 2026-10-29
title: "What's a Programmer?"
status: published
youtubeId: "Ixy2ql7g3-I"
tags: "builders", "vibe coding", "AI", "thoughts"
---

# What's a Programmer?

Actually, let's start with a different question... do you remember the above ad campaign from 8 years ago?

When Apple added a keyboard and stylus to the standard iPad they decided to run a new ad campaign called *"What's a computer?"*

The gist of the ad campaign was that a young girl was doing all sorts of productive work and fun stuff on her iPad around town. She was using it in different formats – tablet, keyboard, sketching with a stylus – but she never really thought about what category of device she was using. She only cared about the current task. The current moment. The current experience.

Near the end of the ad, she's lying on the grass in her backyard and her neighbour leans over the fence (*Home Improvement* style) to ask a casual question:

> *"What are you doing on your computer there?"*

The girl quips back unknowingly:

> *"What's a computer?"*

To the girl, she didn't know the term 'computer' – or at least, she didn't care about the old distinction between a desktop PC and everything else. To her, this was "just an iPad". Obviously that's a very Apple-centric way of framing it – more generally, she just saw it as an appliance for making and doing things. The box disappeared and only the activity remained.

Now it's 2026, and the identity and nature of a programmer has completely changed in much the same way. Which begs the question:

**What's a programmer?**

## Computer, Programmer, Builder... Vibe

Let's rewind to the 1960s (and earlier). Before there were computers, there were... _**computers**_.

For centuries, "computer" wasn't a noun for a device – it was a job description. A computer was someone who calculated numbers and crunched data by hand.

When _electronic_ computers arrived, they replaced the human job of *computing*. And yet the people didn't vanish – they moved one rung up the ladder of abstraction. Instead of performing every arithmetic step themselves, they started writing the instructions for the machine to follow. And so, they became **programmers**.

For the next sixty years, programmers ruled over computing devices. We were a bit like a guild of magicians, casting spells and incantations through arcane arts – aka "programming languages". If you wanted the machine to do your bidding, you had to learn these languages – C, Java, Python, and so on.

And yet, if you zoom out across the entire history of software, the trajectory has always pointed in one direction: punch cards gave way to assembly, assembly gave way to C, C gave way to garbage-collected scripting languages, and scripting languages gave way to high-level frameworks. **Every generation of programming has been an attempt to move closer to plain human intent.**

[DHH recently said that](https://www.youtube.com/watch?v=vDjW_dRyKXY) he never thought he'd love another other programming language more than Ruby, but that he'd recently discovered there indeed was a language he enjoyed coding in even more – it's called **English**! Or as Andrej Karpathy described it: *"The hottest new programming language is English."*

Now we use English to write code. Which means, in a very practical sense, English *is* a programming language – arguably the highest-level one we've ever had.

And where does that leave the humble programmer? Did we just automate ourselves out of a job the same way electronic computers replaced human computers? Are we obsolete?

Or is something subtler going on?

## Programmers versus Builders versus... regular folk

I was using vibe coding and Antigravity to do some data science analysis for a personal topic recently – pulling together public data sources and estimating probabilities across a few different scenarios.

A friend was also interested in the same topic. So I shared with her some of the graphs and probability curves I'd generated and told her I'd used AI to do the analysis.

I was a bit surprised by her response:

> *"I wish I had your super nerd skills 😂"*

My surprise came from the fact she didn't seem to understand that AI had "done the heavy lifting".

So, I replied back to her:

> *"Oh! No, I don’t even have to use my tech skills anymore! AI enables me to do this now, haha. I don't know much about data science, but this analysis that I wouldn’t have had time to do before can now be done in minutes."*

She responded again:

> *"The difference is you know what to tell AI to do, I don’t even have those skills 😂"*

And so once again, I was surprised.

I had tried to reassure her that it was easy – that there was no technical barrier I'd needed to overcome... that I didn't really need to be a programmer or a data scientist to produce what I'd just sent her.

**But was that really true?**

Einstein once said:

***"Even more important than the answer – is the question."***

When I sat with my friend's reply, I realised she was seeing something I was completely blind to. In fact, there were two uncomfortable truths hiding in our exchange:

### 1. I don't realise I'm still bringing a programmer's mind to the task

When I vibe code in Antigravity, it *feels* effortless compared to how I used to work. I'm not getting bogged down in coding syntax or fighting with a new library's API. So my brain tells me: *"Look, I'm not even programming anymore!"*

Except I am.

What I didn't notice – because twenty years of making software and games has wired it into my muscle memory – is *how* I was talking to the agent:

- Brainstorming with the agent as if I was speaking with a senior engineer.
- Breaking a vague hunch down into explicit possibilities and assumptions.
- Asking the agent to explore multiple approaches in different repository branches and compare or graph the differences.
- Looking at results and having a hunch about the underlying cause of a performance issue or a rendering bug – and steering the agent to fix the underlying logic.
- Knowing *what* was even possible to ask for in the first place.
- Or even... having the confidence that perhaps the AI is wrong and has made a mistake, instead of blindly believing it.

**Syntax was never the most important skill of programming – understanding the entire architecture and system was.** For decades the two were fused together because you couldn't express systems thinking to a machine without typing out code.

Now that the syntax tax has dropped near zero, we confuse the absence of typing with the absence of engineering.

### 2. Knowing what to ask starts with being willing to experiment

The second truth that dawned on me is the flip side of the first... how did any of us become programmers in the first place?

Nobody was born knowing what a for loop is. We became programmers because, at some point, we became curious about computers and what we could do with them. We hacked on a script get a game to run. We opened Chrome devtools by accident and got curious.

**We tinkered, we experimented, and we learned the spells. And by being willing to fail, we slowly learned how to ask the right questions.**

When my friend says *"I don't even have the skills to know what to tell AI to do,"* the real barrier isn't a computer science degree. It's the willingness to explore and the belief that there's a secret vocabulary one must know *before* you're allowed to start.

If a person doesn't try casting the spells and failing – if they don't practice asking naive questions, watching the output break, and steering it – they never build the intuition. Even when the programming language is plain English, **you still have to be willing to tinker and never give up.**

And there's an important contrast I noticed between this friend of mine and another who has recently been building a new portfolio website for herself using vibe coding tools like Antigravity and Claude Code. The second friend has been relentlessly exploring and learning how to build what she wants – and she is succeeding. Because she has a goal in mind, she's curious, and she's willing to try. Sometimes I want to give Builders a different title: entrepreneurs. For my second friend, having a concrete project she cared enough about to push through the awkward trial-and-error phase enabled her to overcome the uncertainty of experimentation.

I sat down with that second friend and showed her even more things she could do with these tools. It helped her to have some extra guidance. But I'll save that story and my learnings from that for another time.

## So... what's a programmer?

Think back to that girl in the backyard with her iPad in the Apple advert.

In a few years, a whole generation of builders won't think of themselves as "writing code" any more than she thought of herself as "using a computer." They won't care about the label. They'll just have a problem they want solved, an ephemeral tool they want to exist, or a game mechanic they want to experience – and they'll keep talking to an agent until they succeed.

And for those of us who *did* spend years learning how systems fit together? We haven't been replaced per se. We've leveled up.

Builders are learning how to create software themselves but programmers of old still have an edge. When you strip away the friction of memorising syntax across a dozen languages and frameworks, a programmer's habits – decomposing problems, spotting edge cases, questioning assumptions, and iterating relentlessly – become **force-multiplied**.

The language has changed but there is still a technique to the words and the **thought** of what goes into them. The spells aren't written in arcane syntax anymore – they're woven out of clear thinking, structure, and intent.

So... what's a programmer?

**A force-multiplied weaver of spells.**

================================================================================
ARTICLE: GDPR is my laundry
Canonical URL: https://tomgreenaway.com/blog/gdpr-ai-skill
Markdown URL: https://tomgreenaway.com/blog/gdpr-ai-skill.md
Published: 25 Oct, 2026 (2026-10-25)
Tags: builders, vibe coding, AI
================================================================================

---
date: 2026-10-25
title: "GDPR is my laundry"
status: published
tags: "builders", "vibe coding", "AI"
repo: https://github.com/tcmg/skills/tree/main/skills/gdpr-auditor
prompts: true
---

# GDPR is my laundry

_"Everyone wants AI to do their laundry and not their art. Well, in the world of software engineering the equivalevnt of laundry is... **GDPR compliance**. So voilà, let's write an AI skill to do my proverbial laundry."_

But before that, let's recognise something: Vibe coding is an amazing ability that is enabling builders around the world to create whatever they want without formal coding backgrounds. But are they ticking all the boxes before publishing their work online?

If a builder isn't careful, they could create a website and publish it without complying with European privacy law. You know those little cookie banners you see online? Those are because of GDPR – which stands for General Data Protection Regulation.

Personally I've had this website for a while and it's had different iterations. But recently I've been trying to be more consistent with my blogging, so I decided I wanted to add some analytics to see how that was going (spoiler alert: my traffic is up with **76.5% new users** since I started posting regularly. With the more [navel](/blog/dynamicware.html) [gazing-esque](/blog/js13p.html) articles appearing to be the most popular).

But thinking about analytics on my own site meant I needed to think about the privacy regulations around a feature like that. This thought-process led directly to the idea for a **GDPR AI skill** inside my personal library of Antigravity skills.

Along the way, I learned how others are exploring this space, where privacy actually breaks across a modern web stack, and how combining deterministic scripts with agent reasoning turns a legal text and requirements into an actionable engineering check.

## GD What Now?

For anyone who hasn't had the pleasure: GDPR is short for "the General Data Protection Regulation". Introduced by the EU in 2018. is built on a simple premise – your personal data belongs to you, not the companies that collect it. Even something as small as an IP address or a tracking cookie counts as personal data. Crucially, the law follows where the visitor is, not where the developer lives: if someone in Paris opens your website, GDPR applies to you even if you coded it on a beach in Hawaii.

## Different Regions, Different Defaults

When I moved from Australia to France seven years ago, one of the first things that surprised me on the everyday internet was the sheer wall of GDPR pop-ups on every single website.

Historically, in much of the tech world the default mindset was to **collect first, ask questions later**. People would drop Google Analytics into their site and call it a day. Over time, different countries and governments have implemented various privacy laws. GDPR being one of the most famous.

So suddenly those harmless-looking engineering defaults weren't just bad hygiene – they became real legal risks.

Under the EU's **GDPR** (and the **ePrivacy Directive**), privacy isn't a legal disclaimer you merely slap in a footer at the end of a project. It's an architectural constraint.

A quick summary:

- You can't set non-essential cookies or fire tracking pixels *before* a user explicitly opts in.
- You can't make "Accept All" a big shiny green button while hiding "Reject" behind three sub-menus.
- You can't keep raw IP addresses and emails in server logs forever "just in case."
- When a user asks to delete their account, you actually have to erase or anonymise their personal data – not just flip `is_deleted = true`.

Regulation is complicated, dense, and written by lawyers rather than engineers. Nobody wants to read 99 pages of regulation before shipping a weekend prototype.

**Which is why so many people still make mistakes in this space.**

And worse: if you ask an AI model to build your app without specific guidance, **the model might happily reproduce some of the industry's bad habits.** Why wouldn't it? If it's trained on websites that have made the mistakes themselves.

## Meet the washing machines

Before diving in, I had a look around to see how other developers were tackling privacy and GDPR in the age of coding agents. Unsurprisingly, there are people are actively experimenting in this space – and the existing tools fall into a few interesting buckets:

1. **Traditional "Privacy-as-Code" Scanners**: Tools like [Privado CLI](https://github.com/Privado-Inc/privado-cli) and [Ethyca Fides](https://github.com/ethyca/fides) pioneered static data-flow scanning to help enterprise teams map PII in codebases and generate compliance reports. They're powerful, but they're built for enterprise governance pipelines – and static code analysis alone is completely blind to what happens inside a live browser DOM or a cookie banner.
2. **Encyclopaedic GRC & Legal Skills**: On the AI agent side, repositories like [`Privacy-Data-Protection-Skills`](https://github.com/mukul975/Privacy-Data-Protection-Skills) (which bundles 280+ structured skills across GDPR, CCPA, the EU AI Act, and LGPD) and [`Claude-Skills-Governance-Risk-and-Compliance`](https://github.com/Sushegaad/Claude-Skills-Governance-Risk-and-Compliance) focus on giving agents deep regulatory reference knowledge so they don't hallucinate article numbers when drafting DPIAs, privacy notices, or audit checklists.
3. **Prompt Directives & Regional Law Integrations**: There are also lightweight prompt scanners like [`compliance-scanners`](https://github.com/Valentis-AI-Labs-Ltd/compliance-scanners) and regional setups like [`deutsches-recht-mit-claude`](https://github.com/waldo-van-der-code/deutsches-recht-mit-claude), which pairs a skill with an MCP server connected directly to Germany's federal law portal (`rechtsinformationen.bund.de`).

I wanted to try something more mechanical: **a hybrid engineering skill** that pairs deterministic local scripts (a fast Python static codebase scanner and a headless Playwright browser inspector) with parallel agent reasoning – and actually clicks buttons on `localhost` to see if the app behaves itself.

## Why an AI Skill Fits This Problem Space So Well

As soon as you try to map GDPR articles to actual engineering checks, you realise why neither a pure regex linter nor a pure LLM prompt is enough on its own.

**Regulatory compliance lives at the intersection of UI design, runtime browser behaviour, database architecture, backend data flows, and plain-English legal text.**

Think about what a real privacy audit actually requires:

1. **Cross-modal reasoning**: You have to scan the project folder and backend API routes to see that the app sends data to particular third parties – and then read the English prose on the `/privacy` page to check whether those companies/brands are actually disclosed to the user.
2. **Visual & UX judgement**: Under European Data Protection Board guidelines, a cookie banner with an asymmetric "Reject" link is a deceptive dark pattern. Evaluating whether "Reject All" is on the first layer and visually symmetric with "Accept All" requires inspecting the live DOM and final render of the page.
3. **End-to-end data tracing**: Verifying the "Right to Erasure" means tracing a `DELETE /api/user` endpoint through server calls to database models.
4. **Knowing where code ends and lawyers begin**: Some things are black-and-white technical failures (like firing a hidden pixel before consent). While others require human legal review.

So in my `gdpr-auditor` skill, I structured the workflow into **four phases** combining deterministic scripts, reference data, and parallel subagents:

* Phase 1: Python Static Scan (`scan_gdpr_codebase.py`)
* Phase 2: Playwright 3-State Audit (`inspect_frontend_runtime.mjs`)
* "Phase 3: Parallel Subagents (UI / DB / Backend Tracing)
* "Phase 4: Compliance Report (`gdpr_compliance_report.md`)

Instead of guessing, the skill categorises every finding into one of four explicit dispositions: `CRITICAL_VIOLATION`, `ARCHITECTURAL_RISK`, `NEEDS_LEGAL_REVIEW`, or `COMPLIANT`, complete with clickable file-and-line links and the exact GDPR or ePrivacy article it maps to.

## What I learned along the way

Building the skill and its scanner scripts forced me to break technical compliance down into **four distinct pillars** across the stack. Each one had gotchas that are remarkably easy to miss when you're heads-down building a product.

### Pillar 1: The Frontend & The "Three-State" Browser Runtime

You cannot audit frontend privacy just by reading React or Vue files in a code editor. Too much happens dynamically at runtime via tag managers, third-party scripts, and CSS imports.

In Phase 1, the Python scanner (`scan_gdpr_codebase.py`) first separates `consumer_facing` routes from internal `/admin` routes, checks forms for pre-ticked consent boxes, and flags sensitive tokens or PII written to `localStorage`.

Then in Phase 2, the Node runtime inspector (`inspect_frontend_runtime.mjs`) spins up headless Playwright against the live URL and tests **three distinct runtime states**:

- **State 1 – Zero-Interaction Baseline (Pre-Consent)**: Before interacting with the page, the script intercepts every outbound network request and checks `document.cookie`, `localStorage`, and `sessionStorage` against a curated database (`known_tracker_domains.json`) of 20+ tracking domains and cookie prefixes (`_ga`, `_fbp`, `ph_`, `mp_`, `_hj`). Under the ePrivacy laws, any non-essential tracker firing here is an immediate critical violation.
  - A surprise gotcha: **Google Fonts.** If your site loads fonts directly from `fonts.googleapis.com` on page load, the visitor's browser automatically transmits their IP address to Google's servers without prior consent. Certain German courts have explicitly ruled this unlawful under GDPR. The fix takes two minutes – self-host the `.woff2` files – but if you don't live in Europe, you'd almost never think to check for it.
- **State 2 – The Consent Banner & Dark Patterns**: If a cookie banner is rendered, the script inspects the live DOM and computed styles of every clickable control inside it. It automatically flags dark patterns if:
  - An "Accept" button exists on the first layer without a 1-click "Reject / Necessary Only" button.
  - The "Accept" button's pixel bounding-box area is more than **2.2× larger** than the "Reject" button.
  - "Accept" is rendered as a `<button>` while "Reject" is demoted to an inline `<a>` link.
- **State 3 – Post-Rejection Enforcement**: Finally, the script literally clicks the "Reject All" button, reloads the page, and listens to the network tab again to prove that trackers stay silent and the user's rejection was persisted.

### Pillar 2: Databases, Access Rules & The "Soft-Delete Trap"

Moving down to the database layer opened up a completely different set of headaches.

First, `scan_gdpr_codebase.py` inventories schema files (like sql or other database formats) and separates **Standard PII** (`email`, `phone`, `ip_address`, `geolocation`) from something called **Article 9 Special Category Data** (`health`, `biometric`, `genetic`, `religion`, `political_opinion`, `sexual_orientation`, `ethnicity`) – which requires explicit consent and field-level encryption – fun stuff!

Secondly, it checks **access isolation**. In the era of Backend-as-a-Service tools like Firebase, it is super easy for a vibe-coded app to ship with a table missing basic PostgreSQL security rules or with a Firestore rule set like `allow read: if request.auth != null` – which politely allows *any* logged-in user to read the personal documents of *every other* user in the database.

Lastly, there's what we'll call the **Soft-Delete Trap**.

Engineers can be lazy so it's always tempting to just do soft deletes. This is when a user deletes their account and the programmer decides to just change a setting like `deleted_at = NOW()` or `is_deleted = true` to keep things simpler. Nothing breaks! Your billing history tables don't suddenly have opharns.

Except under **GDPR Article 17 (Right to Erasure)**, if a user deletes their account and you keep their plaintext `email`, `full_name`, `phone_number`, or `ip_address` sitting in that row forever, **you haven't deleted their data.** It's obvious but a lazy developer might take this approach.

Creating the skill made me formalise what a compliant deletion cascade actually looks like: either hard-deleting the row with `ON DELETE CASCADE`, or – if you genuinely need to retain transaction records for tax laws – running an irreversible anonymisation pass that scrubs the PII columns.

### Pillar 3: Backend Logging Sinks & AI Egress

Even if your database is locked down and your cookie banner is spotless, backend servers leak personal data like a sieve in two places: **logs** and **third-party API payloads**.

When we're debugging an auth flow or a webhook, logging object data to the web console is a handy shortcut. But suddenly, plaintext emails, IP addresses, and headers are sitting in a third-party logging sink that can be visible to other parties.

And in 2026, there is a brand-new egress vector that didn't exist when GDPR was written in 2016: **LLM prompts**.

It is extraordinarily easy to grab a full `User` row from some databases and pass the whole object into a prompt template, when the model only needed two non-identifying fields to do its job. Teaching the skill to trace outbound SDK calls and check for data minimisation before the data leaves the server was a valuable check for the whole flow.

### Pillar 4: Transparency & Code-to-Policy Drift

Finally, there is the problem of **drift**.

A company writes a Privacy Policy on day one. Over the next three months, they swap one developer product API for another, add a new analytics tracker, and a new billing provider.

Over time the code iterates but the developer forgets to update the privacy policy.

To catch this automatically, `scan_gdpr_codebase.py` scans the repository for 20 common developer products and immediately cross-checks every discovered vendor against the actual text of the repository's `/privacy` and `/cookies` files – flagging any **undisclosed vendors/integrations** before a user or regulator notices the gap.

## Eating my own dogfood

Of course, once the skill was working, there was an obvious next test: my own site.

So I asked Antigravity to run `gdpr-auditor` against the repository for **tomgreenaway.com**.

Now, this site is about as simple as web architecture gets: a static site built from Markdown files and some EJS templates.

To my relief, a few things were already compliant – mostly from earlier minimalism habits:

- My web font (`NunitoSans.ttf`) and syntax highlighter (`highlight.js`) were already **self-hosted** in `/resources/` rather than pulled from Google Fonts or a third-party CDN.
- My YouTube embeds in `build.js` were already using `youtube-nocookie.com` (Privacy-Enhanced Mode).

**And yet the skill still immediately discovered some mistakes:**

1. **Newsletter Collection Without a Privacy Notice**: In my left sidebar, I have an EmailOctopus newsletter sign-up form. Except I had **no privacy page anywhere on the site**, and no point-of-collection link explaining who processes the email address or how to unsubscribe.
3. **A Third-Party CDN Leak for Mermaid Diagrams**: Even though I had self-hosted `highlight.js`, articles with Mermaid flowcharts were still pulling `mermaid.min.js` directly from `cdn.jsdelivr.net` – leaking visitor IP addresses to a third-party CDN on load.
4. **Missing Edge Privacy Headers**: My `public/_headers` was missing global `Referrer-Policy` and `Permissions-Policy` headers.

I don't think anyone is a fan of cookie pop-ups. (I believe John Gruber calls intrusive modal overlays *"Dick-Overs"*? Vulgar but sentiment is on point honestly.) The absolute last thing I wanted to do to fix my analytics violation was bolt an obnoxious pop-up banner onto the front of my personal website.

So instead of using Google Analytics and adding a banner, I went with **Cloudflare Web Analytics** – which is cookieless, doesn't fingerprint visitors, and means I get lightweight traffic stats while avoiding pop-up banners altogether.

For the rest, it took less than five minutes to:

- Add a plain-English [Privacy Notice](/privacy.html) (`pages/privacy.md`) disclosing Cloudflare and EmailOctopus, and link it directly underneath the newsletter form.
- Download `mermaid.min.js` into `public/resources/` so diagrams render 100% first-party with zero CDN requests.
- Add `Referrer-Policy: strict-origin-when-cross-origin`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, and a strict `Permissions-Policy` to `public/_headers`.

## Conclusion

Nobody wakes up on a Saturday morning excited to audit the privacy of their website – we want to build cool things, test ideas, and ship them to people around the world.

Historically, complicated regulations meant one of two things for small builders: either you ignored them and hoped for the best, or you got bogged down in compliance anxiety and delayed your launch.

But once you take the time to distil a complex domain – whether it's GDPR, accessibility, or security hardening – into a structured skill with deterministic helper scripts, that knowledge becomes **portable and repeatable**.

It isn't a replacement for a lawyer when you're scaling a company. Nothing in a markdown file or a Python script is legal advice. And to be clear, if you use my AI skill you do so at your own risk – I don't indemnify you at all.

But as an extra engineering guardrail? In a world where anyone can ship software globally in an afternoon, having an automated collaborator that understands the local legal rules – and happily folds the proverbial laundry for you – makes the web a little safer, a little more private, and a lot less stressful to build for.

**What other laundry do you need taken care of for you?**

================================================================================
ARTICLE: Dynamicware: Bespoke, Ephemeral & Disposable Tools
Canonical URL: https://tomgreenaway.com/blog/dynamicware
Markdown URL: https://tomgreenaway.com/blog/dynamicware.md
Published: 16 Oct, 2026 (2026-10-16)
Tags: builders, vibe coding, AI, games, thoughts
================================================================================

---
date: 2026-10-16
title: "Dynamicware: Bespoke, Ephemeral & Disposable Tools"
status: published
tags: "builders", "vibe coding", "AI", "games", "thoughts"
---

# Dynamicware: Ephemeral Code, Remixable Tools, and Why Games May Be Different

_"As tools become cheaper to build, they become more bespoke, more ephemeral, and finally more disposable... but thankfully games are not tools..."_

On a recent personal project I needed to do a highly repetitive task in Photoshop involving the magic wand tool and manual operations that would take me 20 minutes and I needed to do it 30~ times over. 

I realised that my Photoshop subscription was no longer active and while five years ago I might've paid again for Photoshop or looked for a free tool for this temporary task, I suddenly had a thought – maybe I can actually vibe code this tool.

Better yet, I could vibe code a completely automated version of the tool that had a pipeline which would do what I was going to do 30 times over. Not only would I avoid paying for Photoshop but I'd actually be faster at the work and if I needed to redo any of the graphics/art in the pipeline, the whole process would be instantly repeatable.

So I prompted Antigravity and had an initial version working within 30 minutes. It literally only did a tiny slice of Photoshop's feature set but it actually did it very fast for a single file. I manually ran that tool over each of the 30 files I needed to operate over.

Then a few weeks later I needed to re-run the tool as the art had changed. So I decided to tweak the tool again with a prompt and make it a completely repeatable pipeline. The tool continued to evolve along the way of my project, becoming more and more precise, and saving more and more time.

Another amazing part of this is that... I know this tool will be mostly tossed away at the end of the project. Just as quickly as I generated it, it will be gone again.

I'm noticing it with others around me working the exact same way. Software tools are so cheap to generate they can be **both** more precise and bespoke than ever, and more ephemeral and disposable than ever.

## From durable appliances to disposable scratchpads and temporary pipelines

For most of computing history, software was expensive to make. And because it was expensive to make, it **had** to last. And because it had to last, it **had** to be generalised.

If a team spent six months building a tool, they couldn't build it for one person's Tuesday afternoon problem. They had to build it for ten thousand people over five years. That economic reality gave us the modern software product: sprawling settings menus, plugin architectures, role-based permissions, and onboarding tooltips explaining buttons that 90% of users never click.

Every settings screen in a piece of traditional software the developer trying to guess every slightly different way a stranger might want the tool to behave.

Now the cost of spinning up a working tool has dropped to a prompt and ten minutes of steering. **When software becomes cheap enough to generate on demand, it stops being an appliance and starts being a scratchpad.**

People are building tiny, hyper-specific tools for the exact problem sitting in front of them right now – a custom review tool for a video pipeline, a bespoke converter for a single batch of assets, a one-off dashboard to watch – and then dropping those tools the moment the task is done.

## Why generalising software is getting harder

As we get used to software that fits your problem like a glove, off-the-shelf software will start to feel strangely claustrophobic.

The hardest part of building a generalised product today isn't writing the code. It's convincing anyone to live inside your feature set. People don't want to be constrained by what a product manager decided was in scope last quarter. They don't want to file a feature request and wait six months, or stitch three subscription-based tools together just to accomplish a task.

**People don't want to adopt a fixed feature set anymore – they want to remix software as they need it.**

If a tool doesn't quite fit my pipeline, I don't want a settings toggle. I want tell an agent to optimise the functionality for my task and get back to work.

That makes life genuinely difficult for anyone trying to sell generalised utility software. When the barrier to building a custom tool was high, users tolerated friction because the alternative was writing ten thousand lines of code from scratch. When the alternative is a three-sentence prompt, "almost fits your workflow" isn't good enough.

## Meet Dynamicware

We have plenty of words for software defined by how it's distributed or monetised: commercial software, open source, shareware, freeware.

We don't really have a good word for software defined by _who it's for_ when that audience is a single person.

While chatting with my colleague [Guillaume Laforge](https://x.com/glaforge), he touched on this phenomena and mentioned the term Selfware, because we were talking about temporary tools that engineers are generating.

But personally I still feel like these tools can be reused (or at least remixed) inside enterprises, and the fact they evolve, or adapt dynamically to our needs, tends me to like another term: Dynamicware.

To me that term emphasises their dynamicism in both features and in time:

- **Built for a moment, not a roadmap.** A piece of Dynamicware might live for an afternoon, a single project, or a single debugging session. Deleting it when you're done isn't technical debt; it's clearing the workbench.
- **Remixed, not configured.** When your needs change, you don't maintain backwards compatibility. You just mutate the code.

Dynamicware isn't replacing foundational infrastructure (yet) – nobody is vibe coding their own browser engine or database kernel before lunch. But the boilerplate, internal tools, and workflow utilities that sit on top? They probably will do so – and more often.

## What does this mean for games?

Although I'm an engineer, I'm also a game designer. So I always consider things through the lens of the games industry as well.

If utility software is dissolving into ephemeral, remixable Dynamicware, it leads to an obvious question for anyone who makes games.

Will games go the same way? If anyone can prompt a playable prototype in an afternoon, will players stop buying games and just generate whatever they feel like playing for twenty minutes on a Friday night?

Perhaps there will be a bit of this but I don't think a traditionally 'designed' game will disappear. Because **utility software is a means to an end, a game is the end in itself.**

When I build a tool to convert audio files or sync draft blog posts, I don't want an _experience_. I want the task finished so I can move on with my day – it's work. If I can collapse that friction to zero with a piece of disposable Dynamicware, that's all I need.

I remember years ago when discussing [game design versus UX design](https://medium.com/google-design/4-fundamentals-of-gamification-44e07d1bc00) with a colleague [Yasmine Evjen](https://x.com/YasmineEvjen) that I said to her: 

***"In UX, the destination is the reward. In a game, the journey is the reward.”***

## Conclusion: Games are different because games are art

When we play a game – just like when we read a novel, watch a film, or listen to a song – we aren't trying to reduce friction. Quite the opposite: playing a game means voluntarily stepping into a set of unnecessary obstacles because we trust the person who placed them there. **Again, the journey is the reward.**

One of the lessons I wrote down years ago after shipping _Duet_ was simple: **pour your whole self into your product and it will speak a unique and resonant truth.**

You can't get that from Dynamicware for a game, because you **can't surprise yourself with your own prompt.**

When you play a great game, you're having a conversation with another human being. You're feeling their sense of timing in the jump physics, their sense of humour in a throwaway line of dialogue. Part of the magic is knowing that another person cared enough polish (or not polish) those details – and that thousands of other players hit that exact same wall and felt that exact same moment of relief when they finally cleared it.

AI is already changing how we _build_ games. Many game developers I know are spinning up temporary tools inside their own pipeline – custom level-editing scripts, throwaway asset tools, quick prototypes to test whether a mechanic is fun before committing a month to it. That part is wonderful. Anything that gets an idea from head to screen in a few hours gives small builders leverage they never had before.

But the finished game? The thing on the other side of all those disposable tools?

**As software becomes more disposable, human point of view becomes the scarce resource.** We'll gladly generate our own tools and throw them away by dinner. When it comes to games, though, there will always be a hunger for something we _didn't_ make for ourselves: a human message, from a human being, wrapped in gameplay we didn't see coming.

================================================================================
ARTICLE: Paris in 3D with Gemini 4 Argon
Canonical URL: https://tomgreenaway.com/blog/eiffel-tower/paris-in-3d
Markdown URL: https://tomgreenaway.com/blog/eiffel-tower/paris-in-3d.md
Published: 15 Oct, 2026 (2026-10-15)
Tags: vibe coding, AI, demos
================================================================================

---
date: 2026-10-15
title: "Paris in 3D with Gemini 4 Argon"
status: published
youtubeId: uCQ3DIswJzM
tags: "vibe coding", "AI", "demos"
demo: https://paris-metro-511808.web.app/
repo: https://github.com/tcmg/paris-metro-3d
prompts: true
---

# Building a 3D miniature of Paris, its monuments, and the métro with Gemini 4 Argon

_*"A city of lights."*_

When you live in Paris, you experience the city in two completely separate ways: the overground streets and the underground metro first created in 1900.

On a standard 2D transit map, those two worlds have nothing to do with each other. *Châtelet* is just a labyrinth; *Chemin Vert* is just a dot on a purple line. You can't see how Line 6 climbs out of the earth onto a stone viaduct to cross the Seine or how Line 4 had to be sunk in giant waterproof caissons beneath the river bed in 1908.

I wanted to see both worlds at once: a living 3D architectural maquette of Paris above ground and the subterranean tunnels underneath. An experience that could also act as a way to learn more about the history of Paris too. As I practiced for the French citizenship interview at the start of this year I thought this virtual map could help inspire and teach others about Paris too.

So I opened [Antigravity](https://antigravity.google) with **Gemini 4 Argon** and built it across many iterative prompts. Here is how it came together into an interactive historical miniature of this fantastic city of lights.

---

## Starting Prompt: A 3D Miniature of Paris and Its Métro

I kicked things off with this prompt:

> *"I'd like to create a web application that uses Three.JS to create a really beautiful simulation of Paris's metro system. The idea is that it will show the metro lines under the city but there will also be a visible 3D recreation of Paris itself on top. The 3D recreation of Paris can be made as a blender model using the Blender MCP server we're installing. There should also be a timeline slider that lets us rewind the look of the city and the metro lines to when the very first paris metro line was created/opened. and we should be able to slide up to 2026 today as new lines and stops are added or removed. the metro lines should be colour coded according to the real metro line colours in Paris. the individual metro stops should also be hoverable to reveal information about them. Paris should look stunning and beautiful, showing major monuments but also the haussemanian design of the buildings. The HUD / UI of the app should be minimalist."*

Gemini 4 Argon stood up a complete Three.js application with animated trains, colour-coded metro lines, and a 1900–2026 timeline scrubber. To make the surface city feel authentic to the real geometry of Paris, I steered it to pull real OpenStreetMap data and let you control the surface opacity with a fade slider:

> *"Great start! Can we change a few things. Make the buildings opaque by default, and add a fade slider so you can make it as transparent as you like... Pull actual street map data to make it feel more accurate to the real city."*

That brought in real geographic coordinate data, the actual winding curve of the Seine around *Île de la Cité* and *Île Saint-Louis*, street-aligned Haussmannian perimeter courtyard blocks, and custom 3D landmarks.

---

## Above Ground: Live Blender MCP, Architectural Specificity, and Stroke Outlines

![Daytime 3D view over Île de la Cité with Notre-Dame highlighted by a 4px black screen-space outline and its Art Deco historical dossier card](/blog/eiffel-tower/1.png)

To build the 3D landmarks, I connected **Antigravity** and **Gemini 4 Argon** to **Blender** using the **Blender MCP server**. The setup is delightfully direct: you leave the Blender desktop app open and running in the background with the MCP add-on enabled, which gives the model a live connection into Blender. Through MCP tools like `execute_blender_code` (which runs Python scripts directly inside the live Blender instance), `get_scene_info`, and `look` (which snaps multi-angle viewport renders — `front`, `right`, `top`, `three_quarter` — and feeds the images straight back into the model's context), Argon can sculpt a model, *look* at what it just built, spot proportion issues, and refine the geometry before exporting it to Three.js.

Even with a live Blender connection, though, my first pass at the monuments was too vague. If you just ask for "major Paris monuments," you get simplistic geometric primitives — a couple of grey boxes for Notre-Dame and a glorified cone for the Eiffel Tower — all hidden underneath giant glowing indicator pillars.

First, I got rid of the pillars so you could actually see the geometry:

> *"At the moment the monuments have a column above them so we can't really see the 3D representations of them. Can we make that much more subtle and really show off the 3D models on the map."*

Then, to get the monuments looking like real miniature architecture rather than board-game tokens, I had to be much more explicit with Argon about using Blender to capture each building's defining silhouette and structural anatomy:

> *"Let's make the 3D models of the monuments much more detailed and recognisable using Blender. Use `execute_blender_code` and `look` to inspect each monument from multiple angles as you build it. I want to see the actual architectural features that define each silhouette: Notre-Dame's twin western bell towers, rose window, flying buttresses, and Viollet-le-Duc spire; the Eiffel Tower's four arched base piers, three observation levels, and tapered iron lattice; the white Romano-Byzantine domes of Sacré-Cœur; the Corinthian portico and colonnaded dome of the Panthéon; the golden dome of Les Invalides; the glass pyramid in the Cour Napoléon at the Louvre; and all 9 historic Grandes Gares railway terminals with their glass-and-iron train sheds and clock towers."*

![The Paris monument collection sculpted in Blender 5.2 via the Blender MCP server](/blog/eiffel-tower/4.png)

Giving the model concrete architectural vocabulary — and telling it to verify its own work with `look` inside the live Blender session — completely changed the output. You can see the collection lined up on the Blender grid above: from the Eiffel Tower, Notre-Dame, the Arc de Triomphe, Sacré-Cœur, and the golden dome of Les Invalides, down to the Centre Pompidou (complete with its red exterior escalator tube), the Grand Palais glass roof, the Concorde Obelisk, the four open-book towers of the Bibliothèque Nationale de France, and the Grande Arche de la Défense.

Once the 3D monuments were in place, we hit a classic 3D UX problem: **hover hijacking**. When you tried to hover your mouse over Notre-Dame on the densely packed *Île de la Cité*, the cursor kept snapping to nearby underground Métro stations (*Cité* or *Châtelet*) instead of the cathedral. And because the monuments were assembled from lots of small details — flying buttresses, spires, and iron trusses — hovering over a building felt finicky.

I asked Gemini 4 Argon to make monument selection unmistakable:

> *"Let's make it so that when you hover over the monuments in the map, they get a 4px black outline."*

To make hovering feel effortless, Argon wrapped each of the 22 monuments in an invisible hit-box (so you don't have to pixel-hunt thin spires or open iron lattice), gave surface monuments priority over underground stations when you're viewing the city from above, and drew a crisp **4px black ink outline** around the hovered monument — paired with an Art Deco information card right next to your cursor.

---

## Night Mode: Fixing Purple Light Rings and Sparkling the Eiffel Tower

![Nighttime 3D view of the Eiffel Tower illuminated with 1,500 diamond xenon strobes, counter-rotating volumetric searchlights, and the 2D SVG Paris Skyline Explorer](/blog/eiffel-tower/2.png)

If you build a 3D simulation of Paris, the Eiffel Tower at night is non-negotiable. It has to glow in warm golden floodlights from the inside out, sweep its dual rotating searchlight beacons across the sky, and burst into its famous five-minute diamond sparkle (*le scintillement*).

Our first attempt at night mode had three bugs that made me laugh out loud when I zoomed in:

1. **The Seine was mysteriously enormous** right next to the Eiffel Tower (*"why is the Seine so wide by the Eiffel Tower?"* — a subtle map-coordinate bug near the *Pont d'Iéna* was ballooning the river width before reaching the *Île aux Cygnes*).
2. **The Eiffel Tower only sparkled around its ankles**, while its upper lattice was striped in flat orange and black segments.
3. **The rotating searchlight cones had a glowing dark-purple ring** floating at the far end of the light beams.

Why would a golden searchlight cone produce a dark purple ring at its tip?

When Gemini 4 Argon inspected the light-beam code, it found the culprit right away: at the far edge where the beam fades out into the night sky, a tiny negative number was sneaking into the lighting calculation. Once the scene's glow filter tried to blur those invalid pixels, it turned the tip of the beam into a neon purple halo. Clamping the fade to zero and smoothing out the falloff erased the purple ring immediately.

Next, I overhauled the tower's night illumination:

> *"Can we make the Eiffel Tower illuminated in a gradient from dark orange to yellow? And can we add a button to trigger the sparkle effect."*

Instead of flat colour bands, Argon lit the iron lattice with a warm vertical gradient from dark amber at the base up to bright gold at the tip — with a dedicated **`✨ Sparkle Eiffel`** button in the top bar so you never have to wait for the top of the hour.

---

## Reworking the `<select>` Dropdown: The SVG Skyline Explorer

Look closely at the right-hand panel in the nighttime screenshot above.

For the first 10 or more prompts, selecting a Métro line or jumping to a monument used standard HTML `<select>` dropdowns, and every control panel was open on screen at once. Functional but ugly.

> *"We have a lot of UI visible by default. Could we minimise as much as possible, maybe just a single settings button in the top right... Can we rethink the way the user selects and views details about the metro lines and monuments? At the moment it's a very boring select dropdown."*

I followed up with another prompt:

> *"For the skyline silhouettes of the buildings, can we make it so the image/column is actually a 2d silhouette of the corresponding building ?"*

We stripped the HUD back to the bone:
- All view and camera controls collapsed into a quiet, centred top pill `VIEW & SETTINGS` and a single 4-button vertical icon strip on the right edge.
- Inside the **Métro (`🚇`)** tab, the dropdown became a route diagram with circular station nodes, transfer badges, and fake train counters.
- Inside the **Monuments (`🏛`)** tab, the dropdown became an interactive **2D SVG West → East Paris Skyline Profile**. Every bar in that chart is a custom-drawn SVG architectural silhouette of its respective building.
- Clicking any silhouette launches a smooth camera flight that cranes up over the rooftops, arcs across the city, and settles into a slow continuous orbit around the building.

---

## X-Ray Mode & Turning the Map into a French Naturalisation Walking Tour

![X-Ray cutaway view of the subterranean Paris Métro network with the French Naturalisation Historical Walking Tour open to Stop 8: Les Invalides, Louis XIV & Napoleon's Civil Code](/blog/eiffel-tower/3.png)

Once we had both the 3D surface architecture and the deep underground Métro network working in harmony, I wanted the historical layer to do more than just list construction dates.

If you go through the **French citizenship process**, you sit an oral interview at the Préfecture called the ***entretien d'assimilation***. You are expected to know the foundational milestones, figures, institutions, and Republican values of France.

Studying those questions from a flat PDF list of flashcards feels abstract. But almost every single milestone in that curriculum happened at a physical place in Paris.

So in the following prompt, I asked Gemini 4 Argon to weave the two together:

> *"Could you add a historical walking tour that connects the monuments to the history of France and Paris as needed by the French naturalisation exam?"*

That became the **`📜 History Tour`** tab shown in the third screenshot above — here paired with **X-Ray Mode**, which fades the Haussmannian surface into an obsidian ghost blueprint so the subterranean engineering takes centre stage:

- You can see the **stratified underground depths** of all 16 lines, the glowing vertical transfer shafts at multi-line hubs like *Châtelet* and *République*, and the **Phantom Stations** (old stations that were discontinued and closed over the years).
- Step through the **15-stop Naturalisation Walking Tour** and the camera flies chronologically through French history across the 3D map — from medieval royal power on the *Île de la Cité* (*Notre-Dame* & *Sainte-Chapelle*), to the Sun King and **Napoleon's *Code civil* at *Les Invalides*** (Stop 8/15 in the screenshot), to the Revolution at *Place de la Bastille* and the *Conciergerie*, to Republican *Laïcité* and the *Panthéon*.
- Each stop pairs the 3D monument and its nearest Métro stations with the exact ***Entretien d'assimilation* exam question and answer** you need to know for the interview.

---

## Conclusion

Looking back through the 20+ prompts in the session log, the thing that impressed me most about **Gemini 4 Argon** wasn't that it generated a working 3D app on the first prompt — plenty of models can scaffold a basic Three.js scene.

What stood out was **how well it understood my clarifications and directions for iterating, improving the 3D world, and the UX itself**:

1. **Debugging visual symptoms**: I never had to tell Argon *how* to fix the purple ring on the searchlight cones or *why* the Seine looked bloated near Pont d'Iéna. I just described what my eyes saw on screen, and it traced the visual symptom straight back to the technical issue.
2. **Architectural specificity**: When I asked for *Grandes Gares* rail corridors or Haussmannian blocks, it didn't drop generic grey boxes onto the map — it built curved glass-and-iron train sheds and zinc roofs.
3. **Rapid UX reinvention**: Going from *"this select dropdown is boring"* to a bespoke 2D SVG West-to-East Paris skyline silhouette chart and an interactive metro network took two quick prompts.

It turns out the best way to understand how Paris fits together — above ground, underground, and across its history — is to build your own.

================================================================================
ARTICLE: JS13K and JS13P
Canonical URL: https://tomgreenaway.com/blog/js13p
Markdown URL: https://tomgreenaway.com/blog/js13p.md
Published: 9 Oct, 2026 (2026-10-09)
Tags: games, AI, vibe coding, thoughts
================================================================================

---
date: 2026-10-09
title: "JS13K and JS13P"
status: published
tags: "games", "AI", "vibe coding", "thoughts"
---

# JS13K and JS13P

_"Every great game jam is defined by its constraint. When the tools evolve, the constraint has to evolve too."_

For those who don't know, [js13k](https://js13kgames.com/) is an annual gamejam competition focused around building optimised JavaScript games in less than 13 kilobytes of code. In the most recent competition, there were more AI-built games submitted than ever before.

That shouldn't be surprising to anyone who has followed the tech industry in the last few years.

Unsurprisingly, there has been backlash from the veteran developers. Apparently there's the open question of why they should bother submitting next year if it means spending time meticulously fine-tuning the packing and design of their games under 13 kilobytes only to have their work buried in a tide of rapidly generated AI projects.

They aren't wrong to feel that way. The spirit of JS13K was always to demonstrate human ingenuity under a constraint – the constraint of file size. If the spirit of the competition becomes unfair, what's the point?

So how do you actually fix it?

## The transparency trap

You can't put the genie back in the bottle. And simply adding a line to the rules that says _"No AI allowed"_ on the honour system doesn't solve the problem when the final artifact is a minified 13 kilobyte file.

Even if everyone wants to play fair, you immediately run headfirst into the boundary problem. **Where do you draw the line on what counts as "using AI"?**

- Is it okay to brainstorm theme ideas or game mechanics with an LLM before you open your IDE?
- What if you write the game loop by hand, but let editor autocomplete fill in the boilerplate WebGL matrix maths? That's been allowed for a long time right?
- What if the code is 100% hand-written, but you used an AI tool to generate a sprite, or a sound effect?
- What if you wrote the game yourself, and then asked an agent at the last minute to shave 600 bytes off the bundle so it fits under 13 kilobytes?

When a rule is both blurry and impossible to verify from the final zip file, two bad things happen at once: honest developers will spend time second-guessing their own workflow, and anyone who quietly vibe-codes an entry in an afternoon can look indistinguishable on the submissions page from someone who spent forty hours hand-tuning jump physics.

So my first conclusion is that without transparency, suspicion poisons the jam.

## My proposal? Introducing JS13P

Instead of trying to police an invisible, blurry middle ground, what if we made it part of the whole competition? And we gave _both_ ways of building games a constraint that actually makes them interesting?

So, what if... the competition was split into two distinct buckets: **JS13K** and **JS13P**.

### 1. JS13K: The classic craft jam (13 kilobytes)

**JS13K** stays true to the traditional spirit of the jam. You have to build a web game under 13 kilobytes of zipped JavaScript, HTML, and assets. Every line of code and every asset is hand-crafted. **No AI allowed.**

How do you solve the transparency problem? **Developers livestream (or perhaps record a continuous timelapse of) their game dev process.**

Requiring a build stream or VOD repository removes the guesswork. More importantly, it turns the craft itself into part of the event – watching someone hand-roll a tiny audio synth or shave 200 bytes off a collision routine is half the magic of JS13K anyway.

### 2. JS13P: The AI-assisted companion jam

Rather than pretending AI doesn't exist, run an official companion event right alongside it: **JS13P**.

What does the P stand for? Prompts, of course.

In JS13P, you still livestream your development process for full transparency, and **any AI tool is allowed** – coding agents, asset generators, level builders, whatever you want to bring to the table.

However, instead of staying under 13 kilobytes of output code, you have to stay under 13 prompts.

And across all 13 of those prompts combined, **your total prompt input cannot exceed 13 kilobytes of text** – roughly 2,200 words in total.

Finally, because AI collapses build time, **JS13P should run in a much shorter window** – a sharp 24- or 48-hour sprint rather than a full month.

## Why 13 prompts changes the game – literally!

Think about why the original 13 kilobyte limit worked so well for JS13K in the first place.

In an era where an empty web page routinely ships several megabytes of framework scripts, forcing developers into 13 kilobytes turned file size into a game design puzzle. You couldn't brute-force your way through it. Every byte had to earn its keep.

Right now, AI-generated games suffer from the exact opposite problem: **infinite retries encourages lazy design.**

When prompts are free and unlimited, anyone can sit in a loop firing off three hundred micro-corrections. The constraint disappears. Capping JS13P at **13 prompts and 13 kilobytes of total prompt text** puts the friction back where it belongs:

- **Every turn becomes high-stakes.** You can't burn four prompts arguing with an agent over a broken canvas resize handler. You have to think clearly about architecture, state, and game feel before you hit enter.
- **The constraint moves from the code to the specification.** How well can you articulate a complete, cohesive game – its mechanics, pacing, visual identity, and edge cases – in 2,200 words across at most 13 steps?
- **The livestream becomes a spectator sport.** Watching a developer sit on Prompt #12 with ten minutes left on the clock, deciding whether to risk their final 800 bytes of prompt budget on a boss fight or a bugfix, has genuine dramatic tension.
- **Every submission ships with its own history.** Because there are at most 13 prompts totalling under 13 kilobytes, every game in JS13P can publish its exact prompt log right next to the "Play" button. Reviewers aren't just grading an opaque bundle, they can see the craft in how the builder steered the model.

## Protecting both crafts

A competition is only fun when everyone agrees on the rules of the sport. Right now, putting hand-crafted 13 kilobyte games and AI-generated games into the same unmarked bucket is like entering a motorbike in the Tour de France: it demoralises the cyclists, and it doesn't even make for an interesting motorsport race.

Hand-crafting a game in 13 kilobytes of raw JavaScript is a test of low-level ingenuity and patience. Will it stick around forever? Probably not, but let's still pay tribute to that old craft while we still can.

Meanwhile, steering an agent to build a genuinely fun game in 13 prompts and 2,200 words of specification is a completely different discipline.

Both can be fascinating to build and watch – as long as each one has a constraint tough enough to filter out the slop:

- **JS13K**: 13 kilobytes of output, zero AI, livestreamed hand-craft process.
- **JS13P**: 13 prompts (and 13 kilobytes of input), any AI, livestreamed over a short sprint.

**You can't ban the new tool – but you can give it a constraint hard enough to turn it into a real competition.**

================================================================================
ARTICLE: A2UI & a 3D Marathon Simulator
Canonical URL: https://tomgreenaway.com/blog/new-race-condition/a2ui-and-the-planner-ladder
Markdown URL: https://tomgreenaway.com/blog/new-race-condition/a2ui-and-the-planner-ladder.md
Published: 7 Oct, 2026 (2026-10-07)
Tags: AI, demos, games
================================================================================

---
date: 2026-10-07
title: "A2UI & a 3D Marathon Simulator"
status: published
youtubeId: "m6EyN2Z7HJk"
tags: "AI", "demos", "games"
---

# Why We Put A2UI Report Cards in a 3D Marathon Simulator

When we started building **Race Condition** for the Google Cloud Next Developer Keynote, one of the technologies we wanted to show off in a real application was **A2UI** (Agent-to-User Interface) – the declarative protocol where an AI agent streams structured UI components back to the client instead of plain text or markdown.

It was a great fit for the project. Not because we wanted the agent to draw the whole application – it doesn't – but because Race Condition is built around a freeform agentic chat panel where you can ask open-ended questions (_"Plan a scenic marathon in Las Vegas for 10,000 runners"_, _"Simulate it"_) and get back rich, interactive report cards about how a course scored or how the race actually unfolded.

Here is how we drew the line between traditional frontend code and A2UI, what the report cards actually look like on the wire, and where we'd take it next.

## What is (and isn't) A2UI

Let's clear up a common misconception first: **not all of Race Condition's UI is built with A2UI.**

If you look at `web/frontend/src/app/components/`, the heavy visual machinery of the app is standard, hand-crafted **Angular 21 web app with Three.js**:

- The **3D Las Vegas viewport** (road splines, building meshes, camera rigs, weather particles, and the 1,000 moving runner sprites) is pure Three.js.
- The **top-level HUD**, camera switcher, countdown overlay, and live runner telemetry bars are standard Angular components.

Where A2UI lives is specifically inside the **agent chat panel** (`ChatNavPanel` → `A2uiControllerComponent`).

```mermaid
flowchart LR
    Agent["Planner Agent<br/>(Google ADK + Gemini 3 Flash)"]

    subgraph Browser["Angular 21 + Three.js Frontend"]
        direction TB
        Chat["Agent Chat Panel<br/>(Freeform Prompting)"]
        Renderer["A2uiControllerComponent<br/>(18-Primitive A2UI Renderer)"]
        Viewport["3D Vegas Viewport & HUD<br/>(Standard Three.js / Angular)"]
        Chat --> Renderer
    end

    Agent -- "validate_and_emit_a2ui (surfaceUpdate + beginRendering)" --> Renderer
    Renderer -- "A2uiActionsService (run_simulation / show_route)" --> Viewport
    Renderer -- "Broadcast Action" --> Agent
```

Why carve out that specific slice for A2UI?

1. **Freeform prompts produce different shapes of answers.** Depending on whether you ask the Planner to design a new route, compare three stored courses from AlloyDB, or summarise a finished 200-tick race, the response needs a different layout.
2. **Chat walls fail the 5-foot test.** When a race finishes or a route is evaluated across seven safety and logistics pillars, dumping thirty lines of bulleted Markdown into a narrow sidebar is unreadable on stage. You want a scannable scorecard with a big headline number, clean dividers, and a button to kick off the next step.
3. **Buttons need to talk back to both the 3D map and the agent.** An A2UI card isn't just a static image; its buttons carry structured actions (`run_simulation`, `show_route`) that bridge the chat conversation directly to the 3D viewport and simulator pipeline.

## Anatomy of an A2UI Report Card

In Race Condition, agents opt into A2UI through a shared skill (`agents/skills/a2ui-rendering/`). The protocol gives the agent a fixed catalogue of **18 capitalized primitives** (`Card`, `Column`, `Row`, `List`, `Tabs`, `Modal`, `Divider`, `Text`, `Image`, `Icon`, `Video`, `AudioPlayer`, `Button`, `TextField`, `MultipleChoice`, `CheckBox`, `Slider`, `DateTimeInput`).

Instead of deeply nested JSON trees – which LLMs love to mangle with mismatched closing braces – A2UI v0.8.0 uses a **flat component array**. Every component has a unique `id`, wraps its literal values in typed objects (`literalString`, `literalNumber`, `literalBoolean`), and references child IDs via `explicitList`.

To render a card, the agent calls the `validate_and_emit_a2ui` tool twice: first with a `surfaceUpdate` payload (defining the flat list of components), and second with `beginRendering` (declaring which component ID is the root).

### 1. The Post-Race & Plan Report Card (`sim_results` / `dashboard`)

When a simulation finishes (or when `planner_with_eval` / `planner_with_memory` scores a course), the agent composes a report card (`surfaceId: "sim_results"`). Look at `web/frontend/src/app/demo-config.ts` and `agents/planner_with_eval/prompts.py` to see what's actually inside:

- **The Header Row**: Split into two columns. The left column stacks a status badge ("PLAN", "SIMULATED", or "STORED") alongside the short simulation/route ID ("#881f"), right above the route title ("Las Vegas Neon Night Marathon"). The right column renders the overall evaluation score as a giant headline number ("91") over a "Score" caption.
- **Core Telemetry Rows**: Key-value `Row` pairs for **Total distance** ("26.2 miles") and **Participants (target / simulated)** ("10,000 / 1,000" — showing the 10,000-runner target event capacity alongside the 1,000-agent simulation cohort), followed by a horizontal `Divider`.
- **The Evaluation Breakdown**: In the compact simulation report card, three 0–100 integer rows summarise Safety Score (80), Runner Experience (80), and City Disruption (100). In the full planning evaluation card (`surfaceId: "dashboard"`), this expands into a 2-column grid covering all **7 evaluation pillars** (Safety Compliance, Logistics Completeness, Participant Experience, Distance Compliance, Community Impact, Financial Viability, and Intent Alignment) plus a **Findings** section flagging MAJOR or MODERATE issues (for example: _"Insufficient emergency access points — Route segments 3-5 lack ambulance-width clearance within 200m"_).
- **The Action Footer**: A primary `Button` ("Run Simulation" or "Re-run Simulation") wired to `"action": {"name": "run_simulation"}`.

Here is a trimmed slice of the actual `sim_results` payload from `agents/planner_with_eval/prompts.py`:

```json
{
  "surfaceUpdate": {
    "surfaceId": "sim_results",
    "components": [
      {
        "id": "tag",
        "component": {
          "Text": {
            "text": { "literalString": "SIMULATED" },
            "usageHint": "label"
          }
        }
      },
      {
        "id": "sim-meta",
        "component": {
          "Text": {
            "text": { "literalString": "#1234" },
            "usageHint": "caption"
          }
        }
      },
      {
        "id": "tag-row",
        "component": {
          "Row": { "children": { "explicitList": ["tag", "sim-meta"] } }
        }
      },
      {
        "id": "title",
        "component": {
          "Text": {
            "text": { "literalString": "Neon & neighbourhoods" },
            "usageHint": "h2"
          }
        }
      },
      {
        "id": "left-col",
        "component": {
          "Column": { "children": { "explicitList": ["tag-row", "title"] } }
        }
      },
      {
        "id": "score-num",
        "component": {
          "Text": { "text": { "literalString": "75" }, "usageHint": "h1" }
        }
      },
      {
        "id": "score-lbl",
        "component": {
          "Text": {
            "text": { "literalString": "Score" },
            "usageHint": "caption"
          }
        }
      },
      {
        "id": "score-col",
        "component": {
          "Column": {
            "children": { "explicitList": ["score-num", "score-lbl"] }
          }
        }
      },
      {
        "id": "header",
        "component": {
          "Row": { "children": { "explicitList": ["left-col", "score-col"] } }
        }
      },
      {
        "id": "dist-l",
        "component": {
          "Text": {
            "text": { "literalString": "Total distance" },
            "usageHint": "body"
          }
        }
      },
      {
        "id": "dist-v",
        "component": {
          "Text": {
            "text": { "literalString": "26.2 miles" },
            "usageHint": "body"
          }
        }
      },
      {
        "id": "dist-r",
        "component": {
          "Row": { "children": { "explicitList": ["dist-l", "dist-v"] } }
        }
      },
      {
        "id": "part-l",
        "component": {
          "Text": {
            "text": { "literalString": "Participants (target / simulated)" },
            "usageHint": "body"
          }
        }
      },
      {
        "id": "part-v",
        "component": {
          "Text": {
            "text": { "literalString": "10,000 / 1,000" },
            "usageHint": "body"
          }
        }
      },
      {
        "id": "part-r",
        "component": {
          "Row": { "children": { "explicitList": ["part-l", "part-v"] } }
        }
      },
      { "id": "d1", "component": { "Divider": {} } },
      {
        "id": "safe-l",
        "component": {
          "Text": {
            "text": { "literalString": "Safety Score" },
            "usageHint": "body"
          }
        }
      },
      {
        "id": "safe-v",
        "component": {
          "Text": { "text": { "literalString": "80" }, "usageHint": "body" }
        }
      },
      {
        "id": "safe-r",
        "component": {
          "Row": { "children": { "explicitList": ["safe-l", "safe-v"] } }
        }
      },
      { "id": "d2", "component": { "Divider": {} } },
      {
        "id": "rerun-txt",
        "component": {
          "Text": { "text": { "literalString": "Re-run Simulation" } }
        }
      },
      {
        "id": "rerun-btn",
        "component": {
          "Button": {
            "child": "rerun-txt",
            "action": { "name": "run_simulation" },
            "primary": { "literalBoolean": true }
          }
        }
      },
      {
        "id": "content",
        "component": {
          "Column": {
            "children": {
              "explicitList": [
                "header",
                "dist-r",
                "part-r",
                "d1",
                "safe-r",
                "d2",
                "rerun-btn"
              ]
            }
          }
        }
      },
      { "id": "card", "component": { "Card": { "child": "content" } } }
    ]
  }
}
```

### 2. Multi-Card Route Lists (`route_list`) and Two-Way Actions

When you ask `planner_with_memory` to _"list the top 3 best routes for the organizer UI"_, it queries PostgreSQL/AlloyDB (`get_planned_routes_data(limit=3)`) and emits a `List` of three `STORED` route cards inside a single `surfaceUpdate` (`surfaceId: "route_list"`), suffixing component IDs with `-1`, `-2`, and `-3`.

Instead of a `"Run Simulation"` button, each card in the list renders two secondary actions:

1. **`"Open Report"`** (`action: {"name": "organizer_show_scorecard"}`) – toggles the expandable summary drawer right inside `a2-ui-controller.component.html` (`unfold_more` / `unfold_less`) so all three cards fit cleanly in the chat column without scrolling off-screen.
2. **`"Show Route"`** (`action: {"name": "show_route", "payload": {"seed": "<route_id>"}}`) – handled by `A2uiActionsService` (`web/frontend/src/app/components/a2ui/a2ui-actions.service.ts`), which either swaps the cached GeoJSON spline on the 3D map immediately or sends a message back to the Planner agent (`Get the route for the seed <id>`) so the agent calls `report_marathon_route` and redraws the 3D Las Vegas course live.

### 3. Teaching the LLM Not to "Freestyle" the Schema

Two practical lessons jumped out while getting Gemini 3 Flash to emit these cards reliably:

First, **validate at the tool boundary**. `validate_and_emit_a2ui` (`agents/skills/a2ui-rendering/tools.py`) checks every component ID, wrapper type, and child reference _before_ the tool finishes. If the model forgets a `literalString` wrapper or references a missing ID, the tool returns a structured list of violations so the model fixes its own JSON inside the same turn.

Second, **lock down schema drift — and let deterministic guardrails emit A2UI directly**. In `prompts.py`, we had to add an explicit `Hard Constraints` block banning rows like `Spectators (expected/attendance)` or float scores like `7.2`, because the model occasionally got creative and added extra marathon metrics that didn't match the card design. And when enforcing hard policy boundaries — such as rejecting an unauthorized budget hike with a styled red `a2ui-Card--financial-refusal` card — don't disable your Python `before_model_callback` guardrail just to let the LLM author the refusal UI. Keep the deterministic callback in front of the model and have the guardrail emit the pre-validated A2UI refusal payload directly, so out-of-policy prompts are blocked before reaching the LLM while the user still gets a rich, structured refusal card.

## Where A2UI could go next in Race Condition

Right now, A2UI only utilises seven of the 18 primitives in the `standard_catalog_0_8_0` spec (`Card`, `Column`, `Row`, `List`, `Divider`, `Text`, `Button`), and only at the bookends of a run (planning and post-race summary).

Because the Angular `A2uiControllerComponent` (`web/frontend/src/app/components/a2ui/a2-ui-controller.component.html`) _already_ implements the remaining primitives – including `Tabs`, `Modal`, `Slider`, `MultipleChoice`, `CheckBox`, and `TextField` – there are some obvious places in Race Condition where A2UI could take over next:

### 1. Live Runner Bio & Medical Triage Cards

During a race, you can watch 1,000 runners move along the Strip and see their status emojis degrade from `🏃` to `🥵` to `🧟`. Imagine clicking a struggling runner in Three.js or asking the chat _"How is Runner #42 holding up?"_ and having the agent emit a live A2UI runner card:

- Their target pace vs. current velocity (`mph`) and hydration percentage (`water%`).
- A scrolling `List` of their recent 5-word `inner_thought` monologues from the tick stream.
- Interactive intervention `Button` controls (`"Trigger Crowd Cheer Boost"` or `"Dispatch Medical Cart"`) that send a targeted event to that single runner's session ID.

### 2. Mid-Race Chaos & Weather Controls (`Slider` + `MultipleChoice`)

The Simulator tracks weather, traffic, and crowd density tick by tick, and we even have a `simulator_with_failure` agent variant. Instead of pre-baking failure scenarios in config, a user could type _"Let me mess with the weather at Mile 18"_ and receive an interactive A2UI chaos card built with `Slider` (Temperature: 65°F–110°F, Wind Speed) and `MultipleChoice` (Road Closure at Bellagio / Hydration Station Shortage) that mutates the Simulator's live session state on the next tick.

### 3. Interactive Course Remediation (`CheckBox` + `Slider`)

When `planner_with_eval` flags a `MAJOR` finding on the Planning Dashboard – say, _"Route segments 3-5 lack ambulance-width clearance within 200m"_ or _"Estimated traffic-control cost exceeds allocated budget by 18%"_ – the findings list today is read-only text. Using `CheckBox` and `Slider` primitives, the Planner could attach a remediation form directly below the findings (_"Add 2 mobile medical tents (+$14k)"_, _"Reroute segment 4 off Las Vegas Blvd"_), letting the organizer apply fixes and re-score the plan with one click.

### 4. Head-to-Head Run Comparisons (`Tabs`)

Once `planner_with_memory` has stored three or four simulations in AlloyDB (`compile_results` records `finished_count`, `dnf_count`, `vitals_trend`, and `notable_events`), asking _"Compare my last two runs on Strip Classic"_ could emit a tabbed A2UI card (`Tabs`) flipping between **Overview Scores**, **DNF & Hydration Curves**, and **Notable Race Events** without needing a single line of new Angular template code.

## The takeaway

- **Don't force A2UI to be your 3D engine.** Let Three.js and standard Angular own the high-frequency viewport and HUD; use A2UI where user intent is open-ended and the response shape depends on what the agent just discovered.
- **Validate A2UI inside the tool loop.** Having `validate_and_emit_a2ui` reject unwrapped strings or dangling component IDs in Python means your frontend renderer never has to guess what the LLM meant.
- **Wire A2UI actions back into your app state.** A report card is twice as useful when clicking `"Show Route"` or `"Run Simulation"` inside the chat bubble drives the 3D map right next to it.

================================================================================
ARTICLE: Adventure & A11Y
Canonical URL: https://tomgreenaway.com/blog/adventure-io22/adventure-io22-accessibility
Markdown URL: https://tomgreenaway.com/blog/adventure-io22/adventure-io22-accessibility.md
Published: 1 Oct, 2026 (2026-10-01)
Tags: games, demos
================================================================================

---
date: 2026-10-01
title: "Adventure & A11Y"
status: published
youtubeId: "hMjtFRqaTsI"
tags: "games", "demos"
---

# Adventure & A11Y: The Invisible Layer Under the Game

Most video games don't do much for blind and low-vision players. That's not a criticism, just the reality of the medium. Games are visual, real-time and spatial, and the usual accessibility toolkit (semantic HTML, alt text, a sensible tab order) assumes a document, not a world you walk around in.

I want to highlight one approach we took to that problem, because I still think it's one of the more interesting design spaces in games: **what does a genuinely screen-reader-friendly video game look like?**

The game was **Adventure**, the browser-based, multiplayer pixel-art world we built for mulitple Google I/O events and its sister events for the Indie Games Accelerator and Indie Games Festival. I designed the accessibility feature together with the team at Set Snail, the agency that I collaborated with to build the project.

Here's the problem in one line: Adventure renders its game world into a single `<canvas>` with PixiJS. To a screen reader, a canvas is a big rectangle of nothing. Every NPC, every gift box, every teleporter is just pixels.

Our answer: **build a second, invisible game out of plain HTML, and keep it in sync with the visible one.**

## The problem: a conference you can't attend

A virtual conference venue isn't really a game. It's a building with doors. Behind those doors are talks, sandboxes, office hours and swag. If you can't navigate the building, you can't attend the conference. And "sorry, the venue is a canvas" isn't a great accessibility statement for a developer event whose content is _about_ building for everyone.

So the bar wasn't "add some `aria-label`s." It was: **a keyboard or screen-reader user should be able to walk around, talk to people, open gifts and teleport across the map**. The same verbs everyone else gets.

## The trick: a parallel DOM, built by game components

The engine mounts one container on top of the canvas and then deliberately makes it invisible, but _not_ hidden:

```ts
// Hide container from view, this is only for the screen-readers.
this._outputContainer.style.pointerEvents = "none";
// Here we use opacity instead of visibility:hidden and display:none;
// We still want the dom elements to be visible og interact-able by screen-readers
this._outputContainer.style.opacity = "0";
```

That one comment is the whole design in miniature. `display: none` and `visibility: hidden` remove things from the accessibility tree. `opacity: 0` doesn't. Real buttons, real focus, real tab order. Just not painted.

What goes _into_ that container is decided by the game itself. Any entity component can opt in by implementing a tiny interface:

```ts
export interface IA11ySettings {
  group?: string; // which <ul> this lands in
  alwaysRender?: boolean;
  priority?: number; // components on the same entity "fight" for the slot
  ignoreAutoFocus?: boolean;
  autoFocusRadiusInTiles?: number; // defaults to 8
}

export interface IAccessible extends Component {
  readonly a11ySettings: IA11ySettings;
  a11y(module: A11yHTMLItem): void;
}
```

Here's an NPC opting in. That's it. That's the whole integration:

```ts
public readonly a11ySettings: IA11ySettings = { group: 'dialog' };

public a11y(module: A11yHTMLItem): void {
  module.addInteractiveButton(
    'talk with ' + this.getPlugin(DialogPopupPlugin).getTranslatedName(this.state.npcId),
  );
}
```

`addInteractiveButton` creates a `<li><button>` inside a group container: a `<div>` with an `<h2>` and a `<ul aria-labelledby>` pointing at it. So a screen reader user hears something like _"dialog, list, 3 items – talk with Ana, button"_. Headings for groups, lists for things, buttons for verbs. Boring HTML. Exactly what you want.

And the button's default click handler doesn't call some special accessibility path. It fires the **same player interaction action** a mouse click on the sprite would:

```ts
private interact = () => {
  const localPlayer = this._component.getPlugin(LocalPlayerPlugin);
  const action = PlayerInteractionBehaviour.actions.interact(this._component.gameObject.state.id);
  localPlayer.applyAction(action);
};
```

One code path for everyone. No parallel "accessible mode" that quietly rots after launch.

```mermaid
flowchart LR
  subgraph Canvas["PixiJS canvas (what you see)"]
    NPC["NPC sprite"]
    Gift["Gift box sprite"]
    WP["Waypoint sprite"]
  end
  subgraph DOM["opacity:0 DOM (what AT sees)"]
    G1["h2 dialog + ul"] --> B1["button: talk with Ana"]
    G2["h2 gifts + ul"] --> B2["button: Open gift"]
    G3["h2 waypoints + ul"] --> B3["button: Go north: Sandbox"]
  end
  NPC -- "a11y(module)" --> B1
  Gift -- "a11y(module)" --> B2
  WP -- "a11y(module)" --> B3
  B1 -- "focus" --> NPC
  B1 -- "click = same interact action" --> Server[("Game server")]
```

## The bit I like most: focus flows _back_ into the world

Here's the catch with invisible buttons. A sighted keyboard user tabs, and… nothing visibly happens. The focus ring is on an element with `opacity: 0`.

So we pushed focus the other direction. Every `focus`/`blur` on a DOM button raises an event on that entity's `AccessibilityComponent`, and the _game_ draws the focus indicator:

- **Sprites get a two-tone outline**: a dark `0x202124` outer stroke and a white inner stroke via `OutlineFilter`, so it reads on both light and dark tiles.
- That outline **wipes in bottom-to-top** over 0.3s using a tiny custom fragment shader. The tween runs to `1.05` rather than `1.0` "to allow the black outline to be 0.05 ahead". Yes, we cared about that.
- **NPC avatars play a highlight animation** on a separate Spine track.
- **Waypoints grow a little arrow that rotates to point at the destination** of whichever button is focused. Tab between "Go north: Sandbox" and "Go east: Stage" and the arrow swings around with a `power3.out` ease, taking the short way round, too.
- **Sound effects helped orientate players** based on the type of floor they stepped on (e.g. footstep sounds were different based on grass, concrete, sand, etc.)

That waypoint arrow is the moment the system stops being a compliance feature and becomes a game mechanic. It's a focus ring that tells you _where you're going_. It passes the 5-foot test: someone looking over your shoulder on a train gets it immediately.

## Focus that follows your feet

Tab order in a static page is easy. Tab order in a world where you're walking around is not. If you're standing next to a gift box, the gift box should be the first thing you reach, not whatever entity happened to load first.

So the a11y plugin listens to the player's position and, on every move, **auto-focuses the closest interactive thing within range**:

```ts
if (distance < item.getAutoFocusRadiusInTiles() && distance < closestDistance) {
  closestDistance = distance;
  closestItem = item;
}
```

The tuning knobs are where most of the design work went:

- **Per-entity radius.** Default is 8 tiles. Waypoints use `0.5`. You have to basically stand on a teleporter before it grabs focus, otherwise walking past one would hijack your keyboard.
- **`ignoreAutoFocus`.** Other players get an "Inspect the lanyard of {nickname}" button, but they never steal focus. In a busy multiplayer room, people walking past you shouldn't yank your screen reader around.
- **Wait for the camera.** Focus doesn't move until the player is roughly centred in view. A later fix accounted for the camera's out-of-bounds offset near map edges. Otherwise the outline would appear on something still off-screen.
- **Graceful fallback.** If the focused entity despawns, focus drops back to the canvas instead of vanishing into `<body>`.

## Making the canvas itself a good citizen

The canvas isn't ignored either. It's focusable and described, and the label updates on every zone change:

```ts
canvas.setAttribute("tabindex", "0");
canvas.setAttribute("aria-label", "Game view - " + zone);
canvas.setAttribute("role", "img");
canvas.setAttribute(
  "aria-description",
  "Move around using arrow keys or W A S D.",
);
```

And because keyboard movement only works while the canvas has focus, the game watches `document.activeElement` every frame. If focus wanders outside the game (say, into chat), it locks movement and shows a `role="status"` hint: _focus the character to move around_. Small thing. Saves a lot of "why won't my character move?" confusion, for everyone, not just keyboard users.

Overlays round it out: dialogs use `role="dialog"` + `aria-modal`, a priority-stacked focus trap (so a modal over an overlay over the game hands focus back down the stack correctly), a visually hidden `<h1>` for the page, and liberal `aria-hidden` on purely decorative stuff like the minimap and tooltips.

## AI-assisted A11Y?

When we made Adventure it was just before the modern boom of Generative AI and last year I started to wonder how I would reimagine Adventure today with some of the AI technologies we now have. That's what led to AIventure which we demoed at Google I/O this year and included in the Gemma 4 demos.

I've published another article about [AIventure here](https://tomgreenaway.com/blog/aiventure/robot-agentic-npcs). It has the beginning of some ideas borrowed from Adventure's A11Y, such as being able to "chat" via the keyboard with Gemini or Gemma to interact with the world. This provides an interesting new approach to designing A11Y for games in my opinion, as we can now provide chat-driven tool calling for interacting with a game's UI and receiving a more textual description of the game world as we play it.

I feel this is a space where AI can greatly assist the user experience.

## Conclusion

1. **Don't make the canvas accessible. Make a DOM twin of it.** Let each game entity describe itself as real HTML, and hide it with `opacity: 0`, never `display: none`.
2. **Route focus both ways.** DOM focus should drive in-world visuals (outlines, animations, a rotating arrow), so sighted keyboard users aren't tabbing blind. And interaction should reuse the exact same action path as a mouse click.
3. **In a spatial world, tab order is a gameplay problem.** Proximity-based auto-focus with per-entity radii and opt-outs (other players, teleporters) is what makes it feel playable rather than technically compliant.

================================================================================
ARTICLE: AIventure & Agentic NPCs
Canonical URL: https://tomgreenaway.com/blog/aiventure/robot-agentic-npcs
Markdown URL: https://tomgreenaway.com/blog/aiventure/robot-agentic-npcs.md
Published: 30 Sep, 2026 (2026-09-30)
Tags: games, AI, demos
================================================================================

---
date: 2026-09-30
title: "AIventure & Agentic NPCs"
state: published
youtubeId: "6yjJ7Prt-RI"
tags: "games", "AI", "demos"
---

# AIventure: Agentic NPCs

There's a pit of lava between you and a switch, and the only thing that can cross it is a small robot who follows you around like an overly keen puppy.

You can't walk it over there. You have to _ask_. In plain English. "Go and flip the switch." And then you stand back and watch it think.

This is the third room of [AIventure](https://github.com/bebechien/AIventure), an Angular + PhaserJS adventure that teaches Gen AI concepts by making you play with them. It was an idea I built earlier this year with my colleague Juyeong Ji from Deepmind DevRel. A sort of spirirtual successor to my old Adventure conference technology, except focused on gamified learning and less on social/multiplayer.

The first rooms are about single tool calls – say the words, a door opens. The robot is where it becomes an agent: **it gets a goal, not an instruction, and has to loop until it's done.**

And – my favourite feature – it has a hard limit on how long it'll keep trying before it sits down and sobs.

## What the robot is told

The whole persona is one line:

> **NPC persona:**  
> _"a helpful bot who can handle the switch"_

And it gets exactly two skills:

```json
[
  {
    "name": "move_to",
    "description": "move agent to x,y coordinate",
    "parameters": {
      "type": "object",
      "properties": { "x": { "type": "integer" }, "y": { "type": "integer" } },
      "required": ["x", "y"]
    }
  },
  {
    "name": "find_switch",
    "description": "search the room for available switch, will return (x,y) coordinate"
  }
]
```

Notice what's missing. There's no `flip_switch`. There's no map in the prompt. The robot doesn't know where the switch is until it asks, and it can't flip anything until it's standing next to it. **The goal only gets solved by chaining the tools together** – which is the whole point of the lesson.

## The loop: ask, act, report back, ask again

The solution page describes the robot's loop in three words: retrieve instructions, execute, re-prompt. Here's how that plays out in the code when Gemma 4 is running locally through LM Studio:

```mermaid
flowchart LR
    P["Player: 'flip the switch'"] --> M["Gemma 4<br/>(LM Studio)"]
    M -- "tool call:<br/>find_switch / move_to" --> G["Game rules<br/>PuzzleRules.ts"]
    G --> R["AgenticNPC<br/>moves on grid"]
    R -- "model-tool-execution-result" --> S["lmstudio.service.ts<br/>appends tool result"]
    S -- "re-prompt with history" --> M
```

1. The model gets the goal and the two tools, and calls `find_switch`.
2. The game's rules table handles it – there's a rule that maps `find_switch` to a `FIND_SWITCH` action and `move_to` to `MOVE_AGENT`.
3. The result comes back to the service as a `model-tool-execution-result` event.
4. The service pushes it into the conversation as a `role: "tool"` message and, once every pending call has an answer, fires the whole history back at the model.

```typescript
// From src/app/services/lmstudio.service.ts (trimmed)
async handleToolResult(result: any) {
  const lastMessage = this.history.at(-1);
  const callTarget = lastMessage.tool_calls.at(-this.waitingToolCallCount);
  this.history.push({
    role: "tool",
    tool_call_id: callTarget.id,
    content: JSON.stringify(result.output),
  });

  this.waitingToolCallCount--;
  if (this.waitingToolCallCount > 0) return;

  // All tools answered – ask the model what to do next
  const response = await fetch(this.apiUrl, { /* same history, same tools */ });
  // ...
}
```

That's the agent loop. No framework, no orchestration layer – **just a chat history that grows by one tool result at a time**, and a model deciding whether it's finished yet.

## The agent is a state machine with feelings

On the game side the robot is an `AgenticNPC`, which extends the regular `MovingNPC`. Its brain is an enum:

```typescript
// From src/game/core/AgenticNPC.ts
export enum AgentState {
  FOLLOW,
  BUMPED,
  THINKING,
  EXECUTE,
  VERIFY,
  GOAL,
  FAILED,
}
```

By default it's in `FOLLOW`, chasing you around the room. Once you've given it a job, `THINKING` and `EXECUTE` are the states that matter – those are when its tool calls get counted (more on that in a second).

It also has a thought queue – timed messages that pop up as bubbles over its head. "Bumped!" when it walks into you. "Waiting.." when it's paused. It sounds like a cosmetic touch, but it's doing real work: **the player can see what the agent is thinking without opening a log.** That passes the five-foot test – someone looking over your shoulder on a train can tell what the robot is up to.

## The bit I'd steal for every agent: a tool budget

Here's the part that deserves more attention than it'll get:

```typescript
// From src/game/core/AgenticNPC.ts
incrementToolUse() {
    this.toolUseCount++;
    if (this.toolUseCount >= CONSTANTS.AGENT_MAX_TOOL_USE) {
        this.setState(AgentState.FAILED);
        this.commandTarget = null;
        this.setThought("I've reached my limit...", CONSTANTS.THOUGHT_BUBBLE.LONG_DURATION);
    }
}
```

Every time a model function call arrives, `InteractionSystem.ts` finds whichever agent is currently `THINKING` or `EXECUTE`, bumps its count, and – if the agent has just tipped into `FAILED` – **drops the call on the floor** instead of passing it on to the trigger system. The counter resets whenever the robot starts a fresh round of thinking.

Once it's failed, the robot sits there with a "Sob Sob" thought bubble. Which is honestly how I feel about most runaway loops I've debugged.

It's a small local model running on a laptop. Sooner or later it'll pick a bad coordinate, or ask for the switch again when it already knows where it is. Rather than pretend that won't happen, the game gives it a budget and a graceful way to say "I can't do this". **A visible failure beats an invisible infinite loop.**

## Movement is dumb on purpose

When the model says `move_to(x, y)`, you might expect A\* pathfinding. You'd be wrong – and that's actually one of the quiz questions on the solution page.

The robot does the simplest thing that could possibly work: step along X towards the target, then along Y. If the next tile is blocked, try the other axis once. It asks a `GridManager` whether a tile is free before moving, so it doesn't end up sharing a square with anything else.

```typescript
// From src/game/core/AgenticNPC.ts (trimmed)
// Simple pathfinding: X then Y
let nextGx = currentGx + stepX;
let nextGy = currentGy;
if (stepX === 0) {
  nextGy = currentGy + Math.sign(dy);
}
if (!this.gridManager.isBlocked(nextGx, nextGy, "npc")) {
  this.moveTo(nextGx, nextGy);
} else if (stepX !== 0) {
  // simple retry on the other axis
}
```

Surprisingly hard to make a robot this unsophisticated – you keep wanting to add things. But the lesson here isn't pathfinding. **The intelligence lives in the loop, not in the legs.** If the robot gets stuck, the model sees that in the next tool result and can pick a different coordinate. That's the agent doing its job.

## Conclusion

- **Give agents goals and small tools, not scripts.** Two functions and a one-line persona were enough to need a genuine multi-step plan.
- **Budget your tool calls.** A hard cap plus a clear FAILED state protects you from the loops you didn't imagine.
- **Show the agent's thinking where the user already is.** Thought bubbles in the game world beat a console nobody's reading.

The full robot lives in `src/game/core/AgenticNPC.ts` on [GitHub](https://github.com/bebechien/AIventure). Go and make it cry.

================================================================================
ARTICLE: 10 Lessons from Duet
Canonical URL: https://tomgreenaway.com/blog/duet-lessons
Markdown URL: https://tomgreenaway.com/blog/duet-lessons.md
Published: 17 Jun, 2019 (2019-06-17)
Tags: games, thoughts
================================================================================

---
title: 10 Lessons from Duet
date: 2019-06-17
state: published
tags: "games", "thoughts"
---

# 10 Lessons from Duet

Recently someone asked me what the key lessons I learned from Duet were. I had never tried writing a definitive list before. Now seemed like as good a time as ever. So here are the key lessons from Duet:

* Narrative can create mystery and **fuel curiosity**. Hook your player with story and mythology.
* Cut all overlapping features — reduce to the **minimum viable gameplay.**
* An interpretable narrative will resonate with more players because the less you explicitly say the more room you leave to imagination and **nothing can compare to the power of imagination.** That’s why movies never live up to the expectations of books.
* Gameplay should be understandable from 5 feet away, i.e. I can discover and understand the game by **looking over someone’s shoulder** on a train.
* Gameplay can be very difficult — **as long as it’s fair,** i.e. it can be frustrating but it shouldn’t be depressing or disheartening.
* There should be something beautiful about the game that people want to share because this leads to word-of-mouth marketing. Beauty can be visual or audible, i.e. **the music is the art of duet.**
* Shorter levels are more digestible. The more digestible, the more players will play. Therefore, paradoxically, **shorter levels can lead to longer play sessions.**
* If you’re thinking of adding extra content to a premium game and the content is big enough to justify a sequel — **go with the sequel** rather than additional micro-transaction content.
* If you can’t win your own game while drunk — **it’s probably too hard.**
* Pour your whole self into your product and it will **speak a unique and resonant truth.**

================================================================================
ARTICLE: Nintendo Switches It Up
Canonical URL: https://tomgreenaway.com/blog/nintendo-switch
Markdown URL: https://tomgreenaway.com/blog/nintendo-switch.md
Published: 17 Mar, 2017 (2017-03-17)
Tags: games, thoughts
================================================================================

---
date: 2017-03-17
title: Nintendo Switches It Up
state: published
tags: "games", "thoughts"
---

# Nintendo Switches It Up

_"You cannot follow us here"._

Nintendo has a long history of innovation in hardware. Since its start in video game consoles they have innovated in hardware alongside their software offerings.

The SNES added shoulder buttons which enabled more simultaneous inputs (thumbs + fingers) from users. The N64 was the first 4 player ready console and invented the analog stick. The Wii made a splash with motion control. The Wii U added a tablet interface.

Meanwhile, the PlayStation 1 followed the SNES with shoulder buttons. The PlayStation multi-tap added 4 players to the PS1 and PS2. The original XBox launched 4 player ready. Sony created the Dual Analog Controller mid-cycle for the PS1. Later, the Kinect and PSMove added their own visions of motion control to chase the Wii. And for a time, Sony and Microsoft discussed the potential of “second screen interfaces” via tablets — but stopped once the Wii U was revealed to be a dud.

Can you spot the trend? Hardware can be highly innovative and act as a unique selling point. However, the success is often short lived as competitors can easily copy hardware features either the generation after (Xbox and 4 player input) or mid-generation (Multi-tap and PSMove). Wherever Nintendo went the other consoles could and often did follow.

The Wii was a tremendous success due to its motion control interface and the resulting casual gaming boom it attracted. In that generation Nintendo did what Nintendo does best. They created a unique piece of hardware and built games that integrated heavily with said hardware.

So what went wrong with the Wii U? Unsurprisingly, innovation for the sake of innovation isn’t enough. The innovation must create a compelling new feature. Motion control was compelling. A tablet that is chained to your house is not. Even when it was announced the Wii U seemed like a confusing concept — a mobile-like tablet interface that isn’t portable?

At a glance, the Switch seems to be just a better implementation of the Wii U concept. Or is it?

On the surface level, the Switch is another attempt at an innovative user interface for video game consoles. Just like the Wii U, it’s a tablet screen interface. Considering the popularity of smartphones and tablet devices this isn’t a terrible area to experiment in video game interfaces for Nintendo.

However, there is more innovation here than simply a new hardware interface. In fact, looking through the lens of the interface it’s clear that the Switch isn’t very different to the Wii U and that neither is really very different to mobile tablets such as the iPad. So should we expect the Switch to succeed where the Wii U failed? The answer is yes, but not because of the interface.

The Switch will succeed because it repositions the home console as a hybrid portable. Finally, a hardware feature that cannot be copied by the competition. Sony and Microsoft both chase the “arms race” of power in their consoles — the best CPU, the best GPU, the best RAM, etc. This entire strategy is totally orthogonal to the strategy of portability.

Therefore Sony and Microsoft can’t copy Nintendo and expand their feature set to encompass the Switch’s unique selling point. In fact, Nintendo has moved the battlefield of its console into a space they have long dominated — the handheld dedicated gaming device.

![image](/resources/blog/switch-graph.png)

Note: Graphs are relative. I’m not suggesting that the Switch is as powerful as the PS4 Pro.

Sure, modern smartphones have completely outsold the Gameboy and Nintendo DS. But when it comes to analysing just the handheld dedicated gaming device market — Nintendo is king.

With iPad and tablet sales waning. More and more F2P smartphone games requiring active internet connections (including Super Mario Run!). And the Switch being $100+ cheaper than an iPad. Perhaps the Switch will cement itself as the best device for a road trip — or plane trip.

It’s dangerous to go alone take… the Switch.

================================================================================
PAGE: Privacy Notice
Canonical URL: https://tomgreenaway.com/privacy
Markdown URL: https://tomgreenaway.com/privacy.md
================================================================================

---
pageType: article
---

# Privacy Notice

This is my personal website and blog (**tomgreenaway.com**). I try to collect as little personal data as humanly possible.

## 1. Analytics & Cookies

This website does **not** use tracking cookies, advertising pixels, or cross-site behavioural profiling.

To understand basic traffic (such as which articles are being read), this site uses **Cloudflare Web Analytics**, which is privacy-first, sets **no cookies**, and does not fingerprint or track individual visitors across websites.

## 2. Hosting & Edge Infrastructure

The site is hosted as static files on **Cloudflare Workers** (Cloudflare, Inc.). When your browser requests a page, Cloudflare processes standard HTTP request metadata (such as your IP address and User-Agent) strictly as necessary to deliver the page and protect the site against abuse (GDPR Art. 6(1)(f) – legitimate interest in operating and securing the website).

## 3. Email Newsletter

If you choose to subscribe to updates in the sidebar, your email address is processed and stored by **EmailOctopus** (Three Hearts Digital Ltd, registered in the UK) solely to send you occasional updates about my games, experiments, and articles (GDPR Art. 6(1)(a) – explicit consent).

- Your email is never sold or shared with third-party advertisers.
- Every email includes a one-click **Unsubscribe** link at the bottom to withdraw consent at any time (GDPR Art. 7(3)).

## 4. Embedded Content

Some articles include embedded **YouTube** videos using YouTube's Privacy-Enhanced Mode (`youtube-nocookie.com`), which prevents YouTube from storing tracking cookies unless you choose to play the video.

## 5. Your Rights (GDPR)

Under the EU General Data Protection Regulation (GDPR) and UK GDPR, you have the right to access, rectify, export, or request erasure of any personal data I hold about you (which, in practice, is only your email address if you subscribed to the newsletter).

To exercise these rights or ask any questions, you can reach out to me via [LinkedIn](https://www.linkedin.com/in/tom-greenaway/) or [X / Twitter](https://x.com/tcmg), or click **Unsubscribe** in any newsletter email.

================================================================================
PAGE: Life Drawing
Canonical URL: https://tomgreenaway.com/lifedrawing
Markdown URL: https://tomgreenaway.com/lifedrawing.md
================================================================================

# Life Drawing

![image](/resources/art/life-1.jpg)
![image](/resources/art/life-2.jpg)
![image](/resources/art/life-3.jpg)
![image](/resources/art/life-4.jpg)
![image](/resources/art/life-5.jpg)
![image](/resources/art/life-6.jpg)
![image](/resources/art/life-7.jpg)

================================================================================
PAGE: About me
Canonical URL: https://tomgreenaway.com/
Markdown URL: https://tomgreenaway.com/index.md
================================================================================

# About me

I'm an <a href="/awards.html">award-winning</a> video game designer.
Co-creator of the <a href="https://www.duetgame.com" target="_blank">mobile-hit Duet.</a>
Co-chair of the <a href="https://www.w3.org/community/games/" target="_blank">W3C Games Community.</a>
Inventor of the Google virtual conference pandemic technology <a href="https://www.youtube.com/watch?v=hMjtFRqaTsI">"Adventure."</a>
A senior developer relations <a href="https://www.linkedin.com/in/tom-greenaway/" target="_blank">product lead and advocate</a> at Google.
<a href="https://www.youtube.com/watch?v=aVTYxHL45SA" target="_blank">Ex-games lead</a> for the Chrome developer team.
Currently focused on <a href="https://ai.google.dev/gemini-api/docs" target="_blank">Gemini AI</a> & Games.
Half South African, I was born in Australia but I live in <a href="https://fr.wikipedia.org/wiki/Paris">Paris,</a> France.

![image](/resources/photos/conf-1.jpg)
<!--
![image](/resources/photos/conf-2.jpg)
![image](/resources/photos/conf-3.jpg)
![image](/resources/photos/conf-4.jpg)
-->

================================================================================
PAGE: Awards
Canonical URL: https://tomgreenaway.com/awards
Markdown URL: https://tomgreenaway.com/awards.md
================================================================================

# Awards

I've been fortunate enough to have received a fair bit
of acknowledgement for my games from the industry. Here is a list of some of them.

Duet:
* Australian Game Developer Awards - Best Game Design 2014
* Australian Game Developer Awards - Best Audio 2014
* App Store Best Games of 2013
* The New Yorker's Best Mobile Games of 2013
* Intel Level Up: Grand Prize - Seattle 2014
* Intel Level Up: Best Action - Seattle 2014
* Intel Level Up: Best Audio - Seattle 2014
* IGF China: Best Game Design - Beijing 2014
* PAX 10 Indie Showcase - Seattle 2014
* PAX East Indie Showcase - Boston 2013
* PAX Australia Indie Showcase - July, 2013
* Freeplay Best Design Finalist - September, 2013
* Freeplay Best Audio Finalist - September, 2013

Basket Fall:
* PAX Australia Indie Showcase Melbourne, 2014

Time Surfer:
* Freeplay Technical Innovation Finalist" - Time Surfer, September, 2013
* PAX East Indie Showcase" - Time Surfer, April, 2013

Bean Dreams:
* PAX East Indie Showcase - Boston 2014

Bean's Quest:
* Freeplay Best Design Honourable Mention", September, 2013
* PAX East Indie Showcase", April, 2012

GameJams:
* Global Game Jam - Best Game - "Checkmates", January, 2013
* Global Game Jam - Best Game - "Omelette Boris", January, 2012

================================================================================
PAGE: Assorted Paintings
Canonical URL: https://tomgreenaway.com/assorted
Markdown URL: https://tomgreenaway.com/assorted.md
================================================================================

# Assorted Paintings

![image](/resources/art/art-1.jpg)
![image](/resources/art/art-2.jpg)
![image](/resources/art/art-3.jpg)
![image](/resources/art/art-4.jpg)
![image](/resources/art/art-5.jpg)
![image](/resources/art/art-6.jpg)

![image](/resources/art/art-7.jpg)
![image](/resources/art/art-8.jpg)
![image](/resources/art/art-9.jpg)
![image](/resources/art/art-10.jpg)
![image](/resources/art/art-11.jpg)
![image](/resources/art/art-12.jpg)
![image](/resources/art/art-13.jpg)
![image](/resources/art/art-14.jpg)
![image](/resources/art/art-15.jpg)
![image](/resources/art/art-16.jpg)

================================================================================
PAGE: The Time Machine
Canonical URL: https://tomgreenaway.com/stories/the-time-machine
Markdown URL: https://tomgreenaway.com/stories/the-time-machine.md
================================================================================

# The Time Machine

He stopped the car. 

He didn’t know where he was. It wasn’t important. Some road between nowheres. 

All he knew was that in that moment. He didn’t want to be here anymore. 

He wanted to go back. But not back to where he came from.

Time travel – is it possible? 

It is.

But it’s dangerous. 

If you go back in time. Be careful.

You might not like what you find. Or maybe worse – you will.

The first rule of time travel. 

You can only visit the past.

The second rule of time travel.

You cannot change the past. 

The third rule of time travel.

You might not find a way back to the present. 

He decided he would do it again. He didn’t want to. But he couldn’t help himself. 

He opened the glovebox. He reached for the device. Battered and old with dust. 

He opened the messages. He scrolled into the past. And he started the time machine. 

A voice spoke. 

“Hi. How are you? Sorry for the delay. I was a bit busy this morning. With the event coming up there were a lot of meetings. So about Chile. Honestly, that trip was one of the best of my life and–”

He stopped the message. 

And snapped back to reality – to the present.

That was a close one, he thought.

Time travel – is it possible?

It is. 

But remember – it’s dangerous.

================================================================================
PAGE: The Dam
Canonical URL: https://tomgreenaway.com/stories/the-dam
Markdown URL: https://tomgreenaway.com/stories/the-dam.md
================================================================================

# The Dam

“Oh, Tom can make us a quiz. He’s good at quizzes!” my Greek ex proposes in front of her family.

“Ahhh… excuse me?” I laugh.

“Oh could you Tom? That would be lovely!” her mother says.

“Of course he can! He knows lots of things and he does lots of quizzes!”

I laugh more and say to my ex, “What are you getting me into?”

“Oh darling, please?” she says with her perfect British accent. Born in Greece yes, but raised by a British mother. That was her.

And so, I accept the challenge. Why not?

What is a quiz – if not a game to be designed.

And so, I retreat upstairs to our bedroom in that house in the British countryside. Her aunt and uncle’s house with all her cousins, her brother and family altogether. A lovely Christmas that deserved a lovely quiz.

I head upstairs and I begin.

Creation?

Everyone thinks it’s a well or a fountain. Something one must approach, work the ropes and pull water from.

They imagine that in the bedroom I am brainstorming. That I am generating the questions. But that’s not what happens.

I open my mind to the ideas and let the creativity in. The questions pour into my mind…

James Bond? Stylistic. Watches. Lots of films. Which brand of watch was most popular? Done.

Picasso? Finish a quote. “Good artists borrow…” Great artists steal. Yeah. That’s good. Quotes are good. Done.

Beatles? I watched the documentary last week about them. Ah yes, what was John Lennon’s first band? Done.

Let’s go techy. PDF. What does it actually stand for? When that which surrounds us is invisible. That’s a good surprise. Done.

In less than thirty minutes I have thirty questions. I imagine myself playing the quiz, I gauge the difficulty. I trim the fat – the less interesting questions, the too hard and the too obvious. Done.

That night I become a quiz master for the first time. It’s not just the creation and the curation that is important but also the execution – the performance.

Every question is a story to be told.

The family loves it. They mention repeatedly “These are great questions. Where did you find them Tom?”

“I… just wrote them. Today.”

They’re impressed and we continue.

The Greek girl’s brother leans over to me during the quiz. 

“Dude. This is a _good_ quiz. How’d you come up with this so easily?” 

I laugh and strike a little pose with a shrug.

“Well, don’t you remember? I’m a game designer.”

He laughs and smiles. “Shit.” he says as he understands.

The quiz ends and they even ask for a second one the next night. The house hums with joy over that Christmas.

Six months later that gorgeous Greek girl and I will have separated. But she’ll have given me a gift – seeds that will flower into a new skill.

I will grab a beer at my local pub and talk to the owners. I’m a regular. Winning friends and influencing them, subtly.

I ask out of curiosity, “So who’s hosting the quiz next week?” 

“Ahh mate. All the quiz masters are out of town. We might have to cancel.”

“Oh… Well, I actually hosted a quiz over Christmas by the way.”

“Oh yeah?”

“Maybe I could try?” I propose lightly.

Try? I know I can. But let’s play the humble card.

“Well mate. The quiz, it’s difficult y’know. Making a good quiz… gotta have some good questions… can’t be too hard, or too easy…”

In my mind I think, yes of course. It requires good curation.

“Well, I have some of my questions from Christmas on my phone. Lemme run a few by you.”

“Sure.”

I scroll through the thirty questions. I look at the bartender and I adjust and optimise them for his palette.

Know your audience.

Making a good quiz is indeed a subtle art. A puzzle to be woven.

It can be difficult to balance it right.

But not for a game designer.

You need the skills of creation, curation and execution.

He calls the other owner of the bar over. I continue the questions. They see I can do it. We discuss the format.

That night, I go home and I let the creativity flow into me. I create the quiz and the next week, it’s a hit.

I become a quiz master. A gift from my Greek ex. 

Creativity?

Everyone thinks it’s a well or a fountain. Something one must approach, work the ropes and pull water from.

But it’s never been a well to me.

For me, it’s a river.

The chaos of the creativity is always at the edge of my mind. It doesn’t take effort to pull the water.

It takes effort, everyday – to hold it back.

It’s a river. And I’m the dam.

================================================================================
PAGE: That Fucking Hat
Canonical URL: https://tomgreenaway.com/stories/that-fucking-hat
Markdown URL: https://tomgreenaway.com/stories/that-fucking-hat.md
================================================================================

# That Fucking Hat

“Hey. Can we talk in private for a sec?” Hazmatic said.

They didn’t use their real names down here. 

“Yeah sure man.” I said as we stepped into the shadows.

It wasn’t hard to find shadows in this place. The illegal catacombs felt like a web and Hazmatic explained the earth of Paris is like swiss cheese – holes and tunnels everywhere.

This place was pure darkness and silence, only people could make it bright and make it talk. And it loved to talk.

“There’s this guy. She’s talking to him at the moment. Over there.”

“She?” I said but I knew the answer.

“Sand.” He gestured towards his girl. Down here that was her name.

“Right.” I replied. I understood as I looked across the room, the cavern.

People were scattered across it, huddled and talking, drinking. Candles around the room – a cheap light source that lasted.

My eyes scanned for her sandy blonde hair and I struggled. But then I found her. I saw the ends of her hair, spilling out from underneath something new. A hat she now wore. An unexpected hat.

So there in the middle of those cataphiles was Hazmatic’s girl. She sat and spoke close to another guy. Bald, hipster moustache and wearing goddamn 1920’s style pants with suspenders for fuck’s sake. They were both leaning in.

“He is always talking to her when she comes down and like, they’ve hung out before and he never made a move apparently. And now she’s with me but… she spends time with him and I don’t want to be jealous but…”

“Right…”

“Anyway… it just... it triggers me.” He revealed.

I wanted to understand. I wanted to help him. “Why?” I asked.

“‘Cause… a long time ago there was this thing that happened with another girl. She fucked with my head. And, I… I don’t know what to do. I want to be cool about it y’know? But you see, see that hat she’s wearing?”

That fucking hat. It had an old time vibe. Circa 1920’s…

“Yeah?” But I already knew the answer.

“That’s his fucking hat. He just put it on her. And she let him. Y’know?”

“Got it.”

I saw the fear in his eyes. I gave him some advice. My own memories of when I’d been like him – and my memories of when I’d been the other guy too. The fear and the fantasy were two sides of the same coin.

“What if we tell her I want to leave? I can’t find my way without you. It can be an excuse?” I suggested. I wanted to help him.

His eyes flicked around the room, “Yeah… Okay. Yeah. Let’s do that. Give me a minute.”

Hazmatic began his goodbyes and I watched the girl as I waited.

She stood up and then moved behind the gentleman from the 1920’s. And while he spoke to another man, I watched her begin to lightly tap his bald head with her fingers. Even I was feeling triggered. I knew it was better that he didn’t see this.

She stopped in the nick of time as Hazmatic turned and approached them.

They spoke and the gentleman ignored them. There was push and pull. I was waved over and as I approached, the kind man and the gentleman spoke together. I looked at Sand and smiled.

I leaned in to her ear, “That’s a nice hat.”

“Oh thanks. Actually, it’s not mine. It’s his.”

Slowly I turned my eyes to him and added softly, “Ah yes… Makes sense.”

My gaze turned back to her. I still stayed close as I looked right into her eyes.

A moment longer and she would’ve understood. In the silence, the catacombs had begun to talk.

“Let’s go?” Hazmatic interrupted the shadows.

The hat was returned and we escaped into the tunnels. There was nothing to talk about now.

At a junction, Sand paused us and took another path. I looked at Hazmatic and he answered my unspoken question, “Toilets. I mean, the closest thing to it at least.”

We waited for her and then a new light emerged from our previous path. The hatted man joined us.

He and Hazmatic played it cool. 

He looked at me, “Who’s this?” the man with a hat said.

“He’s a tourist,” Hazmatic explained.

“Ah, un homme sans nom.”

“I have a name.” I said, “You just don’t know it yet.”

His eyebrows rose briefly with surprise and he reassessed me.

The girl came back from the shadows. I knew he knew she would be here.

They formed a different kind of trio and had one last conversation in the darkness. One last push and pull. 

I wanted to help him… but I already knew this story. There are some lessons which cannot be taught and thus cannot be learned either.

They can only be realised.

Sometimes, on our own – in the dark.

After the final farewells, Hazmatic led the way and I watched Sand stick close to him. He walked confidently now – for he knew these tunnels and his mastery of them gave him strength.

As she followed him, she almost danced. And in the warm glow of their lights I saw her more clearly – a sandstorm.

I followed for a few seconds, stopped myself and then looked back over my shoulder as I saw that fucking hat return to the shadow.

================================================================================
PAGE: Invent It
Canonical URL: https://tomgreenaway.com/stories/invent-it
Markdown URL: https://tomgreenaway.com/stories/invent-it.md
================================================================================

# Invent It

“Maybe it’s time for a change? Just because you started in the games industry. Doesn’t mean you have to stay there, right?” a junior product manager tries to give me advice.

But this future she paints for me. It does not sit well.

Later, over a coffee with another colleague. He asks to hear my story. He listens and tells me, “Okay… Cool. Keep doing that then. Become a product manager connected to games. Lean into it. That’s your strength.”

I’m surprised by his advice but somehow I can already feel he’s right. In this domain, I move confidently. Games were my catacombs.

One month later, there’s talk of a strange virus. China. Wuhan. Corona something?

The pandemic begins and the world…

…stops.

Welcome to 2020. Let’s find out what makes you tick.

All our conferences are cancelled. No more public speaking for me.

My colleagues and the developers around the world… Everyone feels sad.

For what punishments of God are not gifts?

It’s hard to predict what the future will hold now…

And I feel lost…

Time to learn about sourdough starters. Right? Fuck off. 

I had just arrived here. In my dream of Paris. In my petite 30 square metre apartment. 

I am confined and my back is against the wall now. Perfect. 

My kitchen floor has big white tiles with black joints. One day I stare at them – depressed. I put on a record, “While My Guitar Gently Weeps”.

I stand by the window and look out at the sky.

The lyrics begin and an idea starts to unfold…

**_“I look at you all”._**

I think about the developers. They need a way to connect.

**_“See the love there that's sleeping”._**

We can’t meet anymore. No more conferences. They need a virtual world.

**_“While my guitar gently weeps”._**

My skill set. I am a game developer. 

**_“I look at the floor.”_**

Tiled. Grid-like. Game-like. A simple movement system. That’ll do.

**_“And I see it needs sweeping.”_**

I know what must be done. Time to get to work.

**_“Still my guitar gently weeps.”_**

I pick up my skill set… And I begin to prototype an idea.

I code all day, every day and deep into the night. I barely eat. I barely sleep. There is nothing but this.

I dream of new features and small tweaks to make. I leap out of bed each morning and I add more.

After 10 days, the prototype is complete. I show it to my manager and more. It’s greenlit.

It’s a technology for recreating physical conferences into a digital virtual world. Developers walk around with little characters like a video game, all inside the web browser. It connects them to each other. We aren’t alone anymore.

If our communities can’t travel to our conferences anymore – we will bring the conference to them.

It’s the perfect solution to my team’s problems during the pandemic. I leverage my strengths to make it.

I call it “Adventure”. It lives up to its name.

They call me a product manager. But in truth, I become again – what I always was deep down – a creator. 

Predicting the future is troublesome.

I find it much easier to invent it.

================================================================================
PAGE: Duet
Canonical URL: https://tomgreenaway.com/stories/duet
Markdown URL: https://tomgreenaway.com/stories/duet.md
================================================================================

# Duet

“Well… that’s concerning.” I say.

My co-founder Ivan nods.

We’re looking at a graph. Our revenue for our latest game launch. The revenue?

It’s plummeted.

“Why? It’s normal isn’t it?” My brother says, “The featuring from Apple has stopped. So the revenue is dropping. Isn’t that normal?”

He hasn’t caught up to Ivan and I yet.

The game was featured worldwide. Editor’s Choice. The most prestigious featuring Apple could give us. We thought it would be a silver bullet to success. But it wasn’t.

We built exactly what we thought Apple wanted and would feature. And they did. But now it was failing when standing on its own.

“Yeah but, James, if that was the best featuring we could possibly get…”

Ivan jumps in, “And that’s all the revenue it made…”

I take over, “And without featuring, it just goes down to the same revenue as the other games and if we have no real control over this…”

“…then we’re fucked.” Ivan concludes.

“Well, basically yeah.” I say.

There’s a sad silence in the air.

“Oh well…” and my brother cracks a joke.

“It’s not really funny James.” I close my eyes with fatigue and state simply.

Ivan sighs and breaks it down for my brother, “You know my life is like… on hold while we do this, right?”

“Huh?” my brother doesn’t get it.

“He means that his career, his salary progression in the industry, it’s all paused while we try to build this thing.” I explain.

My brother has no respect for that path in life. And neither do I actually – I just haven’t realised it yet.

“I know you’ve never cared about having a career…” I continue and I trip a nerve.

“Oh fuck you.” my brother throws back at me.

I roll my eyes and look at Ivan. He looks back at me and I know what he’s thinking. We are in sync.

Ivan and I go out for a break.

As I wait for my coffee I stare into the distance, beyond Ivan’s shoulder, as I collect my thoughts. We’re still processing everything.

I’m becoming quite angry at my brother. 

And I realise…

**_Anger may make certain choices clearer._**

There’s a silence between us. A pause.

“We should finish that prototype with the balls.” I say.

“The spinning prototype? James hated it though.”

“Mmm. Too simple I believe were his words.” I say.

“He’s not going to like it.”

I look Ivan in the eyes and smile. “Well fuck it. He’ll have to suck it up this time. He can work on something else. This time it’ll be just you and me.”

“Okay. Yeah it’s worth a shot.”

I add business constraints, “But we should do it fast. Six months, absolute max.”

“Sounds good. We’ll need to keep it simple.” he nods.

“We’ll make it minimalist. Without James’ art, it’ll be simpler that way.” I continue.

Ivan is always skeptical of weaker art styles. “Hope we can find good music…”

“Yeah. Something… minimalist as well. It needs to fit.”

Another pause.

“I could try writing a story for it too?” I suggest.

“We’d probably want a proper writer.”

“Let me try.”

“Okay. But about what though? It’s just two balls.” He shrugs.

At the edge of my mind I feel an idea taking shape.

“No. It’s about… the struggle. The game is difficult. It needs to be about something difficult. It can be universal. Something we all go through.”

“The struggle of being an indie game developer?”

He laughs and I laugh too. It’s apt.

But I concentrate again, “Yeah. But… maybe more than that? The pain of struggling. Of losing. Of losing a person maybe? Two balls. Two people. Love. Loss. Codependency? A relationship?”

“What are we going to call it?”

“Something minimalist. Super short. Like a single word…” I suggest.

Again, a pause as I think further. I let the creativity pour in.

“Duet.”

I say this name. And it already sounds like it always was.

Ivan nods.

“Nice.” And his lip curls into a small smile.

Later on, during the development, I write parts of the story. Snippets of advice – like a self-help book.

Little quotes based around the stages of grief.

Denial. Anger. Bargaining. Depression. Acceptance.

I’m writing the Anger chapter and I remember a thought:

**_Anger may make certain choices clearer._**

Perfect.

On another day, I finish a yoga class. I am exhausted by the session and the heat of the room. And in the final resting pose, Shavasana – also known as corpse pose, a metaphorical representation of death and the acceptance of loss – my thoughts flicker as I come in and out of consciousness. 

I imagine shapes in my mind. White blocks. Red and blue. The prototype plays in front of me and I envision new puzzles for the game. I keep them in my head as I wake up.

And as I get up after the class I’m a sweaty mess. I look down at my yoga mat and see all the streaks of sweat marking it. The blood, sweat and tears.

They look like paint splatters and I have another idea.

The next day I race to the office and talk to Ivan. “Can you try these puzzles?” “And what if when you lose, the blocks are splattered with paint?” “Like blood?”

“Creepy. But sure.” he laughs and nods, “Those puzzles should work too. I can test them.”

Great, I think to myself.

After six months of polishing we release the game.

However, there’s a mixup with Apple and they don’t give it their best featuring. Although it still makes good money. And then the featuring ends, as usual.

We assume it’s over. Our last game.

But a few days later. Ivan notices something.

“Hey… the revenue… it’s not dropping?”

He was right.

This game?

It stands on its own.

It becomes our big hit. Our magnum opus.

The story of loss. Of grief. Resonates with people who play it. 

They email us.

Thoughts like… “It helped me get through a difficult time in my life. I was close to the edge. But then I accidentally found your game and the story really helped me. I just wanted to say thank you and please, never stop making games.”

What a gift. My struggles, my denials, my anger, my grief. 

I have transformed it into something beautiful and it helps millions of people.

**_Unexpected things are beautiful._**

But eventually and rather ironically, it will also help one person in particular. A few times in their life, in fact.

Me.

Five years later, I replay Duet twice.

Once while I try to find the strength to walk away from my toxic ex. By playing it, I receive advice from my younger self. And its instructions on how to survive the loss push me to make the decision.

**_Others may try to make you feel guilty._**

**_But guilt can only truly come from within._**

Second, I play it again after I’ve fucked everything up with the girl from Lisbon. Asking again for my own advice, I realise I need to run back to her and fix everything as fast as I can. But I was too late.

**_Change is good for you._**

**_You must move forward…_**

And now, another five years later. Ten years in total since I created Duet. As I fly back to Paris from Spain, I open Duet on my phone and play it again. I receive the advice and its instructions on grief. I am comforted by myself and my old words.

**_Abandon all your expectations._**

**_Sometimes letting go is the harder choice._**

I let go of all my expectations.

================================================================================
PAGE: Adventure
Canonical URL: https://tomgreenaway.com/projects/adventure
Markdown URL: https://tomgreenaway.com/projects/adventure.md
================================================================================

# Adventure

What was Adventure?

Adventure was a web-based virtual conference technology that I created during the first month of the 2020 pandemic.

## The first prototype


## Chrome Dev Summit Adventure in 2020

## I/O Adventure 2021

## I/O Adventure 2022

## Other Adventures

## Adventure's End

================================================================================
PAGE: The Z Word
Canonical URL: https://tomgreenaway.com/games/z-word
Markdown URL: https://tomgreenaway.com/games/z-word.md
================================================================================

# The Z Word

A card game I'm working on with Lyndon D'Arcy.

![image](/resources/art/card-1.png)
![image](/resources/art/card-2.png)
![image](/resources/art/card-3.png)
![image](/resources/art/card-4.png)
![image](/resources/art/card-5.png)
![image](/resources/art/card-6.png)

================================================================================
PAGE: Found
Canonical URL: https://tomgreenaway.com/games/found
Markdown URL: https://tomgreenaway.com/games/found.md
================================================================================

---
pageType: article
---
# Found

tl;dr: a small fan game inspired by Lost. <a target="_blank" href="/prototypes/found/index.html">Play in fullscreen.</a>

<iframe src="/prototypes/found/index.html" width="500" height="500" scrolling="no">
</iframe>

<div class="noindent-p"><button class="inline show-button" href="#more" target="#hidden-info">More info</button><button class="inline show-button" href="#disclaimer" target="#disclaimer-info">Disclaimer</button></div>

<div id="hidden-info" class="hidden">
<p>
	With the recent 20th anniversary of <a href="https://en.wikipedia.org/wiki/Lost_(TV_series)">Lost</a> I rewatched the show and discovered the <a href="https://www.youtube.com/watch?v=oTZ88eUQrJo">Lost Explained YouTube channel</a>. Lost is one of my favourite series of all time. Was it perfect? No. But the bizarre combination of mystery, sci-fi, mystiscism, island adventure and character-focus made it a "one of a kind" television experience in my opinion.
</p>
<p>
While rewatching the show, my brain mulled over the world and how I would envision Lost as a game. So I made this little fan game as a tribute to the series. Some things to keep in mind:
</p>
<ul>
	<li>The gameplay is a mix of adventure, exploration and light RPG elements.</li>
	<li>As the show was “character focused”, I decided there shouldn’t be a single main character. Instead the game is divided into chapters and you switch between characters.</li>
	<li>There are only 5 chapters in total. The first chapter is very simple, just Jack saving people on the beach. In Chapter 2, you play Kate and I add light combat. Chapter 3, it’s boar hunting time :)</li>
	<li>Rather than try to adapt the show as accurately as possible, I decided to simply focus on 5 fun chapters that jump through season 1 rapidly. So you will find some parts don’t match exactly the events of the show – I think twisting the story slightly makes it a bit more mysterious.</li>
	<li>Combat is loosely derived from a mix of games including Grandia, Child of Light and Chronotrigger.</li>
</ul>
<p>
Just to be clear, I’m not going to work further on this. It's just a theoretical mockup of what a Lost game could be like. To any Lost fans who find this – enjoy.
</p>

</div>

<div id="disclaimer-info" class="hidden">
<p>
	Disclaimer: All publicly recognisable characters, settings, plot, visuals, audio, etc. are the property of their respective owners and creators.
</p>

<p>
	The author is in no way associated with the owners, creators, or producers of any previously copyrighted material. No copyright infringement is intended.
</p>

<p>
	The following is a work of pure fan fiction and is non-commercial in nature. It is merely a tribute – inspired by one of the greatest television shows ever produced. 
</p>

<p>Namaste & good luck.</p>
</div>

<script>
window.onload = function () {
	document.querySelectorAll('iframe').forEach((iframe, i) => {
		iframe.addEventListener('keydown', (evt) => {
			evt.preventDefault();
		})
		
		iframe.addEventListener('load', (evt) => {
	alert('load');
		})
	})

	document.querySelectorAll('.show-button').forEach((button, i) => {
		var hiddenDiv = document.querySelectorAll(button.getAttribute('target'))[0];

		var closeButton = foo = document.createElement('div');
		closeButton.innerHTML = `<div class="close-hidden-info">✖</div>`
			
		hiddenDiv.appendChild(closeButton)
		hiddenDiv.classList.add('closeable-info-box');

		button.addEventListener('click', (evt) => {			
			hiddenDiv.style.setProperty('display', 'block');
			button.style.setProperty('display', 'none');
			evt.preventDefault();
		})

		closeButton.addEventListener('click', (evt) => {			
			hiddenDiv.style.setProperty('display', 'none');
			button.style.setProperty('display', 'inline');
			evt.preventDefault();
		})
	
	});
}
</script>

<style>

    iframe {
        width: calc(89vh - (5em + 5vh));
        height: calc(89vh - (5em + 5vh));

		border: 2px solid black;
    }
</style>

================================================================================
PAGE: Duet Original Story
Canonical URL: https://tomgreenaway.com/duet/story
Markdown URL: https://tomgreenaway.com/duet/story.md
================================================================================

# Duet Original Story

## Ignorance

It's so good to see you. Stay close to me and don't touch anything.

You will need a circular state of mind.

This will be easy. Just hold left...

Never give up.

Some things are worth fighting for.

## Denial

This will be full of unexpected twists and turns.

Tread carefully and control yourself.

Trust your instincts.

Change is normal.

Do not deny it.

Embrace it.

## Anger

Anger may make certain choices clearer.

Patterns always emerge. Even in chaos.

This need not define you.

Let it amplify you.

Repeating the same action but expecting new results is a sign of madness.

## Bargaining

It's also a sign that you are human.

Once an action is done it cannot be undone.

You cannot get out of this.

Accept the situation and move on.

To try invites failure.

Resistance is not an option.

## Guilt

It's all a matter of perspective.

Regret has two sides.

The things we do...

And the things we wish we had done.

Others may try to make you feel guilty.

But guilt can only truly come from within.

## Depression

Nothing is wrong with you.

Why are you doing this to yourself?

What exactly are you trying to prove?

You will not win.

All that pain and misery. Was it worth it?

I don't want you to keep going...

## Hope

I want you to want to keep going.

We have no reason to give up.

You probably didn't expect to get this far...

But if we expect something - how can it move us?

Unexpected things are beautiful.

Abandon all your expectations.

## Acceptance

I wish you could see this.

You wouldn't be here if it weren't for me.

Admit it.

Change is good for you.

There is nothing to regret.

Anger is always temporary.

Sometimes letting go is the harder choice.

You must move forward...

But I want you to know one last thing...

**I Am Here**

================================================================================
PAGE: Duet Soliloquy Story
Canonical URL: https://tomgreenaway.com/duet/soliloquy
Markdown URL: https://tomgreenaway.com/duet/soliloquy.md
================================================================================

# Duet Soliloquy Story

## Confusion

It's been a long time. Are you happy to see me?

Everything will feel a bit different at first.

But some things are the same. Just hold left...

I know what you're thinking. Are there more like us?

You're on your own now. Surrender to this reality.

Can we ever go back?

## Relief

Never look back.

There's always a way out.

We'll figure it out. Together.

Did you change in the night?

You don't need to explain yourself to me.

You have always been changing.

## Resentment

It's what you wanted. Wasn't it?

Nobody abandoned you. You're the one who let go.

You thought it was going to be easy. Didn't you?

If it was easy – everyone would do it.

If you cannot break the pattern... it will break you.

You think you missed your shot. Don't you?

## Reunion

Or... did you dodge a bullet?

You must invert the negative.

Maybe this is the best thing that ever happened to you.

I know you've lost so much time...

You want me to teach you?

But some lessons cannot be taught nor learned.

## Reintegration

They can only be realized.

You want a second chance?

Second chances aren't given. They're earned.

Only once you've lost everything can you truly start over.

And if the world has changed. Then you must change with it.

Everything you need to succeed is...

**Still With You**

================================================================================
PAGE: Duet Epilogue Story
Canonical URL: https://tomgreenaway.com/duet/epilogue
Markdown URL: https://tomgreenaway.com/duet/epilogue.md
================================================================================

# Duet Epilogue Story

## Trust

Hello again.

Come on, you remember this... just hold right.

Don't you trust me?

Will you ever trust again?

You must start somewhere.

You must trust yourself first.

## Understanding

It's all about the separation of concerns.

We all have a start and an end.

But where do you end? And others begin?

Don't you want to change your world?

My world is open and limitless.

We can share this world together, you know.

## Control

I know sometimes it feels like you have no control.

Attempting to control the uncontrollable can feel futile.

Even the illusion of control can bring peace.

But control is always temporary.

Is there another way?

Breathe deeply and let yourself go.

## Identity

Who are you?

How did you get here?

Are you looking for me?

What do you need?

What do you want?

You are what you are.

## Initiative

Only through action can we move onward.

You must take the first step.

There are many decisions ahead of all of us.

Some decisions are easy.

Other decisions require more understanding.

Many decisions change our lives forever.

## Intimacy

Open up to me.

You and I are so alike.

Take down your barriers.

Stop looking for a way out.

We can be together.

Don't you want to be together?

We can be closer than this.

We can be better than this.

Love me.

## Integrity

In the end, this is what matters.

Do we trust one another?

Can we build a future together?

Can we live together?

How do we thrive and grow, under pressure?

Grace and integrity are what define us.

Pacifism is not apathy.

The world will go on - will we?

Never forget...

**You Are Not Alone**

================================================================================
PAGE: Duet Encore Story
Canonical URL: https://tomgreenaway.com/duet/encore
Markdown URL: https://tomgreenaway.com/duet/encore.md
================================================================================

# Duet Encore Story

## Anticipation

This is a surprise. I didn't think I'd see you again.

Do you remember how this goes?

Are you sure you want to do this?

To be honest, I've been worried about you.

All this spinning around... always chasing perfection...

You can't go on like this forever.

## Ambition

You want it all.

You turn and turn - why are you so afraid of standing still?

I don't understand you sometimes.

You've come so far, achieved so much - and still it's not enough?

There's only so many hours in a day - or in a life.

What will you do, when this is over?

## Tension

It's okay to get mad.

You're only mad at yourself.

You're letting this get the best of you.

Everything feels so tight sometimes.

Can't you just relax? Do you even remember how?

Push any further - and you will break.

## Perspective

I don't like seeing you this way.

Do we ask too much of ourselves?

Stop trying to do everything.

Sometimes it's impossible to see yourself.

Please listen to me.

You think that you're trapped, but you can be free.

## Release

Trust me. Let go.

There's nothing left to prove.

Only when you see the pattern, can you break it.

I want to help you but you need to let me.

Nothing else matters. Just this moment.

Stop "doing"...

**Just Be**
