Adivinhe a palavra (aka Lingo)


13

O objetivo desse desafio é escrever um programa capaz de adivinhar uma palavra no menor número possível de tentativas. É baseado no conceito do programa de TV Lingo ( http://en.wikipedia.org/wiki/Lingo_(US_game_show) ).

Regras

Dado o tamanho de uma palavra passada como o primeiro argumento em sua linha de comando, o programa do jogador realiza cinco tentativas de adivinhar a palavra escrevendo uma suposição na saída padrão seguida por um único \ncaractere.

Depois de adivinhar, o programa recebe uma string em sua entrada padrão, também seguida por um único \ncaractere.

A cadeia tem o mesmo comprimento que a palavra a adivinhar e é composta por uma sequência dos seguintes caracteres:

  • X: o que significa que a letra fornecida não está presente na palavra para adivinhar
  • ?: o que significa que a letra fornecida está presente na palavra para adivinhar, mas em outro local
  • O: o que significa que a letra neste local foi adivinhada corretamente

Por exemplo, se a palavra a adivinhar for dents, e o programa a enviar dozes, ela receberá OXX?Oporque de sestá correta, eestá fora de lugar oe znão está presente.

Tenha cuidado para que, se uma carta estiver presente mais vezes na tentativa de adivinhação do que na palavra para adivinhar, ela não será marcada como ?e Omais vezes que o número de ocorrências da letra na palavra para adivinhar. Por exemplo, se a palavra a adivinhar for coziese o programa enviar tosses, ela será recebida XOXXOOporque existe apenas uma spara localizar.

As palavras são escolhidas de uma lista de palavras em inglês. Se a palavra enviada pelo programa não for uma palavra válida do tamanho correto, a tentativa será considerada uma falha automática e somente Xserão retornadas.
O programa player deve assumir que um arquivo nomeado wordlist.txte contendo uma palavra por linha está presente no diretório de trabalho atual e pode ser lido conforme necessário.
As suposições devem ser compostas apenas por caracteres minúsculos alfabéticos ( [a-z]).
Nenhuma outra operação de rede ou arquivo é permitida para o programa.

O jogo termina quando uma sequência composta apenas Oé retornada ou depois que o programa fez 5 tentativas e não conseguiu adivinhar a palavra.

Pontuação

A pontuação de um jogo é dada pela fórmula dada:

score = 100 * (6 - number_of_attempts)

Portanto, se a palavra for adivinhada corretamente na primeira tentativa, são dados 500 pontos. A última tentativa vale 100 pontos.

Não adivinhar a palavra concede zero pontos.

O pit

Os programas dos jogadores serão avaliados tentando fazê-los adivinhar 100 palavras aleatórias para cada tamanho de palavra entre 4 e 13 caracteres, inclusive.
A seleção aleatória de palavras será feita com antecedência, para que todas as entradas tenham que adivinhar as mesmas palavras.

O programa vencedor e a resposta aceita serão os que alcançarem a pontuação mais alta.

Os programas serão executados em uma máquina virtual Ubuntu, usando o código em https://github.com/noirotm/lingo . As implementações em qualquer idioma são aceitas desde que sejam fornecidas instruções razoáveis ​​para compilar e / ou executá-las.

Estou fornecendo algumas implementações de teste em ruby ​​no repositório git, fique à vontade para se inspirar nelas.

Esta pergunta será atualizada periodicamente com as classificações das respostas publicadas, para que os desafiantes possam melhorar suas entradas.

A avaliação final oficial será realizada no dia 1º de julho .

Atualizar

Agora, as entradas podem assumir a presença de wordlistN.txtarquivos para acelerar a leitura da lista de palavras para o tamanho atual das palavras para N entre 4 e 13, inclusive.

Por exemplo, há um wordlist4.txtarquivo contendo todas as quatro palavras da letra e wordlist10.txttodas as dez palavras da letra, e assim por diante.

Resultados da primeira volta

Na data de 01/07/2014 - três entradas foram enviadas, com os seguintes resultados:

                        4       5       6       7       8       9       10      11      12      13      Total
./chinese-perl-goth.pl  8100    12400   15700   19100   22100   25800   27900   30600   31300   33600   226600
java Lingo              10600   14600   19500   22200   25500   28100   29000   31600   32700   33500   247300
./edc65                 10900   15800   22300   24300   27200   29600   31300   33900   33400   33900   262600

** Rankings **
1: ./edc65 (262600)
2: java Lingo (247300)
3: ./chinese-perl-goth.pl (226600)

Todas as entradas tiveram desempenho consistente, com um vencedor claro, sendo a entrada em C ++ da @ edc65.

Todos os participantes são incríveis. Até agora, até agora não consegui derrotar @ chinese-perl-goth.
Se mais entradas forem enviadas, outra avaliação será realizada. As entradas atuais também podem ser aprimoradas se você sentir que pode fazer melhor.


1
Só para esclarecer: se o programa leva mais de 6 tentativas de adivinhar a palavra, ele obtém pontos negativos ou apenas zero? Em outras palavras, precisamos de lógica para sair do programa após 6 tentativas para evitar pontos negativos? (Regras dizer zero pontos se o programa não consegue adivinhar a palavra)
DankMemes

1
@ZoveGames após cinco tentativas, seu programa deve sair, mas o mecanismo do jogo o encerrará à força se ele se recusar a fazê-lo :)
SirDarius

