Indovina la parola (aka Lingo)


13

L'obiettivo di questa sfida è scrivere un programma in grado di indovinare una parola nel minor numero possibile di tentativi. Si basa sul concetto dello show televisivo Lingo ( http://en.wikipedia.org/wiki/Lingo_(US_game_show) ).

Regole

Data una lunghezza della parola passata come primo argomento sulla sua riga di comando, il programma giocatore dispone di cinque tentativi di indovinare la parola scrivendo un'ipotesi sul suo output standard seguito da un singolo \ncarattere.

Dopo aver fatto un'ipotesi, il programma riceve una stringa sul suo input standard, seguito anche da un singolo \ncarattere.

La stringa ha la stessa lunghezza della parola da indovinare ed è composta da una sequenza dei seguenti caratteri:

  • X: il che significa che la lettera data non è presente nella parola da indovinare
  • ?: il che significa che la parola data è presente nella parola per indovinare, ma in un'altra posizione
  • O: il che significa che la lettera in questa posizione è stata indovinata correttamente

Ad esempio, se la parola da indovinare è dents, e il programma invia la parola dozes, riceverà OXX?Operché de ssono corretti, eè fuori luogo e oe znon sono presenti.

Fai attenzione che se una lettera è presente più volte nel tentativo di indovinare che nella parola da indovinare, non sarà contrassegnata come ?e Opiù volte del numero di occorrenze della lettera nella parola da indovinare. Ad esempio, se la parola da indovinare è coziese il programma invia tosses, riceverà XOXXOOperché ce n'è solo una sda individuare.

Le parole sono scelte da un elenco di parole inglesi. Se la parola inviata dal programma non è una parola valida della lunghezza corretta, il tentativo viene considerato un errore automatico e Xvengono restituiti solo quelli.
Il programma del lettore dovrebbe presumere che un file con nome wordlist.txte contenente una parola per riga sia presente nella directory di lavoro corrente e possa essere letto secondo necessità.
Le ipotesi devono comprendere solo caratteri alfabetici minuscoli ( [a-z]).
Non sono consentite altre operazioni di rete o file per il programma.

Il gioco termina quando Oviene restituita una stringa composta solo da o dopo che il programma ha effettuato 5 tentativi e non è stato in grado di indovinare la parola.

punteggio

Il punteggio di una partita è dato dalla formula indicata:

score = 100 * (6 - number_of_attempts)

Quindi, se la parola viene indovinata correttamente al primo tentativo, vengono assegnati 500 punti. L'ultimo tentativo vale 100 punti.

Non riuscire a indovinare la parola concede zero punti.

Fossa

I programmi del giocatore saranno valutati cercando di indovinare 100 parole casuali per ogni lunghezza di parole compresa tra 4 e 13 caratteri inclusivamente.
La selezione casuale delle parole verrà effettuata in anticipo, quindi tutte le voci dovranno indovinare le stesse parole.

Il programma vincente e la risposta accettata saranno quelli che raggiungeranno il punteggio più alto.

I programmi verranno eseguiti in una macchina virtuale Ubuntu, usando il codice su https://github.com/noirotm/lingo . Le implementazioni in qualsiasi lingua sono accettate purché vengano fornite istruzioni ragionevoli per la compilazione e / o l'esecuzione.

Sto fornendo alcune implementazioni di test in ruby ​​nel repository git, sentiti libero di prendere ispirazione da loro.

Questa domanda verrà periodicamente aggiornata con classifiche per le risposte pubblicate in modo che gli sfidanti possano migliorare le loro voci.

La valutazione finale ufficiale avrà luogo il 1 ° luglio .

Aggiornare

Le voci possono ora assumere la presenza di wordlistN.txtfile per accelerare la lettura dell'elenco di parole per la lunghezza della parola corrente per N compresa tra 4 e 13 inclusivamente.

Ad esempio, esiste un wordlist4.txtfile contenente tutte le parole di quattro lettere e wordlist10.txtcontenente tutte le parole di dieci lettere e così via.

Risultati del primo turno

Alla data del 01/07/2014, sono state presentate tre voci, con i seguenti risultati:

                        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)

Tutte le voci sono state eseguite in modo coerente, con un chiaro vincitore, essendo la voce C ++ di @ edc65.

Tutti i concorrenti sono davvero fantastici. Finora non sono stato nemmeno in grado di battere @ chinese-perl-goth.
Se vengono inviate più voci, verrà effettuata un'altra valutazione. Le voci correnti possono anche essere migliorate se ritieni di poter fare di meglio.


