Skip to content

Repository files navigation

destermo logo

Destermo

O palpite ótimo do Termo.

Solver de Termo escrito em Python. Você joga, diz a ele as cores que o jogo devolveu, e ele responde qual palavra jogar em seguida.

O solver nunca conhece a palavra secreta. A cada rodada ele guarda as palavras do léxico ainda compatíveis com todo o feedback recebido e escolhe a jogada que minimiza quantas tentativas ainda faltam. No benchmark isso vence todas as partidas, com média de 2,85 tentativas no nível 3 e 3,58 no nível 2.

O repositório tem duas frentes. Uma ferramenta, que sugere a próxima palavra enquanto se joga: um app web (app.py) e uma CLI (solver.py). E uma análise, que mede em tentativas o quanto cada ideia do algoritmo vale de fato (benchmark.py e os resultados). A divisão sai da especificação v1.1; a v1.0 fica no histórico porque os resultados das duas são comparados aqui embaixo.


Como o solver pensa

Esta seção é a espinha do projeto: três níveis de algoritmo, cada um corrigindo um erro do anterior. Tudo que vem depois (código, benchmark, resultados) é consequência dela.

O estado do jogo é um conjunto

O Termo dá 6 tentativas para achar uma palavra de 5 letras. Cada tentativa volta com 5 cores: 🟩 letra certa no lugar certo, 🟨 letra existe fora do lugar, ⬛ letra não existe.

Toda a informação de uma partida cabe num conjunto:

C = as palavras do léxico que produziriam exatamente o feedback visto até agora.

C começa com as 6.046 palavras do léxico e só encolhe. Manter C é a parte fácil, e não é onde os solvers erram: basta reusar a mesma função de feedback do jogo e ficar com quem bate (termo/feedback.py).

A pergunta difícil é a outra: qual palavra jogar agora? Os três níveis são três respostas para ela.

Nível 1 - frequência de letras

"Jogue a palavra com as letras mais comuns entre as candidatas."

É a heurística intuitiva e é o que a maioria dos solvers de Termo por aí faz. No benchmark ela resolve 92,9% das partidas em 4,17 tentativas: melhor que chutar ao acaso, e pouco mais que isso.

O furo é que letra comum não é informação. Uma palavra pode ter cinco letras frequentíssimas e ainda assim deixar 300 candidatas de pé, se todas elas responderem com o mesmo padrão de cores. A heurística otimiza a palavra jogada; o que importa é o que ela faz com o conjunto.

Nível 2 - entropia

O que interessa é como a jogada divide C. Uma tentativa g particiona o conjunto em até 243 grupos (3⁵ padrões de cores possíveis): cada candidata cai no grupo do padrão que ela devolveria. Se um grupo concentra quase tudo, a jogada quase não informa; se os grupos saem pequenos e parecidos entre si, ela informa muito.

Isso é exatamente a entropia de Shannon da partição:

p(padrão) = massa das candidatas que responderiam esse padrão / massa total
H(g)      = -Σ p · log₂ p                                          [bits]

Joga-se o g de maior H. Duas sutilezas que valem tentativas:

  • O espaço de jogadas é o léxico inteiro, não só C. Vale "queimar" uma tentativa numa palavra que não pode ser a resposta, se ela separar melhor.
  • H mede a partição inteira, então g é avaliado contra todas as candidatas de uma vez. Fazer isso 6.046 vezes por jogada é o que exige a matriz de padrões pré-computada (termo/matriz.py).

O prior: nem toda candidata é igualmente provável

O Termo sorteia festa, não leruê. As duas estão no léxico; só uma vai cair de verdade. O arquivo icf do fserb/pt-br dá o Inverse Corpus Frequency de cada palavra (score baixo = palavra comum), e ele vira um peso:

prior(w) = softmax(-ICF(w) / T)

T é um dial contínuo entre os dois primeiros níveis:

T o que acontece
T → 0 corte quase binário: só as palavras mais comuns têm peso
T = 1 padrão do projeto
T → ∞ pesos iguais: recupera a entropia pura, sem prior

O prior entra pesando as candidatas dentro de H; ele nunca restringe quais jogadas testar. Medido no benchmark, vale 0,31 tentativa.

Nível 3 - o objetivo real

Aqui está o pulo do gato, e é a correção que o 3Blue1Brown publicou depois da lição original: bits são um proxy, não o objetivo. O jogo não premia informação, premia acabar cedo, e as duas coisas divergem. A jogada que mais separa costuma ser uma palavra que não pode ser a resposta, ou seja, uma que garantidamente não encerra a partida naquela rodada.

O nível 3 otimiza o número esperado de tentativas, diretamente:

V(S, r) = min_g [ 1 + Σ_{padrão ≠ GGGGG} P(padrão | S) · V(S_padrão, r-1) ]
V(S, 0) = 7        acabaram as rodadas sem acertar

Em voz alta: jogar g custa 1 tentativa; nos casos em que não acerto, herdo o sub-conjunto correspondente e sigo jogando bem a partir dali.

O r (rodadas restantes) não é decoração. Sem ele o solver otimiza um jogo de tentativas ilimitadas e cai na espiral clássica do Termo, prima → urina → brida → crica → criva: cinco candidatas prováveis que se distinguem por uma letra só. Cada jogada dessas maximiza a chance de acertar agora, o valor esperado adora isso, e a partida acaba na 7ª. Com o limite na recursão a busca enxerga a parede e gasta uma jogada separando o grupo.

Tomada de frente, a recursão é intratável. Três coisas a domam, e a primeira é a mais bonita:

  • beam: só os K palpites de maior entropia entram na busca em cada nó. Ou seja: o nível 2 vira o move ordering do nível 3. O proxy não era inútil; o erro era tomá-lo como resposta final.
  • profundidade: abaixo de P níveis a recursão cai na política gulosa do nível 2. Como uma política concreta é sempre um limite superior de V, o valor só melhora quando P cresce: a busca é anytime, e P=0 com beam=1 reproduz o próprio nível 2.
  • memoização + poda: os mesmos estados reaparecem muito, e a soma parcial só cresce, então um palpite é abandonado no meio assim que passa do melhor custo já encontrado no nó.

Os três lado a lado

nível escolhe por 1.500 palavras 300 palavras
1 - freq. de letras letras mais comuns em C 4,171
2 - entropia maior H(g), com prior 3,581 3,397
3 - tentativas esperadas menor V(S, r) 2,853

As duas colunas são baterias diferentes e não se comparam entre si; compare só dentro de uma coluna. O nível 3 roda numa bateria menor porque custa ~47× mais CPU por partida; o head-to-head honesto é a coluna de 300, com as mesmas secretas para os dois.

O nível 3 é o padrão da CLI. O nível 2 continua sendo o padrão do benchmark, onde são 1.500 partidas por estratégia e o custo pesa.


Instalação

Python 3.10+ com numpy. Para o app web, streamlit. Para os gráficos, matplotlib. Para os testes, pytest.

pip install -r requirements.txt

Nada em data/ é versionado, com uma exceção deliberada (as aberturas em cache): as fontes são baixadas do fserb/pt-br na primeira execução e a matriz de padrões se reconstrói em ~4 s. Basta clonar e rodar.

Uso

No navegador

streamlit run app.py

O tabuleiro é a entrada. A palavra sugerida já aparece em fantasma na linha ativa, e cada casa é um botão que cicla pelas cores do jogo a cada clique: ausente, fora de lugar, no lugar. Marque o que o Termo devolveu e aperte Registrar rodada; a linha sobe para o tabuleiro e sai a sugestão seguinte. Para jogar outra palavra que não a sugerida, digite-a no campo abaixo do tabuleiro. Na barra lateral ficam o nível, a temperatura do prior e o espaço ampliado, todos trocáveis no meio da partida.

