Генерация дебютной библиотеки

Discussion in 'Машинное отделение' started by WildCat, 18 May 2006.

  1. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    Принято дебютные библиотеки делать так:

    1. вручную
    2. по базе партий очень сильных игроков
    3. движок сам генерирует дебютные варианты

    Здесь мне хотелось бы обсудить 3-ий подход.
    Может у кого есть особо ценные идеи?
    Коротко мои собственные:
    На любой ход противника находим один лучший ответ (перебор на пару минут). Варианты в которых у нас оценка больше пороговой обрываем т.к. считаем, что получили позу с перевесом. Процедуру запускаем два раза: за белых и за черных.
    Запоминаем оценку не только лучшего, но и всех остальных. Т.к. при продлении варианта может оказаться, что оценка лучшего упадет и лучшим станет другой ход. Тогда придется разрабатывать вариант с этим ходом.

    Разнообразие дебютной игры при таком подходе выглядит проблематичным. Разве что продлевать варианты с одинаковыми оценками. Но большого разнообразия все равно не получим.
    Особенно отсутствие разнообразия может быть критичным в матчах.

    Немного о том, зачем все это нужно.
    Мне предстоит играть матч-реванш против Cake (одна из лучших чекерсных программ) в чекерсные поддавки (29.05 в 23:45 по Москве на www.kurnik.org). Первый матч закончился месяц назад 3-0 (http://www.fierz.ch/checkers.htm). Теперь я ожидаю, что сопротивление будет более серьезным. И мне тоже хочется получше подготовитсья.
  2. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    А баз партий в Чекерсные поддавки вообще не существует?
    Как строить - сначала дерево, потом оцениваем конечные позиции, потом оценки перегоняем в корень.
    То есть оценку не конечных ( в библиотеке) позиций наверно запоминать нет смысла.
    Или Это и имелось в виду?
    Строить дерево наверно лучше постепенно его углубляя, после каждого шага сокращая ветви.
  3. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    Хотя не факт - наверно их надо запоминать, и всю текщую библиотеку загонять в Хеш перед обдумыванием каждого хода...
  4. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    В чекерсные поддавки пока нет сильных игроков. Так что и базам партий взятся неоткуда.

    Все оценки сохраняются. Зачем загонять в хэш не понял.
  5. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    Сейчас формализую все свои мысли, и выложу.
  6. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    Как я вижу алгоритм построения дерева:
    1. Все варинты строим на одинаковую глубину. (те, которые не отсечены)
    2. Изначально дерево сотоит из одного хода, глубина есно один.
    Есть три процедуры по дереву
    1. Углубление дерева на один полуход.
    2. Расширение дерева.
    3. Отсечение некорректных вариантов.

    1. - самое простое. Для всех неотсеченных конечных позиций находим лучший ход, и заносим включая расчитанную оценку после его исполнения.
    3. - тоже несложно. Имея конечные оценку мы можем рассчитать оценку начальной позиции.
    (а так же и всех промежуточных)
    Имея оценку начальной позиции - мы можем задать альфа-бета окно.
    Потом все конечные позиции метим как отсеченные.
    Затем начиная с корня - отсекам все ветви ведущие за пределы окна.
    (точнее снимаем признак отсечения для конечных позиций входящих в окно по дереву - то есть входящих в границы после каждого полухода по пути к листу)
    2. Хеширование тут, сейчас - с мыслями соберусь...
  7. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    Когда с мыслями соберешься? А то понять твою идею без второго пункта затруднительно. Я так понял, что второй пункт это самый основной.
  8. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    Блин, забыл;)))
    в ширину - записываем все оценки по дереву в Хеш, и по границам оценки, уже полученным, начиная с литьев (чтоб спускать измененную оценку) согласно ОКНУ, добавляем в дерево ходы оценка которых входит в ОКНО, и которых еще нет в библиотеке в данной позиции (Окно при этом не меняется!!!!). Либо добавляем один, опровергающий ход, если он существует, и тогда пересчитываем корневую стоимость дерева, и заново производим процедуру Закрытия/открытия ветвей.
  9. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    Я хочу не так. Чтобы добавить один лист к дереву хочу запускать перебор минут на 5.
  10. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    То есть нужен именно алгоритм для выбора нужного места в дереве?
    (время на перестройку дерева, получение новой оценки в корне, и отсечение согласно окну получается занимает ничтожно-малое время по-сравнению с добавлением листа) Наверно нет смысла добавлять именно по одному листу - ненамного больше займет добавление (либо поиск опровергающего хода) согласно окну(окна?)
  11. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    По логике тогда надо начинать с корня... и просмотрев корень начинать обходить позиции на уровне plu=1, начиная с позиций с максимальной оценкой... И после каждого добавления хода (ходов) в узле произодить процедуру перестройки дерева... :)
    (теперь уже два варинта - начинать с корня, и начинать с конечных ветвей :) )
  12. NS Нефёдов Сергей

    • Заслуженный
    • Ветеран
    • Старожил
    Member Since:
    02.05.2006
    Message Count:
    6.811
    Likes Received:
    96
    Репутация:
    3
    Location:
    Санкт-Петербург
    Оффлайн
    Так ежели дерево небольшое, то выбор узла для достройки можно сделать вручную - программе доверить только вычисление оценки в корне, и сортировку ходов в дереве (согласно оценкам по дереву), и так же вручную задавать границы...
  13. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    Вроде бы получилось сделать что-то работающее:

    Code:
    #include <map>
    #include <fstream.h>
    #include <windows.h>
    #include <string.h>
    
    #include "GenBook.h"
    #include "Hash.h"
    #include "MoveGeneration.h"
    #include "Moves.h"
    #include "Random.h"
    #include "Search.h"
    
    int MissCnt;
    std::map <unsigned __int64, BookEntry> BookMap;
    
    bool OpeningsGenerating = false; // чтобы перебор знал, что идет генерация дебютов
    
    void OpenBook()
    {
        int i;
    
        cout << "\nReading book...\n";
    
        ifstream book;
        book.open(BOOK_NAME, ios::in | ios::binary);
        book.seekg(0, ios::end);
        int size = book.tellg();
        book.seekg(0, ios::beg);
        BookMap.clear();
        for (i = 0; i < size / sizeof BookEntry; i++)
        {
            BookEntry b;
            book.read((char *)&b, sizeof BookEntry);
            BookMap[b.key] = b;
        }
        cout << BookMap.size() << " positions in book" << endl;
    
        // создаем случайность
        int cnt = GetTickCount() % 100000;
        for (i = 0; i < cnt; i++) random(1);
    }
    
    void InitBook()
    {
        MissCnt = 0;
    }
    
    int GetBookScore()
    {
        std::map <unsigned __int64, BookEntry> :: iterator i = BookMap.find(HashKey);
        if (i != BookMap.end()) return i->second.score;
        return BOOK_POS_NOT_FOUND;
    }
    
    Move *BookProbe(Move *first)
    {
        if (MissCnt > 3) return 0;
        int best_score = -100000;
        int best_cnt = 0;
        for (Move *m = first; m != PML; m++)
        {
            if (wtm) MakeWhiteMove(m);
            else MakeBlackMove(m);
            m->score = GetBookScore();
            if (m->score != BOOK_POS_NOT_FOUND)
            {
                PrintMove(m);
                cout << '\t' << m->score << endl;
                if (m->score > best_score)
                {
                    best_score = m->score;
                    best_cnt = 1;
                }
                else if (m->score == best_score) best_cnt++;
            }
            if (wtm) UndoWhiteMove(m);
            else UndoBlackMove(m);
        }
        if (!best_cnt)
        {
            MissCnt++;
            return 0;
        }
        MissCnt = 0;
        unsigned int random_number = random(best_cnt);
        int n = 0;
        for (m = first; m != PML; m++)
        {
            if (m->score == best_score)
            {
                if (n++ == random_number) return m;
            }
        }
        // сюда попадать не должны
        return 0;
    }
    
    /***************************\
    *      Book generator       *
    \***************************/
    
    #ifndef _USRDLL // только для консольной версии движка
    
    extern int LastScore; // возвращаемая перебором оценка
    
    #define BOOK_MARGIN 10 // насколько хорошие позы отсекаются
    
    void SaveBook()
    {
        cout << "\nstore book...\n";
        ofstream fout(BOOK_NAME, ios::out | ios::binary);
        for (std::map <unsigned __int64, BookEntry> :: const_iterator i = BookMap.begin(); i != BookMap.end(); i++) {
            fout.write((const char *)&i->second, sizeof(BookEntry));
        }
        cout << "stored " << BookMap.size() << " positions" << endl;
    }
    
    char genbookline[1024]; // строковое представление текущего варианта (для отладки)
    
    // Оцениваем конечную позицию, вызывая перебор
    
    int GenBookScoreLeaf()
    {
        std::map <unsigned __int64, BookEntry> :: iterator p = BookMap.find(HashKey);
        if (p != BookMap.end()) return p->second.score;
        SaveBook();
        cout << genbookline << endl;
        if (wtm) BlackRootSearch();
        else
        {
            WhiteRootSearch();
            LastScore = -LastScore;
        }
        BookEntry b;
        b.key = HashKey;
        b.depth = 0;
        b.score = LastScore;
        BookMap[HashKey] = b;
        cout << endl << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
        cout << "GenBookScoreLeaf - " << genbookline << " - " << b.score << endl;
        cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl << endl;
    
        return b.score;
    }
    
    int GenBookAllOpponentMoves(int depth);
    
    // оцениваем все наши ходы и продлеваем лучшие
    // варианты в которых имеем большой перевес (>= BOOK_MARGIN) обрезаются
    
    int GenBookOurBestMove(int depth)
    {
        int pos = strlen(genbookline);
    
        depth--;
        Move *first = PML;
        if (wtm)
        {
            GenerateWhiteCaps();
            if (first == PML) GenerateWhiteMoves();
        }
        else
        {
            GenerateBlackCaps();
            if (first == PML) GenerateBlackMoves();
        }
        if (first == PML){
            cout << endl << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
            cout << "GenBookOurBestMove: no moves - " << genbookline << " - " << -10000 << endl;
            cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl << endl;
            return -10000;
        }
        int size = BookMap.size();
        bool changed = true;
        int best_score;
        while (changed)
        {
            changed = false;
    
            // ищем лучшие ходы
            best_score = -10000;
            Move *m;
            for (m = first; m < PML; m++) {
                if (wtm) MakeWhiteMove(m);
                else MakeBlackMove(m);
                Move2Str(m, genbookline + pos);
                strcat(genbookline, " ");
                int tmp = GenBookScoreLeaf();
                size = BookMap.size();
                if (tmp > best_score) best_score = tmp;
                if (wtm) UndoWhiteMove(m);
                else UndoBlackMove(m);
                genbookline[pos] = 0;
            }
            if (depth == 0) break;
            if (best_score >= BOOK_MARGIN) break; // имеем уже слишком хорошую позу
    
            // продлеваем лучшие варианты
            for (m = first; m < PML; m++) {
                if (wtm) MakeWhiteMove(m);
                else MakeBlackMove(m);
                BookEntry b = BookMap[HashKey];
                if (b.score == best_score && b.depth < depth)
                {
                    Move2Str(m, genbookline + pos);
                    strcat(genbookline, " ");
                    wtm = !wtm;
                    b.score = GenBookAllOpponentMoves(depth);
                    b.depth = depth;
                    BookMap[HashKey] = b;
                    wtm = !wtm;
                    if (b.score > best_score)
                    {
                        best_score = b.score;
                    }
                    if (b.score < best_score)
                    {
                        // оценка лучшего упала
                        // возможно теперь лучшим будет другой ход
                        // повторяем все сначала
                        changed = true;
                    }
                }
                if (wtm) UndoWhiteMove(m);
                else UndoBlackMove(m);
                genbookline[pos] = 0;
                if (changed) break;
            }
        }
        cout << endl << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
        cout << "GenBookOurBestMove - " << genbookline << " - " << best_score << endl;
        cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl << endl;
        PML = first;
        return best_score;
    }
    
    // для всех ходов соперника вызываем GenBookOurBestMove
    
    int GenBookAllOpponentMoves(int depth)
    {
        int pos = strlen(genbookline);
    
        int score = 10000;
    
        Move *first = PML;
        if (wtm)
        {
            GenerateWhiteCaps();
            if (first == PML) GenerateWhiteMoves();
        }
        else
        {
            GenerateBlackCaps();
            if (first == PML) GenerateBlackMoves();
        }
        if (first == PML)
        {
            cout << endl << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
            cout << "GenBookAllOpponentMoves: no moves - " << genbookline << " - " << score << endl;
            cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl << endl;
            return score;
        }
    
        for (Move *m = first; m < PML; m++)
        {
            if (wtm) MakeWhiteMove(m);
            else MakeBlackMove(m);
            Move2Str(m, genbookline + pos);
            wtm = !wtm;
            strcat(genbookline, " ");
            int tmp = GenBookOurBestMove(depth);
            if (tmp < score) score = tmp;
            wtm = !wtm;
            if (wtm) UndoWhiteMove(m);
            else UndoBlackMove(m);
            genbookline[pos] = 0;
        }
        PML = first;
        cout << endl << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl;
        cout << "GenBookAllOpponentMoves - " << genbookline << " - " << score << endl;
        cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << endl << endl;
        return score;
    }
    
    void CreateBook(char *s)
    {
        OpenBook();
    
        OpeningsGenerating = true;
    
        strcpy(genbookline, "");
        int max_depth = atoi(s);
        for (int depth = 1; depth <= max_depth; depth++)
        {
            if (depth % 2) GenBookOurBestMove(depth / 2 + 1); // варианты за белых
            else GenBookAllOpponentMoves(depth / 2); // варианты за черных
        }
    
        OpeningsGenerating = false;
    
        SaveBook();
    }
    
    #endif
    Определение BookEntry:
    Code:
    struct BookEntry
    {
        unsigned __int64 key;
        int score;
        int depth;
    };
    Предлагайте как сделать лучше!
  14. krey Михаил Кройтор

    • Команда форума
    Member Since:
    10.04.2006
    Message Count:
    3.709
    Likes Received:
    50
    Репутация:
    1
    Location:
    Кишинев
    Оффлайн
    претензий нет :) разве что как преподаватель могу к стилю прикопаться ;)
  15. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    давай
  16. krey Михаил Кройтор

    • Команда форума
    Member Since:
    10.04.2006
    Message Count:
    3.709
    Likes Received:
    50
    Репутация:
    1
    Location:
    Кишинев
    Оффлайн
    ну, во-первых, пользоваться библиотеками языка С совсем не обязательно, когда программируешь на С++. поэтому вместо fstream.h можно использовать fstream, вместо string.h - string, или даже cstring (кстати, разницы между cstring и string.h нет. это одна и та же библиотека). кстати, использование библиотеки С++ string может немного упростить код. а еще, для приведения типов настоятельно рекомендуется использовать модификатор static_cast
    и было бы лучше реализовать все в виде класса.
    но по большому счету притензии не существенны - я сам, когда тороплюсь, не забочусь о таких вещах (то есть, мне тоже пока еще удобней использовать string.h, например). это все дело привычки

    добавлено
    а вообще, все что я написал - фигня. тебе даже, по большому счету, работа со строками не нужна. все равно она только для отладки.
  17. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    fstream.h - это библиотека С++. Есть только одно реальное отличие: если использовать <fstream>, нужно пользоваться std::
    Считаю, что алгоритмически обе реализованы подобным образом, если не одинаковым.

    string.h - это любовь с первого раза. Не могу ее бросить :)

    static_cast - только портят читабельность. И кроме того я лучше компилятора знаю, что можно преобразовывать, а что нельзя.

    Классами есть смысл пользоваться только, если будут использоваться несколько объектов этого класса. В моем случае вполне достаточно модульного подхода. Имена постарался давать так, чтобы не было конфликтов. В крайнем случае можно использовать пространства имен.

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

    Вообще, ко мне уже более пяти лет по стилю не придирались :)
    Обычно, наоборот, обращаются за советами по этому вопросу.

    На самом деле я код выложил, чтобы стали ясны мои идеи. И люди могли высказать более продвинутые. Но что-то никто не торопиться.
  18. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    Результат матча реванша: +2 -1 =1.

    Похоже проигрыш был из-за маленькой дебютной библиотеки. Не успел досчитать 6-ply.
  19. TopicStarter Overlay

    WildCat Коршунов Игорь

    • Команда форума
    Member Since:
    04.05.2006
    Message Count:
    3.599
    Likes Received:
    4
    Репутация:
    0
    Location:
    Гомель
    Оффлайн
    Вот здесь есть статья о автоматической генерации дебютов: http://www.fierz.ch/strategy4.htm

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

    Приоритет(поза) = -СуммаОтклонений(ветка_до_позы) - коэфициент * Глубина(поза)
  20. krey Михаил Кройтор

    • Команда форума
    Member Since:
    10.04.2006
    Message Count:
    3.709
    Likes Received:
    50
    Репутация:
    1
    Location:
    Кишинев
    Оффлайн
    Так это просто замечательно :)
    а я классами пользуюсь, даже если объект будет только один.
    и ПОЗДРАВЛЯЮ С ПОБЕДОЙ В МАТЧ-РЕВАНШЕ!

Share This Page