1
@RichardA sim, certo, não se preocupe com Python, é um cidadão de primeira classe, então não terei problema em executar algum código python :)
SirDarius

1
@justhalf Muito obrigado por isso! Eu posso finalmente continuar!
MisterBla

1
@justhalf boa idéia, na verdade, eu vou tentar implementar isso
SirDarius

Respostas:


5

Pontos C ++ 267700

Uma portabilidade de um mecanismo antigo do MasterMind.
Diferenças do MasterMind:

  • Mais slots
  • Mais símbolos
  • Maior espaço de solução (mas não muito, porque nem todas as combinações de símbolos são permitidas)
  • A resposta é muito informativa, por isso temos mais informações após cada palpite
  • A resposta é mais lenta para gerar e isso é uma pena, porque meu algoritmo precisa fazer muito isso.

A idéia básica é escolher a palavra que minimiza o espaço da solução. O algoritmo é realmente lento para o primeiro palpite (quero dizer 'dias'), mas o melhor primeiro palpite depende apenas da palavra len, por isso é codificado na fonte. As outras suposições são feitas em questão de segundos.

O código

(Compile com g ++ -O3)

#include <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#include <ctime>
#include <cstdlib>

using namespace std;

class LRTimer
{
private:
    time_t start;
public:
    void startTimer(void)
    {
        time(&start);
    }

    double stopTimer(void)
    {
        return difftime(time(NULL),start);
    } 

};

#define MAX_WORD_LEN 15
#define BIT_QM 0x8000

LRTimer timer;
int size, valid, wordLen;

string firstGuess[] = { "", "a", "as", "iao", "ares", 
    "raise", "sailer", "saltier", "costlier", "clarities", 
    "anthelices", "petulancies", "incarcerates", "allergenicity" };

class Pattern
{
public:
    char letters[MAX_WORD_LEN];
    char flag;
    int mask;

    Pattern() 
        : letters(), mask(), flag()
    {
    }

    Pattern(string word) 
        : letters(), mask(), flag()
    {
        init(word);
    }

    void init(string word)
    {
        const char *wdata = word.data();
        for(int i = 0; i < wordLen; i++) {
            letters[i] = wdata[i];
            mask |= 1 << (wdata[i]-'a');
        }
    }

    string dump()
    {
        return string(letters);
    }

    int check(Pattern &secret)
    {
        if ((mask & secret.mask) == 0)
            return 0;

        char g[MAX_WORD_LEN], s[MAX_WORD_LEN];
        int r = 0, q = 0, i, j, k=99;
        for (i = 0; i < wordLen; i++)
        {
            g[i] = (letters[i] ^ secret.letters[i]);
            if (g[i])
            {
                r += r;
                k = 0;
                g[i] ^= s[i] = secret.letters[i];
            }
            else
            {
                r += r + 1;
                s[i] = 0;
            }
        }
        for (; k < wordLen; k++)
        {
            q += q;
            if (g[k]) 
            {
                for (j = 0; j < wordLen; j++)
                    if (g[k] == s[j])
                    {
                        q |= BIT_QM;
                        s[j] = 0;
                        break;
                    }
            }
        }
        return r|q;
    }