A primeira palavra é sempre tarso, em qualquer combinação da barra lateral (veja Opções) — mexer no nível ou no T muda o que vem da segunda jogada em diante, não a abertura. É também por isso que o app responde na hora em qualquer configuração: a abertura fixa não passa pela busca na árvore de ~9 min que a ótima do nível 3 exige fora das configurações versionadas. Essa continua no terminal.

Na linha de comando

python solver.py
Solver de Termo — nível 3: menor nº esperado de tentativas
Digite '?' a qualquer momento para ver os comandos.
léxico: 6046 palavras   T=1.0   beam=10 profundidade=1   abertura=padrao

Calculando a melhor abertura...
  melhor abertura: tarso   (5.97 bits, E=3.02 tentativas, é candidata)
  motivo: melhor abertura - robusto a palavras comuns e raras
  alternativas: tosar*, sertã*, terso*, tória*, tirão*   (* = também é candidata)

[1] tentativa > tarso
[?] feedback de 'tarso' > BBBBB

  candidatas restantes: 223
  sugestão: filme   (3.53 bits, E=1.89 tentativas, é candidata)
  motivo: menor nº esperado de tentativas (E=1.886) entre os 11 melhores palpites
          por entropia (223 candidatas, 5 rodadas, profundidade 1)
  alternativas: cline*, linde*, lince*   (* = também é candidata)

[2] tentativa > filme
[?] feedback de 'filme' > BBBYY

  candidatas restantes: 12
  sugestão: nuvem   (1.37 bits, E=1.41 tentativas, é candidata)
  motivo: menor nº esperado de tentativas (E=1.414) entre os 11 melhores palpites
          por entropia (12 candidatas, 4 rodadas, profundidade 1)
  alternativas: bunda*, bundo*, jundu*   (* = também é candidata)

[3] tentativa > nuvem
[?] feedback de 'nuvem' > GGGGG

  acertou 'nuvem' em 3 tentativa(s). Fim.

6.046 → 223 → 12. Repare no E caindo junto: 3,02 → 1,89 → 1,41 tentativas ainda esperadas. É o número que o nível 3 minimiza, mostrado a cada jogada.

Digite as tentativas sem acento: é o que o jogador faz no term.ooo, que preenche os acentos sozinho. Entrada acentuada também é aceita e normalizada. As sugestões saem na forma acentuada, que é a que aparece na tela do jogo.

O feedback aceita G/Y/B (maiúsculo ou minúsculo), 2/1/0 ou os emojis 🟩🟨⬛. Comandos: voltar desfaz a última rodada, listar mostra as candidatas, ? mostra a ajuda, sair encerra.

Opções

O padrão é o nível 3, com a abertura já em cache no repositório e décimos de segundo por jogada. Para o nível 2 puro (entropia, milissegundos por jogada):

python solver.py --nivel 2

A abertura é sempre tarso, nos dois níveis e em qualquer temperatura. Não é o ótimo de nenhum dos dois critérios (o do nível 3 sob T=1 é tosar; o do nível 2 muda com o T, indo de mesto a cairo), e é essa a razão: tarso é a de maior entropia sob o prior do projeto e empata em primeiro no minimax de arrependimento sobre sete distribuições de secreta, da lista bem curada à uniforme (Robustez da abertura). As ótimas de cada regime vencem no seu e pagam no outro; essa não tem regime ruim. Como a primeira jogada inteira vale ≤ 0,07 tentativa, a estabilidade sai mais barata que o ótimo local — e é uma palavra a menos para decorar quando se mexe no T.

Para abrir pelo ótimo do critério do nível escolhido, que é o que a análise deste README tabula:

python solver.py --abertura otima

Da segunda jogada em diante os dois caminhos são idênticos.

Outras temperaturas do prior (inf desliga o prior e recupera a entropia pura):

python solver.py --t 2
python solver.py --t inf

Para deixar o solver sondar com palavras que o Termo aceita mas nunca sorteia (conjugações verbais: 8.629 palavras jogáveis contra as mesmas 6.046 respostas possíveis):

python solver.py --ampliado

Na primeira vez ele baixa o arquivo conjugações da fonte e monta uma matriz de padrões própria (~10 s). Não espere ganho: a medição está abaixo e deu zero no nível 2; no nível 3 dá negativo, então lá a opção é contraindicada.

Atenção ao combinar --t com --abertura otima. Só a configuração padrão (T=1, beam=10, profundidade=1) vem com a abertura do nível 3 pronta em data/aberturas_nivel3.json; qualquer outra é uma busca a partir das 6.046 candidatas e leva ~9 min, uma vez, antes da primeira sugestão. A CLI avisa antes de gastar o tempo. A abertura padrão não passa por aí: a palavra é fixa e só os bits dela são calculados, em milissegundos. O tamanho da busca é ajustável por --beam (palpites testados por nó) e --profundidade (níveis de busca antes de cair na política gulosa). Cada combinação tem a sua própria abertura em cache.

Benchmark e gráficos

python benchmark.py --bateria realista
python benchmark.py --bateria completo
python benchmark.py --varredura-t
python benchmark.py --nivel3
python benchmark.py --serao
python benchmark.py --areio
python benchmark.py --catalogo
python benchmark.py --ampliado
python -m termo.robustez
python analise.py

Testes

python -m pytest tests -q

Estrutura

O motor é independente da interface. A CLI e o app web são duas camadas finas sobre o mesmo termo/, e nenhuma linha de lógica de solver mora fora dele: pôr o solver numa página não exigiu mexer em nada lá dentro.

Arquivo Papel
termo/feedback.py Normalização de acentos, regra das duas passadas, codificação base-3, detector de padrão impossível
termo/lexico.py Download, filtro de 5 caracteres, agrupamento por forma normalizada, dedupe por ICF, prior
termo/matriz.py Matriz 6.046 × 6.046 de padrões pré-computados (numpy vetorizado); 8.629 × 6.046 com --ampliado
termo/entropia.py Cálculo de entropia, regra de endgame, cache da abertura (nível 2)
termo/nivel3.py Busca na árvore de decisão pelo menor nº esperado de tentativas (nível 3)
termo/robustez.py Número efetivo de palavras (M = 2^H) e minimax de arrependimento sobre a distribuição desconhecida
termo/estrategias.py As estratégias do benchmark
solver.py CLI interativa (só I/O)
app.py App web em Streamlit (só I/O): tabuleiro clicável e cores da marca
benchmark.py Simulação de partidas e métricas
analise.py Gráficos e tabela de resultados

Cada palavra tem duas formas (§2.4): a normalizada (terco), usada em todo cálculo, e a de exibição (terço), puramente cosmética. O léxico final fica em termo_lexico_5letras.txt na forma acentuada; data/ guarda as fontes baixadas e os artefatos gerados.

Dois espaços de índice convivem no motor: as candidatas (o que pode ser a secreta) e as sondas (o que pode ser digitado). No modo padrão são o mesmo conjunto; com --ampliado as sondas viram um superconjunto que tem as candidatas no prefixo; é isso que deixa todo índice de candidata continuar valendo, e o acréscimo fica em termo_sondas_extra.txt.


Resultados

Os números abaixo seguem a mesma cadeia da seção anterior: primeiro o léxico sobre o qual tudo repousa, depois a abertura, depois quanto cada nível vale em tentativas. Todos saem de benchmark.py, que grava os JSONs em resultados/; de lá o analise.py tira os gráficos e a tabela em texto. Os JSONs não são versionados (cada comando abaixo regenera o seu), mas os PNGs e a tabela sim, para que esta seção se leia inteira sem clonar nada.

As quatro baterias foram medidas na mesma máquina, então as colunas s/jogo se comparam entre si. Elas são a única coisa aqui que depende do hardware: médias e distribuições são determinísticas e reproduzem dígito por dígito.

O léxico

Todos os números da §2.2 e §2.3 foram reproduzidos exatamente a partir da fonte:

Passo Resultado
5 caracteres em lexico 6.301
Grupos por forma normalizada 6.046
Grupos com colisão 242 (497 palavras, 7,9%)
Léxico final 6.046
Cobertura do ICF 100%
Palavras com "ç" 172

