youhide@homelab: ~/blog

$cat weed-neighborhood.md

Weather is a function, not a table

O clima é uma função, não uma tabela

I have been building a game called Weed Neighborhood. It is a cozy multiplayer game where you grow plants, breed strains, enter them in seasonal cups, and live in a shared neighborhood with other people's houses, parties and arguments. It runs in a browser, it speaks Portuguese and English, and it is fiction for adults — a stylized little world, not a guide to anything.

Ando construindo um jogo chamado Weed Neighborhood. É um jogo multiplayer tranquilo onde você cultiva plantas, cruza variedades, inscreve elas em campeonatos sazonais e mora num bairro compartilhado com as casas, as festas e as discussões das outras pessoas. Roda no navegador, fala português e inglês, e é ficção para adultos — um mundinho estilizado, não um guia de nada.

The part I want to write about is not the game. It is one rule that decided most of its architecture, and that I keep reaching for in things that have nothing to do with plants.

A parte sobre a qual eu quero escrever não é o jogo. É uma regra que decidiu a maior parte da arquitetura dele, e que eu fico puxando pra coisas que não têm nada a ver com plantas.

A garden nobody is looking at

Um jardim que ninguém está olhando

Plants grow in real time, which means they grow while you are asleep, at work, or have not opened the game in a week. Multiply that by every player, every grow space and every plant in each one, and you have a very large number of things that are quietly getting thirsty at three in the morning.

As plantas crescem em tempo real, o que significa que elas crescem enquanto você dorme, trabalha, ou não abre o jogo há uma semana. Multiplique isso por cada jogador, cada espaço de cultivo e cada planta dentro dele, e você tem um número muito grande de coisas ficando discretamente sedentas às três da manhã.

The obvious approach is to tick all of them, all the time. It is also the one that decides your hosting bill before you have a single player, and I am running this on my own hardware at home.

A abordagem óbvia é atualizar todas elas, o tempo todo. Também é a que decide a sua conta de hospedagem antes de você ter um único jogador, e eu rodo isso no meu próprio hardware, em casa.

Nothing is stored that can be computed, and nothing is computed until somebody asks.
Nada é guardado se pode ser calculado, e nada é calculado até alguém perguntar.

Weather is a function, not a table

O clima é uma função, não uma tabela

Start with the weather, because it is the clearest case. The neighborhood has seasons, rain, heat and cold, and all of it matters to an outdoor plant. The instinct is to simulate weather forward and write it down somewhere.

Comece pelo clima, porque é o caso mais claro. O bairro tem estações, chuva, calor e frio, e tudo isso importa pra uma planta ao ar livre. O instinto é simular o clima pra frente e anotar isso em algum lugar.

It is never written down. The weather is derived from the neighborhood's seed and the moment you are asking about — give it a time and it gives you that day's conditions, the same answer every time, for a day last month or a day next year. There is no weather table, no weather job, and no weather to back up.

Ele nunca é anotado. O clima é derivado da semente do bairro e do momento sobre o qual você está perguntando — dê um horário e ele devolve as condições daquele dia, a mesma resposta toda vez, para um dia do mês passado ou um dia do ano que vem. Não existe tabela de clima, nem rotina de clima, nem clima pra fazer backup.

That is the whole trick, and the benefit is not really the disk space. State you do not store is state that cannot drift, cannot be half-written when the process dies, and cannot disagree with itself between two parts of the system. The cheapest data to keep consistent is the data that does not exist.

Esse é o truque inteiro, e o benefício não é bem o espaço em disco. Estado que você não guarda é estado que não tem como divergir, não tem como ficar escrito pela metade quando o processo morre, e não tem como discordar de si mesmo entre duas partes do sistema. O dado mais barato de manter consistente é o dado que não existe.

A plant nobody is near is not being simulated

Uma planta de quem ninguém está perto não está sendo simulada

The same rule, applied to the plants themselves. A plant that nobody is standing next to is not running. It is a saved snapshot and the time that snapshot was taken, sitting in the database doing nothing at all.

A mesma regra, aplicada às próprias plantas. Uma planta de quem ninguém está do lado não está rodando. Ela é um retrato salvo e o horário em que esse retrato foi tirado, parada no banco de dados sem fazer absolutamente nada.

When you walk into your tent after four days away, the server takes that snapshot and advances it four days in one go: the growth, the water it did not get, the weather it stood through, the pests that found it. Then it saves a new snapshot and goes back to doing nothing. A plant somebody is standing next to runs step by step instead, so you can watch it change.