1
Solo per chiarire: se il programma impiega più di 6 tentativi di indovinare la parola, ottiene punti negativi o solo zero? In altre parole, abbiamo bisogno della logica per uscire dal programma dopo 6 tentativi di evitare punti negativi? (Le regole dicono zero punti se il programma non riesce a indovinare la parola)
DankMemes

1
@ZoveGames dopo cinque tentativi, il tuo programma dovrebbe uscire, ma il motore di gioco lo chiuderà forzatamente se si rifiuta di farlo :)
SirDarius

1
@Richard Sì sì, non preoccuparti di Python, è un cittadino di prima classe, quindi non avrò problemi a far funzionare un codice Python :)
SirDarius,

1
@justhalf Grazie mille per quello! Finalmente posso continuare!
MisterBla,

1
@Justhalf buona idea, cercherò di implementarlo
SirDarius

Risposte:


5

C ++ 267700 punti

Un porting da un vecchio motore MasterMind.
Differenze da MasterMind:

  • Più slot
  • Più simboli
  • Spazio di soluzione più grande (ma non così tanto, perché non è consentita la combinazione di tutti i simboli)
  • La risposta è molto istruttiva, quindi abbiamo più informazioni dopo ogni ipotesi
  • La risposta è più lenta da generare ed è un peccato perché il mio algoritmo deve farlo molto.

L'idea di base è scegliere la parola che minimizza lo spazio della soluzione. L'algoritmo è molto lento per la prima ipotesi (intendo "giorni"), ma la migliore ipotesi prima dipende solo dalla parola len, quindi è codificata nella fonte. Le altre ipotesi sono fatte in pochi secondi.

Il codice

(Compila 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;
}

I miei punteggi

Valutazione con gergo, 100 round:

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

Totale 265'100

Punteggi autovalutati

Ecco i miei punti medi, segnati sull'intero elenco di parole. Non completamente affidabile perché alcuni dettagli dell'algoritmo sono cambiati durante i test.

 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

Secondo questi numeri, il mio punteggio medio dovrebbe essere vicino a 257'800

PUNTEGGIO

Alla fine ho installato Ruby, quindi ora ho un punteggio 'ufficiale':

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

La mia intenzione era quella di creare qualcosa del genere. Purtroppo non sono riuscito a trovare il modo per minimizzare veramente lo spazio della soluzione, quindi l'ho approssimato. E il mio è in Python, quindi è ancora più lento, ahah. Ho anche codificato la prima ipotesi. Il tuo è decisamente meglio del mio per le corde più corte. Puoi testare con la mia implementazione anche sullo stesso set di input da confrontare? Inoltre abbiamo una serie piuttosto diversa di prime ipotesi.
solo

@justhalf Ho provato alcuni round con lingo.go. Non ho verificato con la fossa (non ho installato Ruby). I nostri punteggi sono vicini, penso sia una questione di fortuna.
edc65,

La tua è migliore penso, dal momento che la tua media riportata è migliore del punteggio che ho riportato. Sebbene sembri impiegare molto più tempo.
solo il

Questo sembra essere il giocatore più forte finora. Eseguirò il risultato ufficiale più tardi oggi, rimanete sintonizzati!
SirDarius,

Oops, correzione per il mio commento sopra, ho dimenticato che la mia presentazione è in Java.
solo il

5

Java, 249700 punti (batte il cinese Perl Goth nel mio test)

Ranklist aggiornato:

                        4 5 6 7 8 9 10 11 12 13 Totale
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

Ecco la vecchia classifica usando pit.rb:

                        4 5 6 7 8 9 10 11 12 13 Totale
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

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

Rispetto a @chineseperlgoth, perdo in parole più brevi (<6 caratteri) ma vinco in parole più lunghe (> = 6 caratteri).

L'idea è simile a @chineseperlgoth, è solo che la mia idea principale è quella di trovare l'ipotesi (può essere qualsiasi parola della stessa lunghezza, non necessariamente una delle restanti possibilità) che fornisce il maggior numero di informazioni per la prossima ipotesi.

Attualmente sto ancora giocando con la formula, ma per il tabellone sopra, scelgo la parola che produrrà il minimo per:

-num_confusion * entropia

L'ultima versione utilizza diversi punteggi per trovare la migliore ipotesi successiva, che sta massimizzando il numero di "possibilità singola" dopo l'attuale ipotesi. Questo viene fatto provando tutte le parole nella lista di parole potate (per risparmiare tempo) su tutti i possibili candidati, e vedere quale ipotesi è più probabile che produca "unica possibilità" (cioè dopo questa ipotesi ci sarà una sola risposta possibile) per il prossima ipotesi.