A dedupe escolhe variantes plausíveis: terço (não terco, terçó ou terçô), agora, pique, manto. festa, a palavra de 5 jan 2022 (§2.7), está no léxico. O sinal de validação da §2.3 se confirma: as 6.046 ficam a 20 palavras das 6.026 que o Gabriel Yshay obteve por um caminho independente.

A abertura muda com o objetivo

A primeira jogada não depende de feedback nenhum, então é fixa e vai para o disco. Cada nível escolhe uma:

abertura entropia E[tentativas] posição no ranking de entropia
Nível 2 tarso 5,97 bits
Nível 3 tosar 5,91 bits 3,01

As duas usam exatamente as mesmas cinco letras. tosar é a sexta do léxico em entropia (o nível 2 a lista por último entre as cinco alternativas que oferece) e mesmo assim é a que minimiza o número esperado de tentativas. É a correção do 3B1B em uma linha: maximizar bits não é minimizar tentativas.

O ranking de entropia com T=1, para referência: tarso (5,97), tirão (5,97), tória (5,95), sertã (5,94), teira (5,92), tosar (5,91).

E o serão dos fóruns?

Pergunte a abertura ótima do Termo num fórum e a resposta é serão. Aqui ela não é ótima em nível nenhum: fica em 12º no ranking de entropia. Vale entender por quê, porque o consenso não é bobo: ele otimiza um problema vizinho, e cada uma das três diferenças sozinha já bastaria para tirar serão do topo.

Diferença 1: o léxico de palpites. Quem está à frente dele:

# palavra H posição em frequência
1 tarso 5,975 1126/6046
2 tirão 5,966 3632
3–10 tória, sertã, teira, tosar, toira, tério, terso, teiró 5,95–5,86 2008–4936
11 sorte 5,844 219
12 serão 5,833 83

As dez primeiras são palavras que ninguém digitaria por vontade própria. serão é a 83ª palavra mais comum do léxico: ele é o melhor palpite entre os que um humano considera, que é a restrição que um fórum aplica sem declarar.

Diferença 2: a lista de respostas. Restringindo as candidatas às N mais comuns, com peso uniforme dentro do corte, serão sobe, e no cenário completo do fórum, em que a mesma lista curta também limita os palpites, ele chega ao topo:

N palpites = léxico palpites = as próprias N
200
300 (atrás de tirão)
500 2º (certa)
1000 2º (corta)
1500 3º (corta)

O ponto doce é uma lista de ~200–400 palavras comuns, que é mais ou menos o que se intui como "o que o Termo sorteia". A recomendação é reproduzível, então.

Note que o dial T da §4.2 não chega lá: baixar a temperatura concentra a massa em meia dúzia de palavras em vez de truncar e uniformizar, e a abertura foge para outro lado.

T 2 1 0,5 0,3 0,1
melhor abertura cairo tirão tarso tarso metro mesto
posição de serão 31ª 12ª 109ª 339ª 946ª

Corte duro com uniforme dentro é um regime que o prior contínuo não cobre em ponto nenhum, e o mais próximo dele é T≈2, não T→0.

Diferença 3: o objetivo. Esta é a que sobrevive a tudo. No mundo de 300 palavras, onde serão é o primeiro em bits, ele continua não sendo a melhor jogada. As rivais saem da busca do nível 3 nesse mesmo mundo, e a média é das 300 partidas com a mesma política depois da abertura:

abertura média
sorte 2,770
certa 2,773
terça 2,780
serão 2,810

É a correção do 3B1B aplicada ao consenso popular: serão maximiza os bits e mesmo assim custa 0,04 tentativa a mais. No jogo de verdade (6.046 possíveis, bateria realista) a conta fica em 3,669 contra os 3,581 de tarso.

(As médias jogadas batem dígito a dígito com o E que a busca previu para cada abertura: com profundidade 1 o valor do nível 3 é exatamente 1 + Σ p·V_guloso, e V_guloso é a política do nível 2, que é a que joga da segunda rodada em diante. Os dois caminhos medindo a mesma coisa é um teste de sanidade de graça.)

De onde vem a recomendação, então? Provavelmente do nível 1. Pelo score de frequência de letras sobre as palavras comuns, serão é 4º de 6.046, e as três à frente (oreia, orate, reato) são obscuras: ele é a primeira palavra da lista que alguém reconhece. S, E, R, A, O são simplesmente as cinco letras mais frequentes, e é isso que a intuição dos fóruns está medindo. O léxico completo desfaz até isso: contra as 6.046, serão cai para 38º na mesma heurística.

Só que ninguém sabe em que mundo está

Todo o veredito acima (o nosso e o do fórum) supõe que a lista de respostas é conhecida. Ela não é: é uma hipótese nossa, e as três seções anteriores mostraram que a resposta muda com ela. Sob incerteza a pergunta certa não é "qual é a melhor?", e sim "qual é a menos pior no seu pior mundo?".

Aqui os mundos são abertos: o corte vale para as respostas, mas o léxico inteiro continua digitável; tarso não está entre as 300 mais comuns e mesmo assim é uma jogada legal. Média penalizada em cada mundo, e entre parênteses o que a abertura perde para a melhor daquele mundo:

abertura N=300 N=1500 N=6046 pior caso
sorte 2,760 (+0,000) 3,263 (+0,000) 3,849 (+0,001) 0,001
tarso nível 2 2,770 (+0,010) 3,266 (+0,003) 3,848 (+0,000) 0,010
cairo 2,783 (+0,023) 3,282 (+0,019) 3,862 (+0,013) 0,023
tirão 2,760 (+0,000) 3,292 (+0,029) 3,860 (+0,012) 0,029
tosar nível 3 2,780 (+0,020) 3,287 (+0,024) 3,890 (+0,042) 0,042
serão fóruns 2,787 (+0,027) 3,331 (+0,068) 3,906 (+0,057) 0,068

Duas leituras se sustentam:

  • serão é o pior dos seis. Não é robusto: perde em todos os mundos, e mais onde o mundo é grande. A crítica desta seção sobrevive à mudança de régua.
  • O pior arrependimento da tabela inteira é 0,07 tentativa. A discussão toda (fórum contra solver, nível 2 contra nível 3) vale menos de um décimo de tentativa se a abertura for escolhida com um mínimo de cuidado.

O que esta tabela NÃO autoriza é eleger uma vencedora, e a primeira versão desta seção o fazia. A referência de cada coluna aqui é a melhor das seis candidatas, não a melhor abertura daquele mundo. Com uma régua tirada do próprio conjunto comparado, quem estiver no topo dele termina com arrependimento zero por construção, e o número diz mais sobre a companhia do que sobre a palavra. O veredito de robustez está em E se a própria distribuição for desconhecida?, que usa três mundos a mais, uma referência independente e mede a esperança exata em vez da média de uma bateria. Lá quem se sai bem é tarso, não sorte.

Continua valendo desta tabela o que ela mede de fato: como cada abertura joga em três mundos concretos, e que tosar, otimizado sob o prior de T=1, é quem mais se degrada quando o mundo vira uniforme.

O mapa: os dois botões de uma vez

As duas varreduras de uma dimensão só (cortar a lista, baixar o prior) parecem dar respostas contraditórias. São os dois eixos de um mapa, e vistas assim param de brigar. Cada célula é um mundo, e o nome escrito nela é a melhor abertura ali:

Grade (N, T)

O território de serão é um bloco pequeno e bem delimitado: N entre 200 e 300, com prior fraco (T≥2). Fora dali ele nunca é o melhor. As duas posições conhecidas estão marcadas, e agora se lê a distância entre elas: o padrão do projeto e a hipótese do fórum não são cantos opostos, são vizinhos separados por uma faixa em que sorte e certa mandam.

