Угадай слово (он же Линго)

13

Цель этой задачи - написать программу, способную угадать слово за наименьшее количество попыток. Он основан на концепции телешоу Lingo ( http://en.wikipedia.org/wiki/Lingo_(US_game_show) ).

правила

Учитывая длину слова, переданного в качестве первого аргумента в командной строке, программа проигрывателя имеет пять попыток угадать слово, записав предположение на его стандартный вывод, за которым следует один \nсимвол.

После предположения программа получает на свой стандартный ввод строку, за которой также следует один \nсимвол.

Строка имеет ту же длину, что и слово для угадывания, и состоит из последовательности следующих символов:

  • X: это означает, что данная буква отсутствует в слове угадать
  • ?: это означает, что данная буква присутствует в слове угадать, но в другом месте
  • O: это означает, что письмо в этом месте было правильно угадано

Например, если слово угадать есть dents, а программа отправляет слово dozes, оно получит, OXX?Oпотому что dи sправильно, eнеуместно, а так oи zнет.

Будьте внимательны, если буква присутствует в попытке угадать больше раз, чем в слове для угадывания, она не будет помечена как ?и Oболее раз, чем количество вхождений буквы в слове для угадывания. Например, если слово «угадать» есть, coziesи программа отправляет tosses, оно получит, XOXXOOпотому что есть только одно, sчтобы найти.

Слова выбираются из списка английских слов. Если слово, отправленное программой, не является допустимым словом правильной длины, попытка считается автоматической неудачей, и Xвозвращаются только слова.
Программа проигрывателя должна предполагать, что файл с именем wordlist.txtи содержащим одно слово в строке присутствует в текущем рабочем каталоге и может быть прочитан при необходимости.
Предположения должны состоять только из буквенных букв нижнего регистра ( [a-z]).
Другие сетевые или файловые операции для программы запрещены.

Игра заканчивается, когда Oвозвращается строка, состоящая только из или после того, как программа сделала 5 попыток и не смогла угадать слово.

счет

Счет игры определяется по формуле:

score = 100 * (6 - number_of_attempts)

Так что, если слово правильно угадано с первой попытки, дается 500 очков. Последняя попытка стоит 100 очков.

Неспособность угадать слово дает ноль очков.

Яма

Программы игроков будут оцениваться путем попытки угадать 100 случайных слов для каждой длины слова от 4 до 13 символов включительно.
Случайный выбор слов будет сделан заранее, поэтому все записи должны будут угадывать одни и те же слова.

Победившая программа и принятый ответ будут теми, кто наберет наибольшее количество очков.

Программы будут запускаться на виртуальной машине Ubuntu с использованием кода по адресу https://github.com/noirotm/lingo . Реализации на любом языке принимаются до тех пор, пока предоставляются разумные инструкции по их компиляции и / или запуску.

Я предоставляю несколько тестовых реализаций в ruby ​​в репозитории git, не стесняйтесь черпать вдохновение из них.

Этот вопрос будет периодически обновляться с рейтингом опубликованных ответов, чтобы претенденты могли улучшить свои записи.

Официальная итоговая оценка состоится 1 июля .

Обновить

Записи теперь могут предполагать наличие wordlistN.txtфайлов для ускорения чтения списка слов для текущей длины слова для N от 4 до 13 включительно.

Например, есть wordlist4.txtфайл, содержащий все четырехбуквенные слова, wordlist10.txtсодержащий все десятибуквенные слова и т. Д.

Результаты первого тура

На дату 2014-07-01 было подано три заявки со следующими результатами:

                        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)

Все записи выполнялись последовательно, с явным победителем, являясь записью @ edc65 для C ++.

Все участники довольно классные. До сих пор я не мог даже победить @ Chinese-Perl-Goth.
Если будет представлено больше записей, будет проведена другая оценка. Текущие записи также могут быть улучшены, если вы чувствуете, что можете сделать лучше.