    int count(int ck, int limit);

    int propcheck(int limit);

    void filter(int ck);
};

string dumpScore(int ck)
{
    string result(wordLen, 'X');
    for (int i = wordLen; i--;)
    {
        result[i] = ck & 1 ? 'O' : ck & BIT_QM ? '?' : 'X';
        ck >>= 1;
    }
    return result;
}

int parseScore(string ck)
{
    int result = 0;
    for (int i = 0; i < wordLen; i++)
    {
        result += result + (
            ck[i] == 'O' ? 1 : ck[i] == '?' ? BIT_QM: 0
        );
    }
    return result;
}

Pattern space[100000];

void Pattern::filter(int ck)
{
    int limit = valid, i = limit;
//  cerr << "Filter IN Valid " << setbase(10) << valid << " This " << dump() << "\n"; 

    while (i--)
    {
        int cck = check(space[i]);
//      cerr << setbase(10) << setw(8) << i << ' ' << space[i].dump() 
//          << setbase(16) << setw(8) << cck << " (" << Pattern::dumpScore(cck) << ") ";

        if ( ck != cck )
        {
//          cerr << " FAIL\r" ;
            --limit;
            if (i != limit) 
            {
                Pattern t = space[i];
                space[i] = space[limit];
                space[limit] = t;
            }
        }
        else
        {
//          cerr << " PASS\n" ;
        }
    }
    valid = limit;
//  cerr << "\nFilter EX Valid " << setbase(10) << valid << "\n"; 
};

int Pattern::count(int ck, int limit)
{
    int i, num=0;
    for (i = 0; i < valid; ++i)
    {
        if (ck == check(space[i]))
            if (++num >= limit) return num;
    }
    return num;
}

int Pattern::propcheck(int limit)
{
    int k, mv, nv;

    for (k = mv = 0; k < valid; ++k)
    {
        int ck = check(space[k]);
        nv = count(ck, limit);
        if (nv >= limit)
        {
            return 99999;
        }
        if (nv > mv) mv = nv;
    }
    return mv;
}

int proposal(bool last)
{
    int i, minnv = 999999, mv, result;

    for (i = 0; i < valid; i++) 
    {
        Pattern& guess = space[i];
//      cerr << '\r' << setw(6) << i << ' ' << guess.dump();
        if ((mv = guess.propcheck(minnv)) < minnv)
        {
//          cerr << setw(6) << mv << ' ' << setw(7) << setiosflags(ios::fixed) << setprecision(0) << timer.stopTimer() << " s\n";
            minnv = mv;
            result = i;
        }
    }   
    if (last) 
        return result;
    minnv *= 0.75;
    for (; i<size; i++) 
    {
        Pattern& guess = space[i];
//      cerr << '\r' << setw(6) << i << ' ' << guess.dump();
        if ((mv = guess.propcheck(minnv)) < minnv)
        {
//          cerr << setw(6) << mv << ' ' << setw(7) << setiosflags(ios::fixed) << setprecision(0) << timer.stopTimer() << " s\n";
            minnv = mv;
            result = i;
        }
    }   
    return result;
}

void setup(string wordfile)
{
    int i = 0; 
    string word;
    ifstream infile(wordfile.data());
    while(infile >> word)
    {
        if (word.length() == wordLen) {
            space[i++].init(word);
        }
    }
    infile.close(); 
    size = valid = i;
}