Duas coisas que só o mapa mostra:

  • sorte domina a coluna de T=1 na faixa do meio (N de 300 a 1000). Vale notar que essa faixa é estreita: no minimax sobre a família inteira de distribuições ela nem aparece no pódio, que é de tarso e sertã.
  • A coluna de T=0,1 é escura de cima a baixo, e isso não é vitória de ninguém. Com o prior tão concentrado, a melhor abertura do mundo vale 0,9 bit: não há o que separar, e todas as jogadas empatam em quase-nada. O gráfico usa a distância relativa justamente por isso: em bits absolutos aquele canto pareceria verde.

A troca por trás de tudo

Uma abertura quer duas coisas incompatíveis: separar muito o conjunto e poder ser a resposta. A fronteira de Pareto (as palavras em que ganhar numa exige perder na outra) tem dez palavras, e todas as candidatas que apareceram nesta investigação estão nela:

Entropia contra frequência

muitosobreformapartecontanoitenestaserãosortetarso.

Ninguém errou a conta: cada um escolheu um ponto diferente da mesma curva. O fórum parou em serão porque é o mais informativo dentro da faixa que ele considera plausível; o solver foi até tarso, a ponta da curva, porque não impõe faixa nenhuma. E sorte é o ponto entre os dois (5,84 bits contra os 5,98 de tarso, com um terço do ICF), que é exatamente por que ele ganha quando o mundo é incerto.

O gráfico também fecha a primeira diferença sem precisar de tabela: as dez melhores em entropia formam uma faixa inteira à direita da região das comuns. Ser informativo e ser plausível quase não se sobrepõem no português.

O catálogo: qual palavra para qual cenário

Juntando tudo, "qual é a abertura ótima do Termo" só tem resposta depois de três premissas declaradas: o objetivo (bits ou tentativas), quais palavras podem cair e quais você aceita digitar. Dezesseis palavras diferentes são ótimas em algum cenário.

Mundo completo: qualquer uma das 6.046 pode cair:

objetivo prior abertura
nível 1 - letras mais frequentes oreia
nível 2 - bits T→∞ cairo (6,203 bits)
nível 2 - bits T=2 tirão
nível 2 - bits T=1 (padrão) tarso (5,975)
nível 2 - bits T=0,3 metro
nível 2 - bits T=0,1 mesto
nível 3 - tentativas T=1 (padrão do nível 3) tosar (E=3,009)
nível 3 - tentativas T→∞ tória (E=3,825)

Mundo aberto: só as N mais comuns caem, mas o léxico inteiro é digitável:

N ótimo em bits ótimo em tentativas
300 tirão (5,913) sertã (E=2,760)
1.500 tirão (6,237) torça (E=3,233)
6.046 cairo (6,203) tória (E=3,825)

Mundo fechado: respostas e palpites saem da mesma lista curta. É o cenário dos fóruns, e a coluna da direita é a que faltava:

N ótimo em bits ótimo em tentativas
100 nesta (5,439) nesta (E=2,400)
200 serão (5,725) norte (E=2,670)
300 serão (5,864) sorte (E=2,770)
500 certa (6,001) terça (E=2,904)
1.000 corta (6,069) certo (E=3,135)
1.500 corta (6,185) certa (E=3,261)
3.000 cairo (6,160) corta (E=3,529)
6.046 cairo (6,203) tória (E=3,825)

Duas coisas saltam desta tabela. A primeira é que serão não é o ótimo de tentativas em N nenhum, nem nos dois cortes onde ele é o campeão em bits. A crítica da terceira diferença não era um detalhe do mundo de 300: vale na faixa inteira. A segunda é que a coluna da direita é povoada por palavras banais (norte, sorte, terça, certo, certa, corta), enquanto a da esquerda puxa para cairo e tória. Minimizar tentativas prefere palavras que podem ser a resposta; maximizar bits, não.

(A última linha das duas últimas tabelas é a mesma: sem corte, mundo aberto e fechado são o mesmo mundo. Os dois caminhos dão tória com E=3,8252: é uma checagem de consistência de graça.)

E se a própria distribuição for desconhecida?

Todas as tabelas acima fixam um mundo antes de perguntar qual é a melhor abertura. Esta inverte: trata a distribuição das secretas como incógnita e mede o número efetivo de palavras que cada prior admite,

M(prior) = 2^H(prior)      [uniforme sobre K palavras dá exatamente K]

Nessa régua o prior padrão do projeto (T=1) vale M=447: ele já é uma aposta forte, mais perto de "lista curada" que de "não sei nada":

T 0,25 0,5 1 2 4
M 7,7 60,6 447 1.872 4.149 6.046

M é suficiente, quase sempre. Antes de reduzir tudo a um eixo, o módulo testa se formas diferentes com a mesma concentração escolhem a mesma abertura. Para M=1200 por três caminhos (corte em 6.046/3.000/1.500 com o T ajustado), as três dão tirão, com gap de 0,0000 bit. O mesmo vale em M=300, 600 e 2.400. Em M=4.800 o colapso falha (tirão contra tória): perto do uniforme a forma do corte volta a importar, e a grade abaixo, que usa só T, não distingue esse caso.

A matriz de arrependimento (regret = tentativas a mais que a melhor abertura daquele mundo, tudo em nível 2):

abertura M=300 M=447 M=600 M=1200 M=2400 M=4800 M=6046 pior caso
curada padrão Wordle nada sei
sertã 0,0283 0,0109 0,0000 0,0000 0,0084 0,0198 0,0281 0,0283
tarso 0,0000 0,0000 0,0101 0,0027 0,0112 0,0290 0,0255 0,0290
tória 0,0364 0,0425 0,0323 0,0382 0,0528 0,0437 0,0122 0,0528
corta 0,0617 0,0507 0,0467 0,0192 0,0000 0,0098 0,0000 0,0617
tirão 0,0525 0,0553 0,0643 0,0582 0,0608 0,0481 0,0364 0,0643
tosar 0,0603 0,0412 0,0384 0,0391 0,0385 0,0490 0,0686 0,0686
cairo 0,0702 0,0602 0,0584 0,0546 0,0567 0,0493 0,0392 0,0702
carto 0,0985 0,0833 0,0671 0,0137 0,0042 0,0000 0,0012 0,0985

No papel a vencedora é sertã, com 0,0283 contra os 0,0290 de tarso. Mas essa margem de 0,0007 não sobrevive à própria grade. Tirando uma coluna de cada vez (a composição da grade é escolha nossa, não dado), o pódio inverte:

grade vencedora
completa sertã (0,0283) tarso (0,0290)
sem M=4800 tarso (0,0255) sertã (0,0283)
sem qualquer outra coluna sertã (0,0283) tarso (0,0290)

Uma coluna decide. A leitura honesta é que sertã e tarso estão empatados, e a abertura do nível 2 é robusta, o que é o resultado mais tranquilizador que esta análise poderia dar: o solver já joga uma abertura que não depende do prior estar certo. sertã é a curiosidade, não a recomendação.

Quem paga a conta é tosar, a abertura ótima do nível 3: 0,069, mais que o dobro, e o pior caso dela é justamente o mundo uniforme. É a ressalva do nível 3 vista de outro ângulo: ele otimiza sob o prior, então erra mais quando o prior erra. É por isso que as interfaces abrem por tarso (ver Opções), em qualquer nível e qualquer T: a política que decide as jogadas seguintes é a que o usuário escolheu, mas a primeira palavra é a que não aposta em premissa nenhuma.

Quanto custa usar tosar com a distribuição errada? No máximo 0,069 tentativa. A discussão inteira desta seção cabe em sete centésimos de tentativa, o que é a informação mais útil da tabela: qualquer uma dessas oito aberturas é uma escolha defensável, e trocar de abertura não é onde estão os ganhos.

Levada ao nível 3, sertã não constrói caso para virar padrão:

sertã tosar (ótima do nível 3)
E na raiz, T=1 3,0249 3,0094
bateria realista 1.500 3,4580 3,4893
vitória 99,93% 100,0%