SirDarius
источник
1
Просто чтобы уточнить: если программе требуется более 6 попыток угадать слово, она получает отрицательные баллы или просто ноль? Другими словами, нужна ли логика для выхода из программы после 6 попыток избежать отрицательных моментов? (Правила говорят, что ноль очков, если программа не может угадать слово)
DankMemes
1
@ZoveGames после пяти попыток ваша программа должна завершиться, но игровой движок принудительно прекратит ее, если отказывается это сделать :)
SirDarius
1
@RichardA да, не беспокойтесь о Python, это первоклассный гражданин, поэтому у меня не возникнет проблем с запуском некоторого кода на Python :)
SirDarius,
1
@justhalf Большое спасибо за это! Я могу наконец продолжить!
MisterBla
1
@justhalf действительно хорошая идея, я постараюсь реализовать это
SirDarius

Ответы:

5

C ++ 267700 Баллов

Портирование со старого движка MasterMind.
Отличия от MasterMind:

  • Больше слотов
  • Больше символов
  • Большое пространство для решения (но не так много, потому что не все комбинации символов допускаются)
  • Ответ очень информативный, поэтому у нас есть больше информации после каждого предположения
  • Ответ генерируется медленнее, и это жаль, потому что моему алгоритму приходится делать это много.

Основная идея заключается в выборе слова, которое минимизирует пространство решения. Алгоритм действительно медленный для первого предположения (я имею в виду «дни»), но лучшее первое предположение зависит только от слова len, поэтому он жестко закодирован в источнике. Другие догадки делаются за считанные секунды.

Код

(Скомпилировать с 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;
}

Мои оценки

Оценка на жаргоне, 100 раундов:

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

Всего 265'100

Самооценка баллов

Вот мои средние баллы, набранные по всему списку слов. Не вполне достоверно, потому что некоторые детали алгоритма изменились во время тестов.

 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

Согласно этим числам, мой средний балл должен быть около 257'800

PIT SCORE

Наконец я установил Ruby, так что теперь у меня есть «официальный» результат:

    4       5       6       7       8       9      10      11      12      13   TOTAL
10700   16300   22000   25700   27400   30300   32000   33800   34700   34800   267700
edc65
источник
Моим намерением было создать что-то подобное. Увы, я не мог найти, как действительно минимизировать пространство решения, поэтому я приблизил его. И мой в Python, так что это даже медленнее, ха-ха. Я также жестко закодировал первое предположение. Ваш, безусловно, лучше, чем мой, для более коротких строк. Можете ли вы проверить с моей реализацией также на том же наборе ввода для сравнения? Также у нас есть совсем другой набор первых догадок.
полугодие
@justhalf Я попробовал несколько раундов с lingo.go. Я не проверял с ямой (у меня не установлен Ruby). Наши оценки близки, думаю, это вопрос удачи.
edc65
Твой лучше, я думаю, так как твой средний балл лучше, чем тот, который я сообщил. Хотя, похоже, ты занимаешь значительно больше времени.
Полугодие
Кажется, это самый сильный игрок на данный момент. Я собираюсь запустить официальный результат позже сегодня, следите за обновлениями!
SirDarius
К сожалению, поправка для моего комментария выше, я забыл, что моя заявка на Java.
полугодие
5

Java, 249700 баллов (в моем тесте лучше китайского Perl Goth)

Обновлен рейтинг:

                        4 5 6 7 8 9 10 11 12 13 Всего
Perl chinese_perl_goth.pl 6700 12300 16900 19200 23000 26100 28500 29600 32100 33900 228300
Ява Линго 9400 14700 18900 21000 26300 28700 30300 32400 33800 34200 249700

Вот старый список с использованием рейтинга pit.rb:

                        4 5 6 7 8 9 10 11 12 13 Всего
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
Ява Линго 4800 13100 16500 21400 27200 29200 30600 32400 33700 36100 245000

** Рейтинги **
1: Ява Линго (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)

По сравнению с @chineseperlgoth я проигрываю более короткими словами (<6 символов), но выигрываю более длинными словами (> = 6 символов).

Идея похожа на @chineseperlgoth, просто моя основная идея состоит в том, чтобы найти предположение (может быть любым словом той же длины, не обязательно одной из оставшихся возможностей), которое дает больше информации для следующего предположения.

В настоящее время я все еще играю с формулой, но для табло выше я выбираю слово, которое даст минимум для:

-num_confusion * энтропия