Quindi ad esempio questa corsa:

A partire dal nuovo round, la parola è un vantaggio
Got: seora
Inviato:? XOXX
Got: topsl
Inviato: XOX? X
Got: monaci
Inviato: XO? XO
Got: bewig
Inviato: OXXXX
Got: doni
Inviato: OOOOO
Round vinto con punteggio 100

Dalle prime tre ipotesi, abbiamo già ottenuto "* oo * s" con una "n" da qualche parte e dobbiamo ancora capire un'altra lettera. Ora la bellezza di questo algoritmo è che invece di indovinare parole simili a quella forma, indovina invece la parola che non ha alcuna relazione con le ipotesi precedenti, cercando di dare più lettere, sperando di rivelare la lettera mancante. In questo caso capita anche di ottenere correttamente la posizione per la "b" mancante, e si conclude con la supposizione finale corretta "vantaggi".

Ecco il codice:

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));
        }
    }
}

Fantastico, questa voce è davvero forte! Ricordo di aver visto i giocatori umani nello show televisivo usare una strategia simile quando non erano in grado di indovinare una parola dagli indizi attuali.
SirDarius,

3

Perl

C'è ancora spazio per migliorare, ma almeno batte i giocatori di esempio inclusi :)

Presuppone l'accesso in scrittura alla directory corrente per la memorizzazione nella cache degli elenchi di parole (per renderlo più veloce); creerà i wordlist.lenN.storfile usando Storable. Se questo è un problema, rimuovere read_cached_wordliste modificare il codice per usare solo read_wordlist.

Spiegazione

Innanzitutto, costruisco un istogramma delle frequenze delle lettere in tutte le parole della lista di parole corrente ( build_histogram). Quindi ho bisogno di scegliere la mia prossima ipotesi - che è fatta da find_best_word. L'algoritmo di punteggio sta semplicemente sommando i valori dell'istogramma, saltando le lettere già viste. Questo mi dà una parola contenente le lettere più frequenti nella lista di parole. Se esiste più di una parola con un determinato punteggio, ne scelgo una a caso. Dopo aver trovato una parola, la invio al motore di gioco, leggo la risposta e poi provo a fare qualcosa di utile :)

Mantengo un insieme di condizioni, cioè lettere che possono apparire in una determinata posizione in una parola. All'inizio, questo è solo semplice (['a'..'z'] x $len), ma viene aggiornato in base ai suggerimenti forniti nella risposta (vedi update_conds). Costruisco quindi una regex da queste condizioni e filtro la lista di parole attraverso di essa.

Durante i test ho scoperto che il suddetto filtro non gestisce ?troppo bene, quindi il secondo filtro ( filter_wordlist_by_reply). Ciò sfrutta il fatto che una lettera contrassegnata come ?appare nella parola in una posizione diversa e filtra l'elenco di parole di conseguenza.

Questi passaggi vengono ripetuti per ogni iterazione del ciclo principale, a meno che non venga trovata la soluzione (o non sia più possibile leggere dallo stdin, il che significa un errore).

Codice

#!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
Le mie regole inizialmente vietavano la scrittura su disco, ma lo faccio un'eccezione per consentire la memorizzazione nella cache dell'elenco di parole, perché quello grande che ho trovato rende il tutto fastidiosamente lento da testare :)
SirDarius

Questa voce funziona meglio dei miei tentativi (non ancora pubblicati). Potresti spiegare un po 'il tuo algoritmo?
SirDarius

Ho aggiunto una breve spiegazione; risolto un po 'anche la formattazione del codice.
cinese perl goth

@SirDarius: non credo che ci sarebbe alcuna perdita se un determinato test utilizza un elenco di parole che contiene solo voci della lunghezza corretta. Mentre non dovrebbe essere eccessivamente difficile per un programma ignorare le parole all'interno del file la cui lunghezza è diversa da quella specificata, l'esistenza di tali parole rallenterebbe almeno i test. Inoltre, mi chiedo se ci sarebbe valore nel consentire agli invii di specificare un programma opzionale che, dato un elenco di parole e N, invierebbe all'output standard un elenco di parole formattato in qualsiasi modo sia più utile ...
supercat

... e consentire al programma principale di usarlo piuttosto che un elenco di parole non elaborate (quindi se è necessaria una pre-analisi, dovrà essere fatto solo una volta per ogni lunghezza di parole, piuttosto che una volta per gioco).
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.