Adivina la palabra (también conocida como Lingo)

13

El objetivo de este desafío es escribir un programa capaz de adivinar una palabra en el menor número posible de intentos. Se basa en el concepto del programa de televisión Lingo ( http://en.wikipedia.org/wiki/Lingo_(US_game_show) ).

Reglas

Dada una longitud de palabra aprobada como primer argumento en su línea de comando, el programa del jugador dispone de cinco intentos de adivinar la palabra escribiendo una suposición en su salida estándar seguida de un solo \ncarácter.

Después de hacer una suposición, el programa recibe una cadena en su entrada estándar, también seguida de un solo \ncarácter.

La cadena tiene la misma longitud que la palabra para adivinar y se compone de una secuencia de los siguientes caracteres:

  • X: lo que significa que la letra dada no está presente en la palabra para adivinar
  • ?: lo que significa que la letra dada está presente en la palabra para adivinar, pero en otra ubicación
  • O: lo que significa que la letra en esta ubicación se ha adivinado correctamente

Por ejemplo, si la palabra que se debe adivinar es dents, y el programa envía la palabra dozes, recibirá OXX?Oporque dy ses correcta, eestá fuera de lugar oy zno está presente.

Tenga cuidado de que si una letra está presente más veces en el intento de adivinar que en la palabra para adivinar, no se marcará como ?y Omás veces que el número de ocurrencias de la letra en la palabra para adivinar. Por ejemplo, si la palabra para adivinar es coziesy el programa envía tosses, la recibirá XOXXOOporque solo hay una spara localizar.

Las palabras se eligen de una lista de palabras en inglés. Si la palabra enviada por el programa no es una palabra válida de la longitud correcta, el intento se considera una falla automática y solo Xse devuelven.
El programa del reproductor debe asumir que un archivo llamado wordlist.txty que contiene una palabra por línea está presente en el directorio de trabajo actual, y puede leerse según sea necesario.
Las conjeturas solo deben estar compuestas de caracteres alfabéticos en minúsculas ( [a-z]).
No se permiten otras operaciones de red o archivo para el programa.

El juego termina cuando una cadena solo se compone de O se devuelve o después de que el programa haya realizado 5 intentos y no haya podido adivinar la palabra.

Puntuación

La puntuación de un juego viene dada por la fórmula dada:

score = 100 * (6 - number_of_attempts)

Entonces, si la palabra se adivina correctamente en el primer intento, se otorgan 500 puntos. El último intento vale 100 puntos.

No adivinar la palabra otorga cero puntos.

El hoyo

Los programas de los jugadores serán evaluados intentando que adivinen 100 palabras al azar para cada longitud de palabra entre 4 y 13 caracteres inclusive.
La selección aleatoria de palabras se realizará por adelantado, por lo que todas las entradas tendrán que adivinar las mismas palabras.

El programa ganador y la respuesta aceptada serán los que alcancen el puntaje más alto.

Los programas se ejecutarán en una máquina virtual Ubuntu, utilizando el código en https://github.com/noirotm/lingo . Se aceptan implementaciones en cualquier idioma siempre que se proporcionen instrucciones razonables para compilarlas o ejecutarlas.

Proporciono algunas implementaciones de prueba en ruby ​​en el repositorio de git, no dude en inspirarse en ellas.

Esta pregunta se actualizará periódicamente con clasificaciones para las respuestas publicadas para que los retadores puedan mejorar sus entradas.

La evaluación final oficial tendrá lugar el 1 de julio .

Actualizar

Las entradas ahora pueden asumir la presencia de wordlistN.txtarchivos para acelerar la lectura de la lista de palabras para la longitud de palabra actual para N entre 4 y 13 inclusive.

Por ejemplo, hay un wordlist4.txtarchivo que contiene las palabras de cuatro letras, y wordlist10.txtcontiene las palabras de diez letras, y así sucesivamente.

Resultados de la primera ronda

A la fecha de 2014-07-01, se han presentado tres entradas, con los siguientes 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 las entradas se realizaron consistentemente, con un claro ganador, siendo la entrada de C ++ de @ edc65.

Todos los concursantes son bastante impresionantes. Hasta ahora no he podido vencer a @ chinese-perl-goth.
Si se envían más entradas, se realizará otra evaluación. Las entradas actuales también se pueden mejorar si cree que puede hacerlo mejor.

SirDarius
fuente
1
Solo para aclarar: si el programa lleva más de 6 intentos de adivinar la palabra, ¿obtiene puntos negativos o simplemente cero? En otras palabras, ¿necesitamos lógica para salir del programa después de 6 intentos para evitar puntos negativos? (Las reglas dicen cero puntos si el programa no adivina la palabra)
DankMemes
1
@ZoveGames después de cinco intentos, su programa debería salir, pero el motor del juego lo terminará por la fuerza si se niega a hacerlo :)
SirDarius
1
@RichardA sí, claro, no te preocupes por Python, es un ciudadano de primera clase, así que no tendré problemas para ejecutar algún código de Python :)
SirDarius
1
@justhalf ¡Muchas gracias por eso! ¡Finalmente puedo continuar!
MisterBla
1
@justhalf buena idea, intentaré implementar eso
SirDarius el

