Jogo multiplayer, em produção
O servidor decide, o navegador só desenha: uma simulação determinística que roda igual na máquina de produção e dentro do Chrome de quem está jogando sozinho.
O Fable Football é um futebol 2D em que cada pessoa controla um único atleta, não o time inteiro. Duas exigências puxavam em direções opostas: em jogo competitivo o cliente não pode ser autoridade sobre nada, e ao mesmo tempo o modo de treino precisava funcionar sem servidor algum. A saída foi tirar a regra do jogo dos dois lados e colocá-la num terceiro lugar.
- 60 Hz nos dois ladosSimulação e transmissão de estado
- nenhumaDependências de runtime no pacote da simulação
- 44, contadas pelo próprio laçoBaterias determinísticas no portão do deploy
- 154 verdes, 25 mutantes mortosVerificações da torre de duelos, com prova por mutação
- 62, sob demandaSondas que abrem o jogo num Chrome real
- de 93,8% a 74,1%, monotônicaVitória do jogador do nível 1 ao 11, medida
- 11, entre 1,50 m e 2,20 mAtributos derivados da estatura escolhida
Cliente que calcula é cliente que trapaceia
O jogo roda direto no navegador, sem instalação e sem download: o pilar de produto é chegar à diversão em menos de um minuto, e isso já elimina qualquer arquitetura que peça instalador, loja de aplicativo ou lobby lento. Cada pessoa entra em campo como um atleta: a corrida, o passe errado e o gol são dela, e não de onze bonecos comandados de cima.
A restrição que governa todo o resto veio junto com a promessa competitiva. Se o navegador decidir se a bola entrou, alguém vai reescrever essa decisão, e o problema real não é o trapaceiro isolado: é a comunidade que se desfaz quando ninguém acredita mais no placar. Num jogo em que o mérito é individual, placar sem confiança não vale nada.
Havia, porém, uma segunda exigência puxando para o lado contrário: o mesmo jogo precisava de um modo de treino que funcionasse sem servidor nenhum, com latência zero e sem custo de máquina ligada. Autoridade central de um lado, execução local do outro: as duas coisas ao mesmo tempo, e sem duas regras diferentes envelhecendo em paralelo.
Uma simulação, num pacote que não conhece a tela
A decisão estrutural foi tirar a regra do jogo tanto do servidor quanto do cliente e isolá-la num pacote próprio. São cerca de quinze mil linhas de TypeScript em catorze arquivos, e o pacote não declara uma única dependência de runtime: nem o Phaser, que desenha, nem o Colyseus, que cuida das salas em tempo real, entram ali dentro. Física, goleiro, impedimento e faltas não sabem que existe uma tela.
O servidor executa essa simulação a 60 Hz e transmite o estado no mesmo ritmo: o passo é um sessenta avos de segundo e a taxa de sincronização é exatamente esse valor, os 16,67 milissegundos que casam com o tique. O navegador manda intenção, nunca resultado: para onde quero correr, quanto tempo segurei o chute. Quem valida cada toque na bola é o processo do outro lado.
A física de voo da bola tem uma função só, e ela é a mesma nos dois lados: a mira que o cliente desenha enquanto você carrega o chute chama exatamente o código que o passo autoritativo vai executar. Não existe segunda cópia para divergir com o tempo, que é o modo clássico de um jogo em rede começar a mentir na tela: a linha desenhada prometendo uma curva que o servidor não faz.
O modo offline como prova, e o bug que ele expôs
O Treino roda essa mesma simulação inteiramente dentro do navegador, sem sala e sem rede: a conexão é nula de propósito, e o mesmo passo que o servidor executaria produz os mesmos eventos, os mesmos avisos e os mesmos sons. É a prova mais barata que existe de que a lógica tem uma fonte só, se houvesse duas, os dois modos divergiriam na primeira semana de mudanças na física.
A armadilha apareceu logo em seguida, e ela é a lição mais transferível deste projeto: as baterias determinísticas da simulação passavam verdes enquanto a cena estava quebrada. No treino o atleta ficava travado no centro do campo e a câmera não seguia a bola, porque três funções de mira e câmera do cliente ainda desistiam quando não havia sala, e no treino não haver sala é o desenho, não a falha.
O conserto foi tornar essas costuras cientes do modo offline, e a lição virou mecanismo. Passamos a ter sondas que abrem o jogo num Chrome de verdade, clicam nos botões, jogam e medem o que a bateria não enxerga: se o atleta anda, se a câmera segue, se o console acusou erro. Hoje são 62 delas, executadas sob demanda fora do portão do deploy, e cada classe de bug que escapa ganha a sua.
Determinismo deixa de ser curiosidade e vira portão
Como a simulação é determinística, a mesma partida com a mesma semente produz exatamente o mesmo resultado. Isso deixa de ser propriedade elegante e vira ferramenta: cenários gravados rodam a cada mudança e acusam desvio de um pixel, num tipo de software em que o bug clássico só aparece jogando.
São 44 baterias no portão do deploy, e esse número é contado pelo próprio laço, nunca escrito na mensagem final. A linha antiga trazia "41" cravado, e a regra da casa é que número citado envelhece calado: quem entrasse na lista sem editar a frase faria o log anunciar 41 medindo 42. A lista passou a ser a única fonte do total.
Antes de virar laço, o portão era um comando por linha. O primeiro vermelho abortava o bloco e as dezenas seguintes nunca rodavam, então cada envio revelava um problema em vez de todos. Hoje todas executam, cada falha imprime o fim do próprio log e o passo só falha no fim, com a lista completa do que quebrou.
A régua mais dura, porém, é a prova por mutação. O teste da torre de duelos tem 154 verificações e mata 25 mutantes: o harness quebra a regra de propósito, uma quebra por vez, e exige que alguma verificação fique vermelha. Teste que continua verde com a regra quebrada não é teste, é decoração, e só a mutação separa um do outro.
Dificuldade medida, não estimada
A torre de duelos é uma escada de rivais no modo 1 contra 1: quatro torres de oito adversários, e a torre seguinte só destranca depois de fechar a anterior. A dificuldade, no entanto, não é por torre nem por degrau: é torre mais degrau menos um, o que dá onze níveis e não trinta e dois. O total é derivado no código, nunca escrito: fixar "11" ali faria a tabela mentir no dia em que a grade mudasse de tamanho.
O alvo pedido foi uma curva, não uma sensação: 95% de vitória do jogador no primeiro nível, caindo dois pontos a cada degrau até 75% no último. Medimos com um painel fatorial de 36 políticas de jogador plausíveis, e a escada final entrega 93,8% no nível 1 e 74,1% no nível 11, monotônica nos dez pares e dentro de 1,3 ponto do alvo em toda a régua.
O que mantém esse número honesto é uma ponte entre o jogo e a medição: o harness carrega uma cópia da tabela e o teste confere as duas, campo a campo. Sem isso o "medido" começa a descrever um rival que ninguém joga, que é como todo balanceamento medido apodrece. E deixamos escrito o viés que não sai: a régua é um bot que conduz reto para o gol, enquanto um humano dribla, é ponto de partida de playtest, não a taxa do jogador real.
Uma escolha do jogador, onze números
Quem cria o atleta escolhe uma coisa só: a estatura, de 1,50 m a 2,20 m. Dela saem os onze atributos de forma contínua e determinística, sem distribuição livre de pontos, e a mesma função pura que alimenta a simulação alimenta o controle deslizante da tela: a régua que o jogador vê no cadastro é literalmente a que ele vai jogar.
A queixa que chegou do playtest era que o jogador alto não competia com os baixinhos, e a intuição apontava para falta de jogo de corpo. Medimos, e a intuição estava a um passo do lugar certo: o arco de proteção de bola já era do alto. O que não era dele era velocidade de condução, 84,0 px/s conduzindo, contra 166,3 px/s do menor apenas correndo. Em 48 dos 49 pares de estatura o portador não escapava do marcador nem correndo.
A correção foi um único ponto de ancoragem da régua, e o efeito é graduado por construção: mais 1,2% em 1,50 m, mais 6,8% em 1,85 m e mais 17,9% em 2,20 m, com a condução no topo subindo para 99,0 px/s. Medimos de novo numa matriz de cinco estaturas por cinco, com 108 partidas por célula, para confirmar o que a mudança não fez: o baixinho continua à frente em posse e em chutes. O alto deixou de ser inconduzível, não virou o mais forte.
O que se pode vender, e o que não tem preço
A monetização mudou de pilar por decisão de produto: o jogo assume hoje um pay-to-win leve e equilibrável, em que item pago pode dar vantagem de atributo. A segunda palavra é a que faz o trabalho (equilibrável) e ela não ficou no discurso: virou trava executável no catálogo, cobrada por código.
Vantagem em pontos fixos de atributo está proibida, e o motivo é um número medido: o mesmo "mais N" rende até 4,04 vezes mais para uma estatura do que para outra, então não existe teto de painel que o torne justo entre biotipos. Só a forma percentual passa, porque ela rende o mesmo para todo mundo por construção e o painel de administração regula o valor sem precisar de deploy.
E há uma categoria que continua sem preço nenhum: vantagem de percepção. Enxergar melhor para onde o adversário está olhando é informação, não força: não existe "menos 3% de enxergar" para cobrar em troca, e por isso ela não se equilibra com número algum. O portão mediu 32 direções em 5 regimes com o cabelo pintado na cor da pele, o pior caso possível, em duas execuções idênticas byte a byte; três cortes reprovaram e ficaram fora do comércio, com a geometria deles intacta dentro do jogo.
A trava é cobrada nas duas pontas: no validador de escrita, no servidor, e no de leitura, no cliente. O catálogo é dado editável pelo painel administrativo, e uma trava só na escrita seria contornável por qualquer linha que já estivesse no banco antes de a regra existir.
O jogo está no ar com partida online, treino offline e a torre de duelos, numa máquina em São Paulo que escala a zero quando ninguém está jogando e sobe uma máquina efêmera para aplicar migração antes de a nova versão receber tráfego. Uma instância só, de propósito: o estado das salas vive em memória, e duas máquinas colocariam jogadores em campos diferentes sem se verem. A regra do jogo, essa, continua tendo uma fonte só: provada ao mesmo tempo pelo servidor e pelo navegador.