#1 Um núcleo determinístico, garantido pelo compilador
· Nick
Ainda não há nada para ver, então esta primeira entrada é sobre a parte que ninguém vai enxergar: o núcleo da simulação. O jogo pretende rodar até 1.000 detentos e funcionários a 20 ticks por segundo e explicar por que cada um fez o que fez. Isso só funciona se as mesmas entradas sempre produzirem o mesmo mundo. Decidi que isso seria uma propriedade verificada pela máquina, desde o primeiro dia.
Onde estamos
Estou no primeiro marco, o esqueleto do núcleo. A simulação é uma biblioteca .NET comum, sem código de Godot, e o cliente em Godot 4 por enquanto só abre uma cena 3D vazia. Não existe modelo de mundo, nem blocos, nem personagens, nem saves. Os 1.000 personagens são uma meta; não medi nada nessa escala.
O tick
O tempo no núcleo é uma contagem inteira de ticks, 20 por segundo. Não existe tempo em ponto flutuante. Um tick tem três fases: aplicar os comandos externos, executar os sistemas em uma ordem escrita em um único lugar, e confirmar as mudanças adiadas e avançar o contador. Se um erro escapa de um tick, o mundo é marcado como com falha e para. Hoje a lista de sistemas está vazia; o laço existe para que o primeiro sistema tenha onde entrar.
Comandos e um registro
O mundo só muda por meio de comandos. Um comando é um registro pequeno, com um identificador de tipo estável e uma codificação binária escrita à mão. Para cada um, o mundo o codifica, marca com o tick e um número de sequência, valida (somente leitura), grava no registro e só então aplica. Comandos rejeitados também são registrados, o que deve ajudar nos relatórios de bugs. A semente mais o registro devem reproduzir uma partida exatamente. O formato do arquivo de replay ainda não foi escrito.
Números aleatórios
Escrevi eu mesmo os geradores de números aleatórios (SplitMix64 e xoshiro256**) e proibi o Random da biblioteca padrão no núcleo. O mundo tem quatro fluxos nomeados (comandos, necessidades, IA, eventos), derivados da semente e guardados no estado do mundo, de modo que são salvos e entram no hash. Para o trabalho em que a ordem não pode importar, há um gerador com chave e sem estado, baseado em domínio, entidade e tick. As saídas para a semente 42 estão fixadas, porque isso é, na prática, o formato do replay: mude o gerador e todo replay antigo quebra.
Um hash do mundo
Um pequeno hasher sensível à ordem junta a semente, o tick, os fluxos aleatórios e um resumo do registro de comandos em 64 bits. Duas execuções que deveriam ser idênticas podem ser comparadas com um único número. Por enquanto ele cobre só essas partes, porque o mundo não tem mais nada. Comparar hashes a cada N ticks durante uma partida é o próximo passo.
Transformando o não determinismo em erro de build
Não quis depender de lembrar das regras. O núcleo é compilado com analisadores de APIs proibidas: hora do relógio, números aleatórios sem semente, GUIDs aleatórios, hashes de string randomizados, threads e utilitários paralelos, SIMD e APIs de string que dependem da cultura quebram o build. Outras verificações garantem que não há estado estático mutável e varrem o código compilado atrás de qualquer coisa que tenha passado. Minha máquina usa a localidade russa, com vírgula decimal, o que já é um bom motivo para banir APIs que dependem da cultura.
Depois fui além e proibi no núcleo as coleções hash padrão (Dictionary, HashSet e companhia), já que a ordem de iteração delas é uma fonte clássica de divergência. O núcleo ganha arrays, listas e duas coleções próprias pequenas, com ordem documentada. Não é à prova de furos: uma API que devolve uma coleção hash por meio de uma interface genérica ainda pode passar, e parte da proteção só funciona nos testes.
Como é feito
As ferramentas em volta do código também são automatizadas, e os mesmos scripts rodam no Windows e no macOS. A abordagem geral está em Como é feito.
O que vem a seguir
Ainda em aberto neste marco: o modelo de mundo com andares, os arquivos de save e de replay, o teste de determinismo e um executor de cenários sem interface com uma linha de base de benchmark. Depois vem a visão de construção em Godot. Veja o roteiro.