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

Тема в разделе "Машинное отделение", создана пользователем WildCat, 18 май 2006.

  1. WildCat
    Оффлайн

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    Принято дебютные библиотеки делать так:

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

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

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

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

    NS Нефёдов Сергей баннер

    Репутация:
    3
    А баз партий в Чекерсные поддавки вообще не существует?
    Как строить - сначала дерево, потом оцениваем конечные позиции, потом оценки перегоняем в корень.
    То есть оценку не конечных ( в библиотеке) позиций наверно запоминать нет смысла.
    Или Это и имелось в виду?
    Строить дерево наверно лучше постепенно его углубляя, после каждого шага сокращая ветви.
     
  3. NS
    Оффлайн

    NS Нефёдов Сергей баннер

    Репутация:
    3
    Хотя не факт - наверно их надо запоминать, и всю текщую библиотеку загонять в Хеш перед обдумыванием каждого хода...
     
  4. WildCat
    Оффлайн

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    В чекерсные поддавки пока нет сильных игроков. Так что и базам партий взятся неоткуда.

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

    NS Нефёдов Сергей баннер

    Репутация:
    3
    Сейчас формализую все свои мысли, и выложу.
     
  6. NS
    Оффлайн

    NS Нефёдов Сергей баннер

    Репутация:
    3
    Как я вижу алгоритм построения дерева:
    1. Все варинты строим на одинаковую глубину. (те, которые не отсечены)
    2. Изначально дерево сотоит из одного хода, глубина есно один.
    Есть три процедуры по дереву
    1. Углубление дерева на один полуход.
    2. Расширение дерева.
    3. Отсечение некорректных вариантов.

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

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    Когда с мыслями соберешься? А то понять твою идею без второго пункта затруднительно. Я так понял, что второй пункт это самый основной.
     
  8. NS
    Оффлайн

    NS Нефёдов Сергей баннер

    Репутация:
    3
    Блин, забыл;)))
    в ширину - записываем все оценки по дереву в Хеш, и по границам оценки, уже полученным, начиная с литьев (чтоб спускать измененную оценку) согласно ОКНУ, добавляем в дерево ходы оценка которых входит в ОКНО, и которых еще нет в библиотеке в данной позиции (Окно при этом не меняется!!!!). Либо добавляем один, опровергающий ход, если он существует, и тогда пересчитываем корневую стоимость дерева, и заново производим процедуру Закрытия/открытия ветвей.
     
  9. WildCat
    Оффлайн

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    Я хочу не так. Чтобы добавить один лист к дереву хочу запускать перебор минут на 5.
     
  10. NS
    Оффлайн

    NS Нефёдов Сергей баннер

    Репутация:
    3
    То есть нужен именно алгоритм для выбора нужного места в дереве?
    (время на перестройку дерева, получение новой оценки в корне, и отсечение согласно окну получается занимает ничтожно-малое время по-сравнению с добавлением листа) Наверно нет смысла добавлять именно по одному листу - ненамного больше займет добавление (либо поиск опровергающего хода) согласно окну(окна?)
     
  11. NS
    Оффлайн

    NS Нефёдов Сергей баннер

    Репутация:
    3
    По логике тогда надо начинать с корня... и просмотрев корень начинать обходить позиции на уровне plu=1, начиная с позиций с максимальной оценкой... И после каждого добавления хода (ходов) в узле произодить процедуру перестройки дерева... :)
    (теперь уже два варинта - начинать с корня, и начинать с конечных ветвей :) )
     
  12. NS
    Оффлайн

    NS Нефёдов Сергей баннер

    Репутация:
    3
    Так ежели дерево небольшое, то выбор узла для достройки можно сделать вручную - программе доверить только вычисление оценки в корне, и сортировку ходов в дереве (согласно оценкам по дереву), и так же вручную задавать границы...
     
  13. WildCat
    Оффлайн

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    Вроде бы получилось сделать что-то работающее:

    Код:
    #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:
    Код:
    struct BookEntry
    {
        unsigned __int64 key;
        int score;
        int depth;
    };
    Предлагайте как сделать лучше!
     
  14. krey
    Оффлайн

    krey Михаил Кройтор Команда форума Команда форума

    Репутация:
    1
    претензий нет :) разве что как преподаватель могу к стилю прикопаться ;)
     
  15. WildCat
    Оффлайн

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    давай
     
  16. krey
    Оффлайн

    krey Михаил Кройтор Команда форума Команда форума

    Репутация:
    1
    ну, во-первых, пользоваться библиотеками языка С совсем не обязательно, когда программируешь на С++. поэтому вместо fstream.h можно использовать fstream, вместо string.h - string, или даже cstring (кстати, разницы между cstring и string.h нет. это одна и та же библиотека). кстати, использование библиотеки С++ string может немного упростить код. а еще, для приведения типов настоятельно рекомендуется использовать модификатор static_cast
    и было бы лучше реализовать все в виде класса.
    но по большому счету притензии не существенны - я сам, когда тороплюсь, не забочусь о таких вещах (то есть, мне тоже пока еще удобней использовать string.h, например). это все дело привычки

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

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    fstream.h - это библиотека С++. Есть только одно реальное отличие: если использовать <fstream>, нужно пользоваться std::
    Считаю, что алгоритмически обе реализованы подобным образом, если не одинаковым.

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

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

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

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

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

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

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    Результат матча реванша: +2 -1 =1.

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

    WildCat Коршунов Игорь Команда форума

    Репутация:
    0
    Вот здесь есть статья о автоматической генерации дебютов: http://www.fierz.ch/strategy4.htm

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

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

    krey Михаил Кройтор Команда форума Команда форума

    Репутация:
    1
    Так это просто замечательно :)
    а я классами пользуюсь, даже если объект будет только один.
    и ПОЗДРАВЛЯЮ С ПОБЕДОЙ В МАТЧ-РЕВАНШЕ!