Introdução Link para o cabeçalho

Termo é um jogo de adivinhação de palavras. Diariamente, uma palavra de cinco letras é escolhida aleatoriamente e os usuários devem, através de dicas, identificar que palavra é essa. As regras completas estão no print abaixo:

Como eu já havia criado um grande dicionário de palavras do português brasileiro para um antigo posto do blog chamado A maior palavra da língua portuguesa cujas letras estejam em ordem alfabética , resolvi utilizá-lo aqui novamente. A partir deste dicionário, quero procurar procurar dicas de palavras para jogar Termo.

Criando o Dicionário Link para o cabeçalho

Meu post anterior detalha um pouco melhor a criação do dicionário, mas basicamente eu baixei duas listas de palavras que encontrei na internet, juntei-as e removi as palavras duplicadas.

library(stringi)
library(tidyverse)
palavras_usp <- 
  read.csv(file = "https://www.ime.usp.br/~pf/dicios/br-utf8.txt", 
           header = FALSE)  |> 
  mutate(V1 = stri_trans_general(str = V1, id = "Latin-ASCII"))

dim(palavras_usp)
## [1] 261798      1
palavras_github <- 
  read.csv("https://github.com/pythonprobr/palavras/blob/master/palavras.txt?raw=true", 
           header = FALSE) |>
  mutate(V1 = stri_trans_general(str = V1, id = "Latin-ASCII"))

dim(palavras_github)
## [1] 320139      1
palavras <- 
  palavras_usp |>
  bind_rows(palavras_github) |>
  arrange(V1) |>
  distinct()

dim(palavras)
## [1] 540523      1

Com este conjunto de 540523 palavras únicas, agora eu preciso filtrar apenas aquelas palavras com cinco letras para poder jogar Termo:

termo <- 
  palavras |> 
  mutate(tamanho = nchar(V1)) |> 
  filter(tamanho == 5) |> 
  select(palavra = V1) |> 
  mutate(palavra = tolower(palavra))

head(termo)
##   palavra
## 1   abglt
## 2   aeead
## 3   aneel
## 4   ascii
## 5   aaiun
## 6   aarao

Mas além da lista compilada de palavras de cinco letras, eu necessito alguma técnica para saber qual palavra tentar na hora de preencher o Termo Como é interessante testar palavras cujas letras ocorram frequentemente na língua portuguesa, seria interessante ter um score que diferenciasse palavras com letras mais e menos prováveis. Por exemplo, é esperado que uma palavra como SOMAR proporcione mais letras acertadas do que uma palavra como XEQUE. Afinal, intuitivamente sabemos que S, A e R são letras mais comuns do que X, Q e U, por exemplo.

Descrevo a criação do score na próxima seção.

Criação do Score Link para o cabeçalho

Pensando no problema da criação do score, descobri a página Frequência de Letras da Wikipedia, que me ajuda tremendamente nisso. A partir dela baixeu a tabela de frequência de letras para o português disponível nesta página. Com esta tabela calculei um score para cada palavra do meu banco de dados. O código para isso está a seguir:

library(rvest)
library(janitor)

url <- "https://pt.wikipedia.org/wiki/Frequ%C3%AAncia_de_letras"

frequencia_letras <- 
  html_table(read_html(url)) |> 
  pluck(1) |> 
  clean_names() |> 
  mutate(frequencia = gsub("%", "", frequencia)) |> 
  mutate(frequencia = as.numeric(frequencia))

Neste teste que estou fazendo, criei o score simplesmente somando as frequências das letras de cada palavra, a fim de utilizar as maiores somas em meus chutes. Este valor não tem nenhum significado bem definido, sendo apenas uma pontuação artificial para cada palavra. Para realizar este cálculo, criei as funções valor_letra, que determina a frequência de cada letra do alfabeto de acordo com a tabela obtida a partir da Wikipedia e valor_palavra, que desmembra cada palavra e calcula o score dela, somando as frequências de cada letra.

valor_letra <- function(x){
  
  frequencia <- 
    frequencia_letras |> 
    filter(letra %in% x) |> 
    pluck(2)
  
  return(frequencia)
  
}

# funcao para encontrar o valor de cada palavra

valor_palavra <- function(palavra){
  
  palavra_vetor <- unlist(strsplit(palavra, split = ""))
  soma          <- sum(valor_letra(palavra_vetor))
  
  return(soma)
  
}

Aplicando isso no banco de dados, obtemos os seguintes resultados:

n <- dim(termo)[1]

score <- rep(NA, n)

for (j in 1:n){
  score[j] <- valor_palavra(termo$palavra[j])
}

termo <- 
  termo |> 
  bind_cols(score = score) |> 
  tibble()

termo |> 
  arrange(desc(score)) |> 
  head(20)
## # A tibble: 20 × 2
##    palavra score
##    <chr>   <dbl>
##  1 rosea    52.3
##  2 saroe    52.3
##  3 serao    52.3
##  4 anoes    50.8
##  5 aseno    50.8
##  6 naseo    50.8
##  7 senao    50.8
##  8 adeso    50.7
##  9 sedao    50.7
## 10 maseo    50.5
## 11 mesao    50.5
## 12 estao    50.1
## 13 tesao    50.1
## 14 aceso    49.6
## 15 acoes    49.6
## 16 coesa    49.6
## 17 ecoas    49.6
## 18 escoa    49.6
## 19 secao    49.6
## 20 areno    49.5

Note os maiores scores são das palavras ROSEA, SAROE e SERAO, anagramas formados pelas mesmas cinco letras bastante comuns na língua portuguesa. Podemo comparar palavras distintas, como SOMAR e FUGIU, para ver como estão os scores dela:

termo |> 
  filter(palavra == "somar")