int main(int argc, char* argv[])
{
    if (argc < 2) 
    {
        cerr << "Specify word length";
        return 1;
    }

    wordLen = atoi(argv[1]);

    timer.startTimer();
    setup("wordlist.txt");
    //cerr << "Words " << size 
    //  << setiosflags(ios::fixed) << setprecision(2)
    //  << " " << timer.stopTimer() << " s\n";

    valid = size;
    Pattern Guess = firstGuess[wordLen];
    for (int t = 0; t < 5; t++)
    {
        cout << Guess.dump() << '\n' << flush;
        string score;
        cin >> score;
        int ck = parseScore(score);
        //cerr << "\nV" << setw(8) << valid << " #" 
        //  << setw(3) << t << " : " << Guess.dump()
        //  << " : " << score << "\n";
        if (ck == ~(-1 << wordLen))
        {
            break;
        }
        Guess.filter(ck); 
        Guess = space[proposal(t == 3)];
    }
    // cerr << "\n";

    double time = timer.stopTimer();
    //cerr << setiosflags(ios::fixed) << setprecision(2)
    //   << timer.stopTimer() << " s\n";

    return 0;
}

Minhas pontuações

Avaliação com jargão, 100 rodadas:

4   9000
5   17700
6   22000
7   25900
8   28600
9   29700
10  31000
11  32800
12  33500
13  34900

Total 265'100

Pontuações auto-avaliadas

Aqui estão os meus pontos médios, pontuados em toda a lista de palavras. Não é totalmente confiável porque alguns detalhes do algoritmo foram alterados durante os testes.

 4 # words  6728 PT AVG   100.98 87170.41 s
 5 # words 14847 PT AVG   164.44 42295.38 s
 6 # words 28127 PT AVG   212.27 46550.00 s 
 7 # words 39694 PT AVG   246.16 61505.54 s
 8 # words 49004 PT AVG   273.23 63567.45 s
 9 # words 50655 PT AVG   289.00 45438.70 s
10 # words 43420 PT AVG   302.13 2952.23 s
11 # words 35612 PT AVG   323.62 3835.00 s
12 # words 27669 PT AVG   330.19 5882.98 s
13 # words 19971 PT AVG   339.60 2712.98 s

De acordo com esses números, minha pontuação média deve estar perto de 257'800

PONTUAÇÃO DO POÇO

Por fim, instalei o Ruby, então agora tenho uma pontuação 'oficial':

    4       5       6       7       8       9      10      11      12      13   TOTAL
10700   16300   22000   25700   27400   30300   32000   33800   34700   34800   267700

Minha intenção era criar algo assim. Infelizmente, não consegui descobrir como realmente minimizar o espaço da solução, então eu o aproximei. E o meu é em Python, então é ainda mais lento, haha. Eu também codifiquei o primeiro palpite. O seu é definitivamente melhor que o meu para as cordas mais curtas. Você pode testar com a minha implementação também no mesmo conjunto de entradas para comparar? Também temos um conjunto bem diferente de primeiras suposições.
justhalf

@justhalf Tentei algumas rodadas com o lingo.go. Não verifiquei com o poço (não tenho o Ruby instalado). Nossa pontuação está próxima, acho que é uma questão de sorte.
edc65

A sua é melhor, eu acho, já que sua média relatada é melhor que a pontuação que eu relatei. Embora você pareça levar muito mais tempo.
justhalf

Este parece ser o jogador mais forte até agora. Vou correr o resultado oficial ainda hoje, fique atento!
SirDarius

Ops, correção do meu comentário acima, esqueci que meu envio está em Java.
justhalf

5

Java, 249700 pontos (supera o chinês Perl Goth no meu teste)

Lista de classificação atualizada:

                        4 5 6 7 8 9 10 11 12 13 Total
perl chinese_perl_goth.pl 6700 12300 16900 19200 23000 26100 28500 29600 32100 33900 228300
java Lingo 9400 14700 18900 21000 26300 28700 30300 32400 33800 34200 249700

Aqui está o antigo ranking usandopit.rb :

                        4 5 6 7 8 9 10 11 12 13 Total
ruby player-example.rb 200 400 400 500 1800 1400 1700 1600 3200 4400 15600
ruby player-example2.rb 2700 3200 2500 4300 7300 6300 8200 10400 13300 15000 73200
ruby player-example3.rb 4500 7400 9900 13700 15400 19000 19600 22300 24600 27300 163700
perl chinese_perl_goth.pl 6400 14600 16500 21000 22500 26000 27200 30600 32500 33800 231100
java Lingo 4800 13100 16500 21400 27200 29200 30600 32400 33700 36100 245000