Ela perde na métrica que o nível 3 otimiza, ganha 0,031 tentativa jogando, e troca isso por uma derrota em 1.500 partidas. Somando com o empate técnico acima: não há motivo para mexer na abertura de nenhum dos dois níveis: cada um continua devolvendo o ótimo do seu próprio critério, e é isso que abertura() faz nos dois motores. O que muda é qual palavra as interfaces digitam primeiro, que é decisão de produto e não de algoritmo: entre candidatas separadas por centésimos de tentativa, elas abrem pela que não depende de premissa nenhuma, e a mesma em qualquer T (abertura_padrao()). O que a análise entrega não é uma palavra nova, é a medida de quanto a escolha depende de uma premissa, e a resposta é "pouco, no nível 2; o dobro disso, no nível 3".

Duas ressalvas que a tabela não carrega sozinha. A primeira: minimax de arrependimento é refém do conjunto de mundos admitidos: um M implausível na grade sequestra o máximo sozinho. O piso M=300 é uma escolha, não um dado. piso_empirico em termo/robustez.py está pronto para trocá-la por medição: dado um registro das secretas já sorteadas em data/secretas_sorteadas.txt, a mais rara delas fixa o piso de M por baixo. O arquivo não existe no repositório.

A segunda: a referência de cada coluna é a melhor abertura do beam de entropia, não do léxico inteiro, porque varrer as 6.046 por coluna custaria ~8 h. É o mesmo beam do nível 3, com a mesma justificativa e a mesma consequência: os arrependimentos são limites inferiores.

Isto sai de python -m termo.robustez (~8 min, com cache em data/robustez.json invalidado por assinatura do código) e a tabela vai para tabela.md via analise.py.

Para escolher uma na prática

se você… jogue por quê
não quer apostar em premissa nenhuma tarso menor arrependimento de pior caso sobre sete distribuições, de "lista curada" a uniforme (empatada com sertã), e a de maior entropia sob o prior do projeto. É o que a CLI e o app abrem, em qualquer nível e qualquer T
confia no prior do projeto tosar menor E[tentativas] com T=1 sobre o léxico inteiro: é o ótimo do nível 3, atrás de --abertura otima. Ganha 0,013 tentativa se o prior estiver certo e custa até 0,069 se estiver errado
acha que só palavras bem comuns caem, e mede em bits serão é o ótimo do mundo fechado de 200–300; aqui os fóruns estão certos
acha que só palavras bem comuns caem, e mede em tentativas sorte (N≈300) ou certa (N≈500–1500) ganhar cedo vale mais que informar muito

O catálogo sai de python benchmark.py --catalogo, que grava resultados/catalogo.json. Ele roda separado porque a última linha do mundo fechado é a busca completa do nível 3 sobre as 6.046: sozinha, ~5 min dos ~14 do comando.

O resto da seção sai de python benchmark.py --serao seguido de python analise.py: o primeiro grava resultados/serao.json, o segundo tira dele os dois gráficos acima.

E o areio que todo mundo digita?

serão é a abertura mais recomendada; areio é a mais digitada. São perguntas diferentes, e a segunda começa com um problema que a primeira não tem: areio não estava em lista nenhuma nossa. É conjugação de areiararear e areia estão no léxico, mas nenhuma forma verbal de nenhum dos dois sobreviveu ao arquivo conjugações. Sem estar no espaço de tentativa, a palavra não é mensurável nem jogável aqui, então ela entrou por curadoria explícita, em SONDAS_MANUAIS (termo/lexico.py), que é hoje sua única entrada: 8.629 sondas contra as mesmas 6.046 respostas.

Isso também muda o método em relação ao --serao, e vale declarar: areio não pode ser a secreta e não tem ICF, então posição em frequência, mundo fechado e a fronteira H × ICF não se aplicam a ela. Os mundos aqui são todos abertos — o corte vale para o sorteio, o teclado continua inteiro. Fechar a lista em N apagaria areio do teclado, que é o oposto do que se quer medir.

No ranking de entropia (as 8.629 sondas, léxico inteiro como lista de respostas):

T melhor (H) areio (H) serão
cairo (6,203) 20º (6,028) 37º
5 tória (6,236) 64º (5,920) 10º
2 tirão (6,210) 185º (5,759)
1 tarso (5,975) 344º (5,477) 14º
0,5 tarso (4,980) 390º (4,620) 135º

O padrão é o inverso do de serão, e é o achado da seção. serão melhora quando a lista de respostas encolhe; areio melhora quando o prior some. As duas ficam boas em cantos opostos do mesmo espaço de premissas, e o canto de areio — prior uniforme sobre as 6.046, isto é, "o Termo sorteia leruê tanto quanto porta" — é o menos plausível dos dois.

A grade (N, T) mostra isso de uma vez. Cada célula é a colocação de areio naquele mundo, e entre parênteses o que ela perde em bits para a melhor de lá:

N \ T 5 2 1 0,5
300 313º (-0,48) 335º (-0,48) 363º (-0,50) 401º (-0,51) 397º (-0,35)
1500 157º (-0,43) 191º (-0,45) 235º (-0,49) 349º (-0,50) 390º (-0,36)
3000 96º (-0,36) 132º (-0,40) 201º (-0,47) 345º (-0,50) 390º (-0,36)
todas 20º (-0,18) 64º (-0,32) 185º (-0,45) 344º (-0,50) 390º (-0,36)

areio não é a melhor abertura em canto nenhum da grade — nem no do fórum (N=300, T→∞), onde serão é primeiro e ela é 313ª. O melhor que ela faz é 20º num único vértice.

E jogando, média penalizada, mesma política pós-abertura nas seis:

abertura N=300 N=1500 N=6046 realista (1.500, T=1) pior caso
tarso 2,770 3,263 3,843 3,581 0,010
tirão 2,760 3,278 3,858 3,637 0,015
cairo 2,780 3,273 3,847 3,617 0,020
teria 2,770 3,285 3,856 3,613 0,022
serão 2,787 3,330 3,904 3,662 0,067
areio 2,867 3,409 3,971 3,781 0,147

A última coluna é o maior arrependimento nos três primeiros mundos — a referência de cada um é a melhor das seis ali, tirada do próprio conjunto, então quem lidera zera por construção (a mesma ressalva do --serao; para robustez de verdade, python -m termo.robustez).

areio é última nos quatro mundos, e o pior caso dela é 15× o de tarso e 2× o de serão. Na bateria realista custa 0,20 tentativa por partida, e cinco das 1.500 partidas vão parar na sexta linha — com tarso, nenhuma passa da quinta. Ninguém perde jogo por abrir com ela; é uma abertura decente, não ótima.

De onde vem a popularidade, então. Não da informação: quatro das cinco posições de areio vão para vogais, sobra uma para o R, e das consoantes que mais separam ela não toca em T nem em S — que é justamente o que tarso cobre. Vem da ergonomia: cinco letras distintas, as quatro vogais de uma vez, fácil de lembrar, de digitar e de ler o feedback. É uma troca análoga à da fronteira H × ICF da seção anterior, num eixo que o solver não mede: serão compra frequência com bits, areio compra memorabilidade com bits. Só que aqui o preço é maior — 0,20 tentativa contra os 0,08 de serão.

Sai de python benchmark.py --areio (~2 min), que grava resultados/areio.json.

Nível 1 → nível 2: a entropia compensa?

Esta é a pergunta central da §5.1. Bateria realista: as 1.500 palavras de menor ICF, que é o que o jogo de fato sorteia. média conta só as partidas vencidas; penal. conta derrota como 7.

estratégia média penal. vitória 1 2 3 4 5 6 s/jogo
aleatória 4.269 4.505 91.3% 0 54 299 438 383 196 0.0002
freq. de letras 4.171 4.371 92.9% 0 44 333 523 329 165 0.0011
mais provável 3.707 3.740 99.0% 1 108 555 538 227 56 0.0001
entropia (T=1) 3.581 3.581 100.0% 1 24 664 725 86 0 0.0039

Distribuição de tentativas