## # A tibble: 1 × 2
##   palavra score
##   <chr>   <dbl>
## 1 somar    44.4
termo |> 
  filter(palavra == "fugiu")
## # A tibble: 1 × 2
##   palavra score
##   <chr>   <dbl>
## 1 fugiu    13.1

Como esperado, o score de SOMAR é superior ao de FUGIU. Afinal, intuitivamente, as frequências das letras S, O, M, A e R parecem ser maiores do que as de F, U, G e I, além de ser o somatório de 5 letras em vez de 4. Com esta medida rudimentar, podemos passar à aplicação desta técnica à palavra selecionada para o Termo de hoje, dia 7 de março de 2024.

Como as palavras com maiores scores são ROSEA, SAROE e SERAO, todas as três com a mesma pontuação, eu começarei meu Termo com SERAO.

Já é possível perceber que as letras S, E, R e O não fazem parte da palavra de hoje. Portanto, basta aplicar uma expressão regular no objeto termo para manter apenas as palavras que possuem A em alguma posição e não possuem S, E, R e O para obtermos as nossas próximas palavras candidatas:

termo_02 <- 
  termo |> 
  filter(grepl("a", palavra)) |> 
  filter(!grepl("s", palavra)) |> 
  filter(!grepl("e", palavra)) |>
  filter(!grepl("r", palavra)) |> 
  filter(!grepl("o", palavra)) |> 
  arrange(desc(score))

termo_02 |> 
  head(10)
## # A tibble: 10 × 2
##    palavra score
##    <chr>   <dbl>
##  1 andim    35.6
##  2 mandi    35.6
##  3 minda    35.6
##  4 nadim    35.6
##  5 dunia    35.5
##  6 duina    35.5
##  7 iandu    35.5
##  8 iduna    35.5
##  9 indua    35.5
## 10 undai    35.5

Vou usar ANDIM como meu próximo chute, pois ela é a palavra de maior pontuação que satisfaz este critério:

Agora vemos que D, I e M são letras que não estão na palavra final. Fazendo uma nova filtragem, temos

termo_03 <- 
  termo_02 |> 
  filter(!grepl("d", palavra)) |> 
  filter(!grepl("i", palavra)) |>
  filter(!grepl("m", palavra)) |> 
  arrange(desc(score))

termo_03 |> 
  head(10)
## # A tibble: 10 × 2
##    palavra score
##    <chr>   <dbl>
##  1 cantu    32.5
##  2 tunal    31.4
##  3 culna    31.0
##  4 nucal    31.0
##  5 culta    30.3
##  6 caput    30  
##  7 nguta    30.0
##  8 hunta    29.9
##  9 bantu    29.7
## 10 cangu    29.5

É importante tomar cuidado a parti de agora, pois já temos três informações a respeito da letra A:

  • a letra A está na palavra
  • a letra A não está na posição 1
  • a letra A não está na posição 4

Adicionando isso ao código anterior, junto com o fato de N estar na palavra mas não poder estar na segunda posição, e atualizando o objeto termo_03, temos:

termo_03 <- 
  termo_02 |> 
  filter(!grepl("d", palavra)) |> 
  filter(!grepl("i", palavra)) |>
  filter(!grepl("m", palavra)) |> 
  filter(!grepl("an[a-z][a-z][a-z]", palavra)) |> 
  filter(!grepl("[a-z][a-z][a-z]a[a-z]", palavra)) |> 
  filter(grepl("n", palavra)) |> 
  arrange(desc(score))

termo_03 |> 
  head(10)
## # A tibble: 10 × 2
##    palavra score
##    <chr>   <dbl>
##  1 cantu    32.5
##  2 culna    31.0
##  3 nguta    30.0
##  4 hunta    29.9
##  5 bantu    29.7
##  6 cangu    29.5
##  7 chuna    29.5
##  8 cunha    29.5
##  9 junta    29.0
## 10 naut.    28.6

Como CANTU, CULNA, NGUTA e HUNTA não são palavras constantes no Termo, jogamos com BANTU:

Agora precisamos atualizar nossa última filtragem para manter apenas as palavras da lista que satisfazem todas as outras características, não possuem as letras T e U e que começam com BAN:

termo_04 <- 
  termo_03 |> 
  filter(!grepl("d", palavra)) |> 
  filter(!grepl("i", palavra)) |>
  filter(!grepl("m", palavra)) |> 
  filter(!grepl("an[a-z][a-z][a-z]", palavra)) |> 
  filter(!grepl("[a-z][a-z][a-z]a[a-z]", palavra)) |> 
  filter(grepl("n", palavra)) |> 
  filter(!grepl("t", palavra)) |>
  filter(!grepl("u", palavra)) |> 
  filter(grepl("ban[a-z][a-z]", palavra)) |> 
  arrange(desc(score))

termo_04 |> 
  head(10)
## # A tibble: 2 × 2
##   palavra score
##   <chr>   <dbl>
## 1 banca    24.6
## 2 banha    22

E agora há apenas duas candidatas: BANCA e BANHA. Como só usamos três tentativas até agora, é garantido que acertemos a palavra final, pois teremos testado no máximo cinco palavras:

Bingo! Palavra descoberta.

Logicamente há formas de melhorar o desempenho deste método. A primeira que me ocorre é procurar e utilizar o dicionário criado pelo próprio Termo para escolher as suas palavras. Outra seria procurar um corpus do português com a frequência das palavras, e não das suas letras, para evitar eventuais discrepâncias que certamente existem no score que criei.

Outra forma é adaptar o método que o canal 3 Blue 1 Brown usou para a língua inglesa, via teoria da informação, de modo a obter as melhores palavras para testar a cada passo do Termo.