Plano passo a passo para levar o bot do estado atual (/ping, /register,
/carteira, /trabalhar, /diario, /transferir, /extrato com saldo
único em CDC) até a economia completa descrita em Regras funcionais
consolidadas (doravante "regras").
Stack mantida: Bun + TypeScript + discord.js 14 + SQLite (bun:sqlite) +
Drizzle. Toda regra de economia continua pura em src/shared-domain, e toda
mudança de saldo continua passando por src/database.
| # | Milestone | Depende de | Resultado visível no Discord |
|---|---|---|---|
| M0 | Harness e decisões de transição | — | Nenhum (docs) |
| M1 | Fundação: tempo, parâmetros, RNG, admin, scheduler | M0 | /admin parametro |
| M2 | Livro-razão de partidas dobradas | M1 | /extrato no novo modelo |
| M3 | Conta e banco | M2 | /cadastrar, /perfil, /depositar, /sacar, /pix |
| M4 | Habilidades | M3 | Barras no /perfil |
| M5 | Trabalho e quiz | M4 | /trabalhar, /quiz, admin de perguntas |
| M6 | Notícias e eventos | M1, M5 | Notícia do dia, /admin evento |
| M7 | Crime, roubo e risco | M4, M6 | /crime, /roubar |
| M8 | Prisão e fiança | M7 | /fianca, /trabalhar voluntario |
| M9 | Investimentos | M3, M6 | /investir, /investimentos |
| M10 | Cassino | M4, M6 | Roleta, blackjack, caça-níqueis, corrida |
| M11 | Temporadas, wipe, ranking e cofre | M2–M10 | /ranking, /admin temporada |
| M12 | Balanceamento e hardening | Todos | Valores finais, reconciliação |
| M13 | Release | M12 | Bot em produção |
M6 pode começar em paralelo a M3–M5, já que só depende do resolver de parâmetros de M1. M9 e M10 são independentes entre si.
| # | Milestone | Estado |
|---|---|---|
| M0–M12 | Harness, fundação, ledger, conta e banco, habilidades, trabalho e quiz, notícias e eventos, crime, prisão, investimentos, cassino, temporadas, balanceamento e hardening | ✅ Implementados e cobertos por bun test (reconciliação limpa) |
| M13 | Release | ⏳ Runbook pronto em docs/OPERATIONS.md; faltam as ações no ambiente real (produção, canal de anúncios, início oficial da temporada 1) |
Decisões tomadas onde o plano deixava em aberto (detalhes em docs/RULES.md):
- Moeda CD$;
/register→/cadastrar;/carteira→/perfil;/transferir→/pix;/diarioremovido. - Banco recriado do zero (sem migração de dados); migrations antigas apagadas.
/perfilpróprio é efêmero (sujo e risco); o de outros é público.- Roubo atinge apenas as mãos da vítima; ela é notificada por menção na resposta pública.
- Escala de notícias: cada ponto de peso = ±10% sobre o padrão; 2 notícias por dia.
- Quiz sem resposta no prazo = erro; sessão interrompida por restart é encerrada sem pagar o restante.
- XP de Investidor no vencimento; XP de Sorte em toda aposta resolvida.
/trabalhar voluntario:trueé uma opção do/trabalhar(o Discord não mistura subcomandos e uso direto).- No
/admin,quiz-categoriaequiz-perguntasão grupos separados (o Discord limita a dois níveis de subcomando). - Roleta e blackjack seguem as regras clássicas; o piso de 3% vale para os jogos em que a Sorte atua (caça-níqueis e corrida).
-
bun run checkebun testpassando. - Regras puras em
src/shared-domain/<área>com testes unitários usando relógio e RNG injetados. - Persistência em
src/database/<área>.ts, testada em SQLite:memory:com as migrations reais. - Nenhum
awaitdentro de transação SQLite; nenhuma chamada ao Discord dentro de transação. - Toda movimentação de dinheiro passa pelo
postTransaction(a partir de M2). - Smoke test manual na guild de desenvolvimento. Parcial: o startup, o registro dos 21 comandos e o login foram validados com um banco temporário; os fluxos interativos ainda precisam ser exercitados no Discord.
-
docs/ARCHITECTURE.md,docs/DEVELOPMENT.mdeCLAUDE.mdatualizados quando contratos mudarem.
Objetivo: alinhar documentação e agentes às regras consolidadas antes de escrever código.
| Hoje | Regras | Decisão sugerida |
|---|---|---|
| Moeda CDC (Codify Coins) | CD$ (Codify Money) | Adotar CD$ e renomear formatCdc → formatMoney |
/register |
/cadastrar |
Renomear; manter upsertUser como sincronização de identidade |
/carteira |
/perfil |
Substituir por /perfil |
/transferir |
/pix (banco → banco, cooldown 10s) |
Substituir por /pix |
/diario |
Não existe nas regras | Remover, ou incluir nas regras como fonte de emissão |
Saldo único em wallets.balance |
4 contas por usuário + ledger | Migrar em M2 |
Cooldowns em colunas de wallets |
Cooldowns configuráveis por admin, notícia e evento | Tabela genérica cooldowns em M1 |
- Exportar as regras consolidadas para
docs/RULES.md(fonte única de verdade funcional). - Manter este arquivo (
docs/MILESTONE.md) com o progresso. - Reescrever a seção "Economia" de
docs/ARCHITECTURE.mdcomo visão-alvo: módulos por domínio, ledger, scheduler, resolver de parâmetros. - Atualizar
CLAUDE.md:- Ler
docs/RULES.mdantes de qualquer mudança de economia. - Corrigir a menção desatualizada a "modelo único em
users.ts". - Regra: dinheiro só se move via
postTransaction; nuncaUPDATEde saldo direto. - Regra: relógio e RNG sempre injetados; proibido
Date.now()/Math.random()no domínio. - Regra: valores monetários e XP sempre inteiros (centavos / milésimos).
- Ler
- Definir convenção de pastas por domínio:
src/shared-domain/{money,ledger,time,params,skills,crime,prison,investments,casino,news,season}.
Aceite: docs revisados; nenhuma divergência entre RULES.md e o plano.
Objetivo: construir os serviços transversais de que todos os módulos dependem.
- Relógio injetável (
Clock), com implementação real e fake para testes. - RNG injetável (
Rng), comint(min,max),float,chance(p)epick(weights); implementação com semente para testes. - Dia do bot (
shared-domain/time):- Janelas fixas a partir de uma âncora, com duração configurável.
-
windowAt(instant),nextTurnAt(instant),windowIndex. - Mudança de duração só vale na próxima virada (guardar duração pendente + vigência).
- Parâmetros (
shared-domain/params):- Catálogo tipado de todos os parâmetros, com default, mínimo e máximo (pagamentos, cooldowns, chances, tetos, prazos, taxas).
- Tabela
parameter_overrides(admin) no banco. - Resolver:
default → notícia do dia → evento mais recente, com o admin sempre prevalecendo. Notícias e eventos entram em M6; aqui o resolver já aceita as camadas.
- Cooldowns genéricos: tabela
cooldowns(discord_id, action, available_at)e função pura de checagem (as colunaslast_work_at/last_daily_atdeixaram de existir). - Admin: helper
isAdmin(interaction)usandointeraction.memberPermissions(Administrador ou Gerenciar Servidor). Comandos admin comsetDefaultMemberPermissions. Não exige novo intent. - Scheduler persistente:
- Tabela
scheduled_jobs(id, kind, due_at, payload, idempotency_key UNIQUE, done_at). - Varredura periódica (ex.: a cada 15s) + varredura no startup para recuperar o que venceu com o bot offline.
- Handler por
kind, executado em transação;done_atgravado na mesma transação do efeito. - Job recorrente de virada do dia do bot.
- Tabela
-
/admin parametro ver|definir|limpar. - Log estruturado de toda alteração administrativa (tabela
admin_audit).
Aceite: testes de janela com durações de 6h e 2h, troca de duração no meio da janela e job vencido durante o "offline" sendo executado uma única vez.
Objetivo: substituir wallets.balance + transactions pelo modelo de contas.
-
seasons(id, starts_at, ends_at, status)— criar a temporada 1 já aqui. -
accounts(id, season_id, owner_kind, owner_id, kind, currency, balance, frozen)- Usuário:
limpo_maos,limpo_banco,limpo_investido,sujo_maos. - Sistema:
cofre,emissao,cassino,custodia_corrida,conversao. CHECK (balance >= 0)para contas de usuário;emissaoeconversaoisentas.
- Usuário:
-
ledger_transactions(id, idempotency_key UNIQUE, kind, actor_id, metadata_json, created_at). -
ledger_entries(id, transaction_id, account_id, amount)— amount com sinal.
-
postTransaction({ key, kind, entries })emsrc/database/ledger.ts:- Valida que a soma por moeda é zero (limpo e sujo separados).
- Idempotente: chave repetida devolve a transação existente sem efeito.
- Atualiza
accounts.balancena mesma transação SQLite. - Recusa conta
frozenpara débitos de usuário (prisão).
- Construtores puros de lançamentos por caso de uso (
depositEntries,bailEntries, …) emshared-domain/ledger, todos testados. -
reconcile(): soma de lançamentos = 0 por moeda; saldo em cache = soma dos lançamentos por conta. -
Migration de dados do saldo de— dispensada: o banco foi recriado do zero por decisão explícita (sem dados a migrar).wallets - Remover
walletse a tabelatransactionsantiga (removidas; schema novo em uma única migrationinit). - Adaptar
/extratopara lerledger_entries.
Nota SQLite: não existe
SELECT … FOR UPDATE. Combun:sqlitesíncrono e um único processo,db.transaction()(de preferênciaBEGIN IMMEDIATE) já serializa as escritas. A trava por linha das regras só passa a ser necessária se um dia migrar para Postgres.
Aceite: testes de concorrência simulada (duas transações com a mesma
chave), saldo negativo recusado pelo banco e reconcile() limpo após toda a
suíte.
-
/cadastrar: cria usuário, as 4 contas da temporada ativa, barras em 0 e estado criminal zerado. Idempotente. - Guard
requireAccount: todo comando de economia exige cadastro. -
/perfil [@usuario]: saldos, barras, investimentos, risco e prisão. Decidir o que é público e o que é efêmero (sugestão: sujo e risco visíveis só para o próprio usuário). -
/depositar valore/sacar valor(aceitartudo). -
/pix @usuario valor: banco → banco, cooldown de 10s, sem taxa nem limite, recusa bot, auto-transferência e destino sem conta. O preso pode receber, mas não enviar. - Parser de valores
"10,50"→ 1050 centavos, com testes de borda.
Aceite: fluxo completo com dois usuários de teste; /extrato mostra
cada operação.
- Tabela
skills(discord_id, season_id, skill, xp_milli)comCHECK (xp_milli BETWEEN 0 AND 100000). - Enum:
inteligencia,experiencia_profissional,criminalidade,sorte,investidor. - Funções puras
gainXp,loseXp(com clamp em 0 e 100.000) eskillFactor(xp), que mapeia a barra para o multiplicador de cada efeito (curva definida em M12). - Barras e percentuais no
/perfil. - Histórico opcional de XP (
skill_events) para auditoria.
Aceite: testes de clamp e de arredondamento em milésimos.
- Reescrever no ledger:
emissao → limpo_banco. - Faixa vinda do resolver de parâmetros; multiplicador da Experiência profissional; ganho de XP.
- Reduzir Criminalidade em 0,01% do XP atual.
- Decidir o destino de
/diario(M0).
- Tabelas
quiz_categories,quiz_questions(4 alternativas, 1 correta,published). -
/admin quiz categoria criar|editar|publicar|despublicar|remover. -
/admin quiz pergunta …, com modal do Discord para enunciado e alternativas. -
/quiz categoria quantidade:- Sessão com botões; 10s por pergunta a partir do envio.
- Sem resposta no prazo = erro (confirmar nas regras).
- Acerto: dinheiro (
emissao → limpo_banco) + XP de Inteligência. Erro: penalidade de XP. - Pagamento por pergunta com chave
quiz:{sessão}:{pergunta}. - Sessão persistida (
quiz_sessions) para não pagar duas vezes após restart; sessão interrompida é encerrada sem pagar o restante. - Embaralhar alternativas; não repetir pergunta na mesma sessão.
- Cooldown configurável e limite de perguntas por sessão.
Aceite: quiz completo de 5 perguntas com acerto, erro e timeout; reiniciar o bot no meio não gera pagamento duplicado.
- Tabela
news_templates(id, text, parameter, direction, weight, active). - Definir a escala de
weight(ex.: cada ponto = ±X% sobre o default) e quantas notícias são sorteadas por dia. - Job de virada do dia: sorteia notícias, grava
daily_news(window_index, …)com chave por janela e publica um embed no canal configurado. -
/admin noticia criar|editar|ativar|desativar|listar. -
/noticias: mostra as notícias vigentes. - Tabela
events(id, parameter, value, starts_at, ends_at, created_by). -
/admin evento criar|encerrar|listar. - Ligar as camadas ao resolver de M1 e garantir que cada valor final respeite o mínimo e o máximo do catálogo.
- Configuração do canal de anúncios (
/admin canal noticias).
Aceite: com notícia reduzindo o trabalho e evento fixando o pagamento, o evento vence nesse parâmetro e a notícia continua valendo nos outros.
- Tabela
criminal_state(discord_id, season_id, risk_milli, last_attempt_window). - Domínio puro:
-
arrestChance = min(criminalidade + risco, teto). - Ordem de cada tentativa: sortear prisão → resolver sucesso/falha → aumentar o risco (sucesso e falha igualmente).
- Decaimento na virada: sem tentativa na janela →
risco × 0,85. - XP: sucesso ganha; falha perde 5% do XP que ganharia.
-
-
/crime: sucesso =emissao (sujo) → sujo_maos. -
/roubar @usuario:- Cooldown (admin > notícia).
- Percentual sorteado separadamente para limpo e sujo da vítima (1% até o teto da notícia).
- Limpo roubado passa por
conversaoe chega sujo ao ladrão. - Recusar vítima sem conta, presa, bot ou o próprio usuário.
- Notificar a vítima (DM ou menção) — decidir.
- Job de decaimento ligado à virada do dia.
Aceite: simulação com seed: 1.000 tentativas e chance de prisão crescendo conforme esperado; decaimento correto após uma janela sem crime.
- Tabela
prison_terms(id, discord_id, season_id, starts_at, ends_at, base_value, bail_value, victim_id, status). - Ao prender, numa única transação:
- Confisco:
sujo_maos → conversao → cofre(limpo). -
limpo_maos.frozen = true. - Risco zerado.
- Pena sorteada: mínimo 40% do dia; máximo
min(4h, dia). - Agendar job
prison:release:{id}.
- Confisco:
- Fiança:
max(valor, 100,00) × 5; em tentativa falha, o "valor" é o sorteio do que teria ganho. -
/fianca: debitalimpo_maose depoislimpo_banco(débito permitido apesar do congelamento); 20% para a vítima e 80% para o cofre (100% no/crime). -
/trabalhar voluntario: −1% a 3% do tempo restante; cooldown de 20% do dia; reagenda o job de soltura. - Guard global
requireNotJailed: bloqueia todos os comandos, exceto/fianca,/trabalhar voluntarioe/perfil. - Soltura: descongela, marca o termo como
served, notifica o usuário.
Aceite: prisão durante o "offline" do bot termina corretamente; pagar fiança e o job de soltura disparando no mesmo instante não liberam duas vezes nem cobram duas vezes.
- Tabela
investments(id, discord_id, season_id, principal, rate_bp, term, starts_at, matures_at, status). -
/investir valor prazo(curto/médio/longo):- Mínimo CD$ 10,00; teto total
investido ≤ 50% × (banco + investido). - Taxa sorteada na faixa da notícia vigente; Investidor puxa para o topo, sem ultrapassá-lo; taxa congelada na aplicação.
- Prazo em dias do bot; lançamento
limpo_banco → limpo_investido. - Agendar
investment:mature:{id}.
- Mínimo CD$ 10,00; teto total
- Vencimento:
limpo_investido → limpo_banco(principal) +emissao → limpo_banco(rendimento), creditado mesmo com o usuário preso. -
/investimentos: lista os ativos com data de vencimento (formatDiscordTimestamp). - Ganho de XP de Investidor ao aplicar ou ao vencer — decidir.
Aceite: vencimento com o bot offline pago uma única vez; teto de 50% validado com várias aplicações seguidas.
-
placeBet({ limpo, sujo }): debita das duas contas paracassino. -
payout(valor):cassino (limpo) → limpo_maos— lavagem total. - Sujo perdido:
cassino (sujo) → conversao → cofre. - Apostas mínima e máxima por jogo (parâmetros).
- Sorte afeta apenas caça-níqueis e corrida, com piso de vantagem da casa de 3%.
- Ganho de XP de Sorte (definir quando: por aposta ou por vitória).
- Roleta (europeia, pagamentos clássicos).
- Blackjack (pagamentos clássicos, sessão com botões pedir/parar/dobrar, sessão persistida e timeout que encerra a mão).
- Caça-níqueis (tabela de símbolos e pagamentos configurável, com vantagem da casa calculada).
- Corrida de cavalos:
-
/cavalos valor cavaloabre o lobby;/cavalos entrar valor cavalo. - Apostas vão para
custodia_corridana entrada. -
/cavalos startpelo criador ou largada automática em 20s (job agendado). - Chances por cavalo com base nos participantes e na notícia.
- Taxa de 10% para o cofre → vencedores recebem aposta + 20% do pote → excedente rateado proporcionalmente → sobra para o cofre.
- Animação simples por edição de mensagem (fora da transação).
-
- Script de simulação Monte Carlo por jogo para validar o retorno esperado (RTP) e o piso de 3%.
Aceite: simulação de 1 milhão de rodadas mostra vantagem da casa ≥ 3% nos jogos com Sorte máxima; corrida com 1, 3 e 6 apostadores fecha os lançamentos em zero.
-
/admin temporada iniciar duracao [evento],encerrarestatus. - Duração default de 2 semanas; job de fim de temporada.
- Wipe seletivo (dinheiro / habilidades / risco e prisões / quiz stats):
- Dinheiro: novas contas na nova temporada; investimentos ativos encerrados sem pagamento.
- Habilidades: copiar ou não as barras para a nova temporada.
- Histórico da temporada anterior preservado (nada de
DELETE).
-
/ranking: top 10 porlimpo_maos + limpo_bancoda temporada ativa. -
/admin cofre saldoe/admin cofre transferir @usuario valor(premiação manual entre temporadas), com auditoria.
Aceite: wipe com prisões e investimentos ativos; o ranking da temporada anterior continua consultável.
- Planilha/arquivo de parâmetros com valores iniciais para tudo que
ficou sem número:
- XP por ação e penalidades; curva
skillFactor. - Faixas de trabalho, quiz e crime.
- Teto de chance de prisão e incremento de risco por tentativa.
- Teto default do percentual roubado.
- Cooldowns default de trabalho, quiz, crime e roubo.
- Duração default do dia do bot.
- Escala de peso das notícias e lista inicial de notícias.
- XP por ação e penalidades; curva
- Simulador econômico (script Bun): N jogadores com perfis distintos
ao longo de uma temporada; medir emissão, inflação (
emissao), crescimento do cofre e dispersão do ranking. - Job periódico de
reconcile()com alerta no log. - [~] Backup do arquivo SQLite: removido por decisão (sem backups automáticos).
- Rate limit global por usuário contra spam de interações.
- Revisão de mensagens de erro: nunca vazar estado interno.
- Teste de carga leve: comandos concorrentes do mesmo usuário.
Aceite: simulação de uma temporada sem saldos explosivos; reconcile()
limpo após teste de carga.
- Ambiente de produção (VPS ou container) com volume persistente para o SQLite.
- Registro de comandos: avaliar continuar por guild (rápido) — o bot é single-guild, então manter.
- Runbook em
docs/OPERATIONS.md: deploy, rollback, rodar migration, iniciar e encerrar temporada. - Canal de anúncios e permissões do bot configurados no servidor Codify.
- Temporada 1 oficial iniciada; acompanhamento dos primeiros dias com
ajustes via
/admin parametro, sem deploy.