** Classificações **
1: Java Lingo (245000)
2: perl chinese_perl_goth.pl (231100)
3: ruby ​​player-example3.rb (163700)
4: ruby ​​player-example2.rb (73200)
5: ruby ​​player-example.rb (15600)

Comparado a @chineseperlgoth, perco em palavras mais curtas (<6 caracteres), mas ganho em palavras mais longas (> = 6 caracteres).

A ideia é semelhante a @chineseperlgoth, mas a minha principal idéia é encontrar o palpite (pode ser qualquer palavra do mesmo tamanho, não necessariamente uma das possibilidades restantes) que fornece mais informações para o próximo palpite.

Atualmente, ainda estou jogando com a fórmula, mas para o placar acima, escolhi a palavra que produzirá o mínimo para:

-num_confusion * entropy

A versão mais recente usa pontuação diferente para encontrar a próxima melhor estimativa, o que maximiza o número de "possibilidade única" após a estimativa atual. Isso é feito tentando todas as palavras da lista de palavras removidas (para economizar tempo) em todos os possíveis candidatos e ver qual palpite é mais provável de produzir "possibilidade única" (ou seja, após esse palpite, haverá apenas uma resposta possível) para o próximo palpite.

Então, por exemplo, esta corrida:

Começando nova rodada, a palavra é benção
Got: seora
Enviado:? XOXX
Got: topsl
Enviado: XOX? X
Got: monges
Enviado: XO? XO
Got: bewig
Enviado: OXXXX
Got: boons
Enviado: OOOOO
Rodada ganha com 100 pontos

Desde os três primeiros palpites, já temos "* oo * s" com um "n" em algum lugar e ainda precisamos descobrir mais uma letra. Agora, a beleza desse algoritmo é que, em vez de adivinhar palavras semelhantes a essa forma, ele adivinha a palavra que não tem nenhuma relação com suposições anteriores, tentando dar mais letras, revelando a letra que falta. Nesse caso, ele também obtém a posição do "b" ausente corretamente e conclui com o palpite final correto "boons".

Aqui está o código:

import java.util.*;
import java.io.*;

class Lingo{
    public static String[] guessBestList = new String[]{
                                "",
                                "a",
                                "sa",
                                "tea",
                                "orae",
                                "seora", // 5
                                "ariose",
                                "erasion",
                                "serotina",
                                "tensorial",
                                "psalterion", // 10
                                "ulcerations",
                                "culteranismo",
                                "persecutional"};
    public static HashMap<Integer, ArrayList<String>> wordlist = new HashMap<Integer, ArrayList<String>>();

    public static void main(String[] args){
        readWordlist("wordlist.txt");
        Scanner scanner = new Scanner(System.in);
        int wordlen = Integer.parseInt(args[0]);
        int roundNum = 5;
        ArrayList<String> candidates = new ArrayList<String>();
        candidates.addAll(wordlist.get(wordlen));
        String guess = "";
        while(roundNum-- > 0){
            guess = guessBest(candidates, roundNum==4, roundNum==0);
            System.out.println(guess);
            String response = scanner.nextLine();
            if(isAllO(response)){
                break;
            }
            updateCandidates(candidates, guess, response);
            //print(candidates);
        }
    }

    public static void print(ArrayList<String> candidates){
        for(String str: candidates){
            System.err.println(str);
        }
        System.err.println();
    }

    public static void readWordlist(String path){
        try{
            BufferedReader reader = new BufferedReader(new FileReader(path));
            while(reader.ready()){
                String word = reader.readLine();
                if(!wordlist.containsKey(word.length())){
                    wordlist.put(word.length(), new ArrayList<String>());
                }
                wordlist.get(word.length()).add(word);
            }
        } catch (Exception e){
            System.exit(1);
        }
    }

    public static boolean isAllO(String response){
        for(int i=0; i<response.length(); i++){
            if(response.charAt(i) != 'O') return false;
        }
        return true;
    }