Respuestas:

5

C ++ 267700 puntos

Un portado de un viejo motor MasterMind.
Diferencias de MasterMind:

  • Más tragamonedas
  • Más símbolos
  • Espacio de solución más grande (pero no tanto, porque no se permiten todas las combinaciones de símbolos)
  • La respuesta es muy informativa, por lo que tenemos más información después de cada suposición.
  • La respuesta es más lenta de generar y eso es una pena porque mi algoritmo tiene que hacerlo mucho.

La idea básica es elegir la palabra que minimice el espacio de la solución. El algoritmo es realmente lento para la primera suposición (quiero decir 'días'), pero la mejor primera suposición depende solo de la palabra len, por lo que está codificado en la fuente. Las otras conjeturas se realizan en cuestión de segundos.

El código

(Compilar con 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;
}

Mis puntuaciones

Evaluación con jerga, 100 rondas:

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

Total 265'100

Puntajes autoevaluados

Aquí están mis puntos promedio, anotados en toda la lista de palabras. No es completamente confiable porque algunos detalles del algoritmo han cambiado durante las pruebas.

 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

Según estos números, mi puntaje promedio debería estar cerca de 257'800

PUNTUACIÓN

Finalmente instalé Ruby, así que ahora tengo un puntaje 'oficial':

    4       5       6       7       8       9      10      11      12      13   TOTAL
10700   16300   22000   25700   27400   30300   32000   33800   34700   34800   267700
edc65
fuente
Mi intención era crear algo como esto. Por desgracia, no pude encontrar cómo minimizar realmente el espacio de la solución, así que lo aproximé. Y el mío está en Python, así que es aún más lento, jaja. También codifiqué la primera suposición. La tuya es definitivamente mejor que la mía para las cuerdas más cortas. ¿Puede probar con mi implementación también en el mismo conjunto de entrada para comparar? También tenemos un conjunto bastante diferente de primeras conjeturas.
justhalf
@justhalf Probé algunas rondas con lingo.go. No verifiqué con el pozo (no tengo instalado Ruby). Nuestros puntajes son cercanos, creo que es cuestión de suerte.
edc65
Creo que el suyo es mejor, ya que su promedio reportado es mejor que el puntaje que reporté. Aunque pareces tomar mucho más tiempo.
justhalf 01 de
Este parece ser el jugador más fuerte hasta ahora. Hoy voy a presentar el resultado oficial, ¡estad atentos!
SirDarius
Vaya, corrección de mi comentario anterior, olvidé que mi envío está en Java.
justhalf 01 de
5

Java, 249700 puntos (supera al chino Perl Goth en mi prueba)

Lista de clasificación actualizada:

                        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

Aquí está la antigua lista de clasificación usando pit.rb:

                        4 5 6 7 8 9 10 11 12 13 Total
ruby player-example.rb 200400400500 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

** Rankings **
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)

En comparación con @chineseperlgoth, pierdo en palabras más cortas (<6 caracteres) pero gano en palabras más largas (> = 6 caracteres).

La idea es similar a @chineseperlgoth, es solo que mi idea principal es encontrar la conjetura (puede ser cualquier palabra de la misma longitud, no necesariamente una de las posibilidades restantes) que brinda la mayor cantidad de información para la próxima suposición.

Actualmente todavía estoy jugando con la fórmula, pero para el marcador anterior, elijo la palabra que producirá el mínimo para:

-num_confusion * entropía

