Я не программист и движков не писал. Постепенно разбирался по коду существующих программ. В свое время Игорь Коршунов выкладывал свою программу Murka на форуме Иммортала. Разные версии - от 1200 до 3000. Хороший учебный пример. Я смотрел код, немного менял, компилировал. Может это прозвучит несколько самоуверенно, но для своего уровня более менее разобрался. Если что-то не так, то можно обсудить.
Давно хотел написать гайд по коду движков и выложить где-нибудь на хабре. Уж больно много фейспалма в интернете. Даже на профильных форумах.
Хотелось написать что-то в таком же духе, что выложил выше. Даже вот картинки заранее подготовил. Описать, как все методы работают в системе, в связке. Что важно, а что нет. Такого вроде бы нет в инете. В статьях обычно алгоритмы просто перечисляют, а если где и есть код, то он на уровне середины 70-х (альфа-бета и пара сортировок).
Хочу добраться даже до конкретных примеров из кода Стокфиша, но немного стрёмно, поскольку за всю жизнь написал, отладил и скомпилировал не более 50-100 строк кода. Да и лениво мне стартовать, но сегодня триггер сработал.
Уважаемый booot76.
Прошу вас опубликовать тут последнюю версию учебного макета.
Я думаю будут вопросы по делу.
Предлагаю поэксперементировать и добавить дополнительную нейронную сеть, используемую для быстрой оценки позиций, как в Stockfish.
Еще интересны эндшпильные базы syzygy 3-4-5.
Если не ошибаюсь у вас есть dll для этих баз.
Ну и конечно как и у вас эшдшпиль типа KPvK. (в учебных целях).
Для меня сейчас особо интересна дополнительную нейронную сеть, куда ее можно впихнуть в традиционные алгоритмы и можно ли?
PS
Провел небольшой турнир
Если подскажете как ее опубликовать тут - опубликую . С дополнительными нейросетками пока предлагаю не играться - куда более важные узлы есть. То же и касаемо эндшпилей. Мы тут будем просто из стороны в сторону шарахаться с полусобранным движком. Проблемы у нас не потому что нейросетей не хватает (1 пока достаточно), а в том что переборные алгоримы - "плоские".
В Библиотеке должна появиться кнопка Create a Post
Да, появилась.
booot76 пришлите мне на почту, а я размещу.
Или сами разместите.
Мне кажется, что это будет полезно для всех тех, кто хотел начать, но не знал как
И так я вроде разобрался (на своем уровне) с кодом.
Попутно почитал разные статьи.
Интересный вопрос поднят давным давно -
Что важнее Глупый и быстрый или умный и медленный?
Ответ нам может дать конечно же booot76.
Ждем его с последней версией учебного макета
Кинул макет вам на почту. Разбирайтесь . Добавил туда в принципе основной джентльменский набор современного движка. Макет , конечно, от этого макетом быть не перестал (строго не судите), но "на рыбалку" с ним, наверное, уже можно ходить . Так что выпускаю макет "в свет". Изучайте, пробуйте, задавайте вопросы. По возможности буду заглядывать сюда. Но , если что, лихом не поминайте - время сейчас сложное . Всяко бывает.
Что же касается ответа на ваш вопрос, то зря вы ссылку на чекерс дали . У шахмат и чекерса разные модели игры и даже разный подход к задаче. В шахматах, определенно, умный перебор лучше глупого. Даже если это стоит скорости. А вот в шашках (чекерсе) проблематика немного другая. Дело в том, что там эндшпильные таблицы встречаются по ходу игры ВСЕГДА. И играют несравнимо бОльшую роль. В отличии от шахмат. И досчитаться до них хоть глупым перебором хоть умным, хоть вообще любым - результат будет одинаковый. Там проблема "умного перебора" важна лишь в очень узкий (исчезающе узкий) промежуток игры между дебютной библиотекой и "рыба везде" . Доводилось мне и шашки писать. Вместе с эндшпильной базой.
Тут есть два варианта после публикации
(Заранее прошу прощения, но народ этим функционалом не пользовался, кроме меня , так что проблемки по ходу решим )
MTD(f)
• MTD(f)-Memory-enhanced Test Driver with node n and value f,
алгоритм использующий только поиск с нулевым окном для
определения лучшего хода. В среднем он превосходит Negascout
и Negamax.
• Чаще всего реализуется как поиск с итеративным углублением
MTD(f) 10
• Для того чтобы увеличить производительность еще больше,
можно рассматривать не все перспективные ходы, а только 10 самых перспективных из них.
• Коэффициент ветвления становится фиксированным (10)
существенно увеличивая производительность алгоритма.
• Данная оптимизация рискованна тем, что опасный ход соперника
может оказаться не рассмотренным, но на практике она существенно увеличивает скорость и процент побед.
Есть такой. Точнее был. Я его никогда не пробовал. Это еще с 90-х годов такое известно. Но с них немного воды утекло и к классическому алгоритму немножко полезных довесков было добавлено. Будут ли все они работать на MTD(f) - не факт.
В неподвижном короле с поля а1 (11) за пределы доски можно было уйти максимально только ходом короля-ферзя "по диагонали вниз-влево" на поле 11-11=0.
А на полной доске с поля а1 уже конь может поскакать вниз-влево на поле 11-21=-10. Те же соображения и с полем h8
Мне всегда хотелось что-то покрутить повертеть внутри движка
Что-то включить выключить.
Но пока поигрался с настройками, через UCI
Writeln('option name time type spin min 5 max 8 default 5');
Writeln('option name PolyglotPath type string default <empty>');
Writeln('uciok');
===========================
if pos('setoption',Input_str)>0 then
begin
if pos('time value ',Input_str)>0 then begin
timems:=StrToInt(copy(Input_str,27,1))*1000;
Writeln('timems=',IntToStr(timems));
end;
if pos('PolyglotPath value ',Input_str)>0 then begin
FBookName:=copy(Input_str,35,length(Input_str)-34);
Writeln('FBookName=',FBookName);
end;
continue;
end else
// parse UCI "uci" command
if pos('uci',Input_str)>0 then
begin
....
Решил я скрестить бульдога с носорогом
Пробую постепенно внедрить bitboard.
Пока что удалось, часть функций перевести на bitboard
Warning: Spoiler![ Click to expand ][ Click to hide ]
// Генерируем все псевдолегальные (без проверки на "под шахом") ходы за сторону, чья очередь хода. Сохраняем все полученные ходы в списке.
var
i,Pcolor: integer;
begin
MoveList[0]:=0; // инициализируем список
for i:=11 to 88 do // Просматриваем всю доску
if (Mailbox[i]>=0) then // Смотрим что стоит на легальном поле
begin
PColor:=PieseColor(i,Board);
if Pcolor=Board.SideToMove then // если стоит фигура нашего цвета то генерируем ходы в зависимости от ее типа
begin
if Board.Squares[i]=Pawn*Pcolor then GeneratePawnMoves(i,TypMoves,Board,Movelist) else
GenerateBBMoves(i,TypMoves,Board,Movelist);
end;
end;
if TypMoves=AllMoves then GenerateCastles(Board,Movelist); // рокировки
end;
Warning: Spoiler![ Click to expand ][ Click to hide ]
Procedure GenerateBBMoves(Sq:integer;TypMoves:integer;var Board:Tboard;var MoveList:TMovelist);
// Генерируем псевдолегальные (без проверки на шах) ходы фигур(дальнобойные, конь и король) с поля SQ (цвет фигуры определяется тем чья очередь хода на доске)
// На входе поле фигуры, тип ходов, которые мы хотим генерировать, доска и список куда сохраняем ходы.
var
i,newsq,PColor : integer;
bb: Uint64;
begin
bb:=$0;
//атаки фигур
bb:=bb or (AttacksTable[Board.Squares[sq]](TBitBoard_squares(MailBox[sq]),Board.Occupancy[white] or Board.Occupancy[black])and not Board.Occupancy[Board.SideToMove]);
while bb<>0 do begin
newsq:=Decoder[BitScanForward(bb)];
// Если тип ходов, которые мы генируем "только активные" то проверяем чтобы мы обязательно взяли чужую фигуру
// Для "все ходы" - сохраняем ход без такой проверки
PColor:=PieseColor(newsq,Board);
if (Pcolor<>Board.SideToMove) and ((TypMoves=AllMoves) or (Pcolor=-Board.SideToMove)) then
// Цвет чужой фигуры имеет противоположный знак. У нас 1 - белые, -1 - черные.
begin
inc(Movelist[0]); // Счетчик ходов в списке
Movelist[MoveList[0]]:=MailBox[sq] or (MailBox[newsq] shl 6); // Сохраняем ход в нашей принятой кодировке, смещая поле "куда" на 6 бит влево
end;
bb:=bb and (bb-1);
end;
end;
Ну и функцию
Warning: Spoiler![ Click to expand ][ Click to hide ]
//функция всех атак
Function isAttackedbySide(sq:integer;color:integer;var Board:TBoard):boolean;
// Процедура определяет атаковано ли поле SQ за сторону цвета сolor.
var
i:integer;
bb,occupancy:UInt64;
square:TBitBoard_squares;
begin
result:=true;
//перебираем все фигуры цвета color
bb:=Board.Occupancy[color];
//все фигуры
occupancy:=Board.Occupancy[white] or Board.Occupancy[black];
while bb<>0 do begin
i:=BitScanForward(bb);
//клетка где стоит фигура
square:=TBitBoard_squares(i);
//если фигура атакует клетку sq то выходим
if (AttacksTable[Board.Squares[Decoder[i]]](square,occupancy) and MaskOneBit[MailBox[sq]])<>0 then exit;
bb:=bb and (bb-1);
end;
result:=false;
end;
Я не умею делать мультиплатформенно, поэтому только для 64 битной виндовс.
Чуть подправил функцию isAttackedbySide.
Сравнение Perft 6 начальной позиции на моем ноутбуке - Intel(R) Celeron(R) CPU 1005M @ 1.90GHz
Оригинальный isAttackedbySide и MailBox
Perft 6 - 119060324 nodes. At 29639 msec.4017015 nodes per second
Новый isAttackedbySide и немного bitboard
Perft 6 - 119060324 nodes. At 23255 msec.5119773 nodes per second
Warning: Spoiler![ Click to expand ][ Click to hide ]
Function isAttackedbySide(sq:integer;color:integer;var Board:TBoard):boolean;
// Процедура определяет атаковано ли поле SQ за сторону цвета сolor.
var
temp:UInt64;
count,tp,tp1:integer;
occupancy:UInt64;
square,square1:TBitBoard_squares;
begin
//все фигуры
occupancy:=Board.Occupancy[white] or Board.Occupancy[black];
//атакуемая клетка
square:=TBitBoard_squares(MailBox[sq]);
//ставим суперфигуру на square (ферзь + конь)
//находим все клетки с фигурами на occupancy, атакуемые суперфигурой
temp:=(get_rook_attacks(square,occupancy) or get_bishop_attacks(square,occupancy) or knight_attacks[square]) and occupancy ;
result:=true;
//перебераем все клетки с фигурами, атакуемые суперфигурой
for count:=0 to BitCount(temp)-1 do begin // 64 версия
// get LS1B index
// находим первую клетку
square1:=TBitBoard_squares(BitScanForward(temp));// 64 версия
// pop LS1B
// исключаем первую клеку из temp
temp:=temp and (temp-1);
//выбираем только клетки с фигурами цвета оппонента
if PieseColor(Decoder[ord(square1)],Board)<>color then continue;
//определяем фигуру на этой клетке
tp:=abs(Board.Squares[Decoder[ord(square1)]]);
//определяем, какая из фигур (слон или ладья или конь) может атаковать из sq в square
//BetweenPS[sq,square] - таблица (слон или ладья или конь - может атаковать из sq в square )
tp1:=BetweenPS[square,square1];
if (tp=tp1) or ((tp1<>knight) and (tp=queen)) then begin
//слон или ладья или конь или ферзь - атакует из square1 в square
exit;
end;
//атаки фигуры tp(король или пешка) из клетки из square1 в square
if (AttacksTable[Board.Squares[Decoder[ord(square1)]]](square1,occupancy) and MaskOneBit[MailBox[sq]])<>0 then exit;
end;
result:=false;
end;
Warning: Spoiler![ Click to expand ][ Click to hide ]
Function getSmallestAttacker(sq:integer;var from:integer;var Board:TBoard):integer;
// Возвращает фигуру минимальной стоимости за сторону, чья очередь хода на поле SQ и поле на котором она стоит
var
temp:UInt64;
count,tp,tp1:integer;
occupancy:UInt64;
square,square1:TBitBoard_squares;
//[фигура,позиция=[фигура]+6]
sort_tp:array[1..12] of integer;
begin
Result:=Empty; // По умолчанию мы никого не атакуем при нашей очереди хода
from:=-1; // Невозможное поле где стоит атакующая фигура
FillChar(sort_tp, SizeOf(sort_tp), 0);
//все фигуры
occupancy:=Board.Occupancy[white] or Board.Occupancy[black];
//атакуемая клетка
square:=TBitBoard_squares(MailBox[sq]);
//ставим суперфигуру на square (ферзь + конь)
//находим все клетки с фигурами на occupancy, атакуемые суперфигурой
temp:=(get_rook_attacks(square,occupancy) or get_bishop_attacks(square,occupancy) or knight_attacks[square]) and occupancy ;
//перебераем все клетки с фигурами, атакуемые суперфигурой
for count:=0 to BitCount(temp)-1 do begin // 64 версия
// get LS1B index
// находим первую клетку
square1:=TBitBoard_squares(BitScanForward(temp));// 64 версия
// pop LS1B
// исключаем первую клеку из temp
temp:=temp and (temp-1);
//выбираем только клетки с фигурами своего цвета
if PieseColor(Decoder[ord(square1)],Board)<>Board.SideToMove then continue;
//определяем фигуру на этой клетке
tp:=abs(Board.Squares[Decoder[ord(square1)]]);
//определяем, какая из фигур (слон или ладья или конь) может атаковать из sq в square
//BetweenPS[sq,square] - таблица (слон или ладья или конь - может атаковать из sq в square )
tp1:=BetweenPS[square,square1];
if (tp=tp1) or ((tp1<>knight) and (tp=queen)) then begin
//слон или ладья или конь или ферзь - атакует из square1 в square
sort_tp[tp]:=tp;
sort_tp[tp+6]:=Decoder[ord(square1)];
continue;
end;
//атаки фигуры tp(король или пешка) из клетки из square1 в square
if (AttacksTable[Board.Squares[Decoder[ord(square1)]]](square1,occupancy) and MaskOneBit[MailBox[sq]])<>0 then
begin
//пешка или король - атакует из square1 в square
sort_tp[tp]:=tp;
sort_tp[tp+6]:=Decoder[ord(square1)];
if tp=1 then break;
continue;
end;
end;
for count:=1 to 6 do begin
if sort_tp[count]<>0 then begin
Result:=sort_tp[count];
from:=sort_tp[count+6];
exit;
end;
end;
end;
Perft'ом не проверишь, но мне кажется поглубже заглядывет за 5 сек контроля.
Проведу как я матч, между оригиналом и смесью бульдога с носорогом
PS
Похоже тут надо так
Result:=sort_tp[count]*Board.SideToMove;
Вчера попробовал переписать функцию FindPins, но что-то пошло не так.
Для проверки попытался внедрить функцию EasyPass в Perft
Warning: Spoiler![ Click to expand ][ Click to hide ]
Function Perft(isRoot:boolean;depth:integer; var Board:Tboard):int64;
// Процедура перфоманс теста. принимает на вход позицию и глубину, возвращает количество перебранных легальных ходов. Рекурсивно вызывает саму себя
var
Movelist : TMovelist;
Undo:TUndo;
nodes:int64;
i,nps,cnt:integer;
Pins : int64;
isEasyPass : boolean;
begin
if isRoot then timer:=now; // Если мы в корневой позиции, то стратуем таймер.
nodes:=0;
Pins:=FindPins(Board);
GeneratePseudoMoves(AllMoves,Board,MoveList); // генерируем все псевдолегальные ходы
for i:=1 to MoveList[0] do // Цикл ходов
begin
isEasyPass:=EasyPass(Pins,MoveList[i],Board);
MakeMove(Movelist[i],Board,Undo);// Делаем ход на доске
if isEasyPass or (not isAttackedBySide(Board.King[-Board.SideToMove],Board.SideToMove,Board)) then
begin
// Если король после сделанного хода не остался под шахом (ход был легальный), то идем на глубину ниже
if depth>1
then nodes:=nodes+Perft(false,depth-1,Board)
else inc(nodes); // Если это уже листья дерева, то просто подсчитываем сколько их, легальных.
end;
UnMakeMove(Undo,Board); // Возвращаем ход
end;
// Если мы закончили перебор из корневой позиции, то считаем время и выводим статистику
If isRoot then
begin
cnt:=MillisecondsBetween(timer,now);
if cnt<>0
then nps:=(nodes*1000) div cnt
else nps:=nodes;
writeln('Perft ',depth,' - ',nodes,' nodes. At ',cnt,' msec.',nps,' nodes per second');
end;
Result:=nodes;
end;
Но Perft test не проходит.
booot76, подскажите, как правильно внедрить функцию EasyPass в Perft?