Os três palpites da §5.6 se confirmaram, incluindo o mais específico:

  • Entropia ficou em 3,58 na bateria realista, dentro da faixa 3,5–4 prevista.
  • "Mais provável" chegou perto na média: 3,707 contra 3,581.
  • A diferença real aparece na taxa de vitória e no pior caso. Entropia resolveu 100% das 1.500 palavras e nenhuma partida chegou à 6ª tentativa. "Mais provável" perdeu 15 partidas e chegou à 6ª em 56.

Stress test: léxico completo (6.046 palavras, incluindo obscuridades que o Termo nunca sortearia):

estratégia média penal. vitória 1 2 3 4 5 6 s/jogo
aleatória 4.328 4.564 91.2% 1 154 1078 1900 1561 818 0.0002
freq. de letras 4.144 4.363 92.3% 1 162 1400 2104 1301 615 0.0013
mais provável 4.268 4.459 93.0% 1 145 1201 2027 1496 752 0.0001
entropia (T=1) 3.946 3.957 99.7% 1 56 1560 3150 1164 94 0.0014

O stress test mostra por que a régua importa: "mais provável" depende do prior estar certo. Quando as secretas passam a incluir o léxico inteiro, ela cai de 3,71 para 4,27 e fica atrás da heurística de frequência de letras na média. A entropia degrada de 3,58 para 3,95 e mantém 99,7%.

Quanto vale o prior de frequência

A pergunta da §5.5. Como T→∞ recupera a entropia pura, esta curva compara o nível 1 e o nível 2 do algoritmo de forma contínua:

T 0.5 1 2 5 10
média (penal.) 3.599 3.581 3.627 3.631 3.648 3.887
vitória 100.0% 100.0% 100.0% 100.0% 100.0% 99.8%

Curva de temperatura

O prior vale cerca de 0,31 tentativa: de 3,89 (entropia pura) para 3,58 (T=1). É a resposta empírica para a camada que faltava no artigo brasileiro citado na especificação.

O mínimo cai exatamente em T = 1, o valor padrão da v1. A curva é rasa entre 0,5 e 10 (amplitude de 0,07 tentativa, dentro do ruído de 1.500 partidas) e só piora de verdade em T→∞. Ou seja: ter um prior importa; calibrá-lo com precisão, nem tanto.

Ampliar o espaço de tentativa não paga

O Termo aceita como tentativa palavras que nunca sorteia como resposta: as conjugações verbais que a curadoria removeu. Nada obriga um palpite a poder ser a resposta: ele só precisa separar bem. Somando as formas de 5 letras do arquivo conjugações que ainda não estavam no léxico, o solver passa a ter 8.629 palavras jogáveis contra as mesmas 6.046 respostas possíveis (--ampliado). Uma delas — areio — não vem do arquivo: é curadoria manual, pelo motivo que a seção dela explica.

Sob o mesmo critério, mais opções não podem baixar a entropia máxima de jogada nenhuma. E não baixam: num varredura de 60 estados de meio de jogo, a melhor sonda é uma conjugação em 24 deles. Só que o ganho é de 0,034 bit em média, e a abertura nem muda: tarso continua ótima entre as 8.629.

Bits não são tentativas (§4.1), então o que decide é jogar:

Espaço de tentativa Bateria Média Penalizada Derrotas ms/jogo
6.046 realista (1.500) 3,5807 3,5807 0 3,33
8.629 realista (1.500) 3,5807 3,5807 0 5,13
6.046 completo (6.046) 3,9464 3,9570 21 0,96
8.629 completo (6.046) 3,9400 3,9486 17 1,23

Na bateria realista a diferença é exatamente zero, não "dentro do ruído": zero, nas duas casas que o benchmark reporta. A distribuição chega a se mexer (6 partidas migram de 4 para 3 tentativas, 6 outras vão de 4 para 5), e o saldo cancela.

Na bateria completa, que é enumeração exaustiva e não amostra, ampliar ganha 0,0084 tentativa e converte 4 derrotas em vitórias, de 21 para 17. O ganho é real, e é todo em palavras como gueja, riçar e bimar, que o Termo não sorteia. Custa 28% de CPU por jogada e uma segunda matriz de 52 MB.

No nível 3 ampliar chega a atrapalhar

Restava a suspeita de que o problema fosse o proxy: se ampliar rende bits que não viram tentativas, talvez a busca pelo objetivo real (§4.1) soubesse usá-los. A resposta é não, e é pior que empate:

Espaço de tentativa Média Penalizada s/jogo
6.046 2,853 2,853 0,30
8.629 2,863 2,863 2,41

Ampliar piora o nível 3 em 0,010 tentativa e custa 8× a CPU (300 partidas, beam=10, profundidade=1). O motivo não é estatístico, é estrutural: o beam é um orçamento fixo de 10 palpites por nó, tirado da ordem de entropia. Medindo a composição dele em 40 estados de meio de jogo, 27% dos slots vão para conjugações (2,7 dos 10 em média), e em 9 dos 40 nós metade ou mais do beam é sonda. Cada slot desses é uma jogada que a busca avalia sabendo que ela não pode encerrar a partida, no lugar de uma candidata que poderia.

No nível 2 isso é inofensivo, porque lá a entropia não é só a ordenação: é o critério. No nível 3 a ordem de entropia é apenas o move ordering, e o beam é toda a visão que a busca tem do espaço de jogadas: enchê-lo de palavras que nunca ganham a rodada estreita a busca em vez de alargá-la. Ampliar o beam recuperaria a perda, ao custo que a ampliação queria evitar.

A conclusão é que a separação formal entre as duas listas, que a especificação pôs fora de escopo (§12), estava certa em ficar fora, e agora com dois motivos medidos. No nível 2, porque o léxico de 6.046 já é grande o bastante para conter, em quase todo estado, uma palavra tão informativa quanto a melhor conjugação. No nível 3, porque sondas competem por um recurso escasso (as vagas do beam) contra palavras que ainda podem ganhar o jogo.

O --ampliado fica no repositório porque a medição é o resultado; o padrão continua sendo, e deve continuar sendo, as 6.046.

Nível 2 → nível 3: o proxy contra o objetivo real

A especificação pôs o nível 3 fora de escopo por custo computacional (§4.1). Ele está implementado em termo/nivel3.py, é o padrão da CLI, e o custo agora tem número.

No meio do jogo a diferença é grande

Depois de tarso com o r verde sobram 51 candidatas:

jogada entropia E[tentativas]
Nível 2 (entropia) miúde 2,01 bits 2,03
Nível 3 verde 1,83 bits 1,43

O nível 3 abre mão de 0,18 bit para jogar uma candidata, que pode encerrar o jogo ali. Com T=1 isso não é aposta: verde sozinha carrega 65% da massa de probabilidade das 51. Subindo T o prior achata e a escolha converge para a do nível 2: em T=5 e T→∞ os dois jogam miúde. O dial da §4.2 governa os dois níveis.

Head-to-head

As 300 palavras de menor ICF, mesmas secretas para os dois:

estratégia média penal. vitória 1 2 3 4 5 6 s/jogo
entropia (T=1) 3.397 3.397 100.0% 0 5 174 118 3 0 0.0103
nível 3 (K=10, P=1) 2.853 2.853 100.0% 0 77 191 31 1 0 0.4809

Nível 2 vs nível 3

O objetivo real vale 0,54 tentativa: 3,397 para 2,853, com as duas estratégias resolvendo 100%. O ganho não vem de evitar derrotas: vem de acabar o jogo na 2ª tentativa, que a entropia consegue em 5 partidas e o nível 3 em 77. Maximizar bits é exatamente a política que se recusa a tentar ganhar cedo.

Duas leituras que esta tabela não autoriza, ambas por ser outra bateria:

  • Os 3,397 da entropia não batem com os 3,581 da tabela principal porque são 300 palavras, não 1.500. Compare uma linha com a outra, não com a seção anterior.
  • Os 10,3 ms da entropia não batem com os 3,9 ms de lá pelo mesmo motivo, por uma via menos óbvia: escolher_com_cache amortiza os estados repetidos entre partidas, e 300 jogos reaproveitam bem menos que 1.500. É a mesma conta, diluída por menos jogos.