    public static String getResponse(String word, String guess){
        char[] wordChar = word.toCharArray();
        char[] result = new char[word.length()];
        Arrays.fill(result, 'X');
        for(int i=0; i<guess.length(); i++){
            if(guess.charAt(i) == wordChar[i]){
                result[i] = 'O';
                wordChar[i] = '_';
            }
        }
        for(int i=0; i<guess.length(); i++){
            if(result[i] == 'O') continue;
            for(int j=0; j<wordChar.length; j++){
                if(result[j] == 'O') continue;
                if(wordChar[j] == guess.charAt(i)){
                    result[i] = '?';
                    wordChar[j] = '_';
                    break;
                }
            }
        }
        return String.valueOf(result);
    }

    public static void updateCandidates(ArrayList<String> candidates, String guess, String response){
        for(int i=candidates.size()-1; i>=0; i--){
            String candidate = candidates.get(i);
            if(!response.equals(getResponse(candidate, guess))){
                candidates.remove(i);
            }
        }
    }

    public static int countMatchingCandidates(ArrayList<String> candidates, String guess, String response){
        int result = 0;
        for(String candidate: candidates){
            if(response.equals(getResponse(candidate, guess))){
                result++;
            }
        }
        return result;
    }

    public static String[] getSample(ArrayList<String> words, int size){
        String[] result = new String[size];
        int[] indices = new int[words.size()];
        for(int i=0; i<words.size(); i++){
            indices[i] = i;
        }
        Random rand = new Random(System.currentTimeMillis());
        for(int i=0; i<size; i++){
            int take = rand.nextInt(indices.length-i);
            result[i] = words.get(indices[take]);
            indices[take] = indices[indices.length-i-1];
        }
        return result;
    }

    public static String guessBest(ArrayList<String> candidates, boolean firstGuess, boolean lastGuess){
        if(candidates.size() == 1){
            return candidates.get(0);
        }
        String minGuess = candidates.get(0);
        int wordlen = minGuess.length();
        if(firstGuess && guessBestList[wordlen].length()==wordlen){
            return guessBestList[wordlen];
        }
        int minMatches = Integer.MAX_VALUE;
        String[] words;
        if(lastGuess){
            words = candidates.toArray(new String[0]);
        } else if (candidates.size()>10){
            words = bestWords(wordlist.get(wordlen), candidates, 25);
        } else {
            words = wordlist.get(wordlen).toArray(new String[0]);
        }
        for(String guess: words){
            double sumMatches = 0;
            for(String word: candidates){
                int matches = countMatchingCandidates(candidates, guess, getResponse(word, guess));
                if(matches == 0) matches = candidates.size();
                sumMatches += (matches-1)*(matches-1);
            }
            if(sumMatches < minMatches){
                minGuess = guess;
                minMatches = sumMatches;
            }
        }
        return minGuess;
    }

    public static String[] bestWords(ArrayList<String> words, ArrayList<String> candidates, int size){
        int[] charCount = new int[123];
        for(String candidate: candidates){
            for(int i=0; i<candidate.length(); i++){
                charCount[(int)candidate.charAt(i)]++;
            }
        }
        String[] tmp = (String[])words.toArray(new String[0]);
        Arrays.sort(tmp, new WordComparator(charCount));
        String[] result = new String[size+Math.min(size, candidates.size())];
        String[] sampled = getSample(candidates, Math.min(size, candidates.size()));
        for(int i=0; i<size; i++){
            result[i] = tmp[tmp.length-i-1];
            if(i < sampled.length){
                result[size+i] = sampled[i];
            }
        }
        return result;
    }

    static class WordComparator implements Comparator<String>{
        int[] charCount = null;

        public WordComparator(int[] charCount){
            this.charCount = charCount;
        }

        public Integer count(String word){
            int result = 0;
            int[] multiplier = new int[charCount.length];
            Arrays.fill(multiplier, 1);
            for(char chr: word.toCharArray()){
                result += multiplier[(int)chr]*this.charCount[(int)chr];
                multiplier[(int)chr] = 0;
            }
            return Integer.valueOf(result);
        }

