Two Houses, One Home

How it is built

Two Houses, One Home is a small app for exactly two people. Almost every interesting decision in it came from that number. A world with two people in it can do things a world with a million people in it cannot afford to, and this page is about the ones worth knowing.

Two skies on one screen

Each house has its own sky, and each sky follows that person's real local time rather than the app's. So you can look across the valley in the middle of your afternoon and see that it is already night on the other hill. Nobody is told the time difference. You just see it, every time you open the app, and after a while you stop needing to do the arithmetic.

This is the whole design in one detail. The distance is not a problem the app hides. It is the thing the app is made of.

The ground is drawn on the corners, not the squares

The map is a grid of cells, and each cell holds one material: dirt, grass, water. The obvious way to draw that is to give every material a tile and then draw a border tile for every way two materials can meet. That means a new material costs a new border against every material already there, so the work grows faster than the world does.

Instead the tiles are offset by half a cell, so every tile sits on a corner where four cells meet. Each tile then only has to answer one question: which of my four corners are filled? There are sixteen answers, so every material is exactly sixteen tiles, forever, no matter how many other materials exist. The render grid is one wider and one taller than the map, which is why half a tile of ground hangs past the last cell you can stand on.

four cells, one tile on their corner the tile only has to answer which corners are filled three of four filled, so it is mask eleven sixteen answers, so sixteen tiles, for every material, forever
A tile sits where four cells meet, so it never has to know what any pair of materials looks like together.
The sixteen dirt corner masks in one sheet
All sixteen dirt masks, as drawn. The water sheet is the same sixteen. Both are on the art page.

Nothing random is ever sent

Long distance means two phones on two connections, sometimes very bad ones. So the rule here is that if both of you have to see the same thing, it gets computed rather than transmitted.

Everything scattered across the world, where a plant sprouts, which variant it is, how it leans, comes out of a noise field seeded with the couple's own id. Nothing is stored and nothing is sent. Both phones compute the same field and lay out the same world, and if you delete the app and come back, the same plants are in the same places, because the seed never changed.

The same trick makes a game playable together over a bad line. In Love Taps the entire run of moles, which hole, at what millisecond, for how long, is a pure function of a single seed. One phone names a seed and a moment on the clock. From then on both phones are running an identical copy of the same run, independently, and the only thing that ever crosses the wire is where the hammer went. A slow connection cannot cost you a mole, because the moles were never coming over the connection.

one seed, one moment their phone your phone identical, and neither one was sent a single mole
The run is a pure function of its seed, so both phones build the same one and the connection never gets a say.

One hammer, two of you

The co-op mode is the same board split in half. One of you holds the hammer and cannot see a single mole. The other one sees every mole and cannot touch anything. There is no way to point, no cursor to share, no tapping to signal. The only channel between the two of you is your own voice, which is the channel you already had open.

Halfway through, it swaps, so neither of you spends the whole game being the one who only watches.

Nothing lands on one of you

A standing rule, and it changed several games after they were built: no consequence ever falls on one person alone. If something is lost, it is lost by both of you, and if it is won, it is won by both of you. An app two people share should never be able to make one of them the reason the other one lost something.

The weekly letter follows the same principle from the other direction. Two questions, you both answer, you both seal, and the seal only melts when you both press and hold at the same moment. It cannot be opened alone. Not because it is locked, but because there is nothing there until both of you are.

The database has no readable tables

Every table in this app has row level security switched on with no policies on it at all, which means the answer to reading any table directly is always no. Not "no unless", just no. Everything the app does goes through a small set of database functions that run with the privileges of the person who wrote them, and each one starts by checking the member id and secret it was handed.

The practical effect is that the key shipped inside the app grants nothing. Somebody can read it straight out of the page source, and with it they can call functions that will refuse to do anything without a secret only the two phones hold. And because every amount is decided by the database rather than by the phone, a client that has been taken apart still cannot mint anything: it can ask, and the answer is worked out somewhere it cannot reach.

The sound is not files

Every sound is made in the browser as it plays. Oscillators, filtered bursts of noise, and a curve on the volume, and that is the whole footprint: no audio files at all. Each one is pitched slightly differently every time it plays, but the shift is drawn once and applied to the entire sound, so a little run of notes bends together and stays in tune with itself instead of coming apart.

Two things it took three bugs each to learn

Reach is a distance, not a cell. Three separate features shipped broken in the same way: standing exactly on a thing did nothing, because the code was asking "am I in the same square as it" and a character that wanders by one pixel is sometimes not. Reach is measured in pixels now and re-checked every frame.

Render is a question, not an action. Characters had walk cycles that never played. The code compared where a character was against where it had been, and did that comparison while drawing. React draws twice in development on purpose, so by the second pass it was comparing a position against itself and always concluded nothing had moved. Drawing answers "what does this look like", never "what just happened". The second one belongs in an effect.

What it is made of

React and TypeScript, built with Vite, installable as a progressive web app, and Postgres underneath. It does not use the usual all-in-one client library for the database, because that library also ships sign-in, file storage and serverless functions, none of which this app touches, and it cost forty kilobytes on every first load. What replaced it is a small file that does two things: call a function, and hold a live channel open.

The whole app, art included, is a few hundred kilobytes. It works offline, it installs to a home screen, and there is no account, no email address and no password anywhere in it. Two phones hold a secret each, and that is the entire identity system.

What it is actually made with

Aseprite Every sprite in it. Read straight off the .aseprite files by a script, because there is no command line for it on this machine.
React and TypeScript One route, twenty games, each loaded only when opened.
Postgres Every table locked, everything through functions that check a secret.
Vite Builds in about three seconds, which is why it gets changed so often.
Web Audio All of the sound, none of the files.
Canvas and SVG The drawing that becomes your character, and the sky above both houses.
Service workers It installs, it opens without a browser, and it works with no signal.
Python The tools that pull art out of Aseprite and build these pages.

See the art, or open the app.