Neste post
O Mewtopia Merge é um jogo de fundir gatinhos numa grade 5×6, com slots que destravam por nível, renda passiva que cresce exponencialmente, ganho offline com teto de duas horas, missões e recompensa diária com sequência de sete dias.
Descrito assim, parece um jogo. Descrito com honestidade de engenharia, é um conjunto de funções matemáticas com uma tela em cima.
Tratar dessa segunda forma mudou como o projeto foi construído — e encontrou um erro que a primeira forma esconderia por meses.
A separação
A regra foi simples e absoluta:
A lógica não sabe que existe tela. A tela não sabe como a lógica calcula.
Na prática, três camadas:
modelo/ Dart puro, sem import de Flutter
├─ economia.dart renda por segundo, custo de upgrade
├─ merge.dart regra de fusao, nivel resultante
└─ offline.dart ganho acumulado entre dois instantes
estado/ Riverpod, orquestra o modelo
apresentacao/ widgets e animacao
O detalhe que faz isso funcionar é a primeira linha da primeira camada: nenhum arquivo de modelo/ importa Flutter. Não é preferência de estilo — é uma restrição verificável. Se alguém importar, o teste do modelo passa a exigir ambiente de widget, e a propriedade se perde em silêncio.
Por que isso importa mais em idle que em outros gêneros
Num jogo de ação, o que decide a diversão é o feel — resposta ao toque, peso da animação, timing. Isso se avalia jogando, e teste automatizado ajuda pouco.
Num idle, o que decide tudo é a curva. Quanto rende cada nível, quanto custa o próximo, quanto tempo até o próximo marco. Essas são funções, e função se verifica com aritmética.
| Pergunta de design | Como responder |
|---|---|
| O nível 12 rende quanto por minuto? | chamar a função |
| Em quanto tempo o jogador chega no nível 20? | simular a curva |
| O ganho offline de 2 h supera 10 min ativo? | comparar duas chamadas |
| Um upgrade fica bom demais em algum ponto? | varrer a faixa inteira |
Nenhuma dessas exige abrir o jogo. Todas são impossíveis se a fórmula estiver dentro de um widget.
O relógio é um parâmetro, não uma dependência
Este é o ponto que mais rende e o que mais se erra.
A versão intuitiva:
int ganhoOffline() {
final agora = DateTime.now(); // <- aqui mora o problema
final decorrido = agora.difference(ultimaSaida);
...
}
Ela é impossível de testar sem esperar de verdade, ou sem mexer no relógio do sistema.
A versão testável muda uma linha:
int ganhoOffline(DateTime agora, DateTime ultimaSaida, ...) {
final decorrido = agora.difference(ultimaSaida);
...
}
Agora "duas horas depois" é um argumento. Testar o teto, testar o retorno de sete dias, testar o caso de relógio do aparelho andando para trás — tudo vira chamada de função.
Toda função que consulta o relógio por dentro é uma função que você não consegue testar. Vale para jogo, para assinatura, para promoção com prazo e para qualquer coisa que dependa de tempo. O relógio entra por parâmetro.
O erro que apareceu por causa disso
Com a economia isolada, escrevemos um teste que fazia uma coisa que ninguém faz jogando: somar o rendimento passo a passo por dez mil ciclos e comparar com o valor calculado direto pela fórmula fechada.
Os dois números não bateram.
A causa era arredondamento. O rendimento por ciclo era double, e o acumulado também. Somar milhares de valores fracionários em ponto flutuante acumula um erro que, individualmente, é invisível — e que ao longo de uma sessão longa produzia divergência entre o que a tela mostrava e o que o estado guardava.
A correção:
- acumulado em inteiro, sempre — a menor unidade de moeda é a unidade
doublesó para taxa e multiplicador- arredondamento numa direção só, documentada, aplicado num ponto só
Esse erro não apareceria em teste de interface, não apareceria jogando dez minutos, e apareceria como reclamação de jogador semanas depois — no formato mais difícil de investigar que existe: "meu número tá errado".
Bug de arredondamento em economia de jogo não é bug de matemática. É bug de confiança, e ele chega até você como um jogador irritado, não como um erro no log.
Ganho offline com teto: decisão de economia, não de código
Vale explicar o teto de duas horas, porque parece arbitrário e não é.
Sem teto, quem volta depois de uma semana recebe um valor que torna todo o progresso anterior irrelevante. O jogo se resolve sozinho, e quem joga todo dia é efetivamente punido por jogar.
Com teto baixo demais, o retorno deixa de recompensar e o jogador não volta.
O teto é o botão que equilibra os dois, e é o tipo de número que precisa ser ajustável sem publicar versão nova — o que leva direto à conclusão do post sobre economia no servidor: número de balanceamento no cliente é número que você não pode corrigir.
O que a separação custou
Honestamente: pouco, mas não zero.
Mais arquivos e mais indireção. Uma alteração simples de fórmula toca duas camadas em vez de uma. Para quem está começando o projeto, parece burocracia.
Tentação de furar. A hora em que a lógica precisa de um dado que só a tela tem é a hora em que alguém importa Flutter no modelo "só dessa vez". A defesa é tratar isso como erro de compilação, e não como convenção.
Duplicação aparente. Às vezes o estado precisa de uma versão do dado formatada para exibição, e ela parece repetição do modelo. Não é — é tradução, e ela pertence à camada de apresentação.
Resumo
- Em idle, o produto é a curva. Curva é função, e função se verifica com
aritmética.
- Nenhum arquivo do modelo importa Flutter. A restrição precisa ser verificável,
não combinada.
- Relógio entra por parâmetro. Toda função que chama o relógio por dentro é
intestável.
- Acumulado em inteiro;
doublesó para taxa. Soma repetida de ponto flutuante
acumula erro invisível.
- Simular milhares de ciclos e comparar com a fórmula fechada encontra o que
jogar não encontra.
- Teto de ganho offline é decisão de economia, e precisa ser ajustável sem
publicar versão.
Perguntas frequentes
Como testar ganho offline sem esperar duas horas?
Injetando o relógio. Se a função recebe o instante atual como parâmetro em vez de chamar o relógio do sistema por dentro, testar duas horas é passar um número diferente. Toda função que consulta o relógio internamente é uma função que você não consegue testar.
Por que teto no ganho offline?
Sem teto, quem volta depois de uma semana recebe tanto que o jogo perde sentido — e quem joga todo dia é punido por jogar. O teto mantém o retorno diário valendo a pena. É decisão de economia, não técnica.
double ou inteiro para moeda de jogo?
Inteiro para o valor acumulado, sempre. Ponto flutuante acumula erro em soma repetida, e num idle a soma acontece milhares de vezes. Use double só para taxa e multiplicador, e arredonde numa direção só, documentada.
Vale escrever teste para jogo casual?
Para a interface, raramente compensa. Para a economia, sempre — é a parte que, quando erra, ou quebra o balanceamento ou tira dinheiro de alguém. E é a parte mais barata de testar, porque é função pura.