В последней версии используется другая оценка, чтобы найти следующую лучшую догадку, которая максимизирует число «единичных возможностей» после текущей догадки. Это делается путем опробования всех слов в сокращенном списке слов (чтобы сэкономить время) на всех возможных кандидатах, и посмотреть, какое предположение более вероятно, чтобы произвести «единственную возможность» (то есть, после этого предположения будет только один возможный ответ) для Следующее предположение.

Так, например, этот прогон:

Начиная новый раунд, слово благо
Получил: seora
Отправлено:? XOXX
Получил: топсл
Отправлено: XOX? X
Есть: монахи
Отправлено: XO? XO
Получил: bewig
Отправлено: OXXXX
Есть: благ
Отправлено: ООООО
Раунд выигран со счетом 100

Из первых трех догадок мы уже получили «* oo * s» где-то с «n», и нам все еще нужно выяснить еще одну букву. Теперь прелесть этого алгоритма в том, что вместо того, чтобы угадывать слова, похожие на эту форму, он вместо этого угадывает слово, которое вообще не имеет отношения к предыдущим догадкам, пытаясь дать больше букв, надеясь выявить пропущенную букву. В этом случае случается также получить правильную позицию для пропущенного «b» и завершает с правильным окончательным предположением «благ».

Вот код:

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));
        }
    }
}
justhalf
источник
Круто, эта запись очень сильная! Я помню, как люди-игроки в телешоу использовали похожую стратегию, когда они не могли угадать слово из текущих подсказок.
SirDarius
3

Perl

Есть еще место для улучшений, но, по крайней мере, это лучше, чем в примере игроков :)

Предполагает доступ для записи в текущий каталог для кэширования списков слов (чтобы он работал немного быстрее); создаст wordlist.lenN.storфайлы используя Storable. Если это проблема, удалите read_cached_wordlistи измените код, чтобы использовать толькоread_wordlist .

объяснение

Сначала я строю гистограмму частот букв во всех словах в текущем списке слов ( build_histogram). Затем мне нужно выбрать мою следующую догадку - что сделано find_best_word. Алгоритм оценки просто складывает значения гистограммы вместе, пропуская уже увиденные буквы. Это дает мне слово, содержащее наиболее часто встречающиеся буквы в списке слов. Если есть более одного слова с данным счетом, я выбираю одно случайно. Найдя слово, я отправляю его на игровой движок, читаю ответ и пытаюсь сделать с ним что-нибудь полезное :)

Я поддерживаю набор условий, то есть букв, которые могут встречаться в определенной позиции в слове. На старте это просто (['a'..'z'] x $len), но обновляется на основе подсказок, приведенных в ответе (см. update_conds). Затем я строю регулярное выражение из этих условий и фильтрую список слов через него.

Во время тестов я обнаружил, что вышеупомянутая фильтрация не ?очень хорошо справляется с s, следовательно, второй фильтр ( filter_wordlist_by_reply). Это использует тот факт, что письмо, помеченное как? слово, встречается в слове в другой позиции, и соответственно фильтрует список слов.

Эти шаги повторяются для каждой итерации основного цикла, если только решение не найдено (или невозможно больше читать из stdin, что означает сбой).

Код

#!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 гот
источник
1
Изначально мои правила запрещали запись на диск, но я делаю исключение, чтобы разрешить кэширование списка слов, потому что найденный мною большой делает все это
крайне
Эта запись работает лучше, чем мои собственные (еще не опубликованные) попытки. Не могли бы вы немного объяснить свой алгоритм?
SirDarius,
Я добавил краткое объяснение; исправил немного форматирование кода.
китайский Perl гот
@SirDarius: Я не думаю, что будет какой-либо убыток, если какой-то конкретный тест использует список слов, который содержит только записи правильной длины. Хотя программе не должно быть слишком сложно игнорировать слова в файле, длина которого отличается от заданной, наличие таких слов как минимум замедлит тестирование. Кроме того , интересно , будет ли значение в разрешении представления указать дополнительную программу , которая, учитывая список слов и N, пришлет на стандартный вывод список слов , отформатированный в любом мода является самым полезным ...
Supercat
... и разрешить основной программе использовать это, а не список необработанных слов (поэтому, если требуется некоторый предварительный анализ, его придется выполнять только один раз для каждой длины слова, а не один раз для каждой игры).
суперкат