        public int compare(String s1, String s2){
            return count(s1).compareTo(count(s2));
        }
    }
}

Impressionante, esta entrada é muito forte! Lembro-me de ver jogadores humanos no programa de TV usando uma estratégia semelhante quando não conseguiram adivinhar uma palavra das pistas atuais.
SirDarius

3

Perl

Ainda há espaço para melhorias, mas pelo menos ele supera os exemplos de jogadores incluídos :)

Assume acesso de gravação ao diretório atual para armazenar em cache listas de palavras (para torná-lo um pouco mais rápido); criará wordlist.lenN.storarquivos usando Storable. Se este for um problema, remova read_cached_wordliste altere o código para usar apenas read_wordlist.

Explicação

Primeiro, construo um histograma de frequências de letras em todas as palavras da lista de palavras atual ( build_histogram). Então eu preciso escolher o meu próximo palpite - o que é feito por find_best_word. O algoritmo de pontuação está apenas adicionando os valores do histograma, pulando as letras já vistas. Isso me dá uma palavra que contém as letras mais frequentes na lista de palavras. Se houver mais de uma palavra com uma determinada pontuação, eu escolho uma aleatoriamente. Tendo encontrado uma palavra, eu a envio ao mecanismo do jogo, leia a resposta e tente fazer algo útil com ela :)

Eu mantenho um conjunto de condições, ou seja, letras que podem ocorrer em uma determinada posição em uma palavra. No início, isso é simples (['a'..'z'] x $len), mas é atualizado com base nas dicas fornecidas na resposta (consulte update_conds). Eu construo um regex a partir dessas condições e filtre a lista de palavras através dele.

Durante os testes, descobri que a filtragem acima mencionada não lida ?muito bem com s, daí o segundo filtro ( filter_wordlist_by_reply). Isso tira proveito do fato de que uma letra marcada como ?ocorre na palavra em uma posição diferente e filtra a lista de palavras de acordo.

Essas etapas são repetidas para cada iteração do loop principal, a menos que a solução seja encontrada (ou não é mais possível ler do stdin, o que significa uma falha).

Código

#!perl
use strict;
use warnings;
use v5.10;
use Storable;

$|=1;

sub read_wordlist ($) {
    my ($len) = @_;
    open my $w, '<', 'wordlist.txt' or die $!;
    my @wordlist = grep { chomp; length $_ == $len } <$w>;
    close $w;
    \@wordlist
}

sub read_cached_wordlist ($) {
    my ($len) = @_;
    my $stor = "./wordlist.len$len.stor";
    if (-e $stor) {
        retrieve $stor
    } else {
        my $wl = read_wordlist $len;
        store $wl, $stor;
        $wl
    }
}

sub build_histogram ($) {
    my ($wl) = @_;
    my %histo = ();
    for my $word (@$wl) {
        $histo{$_}++ for ($word =~ /./g);
    }
    \%histo
}

sub score_word ($$) {
    my ($word, $histo) = @_;
    my $score = 0;
    my %seen = ();
    for my $l ($word =~ /./g) {
        if (not exists $seen{$l}) {
            $score += $histo->{$l};
            $seen{$l} = 1;
        }
    }
    $score
}

sub find_best_word ($$) {
    my ($wl, $histo) = @_;
    my @found = (sort { $b->[0] <=> $a->[0] } 
                 map [ score_word($_, $histo), $_ ], @$wl);
    return undef unless @found;
    my $maxscore = $found[0]->[0];
    my @max;
    for (@found) {
        last if $_->[0] < $maxscore;
        push @max, $_->[1];
    }
    $max[rand @max]
}

sub build_conds ($) {
    my ($len) = @_;
    my @c;
    push @c, ['a'..'z'] for 1..$len;
    \@c
}

sub get_regex ($) {
    my ($cond) = @_;
    local $" = '';
    my $r = join "", map { "[@$_]" } @$cond;
    qr/^$r$/
}