Uma ressalva honesta

O nível 3 otimiza o valor esperado sob o prior, e o prior de T=1 é agressivo. A consequência é que ele sacrifica palavras raríssimas de propósito: prefere chutar a candidata provável a gastar uma jogada separando o grupo. Em criva (ICF 19) ele entra na espiral prima → urina → brida → crica e perde uma partida que o nível 2 ganha. Na bateria realista (o que o Termo de fato sorteia) isso nunca acontece, e é justamente onde ele ganha 0,54 tentativa.

O E = 3,01 da abertura é uma esperança ponderada pelo prior sobre o léxico inteiro; a média do benchmark é uma contagem simples sobre as mais comuns. Os dois números medem coisas diferentes e não se comparam.

O custo, e por que ele caiu

Perfilando a busca do nível 3 num estado de 223 candidatas, 95% do tempo estava em escolher o beam (a varredura de entropia do léxico) e só ~2% na recursão que minimiza tentativas. Pior: 87% das varreduras eram em conjuntos de até 8 candidatas, ou seja, percorrer 6.046 palavras para ranquear palpites contra quatro.

O caminho geral de entropias monta uma tabela de 6.046 × 243 baldes; com poucas candidatas isso é 11 MB alocados e zerados para preencher meia dúzia de colunas por linha. Reescrevendo a mesma soma por agrupamento par a par (Motor._entropias_poucas, H = -Σ_c w_c·log₂(massa do grupo de c), custo n·m² em vez de n·243), mais um cache do beam por conjunto:

antes depois
jogada com 223 candidatas 6,96 s 2,03 s 3,4×
abertura do nível 3 953 s 529 s 1,8×
nível 2, bateria realista 10,7 ms 3,2 ms 3,4×

Este par foi medido de uma vez só, na máquina onde a otimização foi feita; o que vale aqui é a razão, não o valor absoluto. As tabelas de benchmark acima são de outra máquina, ~20% mais lenta, daí os 3,9 ms onde aqui se lê 3,2 ms.

A conta é a mesma, não uma aproximação, e isso foi verificado, não suposto: a abertura recalculada dá tosar com 3.0094161966572304, dígito por dígito, e reprocessar as duas baterias principais (7.546 partidas × 4 estratégias) muda apenas os campos de tempo dos JSONs. O nível 2 levou o ganho junto, de graça.

Onde o custo ficou: o nível 3 é 47× mais CPU que o nível 2 (0,48 s por jogo contra 10,3 ms, na mesma bateria de 300), mais os ~9 min da abertura, que vem versionada. Para uso interativo é tranquilo; para varrer 1.500 palavras em seis temperaturas, não. A decisão da especificação continua defensável; o que mudou é que agora ela é uma escolha informada, não uma suposição.

Efeito da migração v1.0 → v1.1

O léxico corrigido melhorou tudo, como a §5.6 antecipava: menos candidatas para separar:

v1.0 (8.996 palavras) v1.1 (6.046 palavras)
Entropia, bateria realista 3.685 3.581
Entropia, stress test 4.110 3.946
Partidas na 6ª tentativa (realista) 1 0
Ganho do prior (T=1 vs T→∞) 0.33 0.31
Matriz 81 MB, 20 s 37 MB, 4 s
Entropia da 1ª jogada 1.4 s 1.0 s
T ótimo na varredura 2 1

A abertura do nível 2 continua sendo tarso nas duas versões.

Afinal, qual é a palavra ótima?

A pergunta que abre o projeto merece uma resposta fechada, e ela tem duas partes.

Se você quer uma palavra para digitar amanhã: tarso. Ela é a única que ganha dos dois lados da investigação: é a abertura do nível 2 sob o prior do projeto (5,975 bits, a maior do léxico) e empata em primeiro no minimax de arrependimento sobre sete distribuições, de "lista bem curada" a uniforme. Todas as outras candidatas vencem num regime e perdem no outro: tosar é melhor se o prior de T=1 estiver certo e o dobro de pior se não estiver; serão só é ótima se a lista de respostas tiver ~300 palavras, e é a menos robusta das oito; sertã supera tarso por 0,0007 tentativa, que uma coluna da grade decide.

É por isso que tarso é a abertura que a CLI e o app jogam, nos dois níveis e em qualquer temperatura: entre candidatas separadas por centésimos de tentativa, elas abrem pela que não depende de premissa nenhuma. Quem confia no prior joga tosar, que minimiza E[tentativas] sob a premissa declarada, e pede por ela com --abertura otima. A tabela de Para escolher uma na prática abre os outros casos.

A segunda parte da resposta é mais útil que a primeira: a abertura vale pouco. O pior arrependimento entre as oito candidatas testadas é 0,069 tentativa. Ao lado das outras decisões deste README:

decisão vale
nível 1 → nível 2: letras para entropia 0,59 tentativa
nível 2 → nível 3: bits para tentativas 0,54 tentativa
o prior de frequência (T=1 contra T→∞) 0,31 tentativa
escolher bem a abertura ≤ 0,07 tentativa

Cada linha é uma diferença medida dentro de uma bateria. As duas primeiras saem de baterias diferentes (1.500 e 300 palavras) e os valores absolutos delas não se comparam entre si, como avisado lá em cima. As diferenças, sim: é sempre a mesma bateria antes e depois da mudança.

Uma ordem de grandeza separa as duas coisas. O debate dos fóruns (e boa parte das seções acima) é sobre a variável menos importante do jogo: o que decide partidas é a política da segunda jogada em diante, não a primeira. É também por isso que quase nada no solver mudou depois de tudo isto: tarso e tosar seguem sendo as aberturas ótimas dos níveis 2 e 3, cada uma no seu critério, e nenhuma linha da busca foi tocada. Mudou só qual palavra as interfaces digitam primeiro, e essa decisão vale, no pior caso, 0,069 tentativa. O que se ganhou foi saber quanto a escolha depende de uma premissa que ninguém pode verificar, e a resposta é: pouco.


Achados e desvios da especificação

O caso de teste 3 da §3.3 continua com o valor errado

A v1.1 corrigiu os casos 5–7 (acentos), mas o caso 3 passou batido: ele já estava errado na v1.0 e não tem nada a ver com normalização.

# Secreta Tentativa Especificação Correto
3 carro rerum BGBYB YBGBB
8 termo metro "verificar" YGYYG

No caso 3, o único verde é o r da posição 2 (rerum[2] == carro[2]), mas a especificação colocou o G na posição 1. Ambos estão corrigidos em test_feedback.py, com a derivação manual no docstring. O que cada caso testa continua o mesmo.

A implementação foi validada de três formas independentes: os 8 casos obrigatórios, propriedades estruturais (simetria da contagem de letras coloridas, consistência de G e Y, idempotência da normalização), e a matriz vetorizada conferida célula a célula contra a implementação de referência em Python puro.

A cedilha (§3.5 e §11) é um interruptor de uma linha

NORMALIZAR_CEDILHA em feedback.py controla se ç vira c. O padrão é True, seguindo a §3.2. Se o teste empírico no term.ooo mostrar que a cedilha é letra distinta, basta trocar para False:

  • com True: terçoterco; calcular_feedback("acude", "açude") == "GGGGG"
  • com False: terçoterço; calcular_feedback("acude", "açude") == "GBGGG"

A troca regenera tudo automaticamente: o léxico é reagrupado (as 172 palavras com "ç" deixam de colidir com as variantes em "c"), e a matriz em cache se invalida sozinha, porque a assinatura é um hash da lista de palavras.

"Excluir conjugações" é um critério de fonte, não de morfologia