La última versión utiliza una puntuación diferente para encontrar la siguiente mejor suposición, que es maximizar el número de "posibilidad única" después de la suposición actual. Esto se hace probando todas las palabras en la lista de palabras podadas (para ahorrar tiempo) en todos los posibles candidatos, y ver qué suposición es más probable que produzca una "posibilidad única" (es decir, después de esta suposición, solo habrá una respuesta posible) para el siguiente conjetura

Entonces, por ejemplo, esta ejecución:

Comenzando una nueva ronda, la palabra es una bendición
Got: seora
Enviado: XOXX
Got: topsl
Enviado: XOX? X
Got: monjes
Enviado: XO? XO
Got: bewig
Enviado: OXXXX
Got: bendiciones
Enviado: OOOOO
Ronda ganada con puntaje 100

De las primeras tres conjeturas, ya obtuvimos "* oo * s" con una "n" en alguna parte y todavía tenemos que encontrar una letra más. Ahora, la belleza de este algoritmo es que, en lugar de adivinar palabras que son similares a esa forma, adivina la palabra que no tiene ninguna relación con las conjeturas anteriores, tratando de dar más letras, con suerte revelando la letra que falta. En este caso, sucede que también obtiene la posición de la "b" faltante correctamente, y concluye con la suposición final correcta "bendiciones".

Aquí está el 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));
        }
    }
}
justo
fuente
¡Impresionante, esta entrada es realmente fuerte! Recuerdo haber visto jugadores humanos en el programa de televisión usando una estrategia similar cuando no podían adivinar una palabra de las pistas actuales.
SirDarius
3

Perl

Todavía hay margen de mejora, pero al menos supera a los jugadores de ejemplo incluidos :)

Asume el acceso de escritura al directorio actual para almacenar en caché las listas de palabras (para que se ejecute un poco más rápido); creará wordlist.lenN.storarchivos usando Storable. Si esto es un problema, elimine read_cached_wordlisty cambie el código para usar solo read_wordlist.

Explicación

Primero, construyo un histograma de frecuencias de letras en todas las palabras en la lista de palabras actual ( build_histogram). Entonces necesito elegir mi siguiente suposición, que se realiza por find_best_word. El algoritmo de puntuación solo agrega los valores del histograma, omitiendo las letras ya vistas. Esto me da una palabra que contiene las letras más frecuentes en la lista de palabras. Si hay más de una palabra con una puntuación dada, elijo una al azar. Después de encontrar una palabra, la envío al motor del juego, leo la respuesta y luego trato de hacer algo útil con ella :)

Mantengo un conjunto de condiciones, es decir, letras que pueden aparecer en una posición determinada de una palabra. Al inicio, esto es simple (['a'..'z'] x $len), pero se actualiza basándose en las sugerencias dadas en la respuesta (ver update_conds). Construyo una expresión regular a partir de esas condiciones y luego filtro la lista de palabras a través de ella.

Durante las pruebas descubrí que el filtrado mencionado anteriormente no maneja ?demasiado bien, de ahí el segundo filtro ( filter_wordlist_by_reply). Esto aprovecha el hecho de que una letra marcada como ?aparece en la palabra en una posición diferente y filtra la lista de palabras en consecuencia.

Estos pasos se repiten para cada iteración del bucle principal, a menos que se encuentre la solución (o ya no sea posible leer desde stdin, lo que significa una falla).

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
perl chino goth
fuente
1
Mis reglas originalmente prohibían escribir en el disco, pero hago una excepción para permitir el almacenamiento en caché de la lista de palabras, porque la gran que encontré hace que todo sea molestamente lento para probar :)
SirDarius
Esta entrada funciona mejor que mis propios intentos (aún no publicados). ¿Podría explicar un poco su algoritmo?
SirDarius
He agregado una breve explicación; También se arregló un poco el formato del código.
Perl chino gótico
@SirDarius: No creo que haya pérdida si alguna prueba en particular usa una lista de palabras que contiene solo entradas de la longitud adecuada. Si bien no debería ser demasiado difícil para un programa ignorar palabras dentro del archivo cuya longitud es diferente a la especificada, la existencia de tales palabras reduciría al mínimo las pruebas. Además, me pregunto si sería útil permitir que las presentaciones especifiquen un programa opcional que, dada una lista de palabras y N, enviaría a la salida estándar una lista de palabras formateada de la manera que sea más útil ...
supercat
... y permitir que el programa principal use eso en lugar de una lista de palabras sin procesar (por lo tanto, si se necesita un análisis previo, solo tendrá que hacerse una vez para cada longitud de palabra, en lugar de una vez por juego).
Supercat