sub remove_cond ($$$) {
    my ($conds, $pos, $ch) = @_;
    return if (scalar @{$conds->[$pos]} == 1);
    return unless grep { $_ eq $ch } @{$conds->[$pos]};
    $conds->[$pos] = [ grep { $_ ne $ch } @{$conds->[$pos]} ]
}

sub add_cond ($$$) {
    my ($conds, $pos, $ch) = @_;
    return if (scalar @{$conds->[$pos]} == 1);
    return if grep { $_ eq $ch } @{$conds->[$pos]};
    push @{$conds->[$pos]}, $ch
}

sub update_conds ($$$$) {
    my ($word, $reply, $conds, $len) = @_;
    my %Xes;
    %Xes = ();
    for my $pos (reverse 0..$len-1) {
        my $r = substr $reply, $pos, 1;
        my $ch = substr $word, $pos, 1;

        if ($r eq 'O') {
            $conds->[$pos] = [$ch]
        }

        elsif ($r eq '?') {
            for my $a (0..$len-1) {
                if ($a == $pos) {
                    remove_cond $conds, $a, $ch
                } else {
                    unless (exists $Xes{$a} and $Xes{$a} eq $ch) {
                        add_cond($conds, $a, $ch);
                    }
                }
            }
        }

        elsif ($r eq 'X') {
            $Xes{$pos} = $ch;
            for my $a (0..$len-1) {
                remove_cond $conds, $a, $ch
            }
        }
    }
}

sub uniq ($) {
    my ($data) = @_;
    my %seen; 
    [ grep { !$seen{$_}++ } @$data ]
}

sub filter_wordlist_by_reply ($$$) {
    my ($wl, $word, $reply) = @_;
    return $wl unless $reply =~ /\?/;
    my $newwl = [];
    my $len = length $reply;
    for my $pos (0..$len-1) {
        my $r = substr $reply, $pos, 1;
        my $ch = substr $word, $pos, 1;
        next unless $r eq '?';
        for my $a (0..$len-1) {
            if ($a != $pos) {
                if ('O' ne substr $reply, $a, 1) {
                    push @$newwl, grep { $ch eq substr $_, $a, 1 } @$wl
                }
            }
        }
    }
    uniq $newwl
}

my $len = $ARGV[0] or die "no length";
my $wl = read_cached_wordlist $len;
my $conds = build_conds $len;

my $c=0;
do {
    my $histo = build_histogram $wl;
    my $word = find_best_word $wl, $histo;
    die "no candidates" unless defined $word;
    say $word;
    my $reply = <STDIN>; 
    chomp $reply;
    exit 1 unless length $reply;
    exit 0 if $reply =~ /^O+$/;
    update_conds $word, $reply, $conds, $len;
    $wl = filter_wordlist_by_reply $wl, $word, $reply;
    $wl = [ grep { $_ =~ get_regex $conds } @$wl ]
} while 1

1
Minhas regras de escrita originalmente proibiu de disco, mas eu faço-lhe uma exceção para permitir o armazenamento em cache da lista de palavras, porque o grande que eu encontrei faz a coisa toda irritantemente lento para teste :)
SirDarius

Esta entrada funciona melhor do que minhas próprias tentativas (ainda não publicadas). Você poderia explicar um pouco o seu algoritmo?
SirDarius 15/05

Eu adicionei uma breve explicação; Também corrigimos um pouco a formatação do código.
chinese perl goth

@ SirDarius: Acho que não haveria perda se algum teste específico usasse uma lista de palavras que contenha apenas entradas do tamanho adequado. Embora não deva ser muito difícil para um programa ignorar palavras no arquivo cujo tamanho não seja especificado, a existência de tais palavras atrasaria no mínimo o teste. Além disso, eu me pergunto se não haveria valor em permitir a entrega para especificar um programa opcional que, dada uma lista de palavras e N, iria enviar para a saída padrão uma lista de palavras formatada em qualquer moda é mais útil ...
supercat

... e permite que o programa principal use essa lista em vez de uma lista de palavras brutas (por isso, se for necessária uma pré-análise, ela deverá ser feita apenas uma vez para cada tamanho de palavra, em vez de uma vez por jogo).
Supercat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.