A v1.1 manda não usar o arquivo conjugações. Isso remove fugia, curas, zarpa, andei, mas não remove abafa, que consta do dicionário geral por si próprio. O critério implementado é o da especificação (a fonte), e o teste test_conjugacoes_foram_excluidas documenta essa distinção para não induzir a erro depois.

As formas que a v1.1 remove não sumiram do projeto: elas voltam como sondas em --ampliado, onde servem de tentativa sem poder ser resposta. O que ganham (nada na bateria realista) está em Ampliar o espaço de tentativa não paga.

A regra de endgame da penúltima tentativa é conservadora

A especificação (§4.5) manda chutar uma candidata na penúltima tentativa quando C é pequeno. Isso nem sempre é ótimo: com 2 tentativas restantes e 3 candidatas, chutar uma candidata dá ~67% de vitória, enquanto uma palavra separadora que distingue as três com certeza dá 100%.

A regra foi implementada como especificado, mas o limiar é configurável (Motor(limiar_endgame=...), padrão 3). Com 2 ele vira inócuo, já que len(C) <= 2 tem tratamento próprio. Fazer isso direito exige minimizar tentativas esperadas, que é exatamente o nível 3, hoje o padrão da CLI.

O chute do endgame é a palavra mais comum, não a de maior prior

Quando não sobra jogada informativa — última tentativa, ou penúltima com C pequeno —, o solver aposta na candidata mais comum do português (menor ICF), em qualquer temperatura.

Parece a mesma coisa que "maior prior", e para todo T finito é: o prior é softmax(-ICF/T), estritamente decrescente no ICF. As duas divergem justamente onde o prior para de informar. Com --t inf ele é uniforme e o argmax devolvia a primeira do índice — nas 40 candidatas de tarso + BBBBB, chutava bebum em vez de civil. Com T minúsculo o softmax satura e zera conjuntos inteiros, com o mesmo efeito.

O T diz "não confio na frequência para pesar a entropia"; não diz que a palavra do dia virou equiprovável. A secreta do Termo é uma palavra que alguém escolheu para o jogo, e o ICF continua sabendo qual é a mais comum — usá-lo no desempate é de graça. Motor.melhor_candidata (massa de probabilidade) segue existindo para a busca do nível 3, que é onde a massa é de fato o que importa.

Detector de feedback logicamente impossível

A especificação (§7.3) pede detectar feedback impossível antes de zerar C, sem dizer como. padrao_possivel faz uma checagem puramente lógica, independente do léxico, com dois critérios:

  1. Ordem dentro de uma letra repetida. A passada 2 pinta de amarelo as ocorrências mais à esquerda, então um B nunca pode preceder um Y da mesma letra. BYBBB para oovxx é impossível.
  2. Falta de casa para os amarelos. Cada amarelo exige uma cópia da letra numa posição não-verde que não seja a dela própria. GGGGY é impossível: sobra uma única posição livre, e é justamente a da letra que precisaria aparecer.

Isso separa "você digitou o feedback errado" de "a palavra do dia não está na nossa lista" (a ressalva §2.6), que são situações diferentes para o usuário.

Salvaguardas contra cache derivado obsoleto

A armadilha da §0.3 é um cache derivado sobreviver à mudança que o invalida. Três artefatos correm esse risco, e cada um se protege pela assinatura da sua entrada:

artefato assinatura custo se passar batido
data/matriz_padroes.npy sha256 das listas de sondas e de candidatas resultados errados
termo_lexico_5letras.txt rejeita formas normalizadas repetidas solver trava ao convergir
data/aberturas_nivel3.json T, beam, profundidade, objetivo, espaço de tentativa e o algoritmo abertura errada, calada

O primeiro já vinha da especificação. O segundo não tinha proteção nenhuma: carregar_exibicao agora rejeita qualquer lista com formas normalizadas repetidas (que é exatamente o que um arquivo da v1.0 pareceria) com uma mensagem dizendo o que fazer, em vez de produzir resultados silenciosamente errados.

O terceiro é o mais escorregadio, porque a "entrada" da abertura do nível 3 não é só um arquivo: é o próprio código da busca. Isso já deu errado uma vez: durante o desenvolvimento, a primeira abertura foi calculada sob o objetivo sem limite de rodadas e continuou parecendo válida depois da correção. Hoje a chave inclui assinatura_busca(), um hash das funções que decidem o valor, comparadas por AST e sem docstrings: mexer na regra do beam, na poda ou na fórmula do custo invalida os 9 min em cache; reescrever um comentário ou reformatar, não. Renomear uma variável invalida também; é falso positivo, mas do lado seguro: recalcular à toa custa tempo, publicar uma abertura errada custa credibilidade.

O falso positivo aconteceu, e valeu o preço. O trabalho de separar candidatas de sondas (--ampliado) trocou self.lexico.exibicao por self.lexico.sondas_exibicao em escolher e o prior por um vetor mais longo em ordenar_por_entropia: nenhuma das duas muda nada no modo padrão, e as duas mudam o AST. A assinatura virou de 33e7f34e para bc195eae e cobrou os 9 min. O recálculo devolveu tosar com E = 3,0094161967 e as mesmas cinco alternativas na mesma ordem: o guard cobrou por uma mudança que não era, e em troca provou que o refactor era neutro justamente onde é mais caro verificar isso à mão.

Motor.entropias fica de fora do hash de propósito. Ela tem oráculo (os testes a comparam com uma referência escrita à mão e com o caminho alternativo), então uma mudança de valor lá é ruidosa, não silenciosa; e é justamente a função que se mexe por desempenho, onde o hash cobraria os 9 min a cada otimização que não muda resultado nenhum. Hash para o que não tem oráculo, teste para o que tem.

Fora de escopo (v1)

Dueto e Quarteto; curadoria manual adicional do léxico; interface web/bot/API.

A separação entre lista de respostas e lista de tentativas válidas também estava aqui, e saiu pela porta dos fundos: está implementada (--ampliado), mas a medição diz que não vale a pena: zero de ganho na bateria realista, 0,008 tentativa na completa, e no nível 3 chega a piorar 0,010 tentativa por 8× a CPU. Ficou como resultado, não como recomendação; os números estão em Ampliar o espaço de tentativa não paga.

O nível 3 também estava nesta lista (§12) e saiu dela: está implementado em termo/nivel3.py e é o padrão da CLI. O motivo da especificação (custo computacional) não desapareceu: ele ficou mensurável, e medido dá décimos de segundo por jogada com a abertura em cache, o que é barato demais para justificar abrir mão de 0,54 tentativa. No benchmark o padrão continua sendo o nível 2: lá são 1.500 partidas por estratégia, e aí os 47× de CPU pesam.

Marca

Os ativos de marca (símbolo, lockup, favicons, ícones de app e banner Open Graph) ficam em brand/. A tabela de qual arquivo usar quando, a paleta e o tamanho mínimo do símbolo estão em brand/README.md.

A paleta é a mesma em qualquer saída colorida (CLI, gráficos, web):

hex papel
verde #3AA394 marca, acerto
dourado #D3AD69 letra fora de lugar
xisto #6E5C62 letra ausente, texto terciário
ardósia #221C1E fundo escuro
osso #F5EFE9 texto sobre escuro

Os gráficos de analise.py derivam suas séries dessas cinco cores.

Não edite os arquivos de brand/ à mão: os PNGs e o .ico são gerados a partir dos SVGs por brand/build.sh (requer cairosvg e imagemagick).

Quando existir um front-end, copie para a raiz do diretório público favicon.ico, favicon.svg, apple-touch-icon.png, icon-192.png, icon-512.png, manifest.webmanifest e og-banner.png, e cole o conteúdo de brand/head-snippet.html no <head>.

Estes ativos não estão sob a licença MIT do repositório.

Fonte dos dados

O léxico de palavras e as pontuações ICF foram derivados do repositório fserb/pt-br, licenciado sob MIT (© 2021 Fernando Serboncini).

About

Resolvedor de Termo (Wordle em portugues) por entropia de Shannon com prior de frequencia

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages