Відгадай слово (він же Lingo)


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повертаються лише s.
Програма програвача повинна припускати, що файл, wordlist.txtщо містить ім’я та містить одне слово на рядок, присутній у поточному робочому каталозі, і його можна прочитати за потреби.
Здогадки повинні складатися лише з алфавітних малих символів ( [a-z]).
Жодні інші операції з мережею або файлами для програми не дозволені.

Гра закінчується, коли Oповертається лише рядок, до складу якої входить, або після того, як програма зробила 5 спроб і не змогла відгадати слово.

Оцінка балів

Оцінка гри задається формулою:

score = 100 * (6 - number_of_attempts)

Тож якщо слово правильно вгадано при першій спробі, дається 500 балів. Остання спроба коштує 100 балів.

Якщо слово не вгадується, це дає нульові бали.

Яма

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

Перемогла програма та прийнята відповідь буде тим, хто досяг найвищого балу.

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

Я пропоную кілька тестових реалізацій в рубіні в сховищі 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)

Всі записи виконувались послідовно, з чітким переможцем, це запис C ++ @ edc65.

Усі учасники конкурсу досить приголомшливі. Я поки що не міг навіть побити @ китайський-перл-гот.
Якщо подано більше записів, відбудеться інша оцінка. Поточні записи можна також покращити, якщо ви відчуваєте, що можете зробити краще.


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

ПІТЬ СКОРО

Нарешті я встановив Ruby, тому тепер у мене є "офіційний" бал:

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

Моїм наміром було створити щось подібне. На жаль, я не зміг знайти, як по-справжньому мінімізувати простір рішення, тому я його наблизив. А моя в Python, так що це навіть повільніше, ха-ха. Я також твердо кодував першу здогадку. Ваш, безумовно, кращий, ніж мій, для коротших струн. Чи можете ви протестувати з моєю реалізацією також на тому ж наборі даних для порівняння? Також у нас є зовсім інший набір перших здогадок.
justhalf

@justhalf Я спробував кілька раундів з lingo.go. Я не перевірився з ямою (у мене не встановлено Ruby). Наші рахунки близькі, я думаю, що це питання удачі.
edc65

З вашим краще, я думаю, оскільки ваш середній показник кращий, ніж оцінка, яку я повідомив. Хоча вам, здається, потрібно значно більше часу.
justhalf

Це здається найсильнішим гравцем поки що. Я збираюся запустити офіційний результат пізніше сьогодні, будьте в курсі!
SirDarius

На жаль, виправлення коментаря вище, я забув, що моє подання знаходиться на Java.
justhalf

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
java Lingo 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
java Lingo 4800 13100 16500 21400 27200 29200 30600 32400 33700 36100 245000

** Рейтинг **
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)

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

Ідея схожа на @chineseperlgoth, моя головна ідея полягає у пошуку здогадки (може бути будь-яке слово однакової довжини, не обов'язково одна з решти можливостей), яке дає найбільшу інформацію для наступного здогаду.

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

-num_confusion * ентропія

Остання версія використовує різні бали, щоб знайти наступну найкращу здогадку, яка максимізує кількість "єдиної можливості" після поточної здогадки. Це робиться, спробувавши всі слова у підрізаному списку слів (щоб заощадити час) на всіх можливих кандидатах, і подивіться, яка здогадка є більшою ймовірністю створити "єдину можливість" (тобто після цієї здогадки буде лише одна можлива відповідь) для Наступна здогадка.

Так, наприклад, цей запуск:

Починаючи новий раунд, слово - це добро
Отримав: seora
Надіслано:? XOXX
Отримав: топсл
Надіслано: XOX? X
Отримали: ченці
Надіслано: XO? XO
Отримав: бейґіг
Надіслано: OXXXX
Отримав: благом
Надіслано: OOOOO
Виграв раунд із оцінкою 100

З перших трьох здогадок ми вже десь отримали "* oo * s" з "n", і нам ще потрібно розібрати ще одну букву. Тепер краса цього алгоритму полягає в тому, що замість відгадування слів, подібних до цієї форми, він замість цього відгадує слово, яке взагалі не має відношення до попередніх здогадок, намагаючись дати більше літер, сподіваючись, розкриваючи пропущену букву. У цьому випадку трапляється також правильно отримати позицію для відсутнього "b", і закінчується правильною остаточною здогадкою "boons".

Ось код:

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

Дивовижний, цей запис серйозно сильний! Я пам’ятаю, як бачили в телешоу люди-гравці, використовуючи подібну стратегію, коли вони не змогли відгадати слово з нинішніх підказів.
СірДарій

3

Perl

Є ще місце для вдосконалення, але принаймні це перевищує включені приклади гравців :)

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

Пояснення

Спочатку я будую гістограму частот літер з усіх слів у поточному списку слів ( build_histogram). Тоді мені потрібно вибрати наступну здогадку - що робиться find_best_word. Алгоритм оцінювання просто додає значення гістограми разом, пропускаючи вже побачені букви. Це дає мені слово, що містить найчастіші букви у списку слів. Якщо з заданою оцінкою є більше одного слова, я вибираю його випадковим чином. Знайшовши слово, я надсилаю його до ігрового двигуна, читаю відповідь, потім спробуйте зробити щось корисне з ним :)

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

Під час тестів я виявив, що згадана фільтрація не справляється ?дуже добре, звідси другий фільтр ( 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

1
Мої правила спочатку забороняли писати на диск, але я роблю виняток, щоб дозволити кешування списку слів, тому що великий, який я знайшов, робить все це набридливо повільним для тестування :)
SirDarius

Цей запис працює краще, ніж мої власні (ще не опубліковані) спроби. Не могли б ви трохи пояснити свій алгоритм?
SirDarius

Я додав коротке пояснення; трохи виправлено форматування коду.
китайський perl goth

@SirDarius: Я не думаю, що було б втрат, якщо якийсь конкретний тест використовує список слів, який містить лише записи належної довжини. Хоча програмі не повинно бути надмірно складно ігнорувати слова у файлі, довжина яких не визначена, наявність таких слів принаймні уповільнить тестування. Крім того, мені цікаво, чи було б корисно в наданні заявок вказувати додаткову програму, яка, даючи список слів і N, надішле на стандартний вихід список слів, форматований будь-яким способом, який є найбільш корисним ...
supercat

... і дозвольте основній програмі використовувати це, а не необроблений список слів (тому якщо потрібен попередній аналіз, його потрібно буде робити лише один раз на кожну довжину слова, а не один раз на гру).
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.