Quando você entra na sua tenda depois de quatro dias fora, o servidor pega aquele retrato e avança ele quatro dias de uma vez: o crescimento, a água que não veio, o clima que ela aguentou, as pragas que acharam ela. Depois salva um retrato novo e volta a não fazer nada. Uma planta de quem alguém está do lado roda passo a passo, pra você ver mudando.

Both paths have to end in the same plant, or the game lies to you. That is the part that costs discipline: the steps are pinned to the game's own clock rather than to when the code happened to run, and the randomness each plant sees is derived from which plant it is and which step this is — so four days computed at once and four days computed one at a time give the same result. It is a property the test suite checks, not a thing I hope is true.

Os dois caminhos têm que terminar na mesma planta, ou o jogo mente pra você. Essa é a parte que custa disciplina: os passos são presos ao relógio do próprio jogo, e não a quando o código por acaso rodou, e o acaso que cada planta vê é derivado de qual planta ela é e de qual passo é esse, então quatro dias calculados de uma vez e quatro dias calculados um a um dão o mesmo resultado. É uma propriedade que a suíte de testes verifica, não uma coisa que eu torço pra ser verdade.

The server knows when something interesting happens

O servidor sabe quando algo interessante acontece

If nothing is running, nothing notices that a plant got thirsty — which is a problem, because being told your plants need you is most of what brings someone back to a game like this.

Se nada está rodando, nada percebe que uma planta ficou sedenta, o que é um problema, porque ser avisado de que as suas plantas precisam de você é boa parte do que traz alguém de volta pra um jogo desses.

So every time a plant is saved, the server also works out the next moment that plant will want something — thirsty, ready to harvest, in trouble — and puts a single reminder in the calendar for exactly then. Nothing polls, nothing scans the database looking for sad plants. The simulation that can tell you the state at any future time can also tell you which future time is worth waking up for.

Então toda vez que uma planta é salva, o servidor também calcula o próximo momento em que ela vai querer alguma coisa — sede, ponto de colheita, encrenca — e põe um único lembrete na agenda exatamente pra ali. Nada fica varrendo, nada fica vasculhando o banco atrás de plantas tristes. A simulação que sabe te dizer o estado em qualquer momento futuro também sabe dizer qual momento futuro vale a pena acordar.

The server is the one telling the truth

Quem diz a verdade é o servidor

The browser never decides anything that matters. It sends what you are trying to do and draws what it is told; the growth, the genetics, the cup scores and the contents of your pockets are all the server's opinion. In a game with competitive seasons, that is not an architectural preference — it is the difference between a championship and a formality.

O navegador nunca decide nada que importe. Ele manda o que você está tentando fazer e desenha o que mandarem desenhar; o crescimento, a genética, as notas dos campeonatos e o conteúdo dos seus bolsos são todos opinião do servidor. Num jogo com temporadas competitivas, isso não é preferência de arquitetura: é a diferença entre um campeonato e uma formalidade.

The rules themselves are written once, in Rust, and the browser runs that same compiled code when it needs to show you a prediction. Not a second implementation in another language that agrees with the first one until the day it quietly does not.

As regras em si são escritas uma vez, em Rust, e o navegador roda esse mesmo código compilado quando precisa te mostrar uma previsão. Não uma segunda implementação em outra linguagem que concorda com a primeira até o dia em que, caladamente, não concorda mais.

Where it runs

Onde ele roda

On my homelab. One server binary and one database file, on a machine in my house, reachable through a tunnel rather than through a port I opened on my router. A hobby project that cannot justify a cloud bill is a useful constraint: it rejects every design where the answer to "what if there are a lot of them" is "rent more machines".

No meu homelab. Um binário de servidor e um arquivo de banco, numa máquina dentro de casa, acessível por um túnel em vez de por uma porta que eu abri no roteador. Um projeto de hobby que não pode justificar uma conta de nuvem é uma restrição útil: ela rejeita todo desenho em que a resposta pra "e se tiver muitos" é "aluga mais máquinas".

You can play it in a browser at weedneighborhood.com, and there are desktop installers in their own repository — a thin shell around the same thing, for people who would rather it had an icon. The game's source is private; the idea in this post is the part worth taking anyway.

Dá pra jogar no navegador em weedneighborhood.com, e tem instaladores de desktop num repositório só deles — uma casca fina em volta da mesma coisa, pra quem prefere que tenha um ícone. O código do jogo é privado; a ideia deste post é a parte que vale levar, de qualquer forma.