Русский
Русский
English
Статистика
Реклама

Шахматы

Перевод Как я программировал шахматную партию против брата

13.06.2021 12:23:01 | Автор: admin


Это история о том, как я попытался выиграть у брата партию в шахматы. Всего лишь гребаную одну игру. Что в этом особенного? Хорош ли я в шахматах? Вовсе нет. Научился ли я чему-то в процессе игры? Тоже нет. Может, это история о путешествии ради путешествия, а не цели? Не совсем. Получил ли я хотя бы удовольствие от этого? Не уверен.

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

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

Почему я вообще связался с шахматами


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

И все же однажды на его вызов я поддался. Стоит ли говорить, что проигрыш был разгромным. Я знал правила и основы игры, так как немного играл еще в детстве, но с навыками брата это ни в коей мере сопоставить было нельзя. В последствии, просматривая анализ игры на chess.com, я увидел, что мое тактическое отставание ход за ходом только росло, пока не достигло оценки в +9 (что равно потере ладьи, слона и пешки против отсутствия потерь противника). В тот момент, утратив всяческую надежду, я сдался. Подобная ситуация повторялась на протяжении еще пары партий, когда я понял с этим нужно что-то делать.

Первым моим решением было углубиться в изучение игры.

Попытка первая: изучение


Моя первая попытка улучшить качество игры состояла в очевидном: обратиться к Reddit и YouTube за рекомендациями других обучающихся. В перерывах между уроками от GM Naroditsky, чтением и решением задачек на Lichess я также сыграл несколько игр со случайными соперниками по интернету. Несмотря на все это мой рейтинг оставался низким (1300 1400 Rapid на Lichess).



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

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

Попытка вторая: изучение противника


Шахматная партия в общем делится на три фазы: дебют, миттельшпиль и эндшпиль. После изучения некоторых базовых матовых шаблонов перейти на стадии эндшпиля от весомого преимущества к победе обычно легко, так что первым вопросом для меня стало получение этого преимущества.

На стадии миттельшпиля преимущество обычно достигается за счет развертывания долгосрочной стратегии и применения тактики. Стратегию можно совершенствовать чтением и изучением принципов игры (мне это нравится), а тактика вырабатывается только через решение задач (что мне особенно не нравится). Поэтому я понимал, что в тактических навыках буду однозначно отставать, учитывая, что мой брат решал на chess.com около 20 таких задач ежедневно. Для меня это был недостижимый предел. Таким образом, оставалась всего одна возможность: получать преимущество на стадии дебюта.

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

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

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

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

Попытка третья: программирование


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

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

У меня также был список из более, чем 500 игр, которые брат сыграл на chess.com. А так как я программист, то естественным подходом для меня стало решить эту задачу инженерным путем.

Я начал скачивать сыгранные им партии с помощью API chess.com и разделять их на игры белыми и черными. Далее я сосредоточился на партиях, где брат играл за черных, потому что чувствовал, что имею больше шансов направить игру в нужное мне русло при игре за белых.

import jsonimport requestsdef get_month_games(player, yyyy_mm):    url = 'https://api.chess.com/pub/player/{}/games/{}'    r = requests.get(url.format(player, yyyy_mm))    if not r.ok:        raise Exception('get_month_games failed')    games = json.loads(r.content)    # Format: {games: [{url, pgn}, ...]}    return games['games']# ...

import chess.pgnimport ioimport jsonwith open('games.json') as f:    data = json.load(f)games = []for game in data:    pgn = io.StringIO(game)    games.append(chess.pgn.read_game(pgn))black_games = [g for g in games if g.headers["Black"] == "playerx"]

Далее я сформулировал задачу так: Учитывая все позиции, которые видел PlayerX, какие из них по завершению дебюта скорее всего окажутся для него наименее выгодными?.

На этот раз задача была четко определена, и работа пошла уже в знакомой мне области. Я решил провести анализ в Python, а именно в блокноте Jupyter, потому что цели создавать переиспользуемый инструмент у меня не было, и нужно было лишь изучить доступные данные для нахождения одного решения.

Оказалось, что в Python уже есть отличные библиотеки для работы с шахматами: python-chess (генерация ходов, оценка и визуализация) и python stockfish (привязки для оценки шахматной позиции с помощью известного шахматного движка Stockfish).

Я преобразовал задачу в граф таким образом: узел это частная шахматная позиция (описанная в нотации FEN). Ребро связывает два узла при том, что целевая позиция оказывается достижима из исходной путем допустимого хода. Для всех игр есть один одинаковый стартовый узел: начальная позиция.

Затем я построил граф всех партий, сыгранных PlayerX за черных, дополнительно обозначив каждое ребро количеством раз, которое был совершен соответствующий ход.

class GamesGraph():    def __init__(self):        self.graph = igraph.Graph(directed=True)    def add_move(self, start_fen, end_fen, uci):        vs = self._ensure_vertex(start_fen)        vt = self._ensure_vertex(end_fen)        try:            e = self.graph.es.find(_source=vs.index, _target=vt.index)            e["count"] += 1        except:            e = self.graph.add_edge(vs, vt)            e["uci"] = uci            e["count"] = 1    @property    def start_node(self):        return self.graph.vs.find(chess.STARTING_FEN)    def _ensure_vertex(self, fen):        try:            return self.graph.vs.find(fen)        except:            v = self.graph.add_vertex(name=fen)            v["fen"] = fen            v["turn"] = chess.Board(fen).turn            return v

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



Здесь начальная позиция обозначена квадратным узлом, цвет указывает, чей в этой позиции ход: белых или черных.

Я также хотел получить оценку каждой позиции в плане преимущества белых, для чего использовал Stockfish. Учитывая, что процесс оценки тысяч позиций требует времени, я решил выполнить его отдельно и создал объект JSON, сопоставляющий каждую уникальную позицию FEN с ее оценкой Stockfish.

from stockfish import Stockfishstock = Stockfish(parameters={"Threads": 8})stock.set_depth(20)stock.set_skill_level(20)def eval_pos(fen):    stock.set_fen_position(fen)    return stock.get_evaluation()# fens - это сопоставление между строкой FEN и узлом графа.for fen, node in graph.fens.items():    node.eva = eval_pos(fen)

Оценка преимущества возвращалась в сантипешках или как мат в X ходов, где положительное число означает преимущество белых, а отрицательное преимущество черных:

{"type":"cp", "value":12}    # Преимущество белых в 12 сантипешек.{"type":"mate", "value":-3}  # Черные получают мат в три хода.

100 сантипешек означают преимущество перед оппонентом в одну пешку, а 300 в одну легкую фигуру вроде слона. Однако стоит обратить внимание, что Stockfish присваивает фигурам значение в зависимости от их позиции, значит вполне возможно иметь преимущество в 1000 сантипешек даже при равнозначном количестве фигур на доске.

Мне нужно было отобразить эту оценку во что-то более удобное для обработки, например в числа между 0 и 1. Для этого я навскидку решил, что преимущество в 300+ будет отображаться в 1.0, а отставание в 300+ в 0. Помимо этого, любой мат в X ходов (даже если X равен 20) будет 1 или 0.

# Возвращает [-1;1]def rating(ev, fen):    val = ev["value"]    if ev["type"] == "cp":        # Закрепить -300, +300. Достаточно захватить фигуру.        val = max(-300, min(300, val))        return val / 300.0    # Мат в X ходов: также max рейтинг.    if val > 0: return 1.0    if val < 0: return -1.0    # Это уже мат, но для белых или черных?    b = chess.Board(fen)    return 1.0 if b.turn == chess.WHITE else -1.0# Возвращает [0;1], где 0 - это min, а 1 - это max преимущество для черных.def rating_black(ev, fen):    return -rating(ev, fen) * 0.5 + 0.5

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

  • В каждой позиции можно оценить вероятность совершения конкретного хода путем деления количества проходов по соответствующему ребру на общее количество ходов, выполненных из этой позиции.
  • Теперь каждое ребро будет иметь вес между 0 и 1, где более высокое значение отражает более высокую вероятность прохода по нему из этой позиции.
  • Тогда вероятность прохождения конкретного пути будет произведением вероятностей всех пройденных ребер.

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

  • Они представляли расстояние, а не вероятность (т.е. чем больше расстояние, тем ниже вероятность выбора пути).
  • Расстояние между двумя узлами являлось суммой весов пройденных ребер (в противоположность произведению вероятностей).

На деле это гораздо легче сделать, чем объяснять. Формула очень проста:

distance(e) = -log(prob(e))

В Python это будет выглядеть так:

def compute_edges_weight(vertex):    all_count = sum(map(lambda x: x["count"], vertex.out_edges()))    for edge in vertex.out_edges():        prob = edge["count"] / all_count        edge["prob"] = prob        edge["weight"] = -math.log(prob)

Логарифмирование вероятности выбора ребра даст отрицательное число, потому что вероятность находится между 0 и 1. При этом не стоит беспокоиться о случае, когда вероятность равна нулю (в результате чего логарифмирование бы дало минус бесконечность), поскольку каждое ребро графа было пройдено не менее одного раза. Чем меньше вероятность, тем более отрицательным получится логарифм, значит инвертирование его знака даст то, что нам нужно, так как:

  • Сумма логарифмов равна логарифму произведения их аргументов: log(a) + log(b) = log(a*b).
  • Чем больше результат, тем ниже определяющая его вероятность.



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

В этот момент я произвольно выбрал минимальное значение преимущества и упорядочил пути по вероятности. Первые несколько путей представляли для меня наибольший шанс выйти из дебюта с преимуществом перед PlayerX.

Доработки


Что я выяснил? Среди выданных этим алгоритмом позиций была следующая (ход белых):



Как видите, черные находятся в очень неловком положении (+8.9 согласно Stockfish), потому что g6, последний ход черных, был ошибкой. Белые продолжат, забирая пешку с e5 и слона. На этом партия для черных практически закончена, так как спасать им придется коня, пешку на h7 и слона. Еще один результат алгоритма был таким (ход белых):



Здесь мы видим мат в один ход (детский мат).

Проблема в том, что эти ошибки PlayerX совершал только несколько раз в своих первых партиях и больше не повторял. Ранние атаки ферзем обычно делают только неопытные игроки, и эффективны они только против игроков того же уровня. Выйдя из разряда начинающих, PlayerX уже долгое время не совершал этих ошибок, потому что более грамотные противники так не ходят. Я знал, что такой дебют использовать не получится, поскольку PlayerX умел против него защищаться.

Еще одна проблема была связана с последовательностями ходов, которые происходили только раз, но из типичных позиций. Вероятность их заключительной позиции оказывалась такой же, что и вероятность последней типичной позиции, потому что каждое ребро имело вероятность 1.0 (учитывая, что другие возможности не разыгрывались). В примере ниже можно проследовать по ребрам 7 и 6 (наиболее распространенная позиция на втором ходу), а затем по одному из ребер с 1-ми. Далее все последующие ходы будут сыграны только раз (потому что данная позиция сложилась только в одном матче), в результате чего каждый ход будет иметь вероятность 1.0.



А вот и вероятности:



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

Известная цитата (Б. Брюстера?): В теории нет разницы между теорией и практикой, а вот на практике есть оказалась в этом случае верна, поэтому мне потребовались кое-какие доработки и самостоятельное исследование, чтобы найти более удачные предполагаемые позиции.

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

def compute_edges_weight(vertex, prob_ceiling=0.9):    all_count = sum(map(lambda x: x["count"], vertex.out_edges()))    for edge in vertex.out_edges():        # Уверенности нет... Установим потолок вероятности (default 90%).        prob = min(edge["count"] / all_count, prob_ceiling)        edge["prob"] = prob        edge["weight"] = -math.log(prob)

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

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



Подготовка


В процессе изучения я остановил свой выбор на следующей позиции:



Согласно Lichess, это защита Алехина (атака двух пешек). В этой позиции для черных есть всего один удачный ход (Nb6), после которого они все равно остаются в менее выгодном положении (+0.6 согласно Stockfish). Однако из этой позиции PlayerX зачастую играет на Nf4, что весьма для него неудачно (+2.3). Я создал на Lichess студию и начал просматривать несколько вариаций (хороших ходов и ходов, сыгранных PlayerX).

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

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

Решающая партия


Все случилось так, словно я глядел в будущее: мы с PlayerX попали в позицию защиты Алехина. Оказавшись в неудобной ситуации, он прозевал своего коня на пятом ходу. Оказывается, что даже игроки намного опытнее тебя начинают одну за другой совершать ошибки, когда попадают в проигрышные условия. Легко играть четко, когда ты побеждаешь, но удастся ли тебе сохранить хладнокровие в противоположной ситуации? На 10 ходу я уже вел с преимуществом +7.1, при котором сложно проиграть, но на этом также завершалась проработанная мной схема. Взгляните, насколько стеснены сейчас черные, и как мои фигуры нацелены напасть на короля:



С этого момента я начал тут и там совершать ошибки, но при этом мне удалось сохранять некоторое преимущество вплоть до 27 хода:



К сожалению, я был очень ограничен во времени (мы играли ускоренную 10-минутную партию), поэтому ходить приходилось быстро. В конечном итоге я совершил фатальные ошибки на 32 и 33 ходах, а еще через один получил от своего недобитого противника мат :/



Вот весь матч (с грубыми ошибками и прочим):


Интерактивный просмотр партии: lichess.org/2qKKl2MI

Выводы


Чему я из всего этого научился? Нескольким вещам, большая часть из которых в ретроспективе выглядит очевидной:

  1. Подготовка под конкретного противника может дать значительное преимущество в дебюте.
  2. Начинающие игроки часто упускают возможность воспользоваться сомнительными ходами соперника. В связи с этим легко получить преимущество, доведя противника до позиции, из которой есть лишь один удачный ход.
  3. Дебют не является определяющим. Если вы не умеете действовать по времени и слабы в тактике, то вполне можете проиграть даже абсолютно выигрышные позиции. Шахматная партия порой решается одним неверным ходом.
  4. Очень важно изучать игру, и нет никакого универсального средства против оппонента, который намного опытнее вас. Однако за счет правильной подготовки разрыв в навыках можно сократить .
  5. Применение к шахматам принципов программной разработки оказалось занятной идеей, особенно учитывая, что самоцелью было победить брата.

Использованный мной код лежит в репозитории. Обратите внимание, что я не включил туда данные, и сам код весьма небрежен. Тем не менее, надеюсь, он окажется полезен, в особенности тем, кто еще размышляет на тему освоения компьютерной науки. Смотрите с ее помощью вполне можно решать задачи из реальной жизни, так что это не просто перемещение битов туда-сюда.

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


Подробнее..

Перевод Победа Segfault-ом и другие эксплойты шахматных движков

23.12.2020 10:20:51 | Автор: admin

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

Прелюдия


Universal Chess Interface (UCI) это открытый коммуникационный протокол, позволяющий шахматным движкам общаться с интерфейсами пользователя. Он поддерживается практически каждым шахматным движком, и через этот интерфейс мы будем подключать наш запутыватель (фаззер, fuzzer).

Stockfish


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

Нотация Форсайта-Эдвардса (FEN)


Позиции в шахматных движках задаются через UCI при помощи команды position. Один из её вариантов это команда position fen, использующая формат под названием нотация Форсайта-Эдвардса, сокращённо FEN (ForsythEdwards Notation).

Вот FEN для начальной позиции в стандартных шахматах:

rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1

Слева направо мы начинаем с позиции фигуры по горизонтали, начиная с 8 (пустые поля обозначаются числом), затем указывается активный цвет (в данном случае w белый), после чего идут поля, относящиеся к рокировке и взятию на проходе, и, наконец, количество полуходов и полных ходов.

Отображение игрового состояния


В начале сессии UCI передача команды d приказывает движку отобразить текущую конфигурацию:

d +---+---+---+---+---+---+---+---+ | r | n | b | q | k | b | n | r | 8 +---+---+---+---+---+---+---+---+ | p | p | p | p | p | p | p | p | 7 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 6 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 5 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 4 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 3 +---+---+---+---+---+---+---+---+ | P | P | P | P | P | P | P | P | 2 +---+---+---+---+---+---+---+---+ | R | N | B | Q | K | B | N | R | 1 +---+---+---+---+---+---+---+---+   a   b   c   d   e   f   g   hFen: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1Key: 8F8F01D4562F59FBCheckers: 

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

Начальные ходы


Приступить к запутыванию (фаззингу) UCI довольно просто. Большинство движков получает ввод по stdin, и большинство стандартных фаззеров поддерживают фаззинг по stdin. Можно начать с самого популярного сегодня фаззера afl.

Мы компилируем последнюю версию Stockfish из исходников, заменяем gcc/g++ на их аналоги из afl (это позволяет нам инструментировать приложение и повысить эффективность фаззинга; но вскоре мы увидим, что на самом деле это необязательно).

Создаём очень простой входной файл:

    setoption name Skill Level value 6    set position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1    d

Даже при такой простой конфигурации мы почти сразу же можем найти крэши

Миттельшпиль


Оказывается, что парсер FEN довольно серьёзный источник уязвимости Stockfish. При передаче подвергнутых фаззингу входных данных FEN движок Stockfish часто вылетает. Однако большинство вылетов довольно неинтересны. Вылет движка ещё до начала игры не даёт нам никаких возможностей выиграть, и, как оказалось, мы можем добиться большего

Сначала нам нужно настроить наши входные данные мы хотим, чтобы движок производил больше действий, обеспечивая таким образом больше возможностей для эксплойтов. На сцене появляется команда go.

    setoption name Skill Level value 6    set position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1    d    go depth 5

Команда go приказывает движку подумать о следующем ходе и придумать решение. У неё есть различные параметры, например, infinite и depth, позволяющие настроить длительность обдумывания и количество рассматриваемых ходов. Стоит также заметить, что фаззинг этого интерфейса может создать множество ложноположительных результатов (в частности, ложные баги unique hang, поскольку движок будет выполнять работу, а сессия фаззинга иногда путает её с тем, что программа ошибочно перешла в бесконечный цикл), но их можно спокойно игнорировать.

Мы снова занялись сессией фаззинга, и спустя множество часов обнаружили баг, который выглядел любопытнее это был вылет, происходивший после вызова go.

Проверив его в gdb, мы увидели, что вылет происходит глубоко внутри NNUE.

Eval::NNUE::FeatureTransformer::UpdateAccumulator (c=BLACK, pos=..., this=0x7fffee800000) at nnue/nnue_feature_transformer.h:379379               acc[k] = vec_add_16(acc[k], column[k]);

NNUE (перевёрнутая EUNN, расшифровывающаяся как Efficiently Updatable Neural Network) это часть ядра Stockfish, реализующая искусственный интеллект, благодаря которому движок стал таким сильным. Похоже, у нас появился путь к функциям вычислений в ядре Stockfish, и настало время проверить, сможем ли мы использовать их, чтобы получить преимущество над машиной.

Эндшпиль


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

position fen 4kb1r/p2rqppp/5n2B2p1B1/4P3/1Q6/PPP2PPP/2K4R w - -dgo searchmoves

Что происходит, когда эта информация передаётся Stockfish?

Stockfish 291120 by the Stockfish developers (see AUTHORS file)position fen 4kb1r/p2rqppp/5n2B2p1B1/4P3/1Q6/PPP2PPP/2K4R w - -d    +---+---+---+---+---+---+---+---+    |   |   |   |   | k | b |   | r | 8    +---+---+---+---+---+---+---+---+    | B |   |   | p | q | B | p | p | 7    +---+---+---+---+---+---+---+---+    |   |   |   | P |   | n |   |   | 6    +---+---+---+---+---+---+---+---+    | Q |   |   |   |   |   |   |   | 5    +---+---+---+---+---+---+---+---+    | P | P |   |   | P | P | P |   | 4    +---+---+---+---+---+---+---+---+    |   | K |   |   |   |   | R | P | 3    +---+---+---+---+---+---+---+---+    |   |   |   |   |   |   |   |   | 2    +---+---+---+---+---+---+---+---+    |   |   |   |   |   |   |   |   | 1    +---+---+---+---+---+---+---+---+    a   b   c   d   e   f   g   hFen: 4kb1r/B2pqBpp/3P1n2/Q7/PP2PPP1/1K4RP/8/8 w - - 0 1Key: 2CCEE92BEE2FC8B8Checkers: f7go searchmovesinfo string NNUE evaluation using nn-62ef826d1a6d.nnue enabledinfo depth 1 seldepth 1 multipv 1 score cp 801 nodes 31 nps 31000 tbhits 0 time 1 pv f7d5info depth 2 seldepth 2 multipv 1 score cp 740 nodes 72 nps 72000 tbhits 0 time 1 pv f7d5 e7d6info depth 3 seldepth 3 multipv 1 score cp 405 nodes 171 nps 171000 tbhits 0 time 1 pv f7d5 e7d6 h3h4 f6d5 e4d5info depth 4 seldepth 4 multipv 1 score cp 556 nodes 206 nps 206000 tbhits 0 time 1 pv f7d5 e7d6 d5c4info depth 5 seldepth 5 multipv 1 score cp 677 nodes 288 nps 144000 tbhits 0 time 2 pv f7d5 e7d6 a7e3info depth 6 seldepth 7 multipv 1 score cp 787 nodes 442 nps 221000 tbhits 0 time 2 pv f7d5 e7d6 a7c5info depth 7 seldepth 8 multipv 1 score cp 865 nodes 645 nps 322500 tbhits 0 time 2 pv f7d5 e7d6 a7c5info depth 8 seldepth 12 multipv 1 score cp 990 nodes 1013 nps 337666 tbhits 0 time 3 pv f7d5 f6d5 d6e7info depth 9 seldepth 12 multipv 1 score cp 912 nodes 3879 nps 646500 tbhits 0 time 6 pv f7d5 e7d6 a7c5 d6b8 d5c4 f8c5 a5c5 b8f4info depth 10 seldepth 15 multipv 1 score cp 878 nodes 7305 nps 664090 tbhits 0 time 11 pv f7d5 e7d6 g3c3 d6b4 a5b4 f8b4 c3c8 e8e7 c8h8 b4d6 a7b8 d6c5 a4a5info depth 11 seldepth 20 multipv 1 score cp 873 nodes 12643 nps 743705 tbhits 0 time 17 pv f7d5 e7d6 a7c5 d6c5 a5a8 e8e7 b4c5 f6d5 a8d5 h7h6 g3d3 e7f6 d5d4 f6g6 f4f5 g6h7fish: Job 2, ./stockfish terminated by signal SIGSEGV (Address boundary error)

Здесь нужно многое объяснить, поэтому давайте начнём с самого очевидного. Строка FEN совершенно неправильна: position fen 4kb1r/p2rqppp/5n2B2p1B1/4P3/1Q6/PPP2PPP/2K4R w - -. Горизонталь 3 (5n2B2p1B1) кодирует 15 фигур!

Однако это интерпретируется как 4kb1r/B2pqBpp/3P1n2/Q7/PP2PPP1/1K4RP/8/8 w - - 0 1, что Stockfish считает достаточно правильным, чтобы продолжить изучение.

Внимательные читатели заметили, что, строго говоря, полученная нотация FEN недопустима чёрным поставлен шах слоном на f7, поэтому ход не может быть за белыми. Однако вместо возврата отсутствия допустимых ходов Stockfish предлагает возможные ходы вплоть до указанной глубины, и на этом этапе вылет возникает глубоко внутри его нейронной сети.

Структура этого бага довольно проста: парсер FEN допускает слишком большие вольности, и это позволяет нам структурировать игры таким образом, чтобы запутать Stockfish. Наша следующая цель увидеть, сможем ли мы развить этот эксплоит.

Разные игры в зависимости от того, чей ход


Рассмотрим входящие данные position fen 4kb1r/p2rqpp1Q6/PPP2PPP/2K4R w k и position fen 4kb1r/p2rqpp1Q6/PPP2PPP/2K4R b k. Это почти одинаковые (зловредные) позиции FEN, только в одной указано, что ход за белыми, а в другой что за чёрными.

Что происходит, когда Stockfish интерпретирует эти входящие данные?

Stockfish 291120 by the Stockfish developers (see AUTHORS file)position fen 4kb1r/p2rqpp1Q6/PPP2PPP/2K4R w kd +---+---+---+---+---+---+---+---+ | Q |   |   |   | k | b |   | r | 8 +---+---+---+---+---+---+---+---+ | P | P |   | r | P | P | P |   | 7 +---+---+---+---+---+---+---+---+ |   | K |   |   |   |   | R | P | 6 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 5 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 4 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 3 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 2 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 1 +---+---+---+---+---+---+---+---+   a   b   c   d   e   f   g   hFen: Q3kb1r/PP1rPPP1/1K4RP/8/8/8/8/8 w k - 0 1Key: A5C501AF3C69574FCheckers: a7 go searchmoves.....info depth 30 seldepth 8 multipv 1 score mate 4 nodes 25542 nps 1216285 tbhits 0 time 21 pv b6c6 d7d6 g6d6 e8f7 e7f8q h8f8 g7f8qfish: Job 3, ./stockfish terminated by signal SIGSEGV (Address boundary error)

Stockfish 291120 by the Stockfish developers (see AUTHORS file)position fen 4kb1r/p2rqpp1Q6/PPP2PPP/2K4R b kd +---+---+---+---+---+---+---+---+ | Q |   |   |   | k | b |   | r | 8 +---+---+---+---+---+---+---+---+ | P | P |   | r | P | P | P |   | 7 +---+---+---+---+---+---+---+---+ |   | K |   |   |   |   | R | P | 6 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 5 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 4 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 3 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 2 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 1 +---+---+---+---+---+---+---+---+   a   b   c   d   e   f   g   hFen: Q3kb1r/PP1rPPP1/1K4RP/8/8/8/8/8 b k - 0 1Key: 0530210BF5930C83Checkers: e7 f7 a8 go searchmovesinfo string NNUE evaluation using nn-62ef826d1a6d.nnue enabledinfo depth 0 score mate 0bestmove (none)

Несмотря на то, что полученная доска имеет одинаково недопустимую конфигурацию, поля шахов неодинаковы (и неверны). Как будто бы Stockfish использует фигуры в исходных (зловредных) входящих данных в своём анализе фигур, ставящих шах. В случае хода чёрных шах ставит фигура на e7, в случае хода белых фигуры, ставящей шах на a7, нет. Одинаковые входящие данные, две различные интерпретации и два разных результата.

Соединяем всё вместе


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

Давайте рассмотрим следующую позицию (ход чёрных): чёрным поставлен шах пешкой на d7, но они могут уйти из под шаха (Kxd7 или e8d7).


Такую игру можно закодировать как 4kb1r/p1PPPppp/4RPPP/7K/8/8/8/8 b k - 0 1, и эта игра допустима.

При обычных условиях Stockfish без проблем найдёт наилучший следующий ход для чёрных:

Stockfish 291120 by the Stockfish developers (see AUTHORS file)position fen 4kb1r/p1PPPppp/4RPPP/7K/8/8/8/8 b k - 0 1d +---+---+---+---+---+---+---+---+ |   |   |   |   | k | b |   | r | 8 +---+---+---+---+---+---+---+---+ | p |   | P | P | P | p | p | p | 7 +---+---+---+---+---+---+---+---+ |   |   |   |   | R | P | P | P | 6 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   | K | 5 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 4 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 3 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 2 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 1 +---+---+---+---+---+---+---+---+   a   b   c   d   e   f   g   hFen: 4kb1r/p1PPPppp/4RPPP/7K/8/8/8/8 b k - 0 1Key: D16F5C3E2F9FABD3Checkers: d7 go searchmove....<b>bestmove e8d7 ponder e7e8q</b>

Теперь рассмотрим входящие данные position fen 4kb1r/p2rqppp/5PPP2PPP/2K4R b k.

Stockfish 291120 by the Stockfish developers (see AUTHORS file)position fen 4kb1r/p2rqppp/5PPP2PPP/2K4R b kd +---+---+---+---+---+---+---+---+ |   |   |   |   | k | b |   | r | 8 +---+---+---+---+---+---+---+---+ | p |   | P | P | P | p | p | p | 7 +---+---+---+---+---+---+---+---+ |   |   |   |   | R | P | P | P | 6 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   | K | 5 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 4 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 3 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 2 +---+---+---+---+---+---+---+---+ |   |   |   |   |   |   |   |   | 1 +---+---+---+---+---+---+---+---+   a   b   c   d   e   f   g   hFen: 4kb1r/p1PPPppp/4RPPP/7K/8/8/8/8 b k - 0 1Key: D16F5C3E2F9FABD3Checkers: d7 e7 

Обратите внимание, как наши зловредные входящие данные кодируются в изучаемую нами игру 4kb1r/p1PPPppp/4RPPP/7K/8/8/8/8 b k - 0 1. Однако здесь есть одно серьёзное отличие Stockfish считает, что чёрный король находится дважды под шахом, на d7 И на e7.

Когда мы пытаемся использовать Stockfish найти следующий наилучший ход, движку это сделать не удается:

go searchmove
info string NNUE evaluation using nn-62ef826d1a6d.nnue enabled
info depth 0 score mate 0
bestmove (none)

Мы успешно сбили Stockfish с толку.

Эпилог


Можно ли использовать что-нибудь из этого на практике? Можем ли мы применить какой-то из описанных здесь эксплойтов для победы в шахматной партии?

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

Вероятно ли это? Не особо, но это определённо в пределах возможного откладывание партии и раньше было обычной частью шахмат, да и сейчас иногда случается, бывает, что происходят перезапуски машины (а следовательно, машине необходимо передать самое последнее состояние игры), к тому же существует активное сообщество игроков, играющих днями, неделями или месяцами при помощи движков и компактных форматов игр. Поэтому такой вектор атак, по крайней мере, теоретически, вполне возможен. (См. также #KingMe Attack.)

Но если подходить реалистичнее, то это было забавное научное исследование о природе искусственного интеллекта и изучение того, как можно обманывать машины, чтобы получить преимущество.

Другие примечания и ссылки



Атака Win by Segfault


Создав из шахматных задач более качественный корпус фаззинга, я получила пример атаки отравленной FEN: создаётся FEN, содержащая правильную комбинацию доски, которая при анализе приводит к segfault-ам. Вылет не всегда воспроизводится стабильно, но происходит примерно в половине случаев (в отладчике в 100% случаев).

position fen r1b2rk1pn1/7n/4o3/6Qq/2BB4/pPP2PPP/R5K1 b - - 1 0d+---+---+---+---+---+---+---+---+| r |   | n |   |   | r | k |   | 8+---+---+---+---+---+---+---+---+|   |   |   |   |   |   |   |   | 7+---+---+---+---+---+---+---+---+| Q | q |   |   |   |   |   |   | 6+---+---+---+---+---+---+---+---+|   |   |   |   |   |   |   |   | 5+---+---+---+---+---+---+---+---+| P | P |   |   | B | B |   |   | 4+---+---+---+---+---+---+---+---+| K |   | p | P | P |   |   | P | 3+---+---+---+---+---+---+---+---+|   |   | R |   |   |   |   |   | 2+---+---+---+---+---+---+---+---+|   |   |   |   |   |   |   |   | 1+---+---+---+---+---+---+---+---+a   b   c   d   e   f   g   hFen: r1n2rk1/8/Qq6/8/PP2BB2/K1pPP2P/2R5/8 b - - 1 1Key: 3E7AABA0F8324B04Checkers:go depth 10info string NNUE evaluation using nn-62ef826d1a6d.nnue enabledinfo depth 1 seldepth 1 multipv 1 score cp -666 nodes 302 nps 302000 tbhits 0 time 1 pv f8f4 a6b6 c8b6 e3f4 a8a4 a3b3info depth 2 seldepth 2 multipv 1 score cp -794 nodes 352 nps 352000 tbhits 0 time 1 pv g8g7 a6b6info depth 3 seldepth 3 multipv 1 score cp -903 nodes 491 nps 245500 tbhits 0 time 2 pv f8f7 a6b6 c8b6info depth 4 seldepth 4 multipv 1 score cp -947 nodes 942 nps 471000 tbhits 0 time 2 pv b6c7 f4c7 a8a6info depth 5 seldepth 5 multipv 1 score cp -911 nodes 1141 nps 570500 tbhits 0 time 2 pv g8h8 a6b6 c8b6Thread 2 "stockfish" received signal SIGSEGV, Segmentation fault.[Switching to Thread 0x7ffff722f700 (LWP 2540040)]Eval::NNUE::FeatureTransformer::UpdateAccumulator (c=BLACK, pos=..., this=0x7fffee800000) at nnue/nnue_feature_transformer.h:379379               acc[k] = vec_add_16(acc[k], column[k]);

Питер Бинделс выяснил, что к выведенной выше строке FEN можно прийти через следующую последовательность ходов по правилам:

d3 c5
Bf4 d5
e3 e5
Qh5 g5
Qxg5 f5
Qxf5 c4
b4 c3
a4 Qe7
Qxe5 Bf5
Qxd5 Bh6
Qxf5 Qe6
Qxh7 Ne7
Qxh6 Nc8
Qg7 Nc6
Qxb7 Nd4
Qxa7 O-O
Qa6 Nxc2+
Kd1 Nd4
Be2 Nf3
Bxf3 Qb6
Be4 Qg6
Nd2 Qxg2
Kc2 Qxf2
Ngf3 Qxf3
Rhf1 Qxf1
Rc1 Qh3
Kb3 Qh6
Nc4 Qc6
Nb6 Qxb6
Rc2 Qc6
Ka3 Qb6
h3

Это показывает, что полученная строка FEN является вполне правильной (хоть и маловероятной) конфигурацией доски.
Подробнее..

DagazServer Встречайте Garbo Chess

21.01.2021 18:22:37 | Автор: admin
Кто мне сказал, не получится?
Если мне хочется, сбудется!

Земфира

Плюнь тому в глаза, кто скажет,
что можно объять необъятное!

Козьма Прутков "Плоды раздумья"


Новогодние праздники вновь навалились внезапно. Такое обилие свободного времени было просто необходимо разбавить какой-то осмысленной деятельностью и я решил приделать к своему серверу бота для игры в Шахматы. Готовых шахматных движков существует множество. Я решил остановиться на Garbochess-JS простой и понятной реализации, на языке JavaScript, названной в честь знаменитой актрисы Греты Гарбо (вы можете видеть её на фотографии).

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


Игра, выбранная на карте, запоминается. Поэтому, после ввода логина и пароля, вы попадаете на вкладку запуска этой игры. Вы можете запустить игру (при помощи кнопки Launch) или просмотреть сессии, ожидающие подключения второго игрока (Join) и ранее сыгранные игры (View), при их наличии. Если вы запустите игру, создастся новая сессия, ожидающая подключения второго игрока. Вы попадёте в игру, сможете сделать ход (если выбрали игру первым игроком), но ожидание ответного хода может занять некоторое время (на самом деле, никто не гарантирует, что кто-нибудь вообще подключится к этой сессии). По этой причине, я рекомендую просматривать список сессий, ожидающих подключения, по интересующей вас игре, перед созданием новой сессии при помощи Launch. Вы избавите себя от лишнего ожидания и, может быть, доставите удовольствие другому человеку.

Другая возможность реализована не для всех игр, но если на форме имеется чекбокс Play against AI, вы можете ничего не ждать вовсе. Эта опция запускает режим игры с ботом, отвечающим на сделанные ходы практически немедленно. Как правило, речь идёт об очень простых ботах, без каких-то сложных вычислений (хотя победить некоторых из них, всё равно, может быть тяжело). Для Шахмат такое решение не подходит вовсе, но возможность поиграть с ботом всё-таки имеется.

Чего хотелось


Обычный встраиваемый в игру бот не является для Dagaz чем-то новым. Технически, такая игра представляет собой html-сборку, включающую в себя модуль выбора оптимального для этой игры хода, в зависимости от текущей позиции. К имени html-файла добавляется суффикс "-ai", позволяющий серверу загрузить правильную сборку, при выборе режима игры с ботом. Думаю понятно, что разработать бота, подходящего абсолютно для всех существующих на свете игр невозможно, но можно использовать одни и те же боты для различных, но сходных между собой игр.

Например, этот бот используется очень часто
(function() {function RandomAi(params) {  this.params = params;  if (_.isUndefined(this.params.rand)) {      this.params.rand = _.random;  }}var findBot = Dagaz.AI.findBot;Dagaz.AI.findBot = function(type, params, parent) {  if ((type == "random") || (type == "solver")) {      return new RandomAi(params);  } else {      return findBot(type, params, parent);  }}RandomAi.prototype.setContext = function(ctx, board) {  ctx.board  = board;}RandomAi.prototype.getMove = function(ctx) {  var moves = Dagaz.AI.generate(ctx, ctx.board);  if (moves.length == 0) {            return { done: true, ai: "nothing" };  }  if (moves.length == 1) {      return { done: true, move: moves[0], ai: "once" };  }  var ix = this.params.rand(0, moves.length - 1);  return {      done: true,      move: moves[ix],      ai:   "random"  };}})();

Это не шутка. Дело в том, что в Dagaz боты выстроены в цепочку и бот рандомного выбора хода используется в тех случаях, когда боты, стоящие выше по иерархии, дать ответ отказались (такое возможно в тех случаях, когда любой ход ведёт к поражению). Разумеется, есть и более сложные боты, но, в целом, все они не очень сильные. Прежде всего, это связано с использованием универсальной (а значит медленной) модели, пригодной для абсолютно любых игр, реализуемых Dagaz.

Второе ограничение связано со временем расчёта хода. Для встроенного бота оно не может превышать 2-3 секунд, поскольку все вычисления производятся непосредственно в браузере пользователя и, в силу однопоточности JavaScript, все другие действия, на это время блокируются. Если задержаться слишком долго, браузер выбросит предупреждение о возможном зацикливании на странице или вообще молча остановит все выполняющиеся скрипты (такое случалось в Safari).

Как бороться с этими двумя бедами понятно. Поскольку у нас есть сервер, надо разработать клиента, соединяющегося с ним по REST и отвечающего на ходы других пользователей. Иными словами, надо разработать внешнего бота. В качестве языка разработки можно использовать всё тот же JavaScript и запускать бота в Node.js, на одном хосте с сервером (или где-то ещё, это не принципиально).

Можно добавить дополнительную функциональность
Если бот ничем не занят, он может создавать для выбранной игры новые игровые сессии, подключаться к ним в качестве второго игрока и ожидать подключения игрока-человека. Все эти хотелки можно реализовать небольшим конечным автоматом, работающим по следующей схеме:


После старта, бот выполняет инициализацию, авторизуясь на сервере, после чего переходит к циклу опроса: ищет на сервере сессии, в которых он должен выполнить очередной ход (TURN), загружает текущую позицию (RECO), передаёт её описание в Garbo Chess, а полученный ответный ход отправляет на сервер (MOVE). Если сессий ожидающих хода нет, бот переходит к проверке наличия ожидающих сессий, созданных ботом (CHCK), если таковых нет и общее количество сессий, в которых участвует бот, меньше заданного, создаёт новую (SESS).

При возникновении ошибок (например, в случае устаревания JWT-токена), бот возвращается к фазе инициализации (INIT) для выполнения повторной авторизации, а если ошибка возникла уже там (при недоступности сервера или чём-то подобном) переходит к фазе STOP и останавливается. Вот так всё это выглядит.

Бот может играть одновременно с несколькими игроками, но в каждый момент времени имеет дело не более чем с одной игрой. Таким образом, если игра ведётся с несколькими людьми, каждому придётся ждать чуть дольше. Кроме того, это не самый эффективный способ использования Garbo Chess. Движок поддерживает режим, при котором анализ игры ведётся непрерывно (запускается в отдельном потоке Web Worker-а), но в этом случае, играть можно только с одним противником.

SAN-ы, FEN-ы, PGN-ы


Прежде всего, с шахматным движком было необходимо договориться. В области компьютерных шахмат существует несколько общепринятых нотаций, но, по большому счёту, важно уметь описывать две вещи: текущую расстановку фигур (кто где стоит) и ход (кто куда ходит и как превращается). В первом случае, фактическим стандартом является FEN и именно в таком виде описание позиции необходимо передавать в Garbo Chess.

С этим была небольшая засада
Разумеется, в Dagaz я тоже описываю позиции. Это функциональность, обойтись без которой очень трудно. Представьте себе, что доиграв до середины партии мы вышли из игры, а потом зашли в неё заново (или просто перегрузили страницу). Было бы глупо прокручивать все ходы с самого начала. DagazServer использует описание позиции, чтобы загрузить правильную расстановку фигур сразу, без утомительной перемотки.

Вернее, он действует ещё хитрее: в случае если ожидается ход противника, загружается последняя расстановка фигур, как и было написано, но если ходить должен игрок, DagazServer загружает предыдущую позицию, возникшую в игре на момент начала последнего хода противника. Следующий ход игра получает обычным образом, уже после загрузки html. Благодаря этому, игрок, загружая игру, всегда видит анимацию последнего хода (или нескольких ходов) противника. А если он не успел рассмотреть этот ход во всех подробностях, всегда можно просто перезагрузить страницу.

Таким образом, модули описания позиции, вроде этого важная часть проекта. У меня их несколько и я использую тот или иной, в зависимости от особенностей игры. Почему я не использовал FEN? Просто потому, что Dagaz это не только Шахматы. Приведу простой пример: возможность рокировки, в FEN кодируется как KQkq, но такое описание слишком завязано на правила традиционных шахмат! Даже в слегка изменённых шахматных вариантах нотацию приходится расширять. В любом случае, мне понадобился FEN и я его сделал.

В ответ на полученное описание, Garbo Chess, проработав некоторое время, возвращает лучший (по его мнению) ход (просто пара позиций вроде e2e4 и тип фигуры, при наличии превращения). К сожалению, на момент разработки бота, на сервере было сыграно уже довольно много партий в Шахматы и я хранил описание хода немного в другом виде. Я не хотел, чтобы эти партии сломались и добавил дефис, разделяющий позиции (тот факт, что старые партии не использовали FEN-нотацию роли не играл, при проигрывании уже завершённых партий, описания позиций не используются).

С нотациями пришлось ещё повозиться
Одной из особенностей минимаксных алгоритмов является их детерминированность. При одинаковых условиях, будет формироваться один и тот же ход. Это может привести к однообразной игре бота, что не очень приятно. Я вижу два способа борьбы с этим:

  • Случайное ограничение времени на расчёт хода
  • Книга дебютов

С первым пунктом всё понятно вместо того чтобы жёстко ограничивать время на расчёт хода, например одной секундой, можно задать вилку из минимального и максимального значений и каждый раз брать случайное число в этих пределах. Что касается книги дебютов, то она полезна сама по себе. Шахматные движки придуманы для миттельшпиля. В дебюте и эндшпиле они не столь эффективны. Для эндшпилей существуют таблицы Налимова, а дебюты обрабатываются специальными справочниками, гораздо более компактными.

В силу своей иерархической организации, дебютные таблицы предоставляют некоторую вариативность. При наличии для позиции нескольких лучших ходов, мы можем использовать случайный выбор. Я уже использовал дебютные таблицы в Dagaz, в формате SGF, но для сервера потребовалось реорганизовать их иначе. Вместо последовательностей ходов от начала партии, мне были нужны FEN-описания позиций со списками соответствующих им лучших ходов (это, кстати, позволяет описывать дебютные ловушки, путём сохранения лучших ходов только одной стороны). Здесь очень помогла утилита pgn2fen, обнаруженная в блоге Николая Кисленко, работающая как с SAN так и с Long algebraic notation.

Далее, я создал пользователя, для того чтобы бот мог заходить на сервер и прописал его id в качестве дежурного бота шахматным играм, для того чтобы frontend автоматически включал бота в созданную игру, при выборе опции Play against AI. В целом, новая схема данных выглядела так:


Здесь есть ещё один момент, о котором стоит сказать. Я добавил табличку ai_settings, для хранения настроек бота по отношению к игроку в конкретной игре. Это, своего рода, рейтинг. Если игрок выигрывает, дополнительное время, выделяемое боту на раздумье, увеличивается. При поражениях оно уменьшается. Таким образом, бот может подстраиваться к силе игрока, с которым он играет.

Что получилось


Это одна из игр бота на сайте. И это не дебютная ловушка (можете проверить это самостоятельно)! При ограничении времени на расчёт хода в 1-2 секунды, бот вполне разумно играет против человека и умеет ставить красивые маты. Возможности бота не ограничиваются классическими шахматами. Подойдёт любая игра, в которой правила перемещения фигур не изменены, например "Шахматы Фишера" или "Шахматы Будённого".

Правда, здесь есть небольшая загвоздка
Шахматный движок должен знать, с какой позиции началась игра. Если первым ходит человек, никаких проблем нет после выполнения хода, FEN-описание позиции передаётся на сервер, но если бот ходит первым, описать позицию некому, поскольку сервер ничего не знает о конкретике игры. Мне пришлось применить небольшой трюк загружать FEN-описание начальной позиции даже в том случае, если игра после загрузки ожидает ход бота.

Может возникнуть, своего рода ситуация гонок, когда бот получает описание сессии раньше, чем интерфейс игры успел загрузиться и сохранить FEN начальной позиции. Понятно, что ни к чему хорошему это привести не может, поэтому я добавил атрибут setup_required, требующий наличия FEN на момент загрузки сессии. Если FEN ещё не сформирован, сервер возвращает ошибку, получив которую, бот просто переходит к повторной авторизации, после чего продолжает выполнение обычным образом.

Также, без каких либо изменений, можно играть в "Шахматы втёмную". Если проявить немного фантазии, можно пойти ещё дальше. Помните, как построены миссии кампании в знаменитой "Battle vs Chess"?

Здесь, в качестве бота, используется обычный шахматный движок Fritz. Если подумать, это становится понятно. Подрыв на мине просто потеря фигуры. Бот не будет ходить на мины в том случае если будет их учитывать это просто невыгодно, а суть миссии как раз в том, чтобы заманивать в ловушку его фигуры. Решение простое информация о минах просто не должна передаваться боту.


Вот что получается: я добавляю в FEN описание мин, но перед передачей в Garbo Chess, просто убираю мины из описания, а после получения ответа возвращаю в позицию те мины, которые не взорвались в результате хода бота. Теперь вы можете заманить бота в ловушку уже на DagazServer.

Что дальше


Есть много шахматных игр: Шатрандж, Макрук бот может играть в них, надо только добавить в Garbo Chess новые фигуры. Есть игры на малых досках. Наконец, можно переосмыслить Garbo Chess, создав на его основе универсальный движок, подходящий для более широкого класса игр. Возможностей для развития много, было бы желание.
Подробнее..

Как желание поиграть в шахматы превратилось в написание своего движка. История и реализация

07.04.2021 14:14:13 | Автор: admin
Всем привет! Меня зовут Борис Николаев, сегодня я хотел бы поделиться с вами своими наработками по технической реализации простого шахматного движка на Kotlin.

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



В принципе это две большие темы. Забегая вперед скажу, что при реализации большую часть времени я потратил именно на сам движок, который обеспечивает правила игры, а не на алгоритм игры компьютера.

В шахматах обманчиво простые правила, которые со всеми особыми случаями не так-то просто сделать единообразно. Я приведу пример реализации логики игры на Kotlin, при этом сознательно не буду рассматривать графическую часть движка для этого есть множество вариантов решений, в том числе и готовых. Ну, а для самых любопытных все исходники проекта я выложил на github.

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


Обозначения для шахматных фигур


Для начала определимся с терминами и названиями. Не все, что стоит на доске, является фигурой (англ. piece). Пешку к фигурам не относят. Но это скорее профессиональный сленг. Мы для простоты все будем называть фигурами.

Познакомимся c фигурами и запомним их вид в нашем движке.
Иконка Фигура
Пешка pawn
Конь knight
Слон bishop
Ладья rook
Ферзь queen
Король king
Обзор фигур, и как они ходят
Здесь кратко напомню, как ходят фигуры.

Пешка (англ. pawn) самая слабая фигура. Фактически, пушечное мясо. Двигается только вперед. Атакует только по диагонали вперед на одну клетку.

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

Ладья (устар. тура, англ. rook) фигура основательная. Хотя бы своим видом, похожим на башню. Ходит по горизонтали и вертикали в любом направлении на любое количество клеток, пока не встретит на своем пути другую фигуру. У каждого игрока по две ладьи, которые ставятся по углам доски.

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

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

Ферзь, он же королева (англ. queen) ходит как хочет: и по горизонтали, и по вертикали, и по диагонали в любом направлении на любое количество клеток. По сути ход королевы является комбинацией ходов слона и ладьи. С точки зрения первоначальной расстановки на доске ферзь ставится рядом с королем и всегда на поле своего цвета.

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

Обозначения ходов


Для этого используем краткую запись, которая на первый взгляд похожа на набор букв и цифр, однако на деле все довольно просто.

Расскажу и покажу наглядно.

Сперва пишется первая буква, обозначающая фигуру (для пешек букву не используем). Затем поле, с которого фигура совершает ход. Координата по Х обозначается буквами от a до h, по Y цифрами, начиная с 1. Очень похоже на морской бой.

Если в ходе была уничтожена вражеская фигура, то пишем x, если никто не пострадал, то будет . Затем указываем поле, на которое перешла фигура. Ну, и в конце обозначаем особые случаи: для шаха +, для мата ++, для пата =. Для превращения пешки указываем в конце тип новой фигуры.

Техническая реализация


На шахматной доске 8 на 8 клеток, всего 64. Поскольку на каждой клетке может находиться только одна фигура, то во всех современных движках шахматные фигуры хранятся в виде битовых полей в типе Long, который как раз имеет разрядность 64 бита. То есть каждая клетка получает порядковый номер от 0 до 63 и если на данной клетке есть фигура, то соответствующий бит равен 1. Но это делается для оптимизации расходов памяти и просчета большого количества комбинаций. Поскольку в моей реализации ничего такого нет, я сделал упор на читаемость кода и решил хранить фигуры в Map, где ключом является вот такой тип Point.

data class Point(    val x: Int,    val y: Int)

Data class это неизменяемый тип, автоматически определяющий такие методы, как equals и hashCode, а потому его можно использовать в качестве ключа мапы. Х расположение фигуры по горизонтали, а Y по вертикали.

Для самой фигуры определим два параметра: цвет и тип.

Цветов в шахматах всего два: белый и черный. Поэтому реализуем это в виде enum class с одним вспомогательным методом other(), возвращающим противоположный цвет (такой метод нам пригодится далее).

enum class PieceColor(val text: String) {    WHITE("white"),    BLACK("black");    fun other(): PieceColor =    when (this) {        WHITE -> BLACK        BLACK -> WHITE    }}

Тип фигуры также определим в виде enum class с параметрами: название, ценность и краткое обозначение.

enum class PieceType(val nameEn: String,val nameRu: String,val price: Int,val notation: String,val useForPromotion: Boolean) {PAWN("pawn", "пешка", 100, "", false),KNIGHT("knight", "конь", 320, "N", true),BISHOP("bishop", "слон", 330, "B", true),ROOK("rook", "ладья", 500, "R", true),QUEEN("queen", "ферзь", 900, "Q", true),KING("king", "король", 20_000, "K", false)}

Признак useForPromotion показывает, может ли пешка превращаться в данную фигуру. Как видим, пешка не может превращаться только в короля и в саму себя.

Поле notation нам потребуется для записи ходов (помним, что для пешки нет специального обозначения).

Теперь мы подошли к такому вопросу, как ценность фигур.

Ценность фигур


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

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

Движок


К этому моменту мы можем полностью хранить текущее состояние игры в виде мапы. Для этого будем использовать поле pieces класса BoardImpl, который и будет по сути нашим движком. Затем, совершая каждый ход, мы будем менять нашу мапу pieces. Все совершаемые ходы будут добавляться в историю (поле history).

class BoardImpl : Board {private val pieces = mutableMapOf<Point, Piece>()private val history = mutableListOf<HistoryItem>()// ...}

Чтобы не хардкодить первоначальное расположение фигур на доске, его можно считывать из текстового файла. Например, файл может выглядеть так:

rnbqkbnrpppppppp................................PPPPPPPPRNBQKBNR

В этом файле 8 строк по 8 символов в каждой, т.е. 1 символ соответствует клетке на доске. Каждая фигура обозначается соответствующей буквой (например, пешка буквой P). Регистр буквы определяет цвет фигуры: верхний регистр это белые фигуры, нижний черные.

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

Генерация доступных ходов


В начале каждого хода движок будет генерировать список доступных ходов (множество клеток доски) для каждой фигуры. Это удобно и для человека (подсвечивать в интерфейсе доступные клетки), и для компьютера (просто выбирать оптимальный ход из полученного множества). Ходы бывают нескольких типов: обычный, ход с превращение пешки и рокировка. Определим интерфейс Turn:

interface Turn {val sourcePiece: Pieceval from: Pointval to: Pointval enemyPiece: Piece?fun execute(pieces: MutableMap<Point, Piece>)fun revert(pieces: MutableMap<Point, Piece>)}

Каждый ход содержит информацию о фигуре sourcePiece, которая этот ход совершает, ее исходную позицию from, пункт назначения to и вражескую фигуру enemyPiece (опционально, если она находится в пункте назначения). Также имеется два метода, принимающие текущее состояние доски: execute() для выполнения хода и revert() для его отмены. По сути это паттерн Команда.

Обычный ход


Этот интерфейс реализует класс NormalTurn. Его метод для выполнения хода execute() выглядит так:

override fun execute(pieces: MutableMap<Point, Piece>) {    pieces.remove(from)    pieces[to] = sourcePiece}

Просто извлекаем текущую фигуру по ключу from и помещаем на новую клетку доски по ключу to. При этом, если там была вражеская фигура, она просто перезатрется, т.е. удалится с доски.

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

override fun revert(pieces: MutableMap<Point, Piece>) {    pieces.remove(to)    pieces[from] = sourcePiece    enemyPiece?.let { pieces[to] = it }}

Ход с превращением


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

override fun execute(pieces: MutableMap<Point, Piece>) {    pieces.remove(from)    pieces[to] = sourcePiece.copy(type = toType)}

Для отмены хода нужно также учесть этот факт и восстановить исходную пешку, которая хранится в sourcePiece. Аналогично обычному ходу, восстанавливаем вражескую фигуру, если она была уничтожена.

override fun revert(pieces: MutableMap<Point, Piece>) {    pieces.remove(to)    pieces[from] = sourcePiece    enemyPiece?.let { pieces[to] = it }}

Генератор ходов


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

interface TurnGenerator {    fun getTurns(        position: Point,         pieces: Map<Point, Piece>    ): Set<Turn>}

Он состоит из одного метода getTurns(), который принимает текущее состояние доски и выбранную фигуру, для которой ищем доступные ходы (в виде ее координаты position). Метод возвращает множество доступных для данной фигуры ходов с учетом расположения других фигур на доске.

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

Метод addPointIfPossible() принимает текущую позицию, относительно которой ищем ходы, состояние доски, а также смещение deltaX и deltaY относительно текущей позиции. В методе мы проверяем, не выходит ли новая точка за пределы доски, а также смотрим, нет ли в полученной точке фигуры. Если фигуры нет ход доступен. Если фигура вражеская тоже ход доступен. Дополнительно сохраняем информацию о вражеской фигуре. Ну, и если в полученной точке есть другая наша фигура, то ход недоступен.

fun MutableSet<Turn>.addPointIfPossible(position: Point,pieces: Map<Point, Piece>,deltaX: Int,deltaY: Int): Boolean {val current = pieces.getValue(position)val from = positionval to = Point(from.x + deltaX, from.y + deltaY)val otherPiece = pieces[to]return to.takeIf { it.x in 0..7 && it.y in 0..7 }    ?.let {        when {                otherPiece == null -> { // нет фигуры                this += NormalTurn(sourcePiece = current, from = from, to = to)                true                }                otherPiece.color != current.color -> { // вражеская фигура                this += NormalTurn(sourcePiece = current, from = from, to = to, enemyPiece = otherPiece)                false                }                else -> { // своя фигура                false                }        }    } ?: false}

Метод generateRectangularTurns() проверяет доступные ходы, распространяясь из исходной точки в четырех направлениях под прямым углом. То есть слева, справа, сверху и снизу. Как только где-то встретили препятствие, то дальше по этому направлению ходы не ищем. Также ограничиваем диапазон поиска с помощью параметра maxRange.

fun generateRectangularTurns(position: Point, pieces: Map<Point, Piece>, maxRange: Int): Set<Turn> {val spaces = mutableSetOf<Turn>()var left = truevar right = truevar top = truevar bottom = truefor (i in 1..maxRange) {        if (left) {        left = spaces.addPointIfPossible(position, pieces, -i, 0)        }        if (right) {        right = spaces.addPointIfPossible(position, pieces, i, 0)        }        if (top) {        top = spaces.addPointIfPossible(position, pieces, 0, i)        }        if (bottom) {        bottom = spaces.addPointIfPossible(position, pieces, 0, -i)        }}return spaces}

Эта логика поиска доступных ходов характерна для ладьи.

Далее идет еще один подобный метод generateDiagonalTurns() для поиска ходов по диагоналям также в четырех направлениях с центром в указанной точке. И тут же ограничиваем поиск по maxRange.

fun generateDiagonalTurns(position: Point, pieces: Map<Point, Piece>, maxRange: Int): Set<Turn> {val spaces = mutableSetOf<Turn>()var leftTop = truevar rightTop = truevar leftBottom = truevar rightBottom = truefor (i in 1..maxRange) {        if (leftTop) {        leftTop = spaces.addPointIfPossible(position, pieces, -i, i)        }        if (rightTop) {        rightTop = spaces.addPointIfPossible(position, pieces, i, i)        }        if (leftBottom) {        leftBottom = spaces.addPointIfPossible(position, pieces, -i, -i)        }        if (rightBottom) {        rightBottom = spaces.addPointIfPossible(position, pieces, i, -i)        }}return spaces}

Эта логика поиска подходит для слона.

Генераторы ходов для каждой фигуры


Начнем реализовывать интерфейс TurnGenerator. Самая простая реализация будет у ладьи и слона, так как мы будем вызывать один из двух утилитарных методов, рассмотренных выше.

Для ладьи вызовем generateRectangularTurns() и укажем максимальный диапазон, в котором следует сгенерить ходы (8 клеток в каждом направлении):

class RookTurnGenerator : TurnGenerator {override fun getTurns(position: Point, pieces: Map<Point, Piece>): Set<Turn> =    generateRectangularTurns(position, pieces, MAX_RANGE)private companion object {        const val MAX_RANGE = 8}}

Для ладьи также будем генерить ходы в диапазоне 8 клеток, но с помощью метода generateDiagonalTurns().

class BishopTurnGenerator : TurnGenerator {override fun getTurns(position: Point, pieces: Map<Point, Piece>): Set<Turn> =    generateDiagonalTurns(position, pieces, MAX_RANGE)private companion object {        const val MAX_RANGE = 8}}

Для ферзя используем аналогичный подход, но будем вызывать оба утилитарных метода, объединив их в одно результирующее множество:

class QueenTurnGenerator : TurnGenerator {override fun getTurns(position: Point, pieces: Map<Point, Piece>): Set<Turn> =    listOf(    generateRectangularTurns(position, pieces, MAX_RANGE),    generateDiagonalTurns(position, pieces, MAX_RANGE)    )    .flatten()    .toSet()    private companion object {         const val MAX_RANGE = 8    }}

Для короля будет все так же, за исключением того, что MAX_RANGE будет равен 1 клетке.

class KingTurnGenerator : TurnGenerator {override fun getTurns(position: Point, pieces: Map<Point, Piece>): Set<Turn> =     listOf(    generateRectangularTurns(position, pieces, MAX_RANGE),    generateDiagonalTurns(position, pieces, MAX_RANGE)    )    .flatten()    .toSet()    private companion object {        const val MAX_RANGE = 1    }}

Для коня нам нужно проверить только те точки, до которых можно добраться ходом, напоминающим русскую букву Г (или английскую L, кому как больше нравится). Если в точке нет фигуры или есть вражеская фигура, то данная точка доступна для хода.

class KnightTurnGenerator : TurnGenerator {    override fun getTurns(position: Point, pieces: Map<Point, Piece>): Set<Turn> {    val spaces = mutableSetOf<Turn>()    spaces.addPointsIfCan(position, pieces, 1, 2)    spaces.addPointsIfCan(position, pieces, 2, 1)    return spaces    }    private fun MutableSet<Turn>.addPointsIfCan(position: Point, pieces: Map<Point, Piece>, absX: Int, absY: Int) {    this.addPointIfPossible(position, pieces, absX, absY)    this.addPointIfPossible(position, pieces, absX, -absY)    this.addPointIfPossible(position, pieces, -absX, absY)    this.addPointIfPossible(position, pieces, -absX, -absY)    }

Самый сложный генератор ходов будет для пешки. Ведь первый ход пешка может прыгнуть сразу на две клетки вперед. При этом пешка атакует только по диагонали. Ну, и еще по достижении края доски пешка может превратиться в одну из других фигур.

Для краткости приведу только основной метод.

class PawnTurnGenerator : TurnGenerator {    override fun getTurns(position: Point, pieces: Map<Point, Piece>): Set<Turn> {    val turns = mutableSetOf<Turn>()    val current = pieces.getValue(position)

Метод getStartY()по цвету пешки определяет ее стартовую позицию по вертикали. Поэтому если текущая позиция равна стартовой, то добавляем к возможным ходам на 1 клетку больше.

val range = if (position.y == getStartY(current.color)) 2 else 1val from = position

Метод getDirectionY() в зависимости от цвета определяет направление хода пешки (приращение по вертикали), так как она может двигаться только вперед.

val directionY = getDirectionY(current.color)for (i in 1..range) {    val to = Point(from.x, from.y + directionY * i)

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

if (to in pieces) {    break} else {    turns.addTurnWithPromotionCheck(position, current, to, null)}

Наконец, проверяем клетки атаки впереди слева и впереди справа от пешки.

   addAttackPoint(position, current, 1, directionY, pieces, turns)    addAttackPoint(position, current, -1, directionY, pieces, turns)    return turns        .filter { it.to.x in 0..7 && it.to.y in 0..7 }        .toSet()}//  другие вспомогательные методы}

Объединяем все вместе


Теперь пришла пора объединить все эти классы внутри нашего движка.

Генераторы выдают допустимые ходы, не учитывая при этом статус нашего короля. Например, фигура прикрывала короля от прямой атаки, перемещаем эту фигуру на другое поле и сразу наступает мат. Поэтому ход другой фигуры, ставящий нашего короля под мат, является недопустимым, и мы на уровне движка должны это отсекать. В этом нам поможет пара утилитарных методов.

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

fun getSpacesUnderAttack(pieces: Map<Point, Piece>): Map<PieceColor, Set<Point>> {    val spacesUnderAttack = mutableMapOf<PieceColor, MutableSet<Point>>()    pieces.entries.forEach { (position, piece) ->        spacesUnderAttack.putIfAbsent(piece.color, mutableSetOf())        spacesUnderAttack.getValue(piece.color).addAll(getTurnsForPiece(position, pieces).map { turn -> turn.to })    }    return spacesUnderAttack} 

Метод isKingUnderAttack() получает множество всех фигур на доске и цвет короля, для которого производим проверку. Внутри метода мы просто берем множество всех клеток, которые атакует другой игрок (kingColor.other() противоположный цвет), и смотрим, есть ли среди этих клеток та, на которой находится наш король.

fun isKingUnderAttack(pieces: Map<Point, Piece>, kingColor: PieceColor): Boolean {    val spacesUnderAttack = getSpacesUnderAttack(pieces).getValue(kingColor.other())    val king = pieces.entries        .first { it.value.type == PieceType.KING && it.value.color == kingColor }    return king.key in spacesUnderAttack}

Теперь мы можем определить метод getTurnsForPiece(), который будет отсекать все ходы, в результате которых наш король попадает под мат.

override fun getTurnsForPiece(position: Point): Set<Turn> {    val turns = UTILS.getTurnsForPiece(position, pieces)    val result = mutableSetOf<Turn>()    val kingColor = pieces.getValue(position).color    // нужно исключить каждый ход фигуры, который ставит её короля под удар    turns.forEach { turn ->            turn.execute(pieces)            if (!UTILS.isKingUnderAttack(pieces, kingColor)) {            result += turn            }            turn.revert(pieces)    }    return result}

Здесь мы получаем все множество допустимых ходов для данной фигуры. Выполняем каждый ход по очереди с помощью execute(), проверяем, не попадает ли король под мат в результате выполнения этого хода, и затем отменяем ход с помощью revert(). И так для каждого из возможных ходов, которые возвращает генератор.

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

Выполнение хода и проверка статуса игры


Когда ход выбран пользователем или компьютером, движок получает выбранный объект Turn и просто выполняет его в методе executeTurn(), попутно проверяя состояние игры (шах, мат или пат) и фиксируя историю ходов.

override fun executeTurn(turn: Turn): GameState {    val selectedPiece = pieces.getValue(turn.from)    turn.execute(pieces)    val state = getGameState(pieces, selectedPiece.color.other())    saveTurnHistory(selectedPiece, turn, state)    return state}

Проверка состояния в методе getGameState() производится по следующей логике:
Игрок может сделать хотя бы один ход? Его король под ударом? Статус игры
Шах
Обычное состояние, игра продолжается
Мат
Пат
Небольшое лирическое рассуждение про пат
Раньше у меня было какое-то упрощенное понятие пата. Я думал, что это когда ни один игрок не может совершить ход. В обоих случаях вражеский король загнан в угол, и по факту битва уже проиграна. Но если он никем не атакован и при этом не может совершить ни один ход (так как под мат ходить нельзя), то это уже ничья. Согласитесь, что это неочевидно?

История ходов может быть представлена в виде списка из элементов Turn. Поскольку каждый объект неизменяемый и хранит всю необходимую информацию о ходе, мы легко можем сохранять всю историю, не создавая каких-то дополнительных сущностей.

Дальше дело за ИИ


В этой части статьи я постарался подробно объяснить логику движка с комментариями к коду. Уже удалось сыграть с компьютером, и результаты были разные: когда-то побеждал я, когда-то уходил побежденным. Движок работает, теперь мне осталось описать другую составляющую алгоритм игры в шахматы для компьютера.

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

Создаем свой шахматный движок алгоритм игры компьютера

21.04.2021 16:08:09 | Автор: admin
Продолжаю рассказывать, как докручиваю свой шахматный движок, и это вторая часть статьи. Она небольшая, здесь я подсвечу настройку ИИ в игре. Сыграем с соперником в лице собственного компьютера.



В первой статье я начал с истории и поделился реализацией ходов. Было много по делу в комментариях, поэтому в планах движок продолжать улучшать. Не претендую на звание гроссмейстера, just for fun. Но, как и для любого программиста, нет предела совершенству :)

Итак, перейдем к реализации алгоритма игры в шахматы для компьютерного соперника. На вход этот алгоритм получает всё множество ходов, которое допускается правилами игры, исходя из текущей ситуации на доске. Компьютеру остается только выбрать самый оптимальный ход.

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

Все исходники проекта выложил на Github.

Задаем алгоритм игры компьютера


Создадим класс с очень амбициозным именем Ai и свойством color, который будет содержать цвет игрока-компьютера (чёрный или белый). Класс содержит единственный публичный метод nextTurn(). Этот метод принимает текущее состояние доски в виде фигур и возвращает объект Turn, который он считает наиболее оптимальным.

class Ai(private val color: PieceColor) {    fun nextTurn(board: Board): Turn { ... }

Внутри создадим копию текущего состояния доски, чтобы у нас была возможность производить пробные ходы для оценки доступных вариантов. Также сразу определим все клетки, которые могут быть атакованы врагом с помощью метода getSpacesUnderAttack():

val pieces = HashMap(board.getPieces())val currentSpacesUnderEnemyAttack = utils.getSpacesUnderAttack(pieces)    .getValue(color.other())

Теперь запросим у движка все доступные ходы для каждой фигуры текущего игрока-компьютера и для каждого из них вычислим профит числовую оценку успешности хода. Всю необходимую информацию сохраняем во вспомогательный data-класс TurnProfitInfo.

val profits = board.getTurnsForColor(color)    .entries.map { (from, turns) ->        turns.map { turn ->                TurnProfitInfo(                        from = from,                        turn = turn,                profit = turn.getProfit(pieces, currentSpacesUnderEnemyAttack)                )        }    }    .flatten()

Сама логика вычисления профита содержится в методе расширения Turn.getProfit(). После вычисления всех профитов найдем максимальный.

val maxOwnProfit = profits.maxOf { it.profit }

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

val turnPriceInfo = profits.filter { it.profit == maxOwnProfit }    .shuffled()    .first()return turnPriceInfo.turn}

Оценка хода, или какая фигура будет уничтожена


Теперь вернемся к методу getProfit(), который выполнен как расширение класса Turn. На вход он принимает текущее состояние доски и клетки, которые могут быть атакованы противником на следующем ходу.

private fun Turn.getProfit(pieces: HashMap<Point, Piece>,currentSpacesUnderEnemyAttack: Set<Point>): Int {var profit = 0

Если в результате хода уничтожаем вражескую фигуру, то прибавляем ее стоимость к профиту. Затем делаем пробный ход.

this.enemyPiece?.let { profit += it.getPrice() }this.execute(pieces)

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

val newSpacesUnderEnemyAttack = utils.getSpacesUnderAttack(pieces)    .getValue(color.other())

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

if (this.from in currentSpacesUnderEnemyAttack && this.to !in newSpacesUnderEnemyAttack) {    profit += this.sourcePiece.getPrice()} else if (this.from !in currentSpacesUnderEnemyAttack && this.to in newSpacesUnderEnemyAttack) {    profit -= this.sourcePiece.getPrice()}

В конце отменяем пробный ход.

this.revert(pieces)return profit}

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

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

Вместо заключения


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

Я находился под впечатлением от сериала Ход королевы, который мотивировал меня чуть глубже разобраться в шахматных правилах и написать более-менее читаемый код. Поэтому я просто взял и сделал такую реализацию шахматного движка, которая понятна мне и, надеюсь, большинству из вас.

Буду рад любой обратной связи и продолжать улучшать алгоритм, поэтому пишите свои вопросы и идеи в комментариях!
Подробнее..

4 угла хорошо, а 6 лучше гексагональные шахматы в консоли и с ботом

21.08.2020 14:20:51 | Автор: admin
Привет!

Мы учимся на первом курсе бакалавриата Прикладная математика и информатика в Питерской Вышке. Во время работы над семестровым командным проектом по С++ мы решили написать компьютерную версию Интеллектора с ботом шахматную игру на гексагональной доске с особыми фигурами.

В этой статье мы расскажем о том, как шла разработка игры, как приручить гексагональную доску, как можно рисовать в командной строке, а также как мы сделали бота, которого почти не можем победить.



В нашей команде 3 человека: Юра Худяков, Валера Головин, Миша Саврасов. Для каждого из нас это первый командный проект, так что в процессе работы мы не только писали на плюсах, но ещё и учились работать в команде и пользоваться системами контроля версий (в нашем случае git). В проекте есть некоторое количество странностей, в частности, велосипедов есть хорошие готовые решения, которыми можно было бы воспользоваться. Однако цель нашего проекта заключалась в том, чтобы получить опыт, так что многое мы придумывали и реализовывали самостоятельно.

Что такое Интеллектор?


Для начала стоит объяснить, что за игру мы писали.

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


Интеллектор отличается от шахмат структурой поля, правилами и набором фигур. Впрочем, многие элементы похожи: например, в игре есть фигура Прогрессор, которая выполняет роль Пешки. Она ходит только вперёд и может превратиться в другую фигуру, когда достигает крайнего ряда.

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

Отличия в механике игры возникают из-за специфики поля. Поле Интеллектора симметрично, и это существенно отличает его от шахмат с их королевским и ферзёвым флангами.

Для понимания этой статьи знание правил и умение играть не потребуются.

Общая архитектура


Что мы хотели получить в нашем приложении?


Для того, чтобы игра заработала, нужно реализовать её основную составляющую: логику игры. В неё входит модель доски и правила ходов. Кроме того, для удобства стоит хранить историю ходов и реализовать отмену/повторение хода.

Доску нужно вывести на экран и позволить пользователю играть. Этим занимается графическая составляющая игры интерфейс. В удобном интерфейсе должны быть меню и настройки.

В конце концов, нужен оппонент для игры. Мы решили сделать для этих целей бота, чтобы игрок мог соревноваться с компьютером. При этом сложность бота должна быть регулируемой.

План приложения:


  1. Логика игры
    • Модель гексагональной доски
      Хранится в виде двумерного массива гексагональных клеток
    • Правила перемещения фигур
      Проверка допустимости хода, получение всех доступных ходов для фигуры, для игрока
    • История ходов
      Отмена и повторение хода
  2. Интерфейс
    Планировали 2 интерфейса: ncurses и Qt. В проекте реализован только ncurses, подробнее в разделе Интерфейс.
    • Вывод поля
      Отрисовка и обновление поля в консоли
    • Перемещение курсора клавишами клавиатуры, игра без мышки
    • Отображение текстовой истории ходов
    • Отображение главного меню
  3. Бот
    • Случайный бот
    • Жадный бот
    • Альфа-бета бот
      Оптимизировано перебирает все ходы

Как это сделано?


Очень упрощенно архитектуру приложения описывает эта схема:


Раздел Game отвечает за всю игровую логику и хранит доску.

Когда включена игра с компьютером, Game взаимодействует с ботом из Bot

Controller берёт данные об игре из Game и передаёт их в Interface для отображения игрокам. Interface в свою очередь возвращает результат взаимодействия с пользователем обратно в Game через Controller.

Промежуточное звено в виде контроллера пригождается в том случае, когда интерфейсов несколько (изначально мы планировали сделать 2 интерфейса: консольный и графический). Все они единым образом общаются с игрой через контроллер, вместо того, чтобы каждый из них делал это по-своему.

Технические подробности


Модель игры


Гексагональная доска


Рассмотрим раздел Game из схемы выше. Он должен хранить доску и обрабатывать всю внутриигровую логику.

Для лучшего понимания можно прочитать статью, из которой мы взяли эту идею.

Всю доску мы будем хранить в двумерном массиве клеток, элементы которого индексируются парами координат (w,h) как показано на рисунке ниже. Такой выбор координат кажется естественным, но он неудобен для описания перемещения фигур (проследите, скажем, как меняются координаты при перемещении по диагонали).


Координаты клеток по горизонтальной оси w и вертикальной оси h

Для удобства описания перемещения фигур мы будем использовать трехмерные координаты. Выберем некоторую клетку в качестве опорной с координатами {0,0,0} (для удобства она будет совпадать с клеткой (0, 0) массива).


Трехмерные координаты клеток относительно центральной клетки с координатами {0,0,0}

Смещение вдоль диагонали справа налево, снизу вверх обозначим координатой x, смещение снизу вверх координатой y и смещение вдоль диагонали слева направо, снизу вверх координатой z. При переходе в соседнюю клетку соответствующая координата будет меняться на единицу. Таким образом, каждая клетка получает три координаты, как на рисунке выше.

При этом клетки нумеруются неоднозначно. Например, если из центральной клетки с координатами {0,0,0} мы сдвинемся влево вниз, а затем вверх, то получим координаты клетки {0,1,-1}. Но эта же клетка имеет координаты {1,0,0} если прийти в нее напрямую из центральной клетки, как видно на предыдущем рисунке.


Другой вариант задания координат клетки {1,0,0}.

Обход любой клетки по циклу влево-вниз вверх вправо вниз приводит нас в ту же клетку, но добавляет к её координатам вектор {-1,1,-1}, сумма координат которого равна -1. Выполняя мысленно такой обход в ту же или в обратную сторону несколько раз, мы можем изменять координаты любой клетки на эквивалентные, которые будут отличаться на вектор, пропорциональный {-1,1,-1}. Чтобы избавиться от неоднозначности, в каждом классе эквивалентности выберем в качестве представителя тройку координат, сумма которых равна нулю. Такой выбор координат является единственным (докажите!).

Опишем далее алгоритм перевода из двумерных координат в трехмерные координаты и наоборот внутри класса Position.

Position(int w, int h) // конструктор из двумерных координат        : x_{-w/2  w % 2 - h}        , y_{w % 2 + 2 * h}        , z_{w / 2  h} {}int posW() const { // метод для получения первой координаты двумерного массива    return -x_ + z_;}int posH() const { // метод для получения второй координаты двумерного массива    return (x_ + z_  (x_ + z_)%2) / 2 + y_;}

Обратите внимание, что конструктор выдает координаты (x,y,z), сумма которых равна нулю. При этом конвертация координат (x,y,z) в (w,h) работает корректно для любого набора координат (сумма не обязательно должна быть равна нулю).

Как мы нашли все эти формулы? Методом научного тыка: путем анализа изменения трёхмерных координат при сдвиге одной из двумерных координат на 1 (конструктор) и в обратную сторону (методы).

Пользуясь трехмерными координатами, мы легко можем проверить, что клетки лежат на одной прямой. Например, для проверки, что две клетки лежат на одной диагонали z, надо найти вектор, соединяющий эти клетки, и проверить, что в его классе эквивалентности есть вектор вида {0, 0, z}. Z может быть любым это число дает расстояние между клетками. Очень просто будет реализовывать проверку хода на корректность и находить все клетки, доступные для хода.

В двумерном массиве, который представляет доску, будем хранить информацию о расположении фигур. В каждой клетке, если там стоит какая-то шахматная фигура, будем хранить её тип и цвет.

В нашей реализации в классе доски мы храним только типы фигур в клетках. Нам нужен класс, который сможет по этой доске находить все возможные ходы для фигур и проверять корректность ходов.

Ходы для фигур


Мы сделали класс FigureMoveValidator, у которого 6 наследников для каждого типа фигуры (можно было обойтись и без наследников, если в каждом методе делать switch case на тип фигуры). Конструктор класса имеет 2 параметра: позиция и ссылка на доску. Также в классе есть два метода allMoves и checkMove.

Рассмотрим метод allMoves. Для нахождения всех ходов, составим массив векторов возможных смещений и пройдемся по нему. Для фигур, которые двигаются на один шаг, надо проверить, что мы не выпрыгнули с доски и не попали в клетку, где стоит своя фигура. Для фигур, которые двигаются на несколько клеток по прямой, прибавляем вектор хода, пока проходит предыдущее условие.

Теперь checkMove. Мы помним, что умеем проверять, лежат ли фигуры на одной прямой. Если проверим, что на этой прямой нет других фигур, то получим готовый метод для Доминатора (аналог ладьи). Если фигуры лежат на одной прямой, то мы можем найти расстояние между ними, и получить методы для Прогрессора (пешка), Дефенсера (ходит как король), Интеллектора (король, только не может рубить) и Либератора (может ходить через одну клетку в любую сторону). Остался еще агрессор (аналог слона), который ходит в клетки по диагоналям в шесть направлений от текущей (из клетки {0, 0, 0} в {0, 1, 1}, в {0, 2, 2} и т.д.: см. серые клетки на картинке ниже). Для этой фигуры можно попробовать обнулить одну из координат и проверить, что оставшиеся 2 координаты равны по модулю (спасибо трёхмерным координатам).


Возможные ходы для агрессора

Теперь надо придумать, что делать с ходами. Создадим класс Move, который будет хранить всю необходимую информацию для хода. Мы решили хранить 2 позиции и 4 фигуры: позицию, с которой ходит фигура, позицию, куда она придет, и информацию о том, какие фигуры стояли в каждой из этих клеток и какие будут стоять после применения хода. Это позволит легко реализовать систему истории ходов и откат ходов.

Интерфейс


Архитектура


Приложение написано на консольной библиотеке ncurses (вот туториал к ней). Эта библиотека позволяет создавать псевдографику в консоли. К примеру, на ней основаны Midnight Commander и Nano.

Выбор может показаться весьма странным: есть много других библиотек, более красивых, удобных и кроссплатформенных. Он связан с тем, что изначально мы планировали сделать 2 интерфейса: консольный и графический. Написать 2 интерфейса к моменту сдачи проекта мы не успели и вместо этого сделали больше фич в консольной версии. Хотя архитектурно приложение всё ещё рассчитано на различные интерфейсы.

Есть 2 основные сущности: отображение и контроллер. Отображения показывают картинку игрокам, а контроллер является посредником между разными отображениями и внутренней моделью игры.

Отображение обрабатывает всё взаимодействие с пользователем: положение и перемещение курсора, выбор фигуры, подсветку доступных полей, завершение игры и прочее. Действия, которые затрагивают доску, обращаются к контроллеру и передают/получают необходимую информацию в/из модели.

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

Вот что происходит, если игрок захочет узнать доступные для хода поля:

  1. Игрок перемещает курсор на поле с фигурой и нажимает пробел
  2. Поле с фигурой помечается как выбранное
  3. Интерфейс обращается к методу selectCell у контроллера
  4. Метод selectCell обращается к методу allFigureMoves у модели
  5. allFigureMoves создаёт FigureMoveValidator, который вычисляет все доступные ходы
  6. allFigureMoves передаёт найденные ходы обратно контроллеру
  7. Контроллер передаёт их интерфейсу
  8. Интерфейс перерисовывает поле, подсветив доступные поля


Курсор находится в середине доски: на бледно-синей клетке. До того, как переместить его в это положение, пользователь выбрал фигуру. Доступные ей ходы подсвечиваются зеленым цветом, а сама выделенная клетка фиолетовым.

Как рисуется поле?


Консольный интерфейс отрисовывается в прямоугольном окне со строчками текста. Если ставить символы в нужных местах и раскрашивать их, получается картинка:


(Злой Пакман, нарисованный буквами о)

Функция move(int y, int x) в ncurses меняет текущую позицию, а функция addch(chtype c) добавляет символ и смещает текущую позицию на 1 вправо (x > x+1).

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

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

Ncurses позволяет разработчику изменять атрибуты текста при выводе их в консоль (цвет, жирность, мерцание). Для этого в коде пишется:

attron( *attributes* );addch(c);attroff( *attributes* );

Каждый символ имеет собственный цвет и цвет фона. Современные консоли поддерживают максимум 256 цветов, поэтому приходится работать с ограниченным набором: довольно грустно с точки зрения цветового дизайна.

Изображения для вывода можно хранить в коде (так мы делали изначально), а можно в отдельных файлах и читать из них при запуске программы. Для этого мы придумали собственный формат файла *.btn.

В нём хранится текстовая картинка, которую прочитает и выведет игра. Например, фигура, или надпись White wins/Black wins, или кнопка меню. При этом иногда может понадобиться прозрачность, чтобы не перезатирать нарисованное ранее. Для этого можно в первую строчку добавить решётку # и после список прозрачных символов, которые будут проигнорированы при выводе.

Например, пусть у нас на экране нарисованы 3 прямоугольника:


Добавим в центр прямоугольник из такого файла:

#CAAAAAAAAAACCCCCCCAACCCCCCCAACCCCCCCAACCCCCCCAACCCCCCCAAAAAAAAAA

И получим следующую картинку:


(жёлтым подсвечено для наглядности)

Особенно этот формат пригодился при разработке меню.

Меню


В игре также есть меню, которое содержит настройки и отрисовывается до начала игры и после её завершения.

Кнопки меню читаются из файлов *.btn. На этих кнопках должен быть крупный красивый текст. Красиво рисовать с помощью ASCII символов мы не умеем, зато умеет figlet утилита для вывода ASCII-текста разными шрифтами:


Кнопки обрамляют надписи, прочитанные из файла:


Затем они центрируются и последовательно выводятся:


В некоторые менюшки можно провалиться и, например, настроить сложность и цвет бота:


Самая интересная часть разработки системы меню это объединение её элементов в одну систему. Этим занимается отдельный элемент системы, который мы назвали мультиплексор. Название навеяно терминальными мультиплексорами.

Мультиплексор принимает нажатую пользователем клавишу и рассылает её во все отображаемые сейчас меню. Каждое меню само решает, что с клавишей делать: проигнорировать или как-то обработать. Результат обработки меню возвращает мультиплексору, который решает, что делать дальше: закрыть меню, создать новое, изменить настройки, закрыть приложение

Такой подход оказался удобен для наших потребностей, хотя в общем случае его может не хватить: 2 разных меню могут реагировать на одну и ту же клавишу, и у пользователя должна быть возможность выбрать, какое именно меню должно реагировать. Решением могла бы быть специальная комбинация клавиш, которая бы позволяла переключаться между разными меню. Например, как в tmux. Но это overkill и он не потребовался.

Бот


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

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

Полный просчет всего дерева ходов слишком сложная задача, поэтому мы просчитывали только первые несколько ходов (забегая вперед, скажу, что получилось просчитать на 6 ходов вперед). Все состояния на доске на определенной глубине мы считали листьями дерева обхода.

Всего в игре есть три разных вида ботов:

  • RandomBot бот делает случайный возможный ход. Создавался для тестирования.
  • GreedyBot бьет самую дорогую фигуру противника, а если не может бить, ходит случайно.
  • AlphaBetaBot бот, который использует алгоритм альфа-бета отсечения для выбора лучшего хода в дереве игры.

Когда начали экспериментировать с оптимизациями, мы поняли, что без юнит-тестов для бота обойтись не получится, поэтому создали брата-близнеца для AlphaBetaBotа, которого назвали OptimizedAlphaBetaBot. Все идеи для оптимизации мы испытывали именно на OptimizedAlphaBetaBot, а юнит-тесты помогали убедиться, что два брата-близнеца находят одинаковый по полезности ход. RandomBot сослужил нам хорошую службу, генерируя случайные расстановки фигур на доске. Для этого достаточно было попросить RandomBotа сходить несколько десятков раз за обе стороны.

Всего в OptimizedAlphaBetaBot были реализованы 3 крупных оптимизации (здесь они представлены в порядке убывания полезности):

  • Использование откатов ходов. После этой оптимизации больше не нужно было множество раз копировать доску, чтобы сделать какой-то ход.
  • Написание нового класса с говорящим названием FigureKeeper, который хранит список фигур каждого цвета, которые сейчас есть на доске. Стало возможным обойти один std::vector вместо всей доски.
  • Запоминание уже обработанных позиций с помощью std::unordered_map и Zobrish hashing.

Кроме крупных оптимизаций были и более мелкие. Например, если перед сортировкой просчитать все стоимости позиций на доске с учетом определенного хода, то сортировать уже нужно не сложные объекты Move, а просто intы.

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

Однако архитектура бота все еще поддерживает добавление новых оценочных функций. Для этого нужно определить всего три вещи:

  1. Функцию, если нужно посчитать стоимость с нуля по данному расположению фигур
  2. Дельта-функцию, которая должна по данному ходу быстро пересчитать стоимость.
  3. Номер этого набора функций для конструктора специального класса FunctionSet.

Можно включить битву ботов и наблюдать за процессом.


Игра 2 ботов одинаковой сложности (4 уровень из 6 возможных). Курсор всю игру стоит по центру поля

Заключение


Мы реализовали игру, подобную шахматной, но с другими правилами и необычной доской. Нашей реализации есть куда расширяться. Сам Интеллектор тоже сейчас развивается и меняется: недавно вышло обновление правил, которое мы пока не поддержали в своём приложении. Например, теперь нельзя пересекать центральную линию первые 2 хода.

Кроме того, есть разные фичи, которые мы изначально планировали, но к сдаче проекта реализовать не успели. Например, в этом приложении очень хотелось бы видеть сетевую игру. Также не помешал бы красивый кроссплатформенный интерфейс, например, на Qt.

Возможно, в ближайшее время всё или часть из этого появится. А пока что спасибо за чтение!

Github-репозиторий
Подробнее..

Recovery mode Странные шахматы как тестовое задание

17.06.2021 22:13:59 | Автор: admin

Добрый вечер хаброжители!

Сегодня я предоставлю вашему вниманию небольшую статью не связанную с моими предыдущими статьями. Где-то более года назад мне пришлось делать одно тестовое задание чтобы устроится на работу. Выполнил я его в срок, но конкуренция была большая и скорее всего взяли человека который сделал его с помощью рекомендованных технологий и красочнее.

Суть задачи, есть доска 8 на 8 клеток. У игрока есть 9 шашек они расположены в углу доски в квадрате 3 на 3, у противника тоже столько же шашек и они расположены симметрично по диагонали в другом углу в квадрате 3 на 3. Каждый игрок ходит по очереди, нужно дойти шашками на места изначального положения соперника через всю доску, кто первый дошел тот и победил. Ходить можно только на пустые клетки и только вверх, вниз, влево и вправо(по диагонали нельзя!).

Управление я добавил мышью, а играть против компьютерного алгоритма. Черные - человек, белые - ИИ.

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

Немного кода для наглядности:

Game::Game(){run = true;//флаг признак нажатия кнопки выхода F5Matrix = new int* [8];//Поле 64 ячейки - значения 0 - для пустой ячейки, для игрока каждая пешка-шашка от 1 до 9, для компьютера значения в матрице от 10 до 18for (int i = 0; i < 8; i++)Matrix[i] = new int[8];//Квадраты координат нужны чтобы программа знала какие ячейки над указателем мыши, 64 квадратаQuadCoorXleft = new int* [8];//каждой ячейки матрицы Matrix соответстует квадрат координат для мыши xleft означает левую координату xQuadCoorXright = new int* [8];//xright - правая xQuadCoorYdown = new int* [8];//верхняя y координатаQuadCoorYup = new int* [8];//нижняя y координатаfor (int i = 0; i < 8; i++){QuadCoorXleft[i] = new int[8];QuadCoorXright[i] = new int[8];QuadCoorYdown[i] = new int[8];QuadCoorYup[i] = new int[8];}//Координаты пешек для отрисовкиChessX = new double[18];//XChessY = new double[18];//Y//Выделяемая пешка ее координаты и значенияActiveX = -1;//XActiveY = -1;//YActive = -1;//Valuefirstplayer = true;//флаг того что можете игрок 1й ходитьsecondplayer = false;//флаг того что можете игрок 2й ходитьai = new bool[18];//ячейки флаги того что пешка на финишной позицииchessai tmp;for (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i+1;Ai.push_back(tmp);//Вектор с флагами финиша каждой пешки для искуственного интеллекта}}aicountfirstrow = 0;//счетчик кол-ва пешек ИИ(искуственного интеллекта) на верхней строчке(0-я)aicountsecondrow = 0;//счетчик кол-ва пешек ИИ на предверхней строчке(1-я)aicountthirdrow = 0;//счетчик кол-ва пешек ИИ на предпредверхней строчке(2-я)}

Для отрисовки и захвата мыши используется библиотеки OpenGL и SDL2.

void Draw_Circle(){//Отрисовка круга(пешек-шахмат) черногоfor (int i = 0; i <= 50; i++) {float a = (float)i / 50.0f * 3.1415f * 2.0f;glVertex2f(cos(a), sin(a));}}void Draw_Circle_Fill(){//Отрисовка круга(пешек-шахмат) белогоfor (int i = 0; i <= 50; i++) {float a = (float)i / 50.0f * 3.1415f * 2.0f;glVertex2f(0.0, 0.0);glVertex2f(cos(a), sin(a));}}

Самое удобное что можно использовать glTranslatef чтобы заполнить доску шашками с перемещениями

...for (int i = 0; i < 9; i++){glPushMatrix();glTranslatef(ChessX[i], ChessY[i], 0);glScalef(0.05, 0.05, 1);glBegin(GL_LINE_LOOP);Draw_Circle();glEnd();glPopMatrix();}//Рисуем белые пешки ИИfor (int i = 9; i < 18; i++){glPushMatrix();glTranslatef(ChessX[i], ChessY[i], 0);glScalef(0.05, 0.05, 1);glBegin(GL_LINES);Draw_Circle_Fill();glEnd();glPopMatrix();}...

Ходы игрока:

void Game::Move_Up(){//Ход игрока вверхif (Active > 0 && ActiveX != 0 && Matrix[ActiveX-1][ActiveY] == 0)//Если выделенная пешка и не самая верхняя строчка и ячейка выше пустая{Matrix[ActiveX-1][ActiveY] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке выше текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessY[Active-1] += 0.2;//перемещаем координату У пешки вверх для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << " Player MoveUp " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Down(){//Ход игрока внизif (Active > 0 && ActiveX != 7 && Matrix[ActiveX+1][ActiveY] == 0)//Если выделенная пешка и не самая нижняя строчка и ячейка ниже пустая{Matrix[ActiveX+1][ActiveY] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке ниже текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessY[Active-1] -= 0.2;//перемещаем координату У пешки вниз для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "Player MoveDown " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Right(){//Ход игрока вправоif (Active > 0 && ActiveY != 7 && Matrix[ActiveX][ActiveY+1] == 0)//Если выделенная пешка и не самая правая строчка и ячейка справа пустая{Matrix[ActiveX][ActiveY+1] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке справа текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessX[Active-1] += 0.2;//перемещаем координату Х пешки вправо для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "MoveRight " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Left(){//Ход игрока влево if (Active > 0 && ActiveY != 0 && Matrix[ActiveX][ActiveY-1] == 0)//Если выделенная пешка и не самая левая строчка и ячейка слева пустая{Matrix[ActiveX][ActiveY-1] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке слева текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessX[Active-1] -= 0.2;//перемещаем координату Х пешки влево для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "MoveLeft " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}

Ходы компьютера почти аналогичны ходам игрока, в конце будет ссылка на полный код игры, если кому интересно.

void Game::ReccurentWalk(){//Реккурентный ход ИИcurrent = -1, currentI = -1, currentJ = -1;//изначально выделенная пешка не определенаfor (int i = 0; i < Ai.size(); i++)//поиск по массивуif (!Ai[i].ai)//если не завершены ходы для конкретных пешек{if (Check_MoveUp(Ai[i].value) || Check_MoveLeft(Ai[i].value))//Можно ли походить вверх или влево?{current = Ai[i].value;//запоминаем текущую пешкуbreak;}else{//Если походить нельзя стираем из массива ходов пешкуstd::vector<chessai>::iterator position = std::find_if(Ai.begin(), Ai.end(), find_s(Ai[i].value));if (position != Ai.end()) // == vector.end() means the element was not foundAi.erase(position);}}for (int i = 0; i < 8; i++)for (int j = 0; j < 8; j++)if (Matrix[i][j] == current)//ищем в матрице пешку и запоминаем индексы{currentI = i;currentJ = j;break;}if (currentI != -1 && currentJ != -1)//если какая либо найдена ходим либо вверх либо влево{if (!Move_UpAI(currentI, currentJ))if (!Move_LeftAI(currentI, currentJ)){ReccurentWalk();}}else{//если не найдена заполняем массив ходов снова пешкамиchessai tmp;for (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i + 1;Ai.push_back(tmp);}}//ищем ту которая может походить вправо или внизfor (int i = 0; i < Ai.size(); i++)if (!Ai[i].ai){if (Check_MoveRight(Ai[i].value) || Check_MoveDown(Ai[i].value)){current = Ai[i].value;break;}else{//если не может то стираем из массиваstd::vector<chessai>::iterator position = std::find_if(Ai.begin(), Ai.end(), find_s(Ai[i].value));if (position != Ai.end()) // == Vector.end() means the element was not foundAi.erase(position);}}//ищем ее индексы в матрицеfor (int i = 0; i < 8; i++)for (int j = 0; j < 8; j++)if (Matrix[i][j] == current){currentI = i;currentJ = j;break;}//ходим вправо или внизif(!Move_RightAI(currentI, currentJ))if (!Move_DownAI(currentI, currentJ)){std::cout <<"Artificial Intellegence asked: WTF?" << std::endl;}}chessai tmp;if(Ai.empty())//если список ходов пуст заполняем снова всемиfor (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i + 1;Ai.push_back(tmp);}}}

Ну и собственно опишу словами, что делает ИИ. Он собирает три пешки вверху на 0-ой строке и если ходить нельзя влево, то двигает вверх остальные 3 пешки на 1 строку, аналогично и на 2-ю строку. Если ходить влево и вверх нельзя, то он берет любую шашку и двигает либо вниз либо вправо если можно конечно, то есть стремится всегда двигать влево или вверх( при условии что выше меньше двух шашек в строке).

Ну собственно и геймплей:

https://youtu.be/XaQVeSKdQcs

И ссылка на исходный код:

https://github.com/Beginerok/DominiGames

Подробнее..

Перевод Как игры стали движущей силой двух школ исследований ИИ

03.09.2020 10:04:27 | Автор: admin
Сегодня мир штурмом захватывает ИИ, основанный на глубоком обучении и нейронных сетях. Однако многие алгоритмы, управляющие поиском в вебе и построением автомобильных маршрутов, гораздо старше, они уходят корнями в так называемый старый добрый ИИ, также известный как символический искусственный интеллект, являвшийся основным видом ИИ с 1950-х до конца 1990-х. Затмевание символического ИИ глубинным обучением иллюстрируется двумя важнейшими вехами в истории искусственного интеллекта, каждая из которых связана с победой ИИ-системы над лучшим игроком-человеком.


Чемпион мира Гарри Каспаров победил компьютер IBM Deep Blue в 1996 году, но потерпел поражение в 1997 году, проиграв со счётом 4 к 2.


Произошедшая в 1997 году победа компьютера IBM Deep Blue над гроссмейстером и чемпионом мира Гарри Каспаровым считается триумфальным водоразделом в истории технологий, сравнимым с высадкой на Луну. Кажется, что она продемонстрировала, что компьютеры могут побеждать людей в том, что считалось свойственным только нам: в мышлении1. Технологии символического ИИ, которые использовались компьютером DeepBlue, сегодня считаются устаревшими, особенно для более сложных игр наподобие го, которая была изобретена в Китае две с половиной тысячи лет назад. Но в 2016 году чемпиона мира по го Ли Седоля победила система ИИ AlphaGo подразделения Google DeepMind. Это событие исследователь и венчурный капиталист Ли Кайфу назвал моментом Спутника для Китая2: он считает, что именно оно заставило Китай вложить миллиарды долларов в исследования ИИ, чтобы догнать и, возможно, даже перегнать США. Победа AlphaGo иллюстрирует расцвет новой парадигмы ИИ глубокого обучения и нейронных сетей, ставшей основой современной революции искусственного интеллекта.

Почему игры наподобие шахмат и го были столь важны в истории ИИ? Пионеры исследований искусственного интеллекта, среди которых были Герберт Саймон, Аллен Ньюэлл, Джон Маккарти и Марвин Минский, рассматривали интеллект человека сквозь традиционную призму западной философии, основы которой заложил ещё Аристотель. Этот маскулинный, евроцентричный взгляд на интеллект, корнями уходивший в декартово разделение мышления и тела, отдавал приоритет мозговым навыкам логике, математике и способности решать задачи в ущерб телесным, эмоциональным, социальным и культурным формам интеллекта. Они считали, что если Разум (т.е. Логика) отличает Человека от Зверя, то именно Логика должна быть основой интеллекта.


Блез Паскаль был философом и математиком. В 1640-х он изобрёл в помощь своему отцу, сборщику налогов, машину, способную выполнять сложение.

Многие западные философы и математики, от Блеза Паскаля до Джорджа Буля и Бертрана Рассела, стремились или сделать вычисления/логику, которые они приравнивали к самой мысли, более строгими математически (более формальными), или предпринять ещё один шаг механизировать их. Сам Паскаль изготовил для этих целей вычислительную машину, а кульминацией этого импульса западной мысли стало изобретение в 20-м веке цифрового компьютера. Пионеры изучения ИИ 1950-х и 1960-х рассматривали игры как ещё один способ демонстрации людьми интеллекта решением задач. Если бы исследователям ИИ удалось бы имитировать то, как это делают игроки, то они бы смогли автоматизировать этот процесс. Отрасль математики под названием теория игр, применяемая в экономике и военном деле, была основана математиком и компьютерным пионером Джоном фон Нейманом; она обеспечила оптимизацию стратегий и алгоритмов, широко применяемых в компьютерных науках. Пионер исследования ИИ Герберт Саймон применял данные теории и в информатике, и в экономике (в этой области он получил Нобелевскую премию). Следовательно, мысль о том, что игры могут серьёзно моделировать аспекты реального мира, была центральной на ранних этапах развития компьютерных наук. В частности, поскольку первые компьютеры испытывали трудности с моделированием сложности реального мира, игры считались упрощённым микромиром, ограничения и правила которого хорошо понятны компьютерам, обеспечившим возможность быстрого прогресса в 1960-х.


Шахматный автомат Вольфганга фон Кемпелена Турок, управлявшийся спрятанным внутри живым игроком.

Шахматы, в частности, исторически считались на Западе вершиной интеллектуальной деятельности. Это была интеллектуальная игра, ассоциирующаяся с логикой и стратегией. Вспомните мистера Спока из Звёздного пути, побеждающего игроков-людей в 3D-шахматы. Даже в 18-м веке европейская элита была восхищена идеей машин, способных играть в шахматы. Вольфганг фон Кемпелен получил известность благодаря своему Механическому турку шахматному автомату, изготовленному для австрийской императрицы Марии-Терезии и победившему Бенджамина Франклина и Наполеона. Позже выяснилось что турок был фальшивкой и внутри него прятался живой игрок; тем не менее, он поразил воображение Эдгара Аллана По и Чарлза Бэббиджа. Интерес к шахматам как к показателю интеллекта распространился и на математиков, заложивших теорию вычислений в 20-м веке: Алана Тьюринга, Клода Шеннона, Джона фон Неймана, Норберта Винера и, разумеется, на пионеров ИИ Герберта Саймона, Аллена Ньюэлла и Джона Маккарти. В частности, Ньюэлл и Саймон считали шахматы образцовой задачей для ИИ, идеально подходящей для предпочитаемого ими решения: поиска.


В 1947 году первопроходец в области компьютерных наук Алан Тьюринг разработал свою первую теоретическую шахматную программу как пример машинного интеллекта.


Исследователь MIT и Bell Labs Клод Шеннон (справа) разработал современную теорию информации и опубликовал в 1950 году первую статью о том, как писать компьютерные шахматные программы. Также он создал в MIT эту шахматную машину на основе реле (на фото). Слева чемпион по шахматам Эд Ласкер (прибл. 1950 год).


Бетти и Клод Шенноны на третьем Чемпионате мира по компьютерным шахматам в Линце, Австрия, 1980 год.

В поисках решения


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


Иллюстрация решения произвольного дерева игры.

Минимакс



Ветвящееся дерево решений игры в крестики-нолики. Как и в шахматы, а также во множество других игр на двух игроков, в крестики-нолики можно играть при помощи алгоритма минимакс.

Допустим, нам нужно создать робомышь, ищущую выход из лабиринта (примерно этим занимался Клод Шеннон в 1950 году). Если она попадает на перекрёсток с четырьмя дверьми, то может двигаться вправо, вперёд и влево, но ей запрещается возвращаться назад. Так мы получаем три возможных варианта выбора. Компьютерные учёные сказали бы, что мышь имеет коэффициент ветвления, равный 3. Простейший способ программирования компьютера для прохождения (решения) лабиринта заключался бы в проверке по очереди каждого варианта, или ветви. Это называется поиском методом грубого перебора (brute force): мы тестируем каждый вариант. Однако наша мышь, разумеется, попадёт на ещё один перекрёсток, прежде чем ей предоставится возможность вернуться, чтобы проверить все остальные варианты на первом перекрёстке. Каждый раз, когда она достигает нового перекрёстка, мышь может выбрать между ещё тремя новыми путями. Мы можем задать количество перекрёстков, которое мышь может обыскать в глубину, прежде чем вернуться назад и попробовать другой путь.


Клод Шеннон передвигает свою электрическую мышь в лабиринте (прибл. 1952 год).

Это называется глубиной поиска, а в контексте игр упреждающим просмотром (look ahead). Как видите, количество путей, которые необходимо обыскать мыши, очень быстро нарастает: как 3 (коэффициент ветвления), умножаемый на себя же столько раз, сколько мы упреждающе проверяем в дереве решений. Другими словами, задача растёт экспоненциально. В отрасли ИИ это часто называют проблемой комбинаторного взрыва.

Шахматная доска в первых теориях


Подобный метод можно использовать и в шахматах. На каждом ходе игрока у нас есть выбор из максимум 38 возможных допустимых ходов, то есть шахматная задача имеет коэффициент ветвления 38. Для выбора лучшего из этих 38 ходов используется количественный метод оценки относительной выгоды одной шахматной позиции по сравнению с другой. Это называется оценочной функцией. Среднестатистическая партия в шахматы проходит за 42 хода, и поскольку игроков двое, это нужно умножить на два, что даёт нам приблизительно 3884 больше, чем количество звёзд во Вселенной. Ещё на ранних этапах истории ИИ стало понятно, что такой поиск грубым перебором шахмат и других задач просто не сработает на оборудовании того времени; вариантов слишком много, а компьютеры слишком слабы. Клод Шеннон одним из первых использовал алгоритм "минимакс" в компьютерной шахматной программе (этот алгоритм и сегодня является основой большинства шахматных программ), заметив, что благодаря человеческому знанию и опыту в игре можно быстро отсечь множество ветвей, не рассматривая их. Герберт Саймон и Аллен Ньюэлл предложили использовать эвристики, или эмпирические правила, которые люди частью используют при решении задач; чаще всего они срабатывают, но это случается не всегда. Эвристики это тот вид человеческого знания, который можно запрограммировать в компьютер.

Шахматы



Шахматы имеют намного больше ветвей в своём дереве решений, чем крестики-нолики. Подсчитано, что количество вариантов шахматных партий примерно равно 10120, а это больше, чем количество атомов во Вселенной.

Bounded Lookahead



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

Одной такой эвристикой, доказавшей свою полезность в шахматах, является "альфа-бета-отсечение". Это означает, что если программа определила, что одному из ходов может с лёгкостью противодействовать противник, то другие способы, которыми он может противодействовать тому же ходу, искать не нужно. Дальнейший поиск по этому пути можно игнорировать, отсекая всю эту ветвь от дерева решений. Это может значительно уменьшать коэффициент ветвления, с 38 до 6, а иногда и до 3. Кроме того, учитывая ограничения компьютеров того времени, большинство программ могли заглядывать только на 4 хода вперёд. Одна из первых шахматных программ, способных компетентно играть против любителей, была создана приблизительно в 1959-1962 гг студентом MIT Аланом Котоком под руководством Джона Маккарти. В программе Котока-Маккарти использовалось альфа-бета-отсечение.


Джон Маккарти, играющий в шахматы на IBM 7090 Стенфордского университета. Маккарти использовал усовершенствованную версию программы Котока для игры в шахматы по переписке с советской программой, разработанной в московском Институте теоретической и экспериментальной физики Георгием Адельсоном-Вельским и его коллегами. В 1967 году матч из четырёх партий, продолжавшийся в течение девяти месяцев, был выигран со счётом 3-1 советской программой.

История Алана Котока из первых уст



В 1959 году первокурсники MIT Алан Коток, Элвин Р. Берлекамп, Майкл Либерман, Чарльз Ниссен и Роберт А. Вагнер начали работу над шахматной программой на основе исследований пионера искусственного интеллекта Джона Маккарти. Ко времени их выпуска в 1962 года программа уже могла выигрывать у любителей.

Ньюэлл и Саймон считали, что все задачи ИИ, например, шахматы, можно решить поиском в сочетании с эвристиками, или эвристическим поиском. Эвристический поиск был центральной идеей, лежавшей в основе первых прорывов Ньюэлла и Саймона программ Logic Theorist и General Problem Solver, а также стал важнейшим столпом в их теории о том, что интеллект и людей, и машин заключается в простой манипуляции символами, фундаментальными строительными кирпичиками математики и языка. Эта гипотеза физической системы символов стала тем предположением, на котором был основан весь проект символического искусственного интеллекта, от его зарождения в 1950-х до начала 2000-х. Эта теория, постулировавшая эквивалентность мозга компьютеров и человека, стала чрезвычайно влиятельной и в когнитивной психологии, а в позже даже попала в популярную культуру благодаря произведениям в жанре киберпанк, в которых люди могли загружать свои мозги в Интернет или заменять их чипами.


Пионеры исследований искусственного интеллекта Аллен Ньюэлл (справа) и Герберт Саймон, которые совместно с Клиффом Шоу разработали такие ИИ-программы, как Logic Theorist, General Problem Solver и шахматную программу NSS (в которой использовалась аппроксимация альфа-бета-отсечения). Все эти программы работали на компьютере JOHNNIAC корпорации RAND.

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


Ханс Берлинер (на заднем плане), Мюррей Кэмпбелл (слева) и Фэн Сюн Сюй на 20-м ежегодном Чемпионате по компьютерным шахматам ACM в Рино, Невада. Первое место поделили две команды HiTech (команда Берлинера) и Deep Thought (команда Кэмпбелла и Сюя); обе они представляли Университет Карнеги Меллона. Трёх участников команды Deep Thought (в том числе Кэмпбелла и Сюя) позже наняла компания IBM для создания Deep Blue.

Когда компьютеры стали быстрее, они смогли заглядывать вперёд более глубоко, на 6, 7, 8 ходов, с лёгкостью побеждая программы, прогнозировавшие всего на 4 хода вперёд. Был обнаружен более эффективный алгоритм поиска, называющийся поиском с итеративным углублением; он мог постепенно увеличивать глубину поиска по тому пути, который выглядел наиболее перспективным. Впервые он был применён в Chess 4.53 Дэвида Слейта и Ларри Эткинса первой программе, выигравшей в 1976 году в человеческом турнире по шахматам. Увеличение объёма памяти также позволило программам сохранять ранее рассмотренные позиции, ещё сильнее уменьшая объём необходимых поисков. Всеми этими инновациями (альфа-бета-отсечение, итеративное углубление, сохранение проверенных позиций и базы данных дебютов и эндшпилей) свободно обменивались разработчики шахматных программ на турнирах по компьютерным шахматам, поэтому они стали стандартными приёмами.


В 1977 году Кен Томпсон (более известный как соавтор операционной системы Unix) и Джо Кондон из Bell Laboratories спроектировали Belle специализированную машину для игры в шахматы. Специализированное шахматное оборудование Belle и база данных эндшпилей совершили революцию в компьютерных шахматах.


Один из разработчиков Belle Кен Томпсон на 13-м Североамериканском чемпионате по компьютерным шахматам. Специализированная шахматная машина Belle победила в турнире, а второе место заняла программа Cray Blitz. С 1970 по 1994 годы Ассоциация вычислительной техники (Association for Computing Machinery, ACM) проводила ежегодные компьютерные шахматные турниры с целью создания платформы для обмена идеями.


На протяжении 1980-х Belle, разработанная Кеном Томпсоном и Джо Кондоном из Bell Labs, была сильным соперником на шахматных турнирах. На фото показаны шахматные программы Belle и CHAOS на WCCC 1980 года, проводившейся в Линце, Австрия. Belle играла с программой CHAOS Университета Мичигана в примерно равном по силам матче и победила.


Belle сражается с Chess 4.0 на 4-м Чемпионате мира по компьютерным шахматам (WCCC), проводившемся в Нью-Йорке в 1983 году. На заднем плане слева направо: Кен Томпсон, Фредерик Фриндел и Джо Кондон. На переднем плане слева направо: разработчики Chess Дэвид Слейт и Уильям Бланчард. Турнир выиграла работавшая на суперкомпьютере шахматная программа Cray Blitz, второе место заняла Bebe.

Несмотря на прогресс программного обеспечения, с ростом скоростей компьютеров в 1970-х шахматные программы автоматически становились лучше без каких-либо инноваций в ПО. К 1980-м годам доминирующим фактором прогресса в компьютерных шахматах стало использование оборудования для ускорения поиска. Они превратились в задачу проектирования компьютеров, а не задачу для ИИ. В 1997 году Deep Blue по-прежнему в основном использовал те же программные приёмы, что и шахматные программы за 20 лет до него; тем не менее, ему удалось победить Каспарова в основном благодаря тому, что он был быстрым компьютером со множеством специализированных параллельных процессоров. В каком-то смысле, в процессе роста скорости компьютеров шахматные программы становились менее интеллектуальными.


Печатная плата Deep Thought I, 1988 год. Специализированная шахматная машина Deep Thought, разработанная студентами Университета Карнеги Меллона, стала предшественницей Deep Blue.


Deep Blue.


Команда разработчиков IBM Deep Blue (Джо Хоун, Джоел Бенджамин, Джерри Броди, Фэн Сюн Сюй, С. Дж. Тан и Мюррей Кэмпбелл).


Гарри Каспаров пожимает руку Фэн Сюн Сюю в первой повторной игре 1997 года против Deep Blue (Нью-Йорк).

В 1980-х поиск в глубину как доминирующая тематика в исследованиях ИИ уже находилась в упадке. Начиная с 1960-х, такие исследователи, как Эд Фейгенбаум из Стэнфорда, создавали так называемые экспертные системы, в которых большие объёмы экспертных человеческих знаний вкладывались в программы ИИ в виде правил if-then. Как и в случае с первыми эвристическими программами, эти правила программировались в код ПО, но в отличие от эвристических систем, базы знаний отделялись от логических частей программы (машин вывода). Фейгенбаум и другие приверженцы экспертных систем утверждали, что знание сила. Другими словами, они считали, что большая база знаний компенсирует отсутствие сложных рассуждений: чем больше знаний, тем меньше поиска, и наоборот.

Тигр в клетке: применение систем баз знаний, лекция Эдварда Фейгенбаума, 1993 год



Обсуждение истории ИИ на AAAI-17: экспертные системы, 2017 год



В 1980-х экспертные системы породили множество коммерческих компаний. Вся эта деятельность почти никак не коснулась шахматных программ, которые в то время разворачивались в другом направлении: обратно к поиску грубым перебором при помощи специализированного оборудования. Ведущими шахматными машинами этого типа были Belle Кена Томпсона из Bell Labs и два отдельных проекта Университета Карнеги Меллона: HiTech Ханса Берлинера с Фэн Сюн Сюем и Deep Thought Мюррея Кэмпбелла, который позже превратился в Deep Blue компании IBM. То есть к моменту, когда машина победила Каспарова, шахматные программы уже практически перестали быть связанными с общей сферой исследования ИИ, хоть и обеспечивали хорошую рекламу.

Однако бОльшую тревогу вызывало то, что к началу 1990-х подвергся атаке проект символического ИИ, основанный на гипотезе физической системы символов Ньюэлла и Саймона. Его критики, в частности, философ Хьюберт Дрейфус, начали подвергать сомнению проект символического ИИ ещё в 1960-х, обосновывая это тем, что философское предположение о разделённости мозга и тела было неверным и устаревшим. Философы 20-го века, например, Мартин Хайдеггер, утверждали, что человеческую мысль невозможно отделить от телесного опыта и непосредственного культурного окружения субъекта.

Исследователи ИИ очень резко реагировали на критику Дрейфуса (впрочем, он и сам был не особо дипломатичен): ведущие авторитеты в области ИИ угрожали журналам, когда те публиковали работы Дрейфуса. Они злорадствовали, когда Дрейфуса, который не очень хорошо играл в шахматы, победила шахматная программа MacHack Ричарда Гринблатта. Однако успех программ в шахматах не доказывал ошибочности критики Дрейфуса. На самом деле, сам факт того, что шахматные программы наподобие Deep Blue использовали поиск грубым перебором, означал, что они не играют особой роли в более масштабном проекте создания ИИ общего назначения. Драма оглушительного поражения Каспарова превозносилась как веха победы Машин над Человеком, но на самом деле это был триумф инженеров Deep Blue над одним игроком в шахматы. И создатели Deep Blue не утверждали, что их компьютер обладал интеллектом. Они говорили: если бы в здании начался пожар, то Каспаров был бы достаточно умён, чтобы убежать, а машина осталась бы на месте. И хотя ранее пионер ИИ Джон Маккарти считал шахматы главной задачей ИИ, после победы Deep Blue он критиковал шахматы за то, что с их помощью не удалось разработать ни одной новой теории о том, как можно имитировать человеческий интеллект.


СМИ изображали повторную игру 1997 года между чемпионом мира по шахматам Гарри Каспарова и специализированного суперкомпьютера IBM Deep Blue как битву между человеком и машиной. На обложке Newsweek её назвали Последней линией обороны мозга. Подобные взгляды преувеличивали мощь компьютера и минимизировали труд людей, создавших саму машину.

К началу 1990-х исследователи начали воспринимать критику Дрейфуса серьёзно и стали придумывать новые виды ИИ, например такие, которые подчёркнуто имеют тело, типа роботов Родни Брукса4, или те, которые имеют дело с эмоциями. Как мы увидим во второй части статьи, в 2000-х заменять символический ИИ начинает совершенно другая традиция ИИ, называемая машинным обучением. Машинное обучение способно выполнять задачи, с которыми символический ИИ никогда не умел справляться лучше человека, например, распознавание лиц или понимание человеческой речи. Это же относится и к игре, в которую машины не могли конкурентоспособно играть при помощи эвристического поиска, а именно к го.

Тем не менее, хотя поиск потерял своё величие в качестве основной методики ИИ, он никогда не терял своей пользы в более широкой области компьютерных наук. Был достигнут значительный прогресс в совершенствовании алгоритмов поиска для оптимального и эффективного решения задач. Эта методика настолько фундаментальна, что создание деревьев решений и поиск по ним распространены чрезвычайно широко; почти невозможно перечислить все программы, в которых она используется.

Поиск играет определённую роль в любой задаче получения информации, от выполнения запросов к базам данных до поиска по вебу. Алгоритм поиска A*, впервые изобретённый для робота Shakey института SRI, повсеместно используется для построения маршрутов для автономных транспортных средств и GPS-приложений. И даже сегодня ИИ-программы, играющие в игры при помощи машинного обучения, применяют разные виды поиска, пусть он и не является больше самым интересным их компонентом. Однако, как и другие техники, которые раньше считались искусственным интеллектом, современный поиск считается просто ещё одной базовой компьютерной техникой, не более интеллектуальной, чем обычная программа. Это иллюстрирует исторический паттерн развития ИИ: как только он становится стандартным и автоматическим, люди больше не считают его интеллектом. Раньше когда говорили об ИИ, то, скорее всего, имели в виду поиск. Когда ИИ упоминают сегодня, то обычно подразумевается преемник символического ИИ машинное обучение.

Во второй части статьи мы расскажем о революции, совершённой в искусственном интеллекте машинным обучением, о глубине различий глубинного обучения от поиска и символического ИИ, а также о том, как AlphaGo компании DeepMind использовал глубинное обучение для победы над чемпионом мира по го Ли Седолем.

Примечания


1. Nathan Ensmenger, Is Chess the Drosophila of Artificial Intelligence? A Social History of an Algorithm, Social Studies of Science 42, no. 1 (February 2012): 22, https://doi.org/10.1177/0306312711424596.

2. Kai-Fu Lee, AI Superpowers: China, Silicon Valley, and the New World Order. (Boston; New York: Houghton Mifflin Harcourt, 2019), 15.

3. Stuart J. Russell and Peter Norvig, Artificial Intelligence: A Modern Approach, 3rd ed., Prentice Hall Series in Artificial Intelligence (Upper Saddle River, NJ: Pentice Hall, 2010), 110.

4. Rodney A. Brooks, Elephants Dont Play Chess, Robotics and Autonomous Systems, Designing Autonomous Agents, 6, no. 1 (June 1, 1990): 315, https://doi.org/10.1016/S0921-8890(05)80025-9.



На правах рекламы


Если для работы необходимы серверы с мгновенной активацией на Linux или Windows, то вам однозначно к нам сервер готов к работе через минуту после оплаты!

Подробнее..

Перевод Какими могут быть идеальные шахматы

21.12.2020 14:15:23 | Автор: admin
image


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

Сегодня я хочу рассказать вам о нескольких разновидностях классических шахмат: Crazyhouse, больших шахматах и сёги. Также я хочу предложить вам комбинацию из первых двух вариантов думаю, когда я попробую в нее сыграть, эта игра станет моей любимой.

I


Сёги это версия шахмат, родом из Японии, и она сильно отличается от западных шахмат и западные шахматы, и сёги непрерывно эволюционировали из индийской игры чатуранга. Основное различие между сёги и привычными нам западными шахматами заключается в том, что игрок, взявший фигуру оппонента позже может выставить ее как свою собственную. Впрочем, если бы игры отличались только этим, то партии в сёги были бы безумными. Фигуры в западных шахматах настолько сильны, а король настолько слаб, что игра была бы переполнена опасными ситуациями. Игрокам потребовалось быть постоянно бдительными в отношении потери фигур, а если не уделять достаточно внимания, то можно получить мат за несколько ходов.

Фактически, эта версия как раз и является игрой Crazyhouse, и ее особенность как делает игру столь любимой и увлекательной, так и препятствует тому, чтобы ее воспринимали так же серьезно, как и традиционные шахматы. Существует два решения этой проблемы: можно усилить короля, что даст ему больше мобильности, которая позволит ему лучше уклоняться от безумия, создаваемого павшими фигурами, либо фигуры можно ослабить, что сделает их намного менее мощными. В частности, можно ослабить их влияние на больших расстояниях. В сёги ослаблены фигуры. Дальнобойные и мощные фигуры из шахмат заменены на фигуры с ограниченной мобильностью. Так, например, копье в сёги двигается как ладья, но может ходить только вперед (таким образом, копье может занимать положение только на пути перед собой). Ума (от яп. конь) двигается как конь, но может смещаться только в два крайних положения перед собой. Также в сёги есть серебряные и золотые генералы, которые могут двигаться в подмножествах направлений короля. Поскольку каждая отдельная фигура в сёги ненамного сильнее короля, самому королю легче уклоняться от угроз, создаваемых отдельными фигурами. Мат, в свою очередь, можно поставить только тогда, когда фигуры используются согласованно для создания ловушки для короля (это основа цумэ-сёги, аналога шахматных задач с постановкой мата. Их интересно решать, и я рекомендую попробовать их, чтобы понять, чем мат в сёги отличаются от мата в традиционных шахматах).

Думаю, сёги и Crazyhouse могут решить мои проблемы с традиционными шахматами: игры часто заканчиваются ничьими, а эндшпили редко бывают интересными. Иногда в эндшпилях традиционных шахмат встречаются интересные головоломки, но финалы в сёги обычно более веселы и увлекательны.

I a (взгляд со стороны)


Продолжая тему с сёги и Crazyhouse, должен отметить, что фигуры в японской игре заметно отличаются от традиционных:

image

Мне очень нравится то, как эти фигуры выглядят. Также они решают проблему, связанную с выбыванием фигур: если использовать традиционные шахматные фигуры, то потребуется два набора. Армия каждого из игроков может быть удвоена, потому что они могут захватывать фигуры противника. Фигуры из сёги же можно просто переворачивать обратной стороной по отношению к оппоненту таким образом, одного набора фигур будет достаточно. Впрочем, хоть это решение и работает, мне кажется что играть в Crazyhouse с фигурами из сёги неверно Crazyhouse все же основана на традиционных шахматах, и мне кажется, что лучше использовать обычные шахматные фигуры. Для меня идеальным решением было бы использование ассиметричных традиционных фигур с привычном дизайном: у фигур можно сделать плоскими вершину и основание, чтобы их можно было переворачивать и менять цвет (один конец будет черным, а другой белым). Линию разделения цвета можно сделать диагональной, чтобы один игрок видел один цвет, а его соперник другой.

II


Неоднократно отмечалось, что в традиционных шахматах ощущаются завершенность и полнота: ладья и слон двигаются в определенных направлениях перпендикулярно/параллельно полю битвы или по диагонали, соответственно. Если вокруг каждой фигуры отметить квадраты 5х5 клеток, то там будут места, куда может попасть конь, но не могут попасть ладья и слон. А ферзь, по сути, представляет собой комбинацию ладьи и слона. Все эти правила кажутся взаимосвязанными, почти совершенными и платоническими. Почти идеально, ведь в традиционных шахматах нет двух комбинаций: ладья + конь и слон + конь. А ведь именно такими фигурами (они называются маршал и кардинал, соответственно) было бы интересно играть, хотя не стану спорить без них в шахматы играть лучше. Существует несколько версий игры с такими фигурами, наиболее известная Шахматы Капабланки, большие шахматы, придуманные чемпионом мира по шахматам Дж. Р. Капабланкой и гейм-дизайнером Кристианом Фрилингом. Отличительная особенность этой игры в том, что в ней используется поле размером 10х8 клеток (широкими сторонами к игрокам), в то время как в больших шахматах используется доска 10х10, на которой у всех фигур есть свободные столбцы позади (кроме ладей, они находятся в углах). Что насчет рокировки? Все просто: рокировки в больших шахматах нет.

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

III


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

Давайте рассмотрим один из вариантов исправления баланса в Crazyhouse: вместо того, чтобы ослаблять фигуры, можно усилить короля. Его можно сделать более проворным и дать ему возможность выживать в условиях массового выбывания фигур. Я еще не тестировал эту идею, но мне кажется уместным добавить королю возможность ходить назад на 4 позиции, доступные коню. У короля добавится умеренная доля мобильности, а ограничение на движение назад гарантирует, что он будет защитным элементом и не получит атакующей силы. Плейтесты могут показать, что это слишком слабая способность (я и не думал, что она будет слишком сильной). Если так оно и окажется, то стоит иным образом изменить возможности передвижения короля. Ясно, что повышение его мобильности добавит играм с массовым выбыванием фигур баланса.

Идеальные шахматы


Итак, отличия идеальных для меня шахмат от традиционных будут заключаться в следующем:

  • Игра должна вестись на поле размером 10х10 клеток (вместо традиционных 8х8). Я считаю, что более широкая доска сделает игру более увлекательной и глубокой
  • В игре должны быть маршалы (ладья + конь) и кардиналы (слон + конь), а фигуры должны расставляться как в больших шахматах (чтобы исключить рокировки)
  • Когда фигура взята, захвативший ее игрок должен иметь возможность вывести ее на поле (работает так же, как в Crazyhouse или сёги)
  • В дополнение к своим обычным ходам, король должен иметь возможность двигаться назад на 4 клетки из ходов коня. Фигуры могут быть взяты такими ходами.


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

Подробнее..

Так какими же должны быть идеальные шахматы?

24.12.2020 00:21:35 | Автор: admin

Так какими же всё таки должны быть идеальные шахматы?

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

Чтобы понять какими могут быть идеальные шахматы, нужно сначала понять что не так с шахматами нынешними (классическими). Давайте попробуем сформулировать недостатки не только геймплея как такового, но и недостатки связанные с психологическим восприятием того, что происходит во время игры.

Проблемы объективные

1. Проблема инвентаря при изменении правил.

2. Бесконечные ничьи достигаемые тем или иным способом.

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

Проблемы субъективные (связанные с психологией восприятия)

1. Пат. (как проблема связанная с психологией восприятия)

2. Недостаточно материала для мата.

3. Проблемы связанные с слишком сильными изменениями в правилах.

Любые проблемы желательно решать так, чтобы не плодить новые проблемы. Исходя из этой предпосылки и давайте попробуем решить нынешние проблемы шахмат.

1. Как говорится, начнём по порядку!

Я не знаю сколько в мире насчитывается шахматных досок и фигур к ним. Полагаю, что можно говорить о десятках, а может и сотнях миллионов комплектов и никто их не будет менять, даже если предложить идеальную игру на доске 10х10 с добавлением новых фигур.

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

Значит определились. Доски остаются 8х8. Фигуры те же.

2. Ничьи

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

а. Пат.

б. Вечный шах.

в. Троекратное повторение позиции.

г. Правило 50 ходов.

д. Недостаточно материала для мата.

е. Ничья по соглашению сторон.

а. Одна из проблем, это пат и патовые идеи.

Новичкам не понятно, почему король попавший в пат, это ничья? Мы же проецируем это на наши представления о мире. Если вражеского полководца не убили, а обездвижили, то в этот момент мы не теряем опцию его прирезать. Это можно сделать в любой момент, если понадобится. То есть задача выполнена. В реальности, взять живым, даже зачастую ценится больше, чем доставить мёртвым.

Конечно, те люди, которые постоянно играют в шахматы свыкаются с данной им реальностью и уже не считают это чем-то неправильным, но если эта проблема приносит нам головную боль сразу двумя способами ничьи и психология, почему бы её не устранить?

Устраняем. Приравняем пат к мату. Пат победа. Тут всё просто. Едем дальше.

б, в, г. Эти проблемы так просто не решить.

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

Но что если в данном случае мы поставим чёрных афроамериканцев и белых в разные условия? Если у нас на старте игры, белые имеют небольшой бонус в виде лишнего темпа (хода для непосвящённых), то почему мы не можем слегка подравнять эту несправедливость, а попутно ещё и решить проблемы с ничейными результатами?

Решаем. Любой из встреченных вариантов (б, в, г), приравнивается к победе чёрных.

Решение простое, элегантное, как говорится, дёшево и со вкусом. Мы заранее говорим белым: Да, вы первые ходите, но не бесплатно. Не допускайте вот этого.

д. Недостаточно материала для мата.

Тут мы снова сталкиваемся с двумя проблемами одновременно. Ничейные результаты от которых мы хотим максимально избавить шахматы и психология восприятия. Представим ситуацию. У начинающего игрока остался слон (конь) и король, а у его противника только король. Современные правила шахмат трактуют эту ситуацию как ничью ввиду недостаточности материала для мата, но на взгляд неофита это абсурд. Как? У него только король, а у меня ещё целый слон (или конь)! Фигура приравниваемая по силе к трём пешкам! А мне достаточно и одной, чтобы провести ферзя и победить. А тут три пешки. Как это ничья?

Выглядит и правда не очень. И если вам не понравилось как выглядит вышеописанное, то представьте теперь ситуацию, где у вас остался король и целых два коня! А у противника только король. И это тоже ничья.

Как решать проблему?

Как ни странно, тоже довольно не сложно. Достаём старое правило оголения короля, сдуваем с него пыль и имеем элегантное решение.

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

Но есть нюанс (даже три), но и их не сложно предусмотреть. Итак, нам нужно предусмотреть 3 возможных варианта.

*. Пешечный эндшпиль, где у одной из сторон остался только король, но он в ближайшее время съест пешку (пешки) противника.

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

*. Ситуация при которой слон или конь одной стороны, съел что-то последнее у другой. Тут нужно удостовериться, что король слабейшей стороны может или не может гарантированно съесть оставшуюся фигуру.

Самый сложный вариант в данном случае конь в углу. Королю может понадобится 3 хода, чтобы съесть этого коня, при этом конь спастись не может. Значит мы можем предусмотреть правило, при котором если нет пешек на доске, оголённый король имеет в своём распоряжении 3 хода, после которых засчитывается поражение. За эти 3 хода король гарантированно съест любую одинокую фигуру противника, если такая возможность теоретически существует.

*. Голые короли. Тут всё просто. Мы уже определили, что любая ничья, в пользу чёрных. Так что мы просто используем то же самое правило, что используется для решения проблем б, в, г.

е. Ничья по соглашению сторон.

Скажите мне, в каком-нибудь другом виде спорта вы видели ничью по соглашению сторон?

Может быть футболисты на пятой минуте пожимают друг другу руки и расходятся по домам, потому что обе команды устраивает ничья?

Или может быть боксёры на ринге решили, что на сегодня хватит получать по лицу, обнимаются и идут по домам без синяков и рассечений?

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

3. Осталось самое сложное, /розы/ дебют

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

Однако, есть очень интересное предложение Марка Дворецкого, которым он поделился на страницах журнала 64 шахматное обозрение в первом номере от 2008 года. Статья называется Компоненты успеха и в общем-то посвящена не только проблемам современных шахмат, но часть статьи именно так и называется проблемы современных шахмат.

Марк Израилевич никак не называет свою вариацию шахмат, а лишь рассказывает о сути предложения, поэтому я долгие годы называю эти шахматы шахматы Дворецкого, за не имением официального названия.

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

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

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

Заключение. Проблемы, связанные с слишком сильными изменениями в правилах

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

Если бы такой задачи не стояло, можно было бы внести более радикальные изменения. Например, отменить мат и вместо него ввести правило съедания короля противника. Кто съел короля противника, тот победил.

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

Это позволило бы в целом кардинально упростить правила шахмат. Например, не нужно правило, при котором короли не могут приближаться друг к другу. Пусть подходит вражеский король, мы его съедим нашим королём. И много, много других правил становятся не нужными.

Но такие изменения, это уже колоссальные изменения с точки зрения внутреннего мира шахматиста. Как правило это вызывает шок и отторжение. Проверено.

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

И напоследок,

Добро пожаловать в новый мир шахмат!

Подробнее..

В поисках инженерной культуры Arzamas и DataArt запустили совместный исторический проект

04.03.2021 20:15:20 | Автор: admin

Просветительский проект Arzamas и компания DataArt запустили онлайн-курс о роли ЭВМ и кибернетики в искусстве и массовой культуре в СССР. Проект построен как экскурсия по виртуальному музею, сейчас, кроме основной экспозиции, доступны два тематических зала: один посвящен кибернетическому изобразительному искусству, музыке и поэзии, второй шахматам. Экспозиции о фантастике, неформальной кибернетике, гуманитарных науках и профессии советского программиста откроются в марте и апреле.

Проект строится не столько вокруг самого железа и технических деталей разработки ЭВМ, сколько вокруг профессии эвмщика и отражения советской кибернетики в сознании современников. Авторы курса из Arzamas, как и команда IT-музея DataArt, верят, что это важно. На наш взгляд, в республиках Советского Союза и социалистических странах Восточной Европы во второй половине XX века сформировалась особая инженерная культура. Некоторые ее черты IT-специалисты могут и сейчас наблюдать в локальных профессиональных сообществах. Впрочем, эта идея пока остается гипотезой.

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

Мы стараемся смотреть на материал, который продолжаем набирать самостоятельно и вместе с нашими партнерами, в том числе и с этой точки зрения. К тому же, человеческие истории и неожиданный взгляд на знакомые проблемы нам попросту нравятся. Думаем, совместный проект с Arzamas может быть интересен всем, кто увлекается историей науки и технологий, искусством и социальной антропологией, в частности, задумывается, как достижения в IT влияют на повседневную жизнь и представления людей об окружающем мире.

В заключение мы хотим предложить читателям Хабра небольшой опрос.

Подробнее..

Шахматные алгоритмы, которые думают почти так же, как человек, только лучше

26.02.2021 16:11:32 | Автор: admin

Когда создавались первые вычислительные машины, их воспринимали только как дополнение к человеческому разуму. И до недавнего времени так и было. Программисты учили компьютеры играть в шахматы с 1960-х годов. И тогда победа у игрока-новичка уже считалась большим прогрессом. О серьёзных матчах даже не задумывались.

В 1980-х программа Belle достигла рейтинга Эло в 2250 пунктов, что примерно соответствует рейтингу мастера спорта. И с того времени развитие компьютерных шахмат вышло на совершенно новый уровень.

Сначала честь человечества не смог защитить Гарри Каспаров в 1996 году, а сегодня уже создана нейросеть с рейтингом около 5000 Эло, что в разы превосходит даже сильнейших игроков.

Сегодня разберёмся, как работают шахматные алгоритмы и почему нейросеть Alpha Zero думает практически так же, как человек, только лучше.


Как работает шахматный движок: от механического перебора вариантов до умного выбора

У шахмат довольно простые правила. Две противоборствующие стороны, шесть разновидностей фигур и одна цель дать мат сопернику.

Но при этом вариативность шахмат просто огромна. Существует 400 уникальных комбинаций первого хода 20 вариантов первого полухода белых и 20 вариантов ответа чёрных. С каждым последующим ходом количество уникальных позиций увеличивается на степень.

Общее количество уникальных партий в шахматы составляет примерно 10120, что на 1040 превышает количество атомов во Вселенной.

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

В шахматной теории у каждой фигуры есть своя ценность, которая измеряется в пешках:

  • Конь 3 пешки;

  • Слон 3 пешки;

  • Ладья 5 пешек;

  • Ферзь 9 пешек;

  • Пешка 1 пешка.

Король бесценен, потому что его потеря означает проигрыш партии.

Анализ современных машин подтверждает истинность такой оценки. Так, в зависимости от позиции на доске компьютер оценивает ферзя в 912 пешек, ладью в 56, коня и слона в 35. Короля же машина оценивает в 300 пешек. Это задаёт максимальную границу оценки.

Анализ современных машин подтверждает истинность такой оценки. Так, в зависимости от позиции на доске компьютер оценивает ферзя в 912 пешек, ладью в 56, коня и слона в 35. Короля же машина оценивает в 300 пешек. Это задаёт максимальную границу оценки.

Чтобы было более понятно, преимущество в 0,5 пешки это уже неплохо для шахматиста. В целую пешку серьёзный перевес. В 3 пешки подавляющее преимущество, которое можно практически без проблем довести до победы.

Но счётные возможности машины ограниченны. Иногда она показывает оценку в +51 или что-то вроде. Это означает, что алгоритм видит колоссальное преимущество белых в позиции и материале, но не может найти конкретный путь к мату.

Минимакс, или прямой перебор вариантов, в таком случае не работает. Даже КМС без проблем найдёт на доске мат в 3 хода в миттельшпиле, когда на доске ещё много фигур. А программе для этого нужно будет перебрать свыше 750 млн. полуходов.

Даже если программа перебирает 1 млн вариантов в секунду, чтобы найти мат в 3 хода, ей понадобится до 750 секунд, или 12,5 минут.

И это глубина в 3 хода. В стратегических позициях, где развитие игры идёт с учетом на пять или десять ходов вперёд, такие программы и вовсе будут бесполезными.

Поэтому для анализа позиции используется алгоритм под названием альфа-бета-отсечение.

Система анализирует начальные варианты ходов и сразу отсекает те из них, которые ведут к мгновенному ухудшению оценки.

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

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

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

function alphabeta(node, depth, , , maximizingPlayer) is    if depth = 0 or node is a terminal node then        return the heuristic value of node    if maximizingPlayer then        value :=         for each child of node do            value := max(value, alphabeta(child, depth  1, , , FALSE))             := max(, value)            if    then                break (*  cutoff *)        return value    else        value := +        for each child of node do            value := min(value, alphabeta(child, depth  1, , , TRUE))             := min(, value)            if    then                break (*  cutoff *)        return value

За код особо не ругайте.

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

Из всего множества вариантов развития событий программа выбирает ряд линий, которые в перспективе ведут к улучшению позиции. Их она анализирует более глубоко на 1520 ходов вперёд, чтобы отсечь возможные проигрышные варианты. В результате она выбирает лучшую из возможных линий и делает ход.

После ответа соперника ситуация снова анализируется по тому же алгоритму. Сначала отсекаются заведомо проигрышные линии (таких порядка 95 %), а затем путём более глубокого анализа перспективных вариантов выбирается лучший из них.

Новая эра в шахматных движках: нейросеть Alpha Zero

В 2017 году компания Deep Mind объявила о создании нейросети Alpha Zero. Тестировать её решили на трёх самых популярных стратегических настольных играх: шахматы, го и сёги.

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

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

Alpha Zero не использует ничего, кроме правил. Ей просто дали стартовую позицию, объяснили, как ходят фигуры, и цель игры поставить мат сопернику. И всё.

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

В декабре 2018 года Alpha Zero во второй раз сразилась с самой последней версией движка Stockfish.

Исследователи провели 1000 партий с контролем 3 часа на партию плюс 15 секунд на ход.Alpha Zero одержала уверенную победу, выиграв в 155 партиях, сыграв вничью 839 партий и проиграв только 6.

Более того, Alpha Zero одерживала победу даже в партиях с форой по времени на обдумывание. Имея в 10 раз меньше времени, чем у противника, нейросеть всё равно победила в суммарном итоге. Только 30-кратная фора во времени смогла уравнять шансы и дать Stockfish примерно равную игру 3 часа у движка и всего лишь 6 минут у нейросети.

Alpha Zero анализирует лишь 60 000 позиций в секунду, а тестируемая версия Stockfish 60 млн. позиций. Для достижения аналогичных результатов анализа нейросети нужно в 1000 раз меньше ресурсов, чем движку.

Секрет успеха в качественно другом уровне анализа. Нейросеть использует метод Монте-Карло, который высчитывает математическое ожидание комплекса ходов.

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

И, что гораздо более важно, при оценке ситуации Alpha Zero учитывает стратегическую позицию.

Давайте рассмотрим на примере одной из партий.

После 20-го хода на доске творится невообразимая стратегическая борьба. Но если нейросеть шаг за шагом минимально укрепляет свою позицию, избавляясь даже от призрачных слабостей, то движок с 24-го по 29-й ход просто топчется на месте ладьёй.

Интересно, что Stockfish в упор не видит стратегических решений Alpha Zero, оценивая позицию как абсолютно ничейную. Но в результате минимальных укреплений позиции к 39-му ходу оказывается, что все фигуры белых активны, а чёрный конь и слон занимают пассивную оборонительную позицию. А после размена ферзей и ладей даже Stockfish оценивает преимущество нейросети в +2,2. Ещё несколько ходов и король черных зажат в углу доски, а конь в одиночку не способен справиться с проходной пешкой. Поэтому программа сдалась.

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

Тем не менее нейросеть умеет играть позиционно не хуже человека и при этом идеально играет тактические позиции, где преимущество достигается в течение 5 или меньше ходов.

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

Многие теоретики считают, что благодаря шахматным компьютерам повысился и средний рейтинг топовых шахматистов. Ведь современные тренировки включают глубокую проработку компьютерных вариантов и разбора партий движками. Средний рейтинг ведущих топ-100 шахматистов в 2000 году составлял 2644 пункта Эло, а в январе 2021 года 2715. За 20 лет среднее значение увеличилось на 71 пункт.

Сегодня человек уже не способен соревноваться с компьютером в шахматах. Нейросеть вобрала в себя все преимущества человеческого шахматного мышления, но при этом лишена его недостатков.

Она умеет мыслить позиционно и при этом не допускает зевков и ошибок. И самое интересное в этом ситуации шахматы для Alpha Zero являются только тестировочным полигоном, где система оттачивает навыки работы. Реальные же её цели Google не раскрывает. Поэтому здесь может быть всё что угодно: от анализа изменений климатической ситуации до создания системы идеально персонифицированной рекламы. А как вы считаете, для чего создают настолько мощную нейросеть?

Создать своего гениального цифрового шахматиста или получить Level Up по навыкам и зарплате можно пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение. Узнайте подробности.

Другие профессии и курсы
Подробнее..

Шахматы на Delphi. Как я изобретал велосипед

11.04.2021 00:10:54 | Автор: admin

Писать игровой AI очень интересно и увлекательно - не раз убеждался в этом на личном опыте. Недавно, случайно наткнувшись на код своего старого проекта шахматной программы, решил его немного доработать и выложить на GitHub. А заодно рассказать о том, как он создавался и какие уроки преподал мне в процессе работы.

Начало

Это случилось в 2009 году: я решил написать простую шахматную программу, чтобы попрактиковаться в разработке игрового AI. Сам я не шахматист и даже не сказать что любитель шахмат. Но задача для тренировки вполне подходящая и интересная. Кроме того, играя в шахматы на доске или в программе, мне всегда было любопытно, почему тот или иной ход сильнее другого. Хотелось возможности наглядно видеть всё дерево развития шахматной позиций. Такой фичи в других программах я не встречал - так почему бы не написать самому? Ну а раз уж это тренировка - то придумывать и писать нужно с нуля, а не изучать другие алгоритмы и писать их собственную реализацию. В общем, думаю, дня за три можно управиться и сделать какой-то рабочий вариант :-)

Первая версия

Обычно шахматные движки используют поиск в глубину с алгоритмом "ветвей и границ" для сужения поиска. Но это не очень-то наглядно, поэтому решено: мы пойдём своим путём - пусть это будет поиск в ширину на фиксированную глубину. Тогда в памяти будет полное дерево поиска, которое можно как-то визуализировать. А также выяснить: а) на какую глубину можно просчитать шахматную игру в рамках имеющихся ресурсов CPU и памяти, б) насколько хорошо или плохо будет играть такой алгоритм?

Надо сказать, что на тот момент у меня был 2-ядерный процессор с 2 или 4 Гб памяти (точно уже не помню), 32-битная винда и 32-битный компилятор Turbo Delphi Explorer. Так что если временем работы ещё можно было как-то пожертвовать, то доступная процессу память была ограничена 2Gb. Про PE flag, расширяющий user memory до 3Gb я тогда не знал. Впрочем, поскольку память кушают и система, и Delphi и другие программы - для шахмат, чтобы не уходить в своп, доступно менее гигабайта.

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

  • UI - основное окно, отрисовка доски с фигурами.

  • Игровая логика - составление списка возможных ходов, выполнение хода, детекция завершения игры.

  • AI:оценка - оценочная функция позиции.

  • AI:перебор - поиск в ширину через очередь.

  • UI:браузер - окно визуализации дерева поиска, в котором можно наглядно изучать как всё работает.

Выяснилось что:

  • Поиск на глубину 3 полухода работает быстро - меньше секунды, и расходует немного памяти - 5-15 Мб. А вот поиск на глубину 4 полухода работает уже довольно долго и расходует большую часть доступной памяти. В отдельных ситуациях памяти и вовсе не хватает.

  • При глубине поиска в 3 полухода уровень сложности - "младшая школа": компьютер вроде как-то играет, не позволяет "зевнуть" фигуру, не упускает возможность поставить "вилку". Но в то же время допускает грубые ошибки и легко попадается в ловушки. В общем, очень слабый соперник.

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

Таким образом, вырисовались направления дальнейшей работы: углубить поиск, научить позиционной игре а также научить играть в эндшпиле. И многие проблемы можно решить за счёт оценочной функции.

Оценочная функция

Функция оценки позиции - важнейший компонент любого шахматного движка. Где-то она предельно простая и быстрая - учитывает лишь количество фигур и их стоимость, а где-то - сложная и учитывает множество факторов. Поскольку в моём случае количество оцениваемых позиций ограничено объемом памяти, имеет смысл использовать сложную оценочную функцию и заложить в неё как можно больше факторов.

В итоге пришел к примерно такому алгоритму:

  • Для каждого игрока:

    • Подсчитать стоимость фигур: конь - 3, ладья - 5 и т.д. Начальная стоимость пешки - 1, но она растёт по мере её продвижения.

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

    • Определение какие поля находятся под боем и кем. Это медленная операция - основная часть времени выполнения оценочной функции тратится именно здесь. Зато польза от неё колоссальная! Незащищённая фигура под боем на ходу противника - минус фигура. Защищённая - минус разность стоимости фигур. Это позволяет получить эффект углубления поиска на 1-2 уровня.

    • Если остался один король: штраф за расстояние от центра доски и штраф за расстояние до короля противника. Такая формула в эндшпиле заставляет AI стремиться прижать короля противника к краю доски, т.е. получить позицию, из которой можно найти возможность поставить мат.

  • Итоговая оценка = (white_rate - black_rate) * (1 + 10 / (white_rate + black_rate)). Эта формула делает разницу более значимой в эндшпиле, заставляя отстающего игрока избегать размена фигур, а ведущего - наоборот, стремиться к размену.

Углубление поиска

Прежде всего, несмотря на доработку оценочной функции, углубление поиска необходимо для ходов со взятием а также шахов. Для этого добавлен новый атрибут узла - вес, который используется вместо глубины. Если дочерний узел порождается обычным ходом - его вес уменьшается на 1, если ходом со взятием - на 0.4, если ходом с шахом - не уменьшается вовсе. Узлы с положительным весом возвращаются в очередь поиска и получают продолжение.

Кроме того, нужно развивать наиболее перспективные направления - ветки с наибольшей оценкой.

В итоге алгоритм получился такой:

  1. На первой стадии строится дерево с базовой глубиной 3 (при этом ветки со взятиями и шахами могут достигать заметно большей глубины).

  2. Оценка дерева алгоритмом минимакс.

  3. Если выполнены критерии принятия решения - выбирается ветка с наилучшей оценкой и алгоритм завершается.

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

  5. Переход к следующей стадии: добавляем вес всем листьям дерева и продолжаем поиск. После завершения переходим к п 2.

Критерии принятия решения:

  • Осталась единственная ветка - выбора нет.

  • Одна из веток имеет оценку существенно более высокую чем у остальных - выбираем её.

  • Истекло время на ход - выбираем ветку с наилучшей оценкой.

Кэширование

В процессе работы больше всего времени тратится на оценочную функцию. Но бывает, что в ходе поиска одни и те же позиции встречаются несколько раз: в одно и то же состояние можно прийти различными путями. Возникает мысль: почему бы не кэшировать результат оценочной функции?

Всё просто: нужно вычислить хэш от позиции и если в кэше уже есть оценка с таким хэшем - использовать её вместо вычисления. А если нет - вычислить и сохранить в кэше оценок. Основная проблема - нужна достаточно качественная и быстрая хэш-функция. После множества экспериментов получилось написать приемлемую функцию, вычисляющую 64-битный хэш. Общее количество возможных шахматных позиций значительно больше 264, но количество позиций, оцениваемых в ходе партии значительно меньше 232, поэтому более-менее качественная хэш-функция должна свести вероятность хэш-коллизий к минимуму.

Процент "попаданий" в кэш в ходе игры получился в районе 30-45%, но в эндшпиле достигает 80-90%, что даёт ускорение почти в 5-10 раз, а следовательно позволяет увеличить глубину поиска. Неплохой выигрыш!

Что получилось?

Я добавил библиотеку дебютов, и в таком состоянии программа играла уже довольно неплохо - примерно на уровне 2-го разряда, а может и чуть сильнее. Результат, в принципе, достойный - можно остановиться. Хотя были вполне очевидны недостатки и направления развития:

  • AI работает в один поток - ресурс CPU задействован не полностью.

  • А что если предоставить больше памяти?

  • AI думает только во время своего хода. Почему бы не использовать время соперника для просчёта наиболее вероятных продолжений?

  • Главная слабость: детерминированность. Каждый ход рассматривается как отдельная задача, каждая позиция приводит к детерминированному решению. Достаточно выиграть один раз, чтобы запомнить последовательность ходов, которая всегда приведёт к победе. А играть с таким соперником неинтересно.

Однако, к этому времени код проекта уже достаточно "замусорился" и требовал рефакторинга. Поскольку на него и так уже было потрачено много времени, я решил его забросить.

Дальнейшее развитие

Недавно наткнувшись на этот заброшенный проект, решил всё-таки привести его код в порядок и доработать. Доработки сделал такие:

  • Многопоточность: сейчас у меня уже 8-поточный, а не 2-поточный CPU, поэтому многопоточный вариант даёт серьёзную прибавку в скорости.

  • 64-битный режим: кроме возможности использовать больше памяти, было любопытно, будет ли алгоритм работать быстрее на архитектуре x64. Как ни странно, оказалось что нет! Хотя отдельные функции на x64 работают быстрее, в целом версия x86 оказалась на 5-10% быстрее. Возможно 64-битный компилятор Delphi не очень хорош, не знаю.

  • Больше памяти: даже в 32-битном режиме за счёт PE-флага расширения адресного пространства доступной памяти стало больше. Однако практика показала, что больше 1 Гб памяти все-равно не нужно - разве что для хранения "обрезанных" ветвей дерева. К усилению игры увеличение памяти не приводит.

  • Непрерывность поиска: теперь дерево поиска не создаётся с нуля каждый ход, а создаётся лишь в начале партии. Когда делается ход - неактуальные ветви дерева обрезаются а поиск продолжается с того состояния, которое было. В том числе и во время хода соперника - идёт проработка перспективных продолжений, поэтому когда соперник сделает ход, поиск продолжится не с нуля. Быть может и вовсе получится сразу же сделать ход. Момент времени, когда игрок сделает ход - это фактор случайности, который делает игру недетерминированной. Теперь уже нельзя запомнить выигрышную последовательность ходов.

  • Оценка дерева. Ввёл новый атрибут узла - качество оценки. Качество равно сумме качества всех прямых потомков, умноженное на коэффициент затухания. Т.е. качество оценки показывает проработанность ветки.

  • Углубление поиска. Качество оценки вместе с самой оценкой учитывается при выборе веток для углубления поиска. Чем выше оценка и чем ниже её качество - тем выше приоритет такой ветки для её развития. Потому что мало смысла до посинения развивать и так хорошо проработанную ветку с максимальной оценкой, которая уже вряд ли существенно изменится, когда она сравнивается со слабо проработанными ветками, чья оценка может значительно измениться.

  • База оценок и самообучение. В процессе игры какие-то позиции детально прорабатываются и получают оценку с высоким качеством. Почему бы не сохранять такие оценки для использования в других партиях? Это ещё одна фича, которая делает игру недетерминированной.

В результате этих доработок AI стал сильнее, и вполне уверенно обыгрывает старую версию игры. Я провел несколько партий против AI на chess.com и выяснил, что уровень моей программы примерно соответствует рейтингу 1800-1900. Прогресс есть, и это хорошо!

Программирование игрового AI - занятие чертовски затягивающее: всегда хочется добиться большего. И хотя у меня по прежнему есть масса идей для дальнейшего развития, наступает момент, когда надо остановиться. Думаю, он наступил. Однако если кто-либо желает - может взять мой код, побаловаться, поэкспериментировать, что-нибудь реализовать. Благо, сейчас Delphi доступен каждому благодаря бесплатной Community Edition, не говоря уже про бесплатный Free Pascal и Lazarus. Код проекта (а также скомпилированный exe-шник) можно взять тут: https://github.com/Cooler2/chess (для компиляции понадобится также кое что из https://github.com/Cooler2/ApusGameEngine). Спасибо всем, кто дочитал :-)

Подробнее..

О шахматах. И не только

02.12.2020 10:08:40 | Автор: admin
Сегодня не будет тяжких раздумий о настоящем и будущем компьютерной индустрии. Сегодня я хочу рассказать об одном из своих хобби. Я играю в массу разных игр: футбол, хоккей, теннис (большой и маленький), покер, преферанс, биржа и т.п. Но мой профильный вид спорта шахматы. Дальше кандидата в мастера моя карьера на этом поприще не продвинулась, но любовь к древней игре я сохраняю уже 4 десятка лет. Интересно, что она вполне ужилась с другим увлечением программированием, породив интерес к искусственному интеллекту и теории игр. И разумеется, последние прорывы в этой области связанные с феноменальными успехами проекта AlphaZero не могли пройти мимо меня.
image
Тогда я просто сидел и восхищался партиями AlphaZero против Stockfish. А сейчас вернулся к теме в связи с задачей оптимизации нейронных сетей, которой иногда приходится заниматься по работе (увы, меньше чем хотелось бы). Как мне кажется, задачи эти могут оказаться тесно связанными, поэтому захотелось как то систематизировать свои идеи.
Шахматы игра с полной информацией, основанная на переборе вариантов (так же как шашки, го и т.п.).
image
Проблема однако в том, что дерево вариантов в шахматах растет достаточно быстро (хотя и значительное медленнее, чем в го). Например, при полной доске фигур в спокойной позиции у каждой стороны есть примерно по 10 разумных продолжений. Таким образом, всего за 3 хода черных и белых (6 полуходов) мы можем получить миллион позиций из данной. Также, примем во внимание то, что средняя партия между людьми продолжается 40-50 ходов (между компьютерами 80-100). Таким образом, мы придем к выводу, что полный просчет дерева вариантов для большинства позиций невозможен а значит мы должны ориентироваться на частичное обрезание дерева перебора, как по ширине, так и в глубину. А теперь давайте посмотрим, как люди и машины справлялись с этой проблемой. Начну я с небольшого исторического обзора.

Белковые шахматы.

Шахматы известны порядка 1400 лет, но первые большие турниры по ним начали проводиться в середине XIX века. Это было время открытых и романтических сражений. Соперники старались побыстрее ввести в бой фигуры, вскрыть позицию и начать атаку на короля. С материальными и позиционными уступками никто особенно не считался. Но как ни удивительно, первым официальным чемпионом мира стал антагонист романтических шахмат Вильгельм Стейниц.
image
Он заложил основы позиционной игры. Во многом благодаря Стейницу мы стали оперировать такими понятиями, как пешечная структура, сильные и слабые поля, хорошие и плохие фигуры. Именно это и привнесло в шахматы элемент стратегии, основанной на долгосрочных преимуществах. Стейниц развивал позиционный подход и неумолимо наказывал своих противников за материальные жертвы и позиционные огрехи. О первом чемпионе очень хорошо сказал Эммануил Ласкер, сменивший его на шахматном троне: Одаренность Стейница как практического игрока была ниже, чем одаренность Блэкберна или Цукерторта, которых он все же победил, ибо был великим мыслителем, а они нет. Стейниц сформулировал базовые принципы оценки позиции и вытекающие из них планы игры на языке высокого уровня (в данном случае немецком). Соответственно, он сделал их доступными для изучения другими людьми. Это и сформировало то, что мы называем человеческим подходом к шахматной игре. Мы очень серьезно обрезаем дерево вариантов в шахматах, основываясь на позиционных принципах. Какие-то ходы отбрасываются потому, что ведут к плохой позиции на просчитываемом горизонте. Kакие-то потому что ведут к долговременным уступкам, другие потому что являются бесцельными. В итоге мы просчитываем совсем небольшую часть возможных вариантов.
Дальнейшее понимание шахмат было по сути развитием идей, заложенных первым чемпионом. Появились такие понятия как блокада, профилактика, доминация. Шахматисты стали изучать принципы разыгрывания типовых позиций, возникающих из различных дебютов(замкнутые цепи, изолированная пешка и т.п.). Так или иначе, изучались позиции близкие к материальному равновесию. Но бывали и исключения например, молодой Михаил Таль играл в ином стиле. Он создавал острые несбалансированные позиции с нарушением соотношения материала (позднее подобную игру демонстрировал и Гарри Каспаров). Не привыкшие к подобной игре противники пасовали один за другим. Таль стал чемпионом мира в 1960, но год спустя проиграл матч-реванш. Во второй половине ХХ века фокус исследования сместился в сторону начала партии дебюта. С легкой руки Mихаила Ботвинника (6-го чемпиона мира) и Гарри Каспарова (13-го) львиную долю своего времени шахматисты стали уделять проработке конкретных дебютных вариантов. Все больше используя компьютеры в этом процессе. В результате многие варианты в популярных дебютах разработаны вплоть до позиций, где результат игры предопределен. Это приводит к определённому выхолащиванию шаxмат, а также к необходимости запоминать огромное количество вариантов, чтобы не быть разгромленным уже в дебюте. Неудивительно, что последнее время маятник качнулся в обратную сторону. Нынешний чемпион мира Магнус Карлсен скорее стремится к тому, чтобы получить по итогам дебюта не перевес, а игровую позицию, не заезженную вдоль и поперек компьютерными движками. Тяжесть борьбы переносится на более поздние стадии партии (миттельшпиль, эндшпиль).

Силиконовые шахматы.

По меткому выражению Александра Кронрода, шахматы являются дрозофилой искусственного интеллекта. Их изучение началось с появлением первых компьютеров и привлекало таких пионеров как Алан Тьюринг и Клод Шеннон. Именно Шеннон выдвинул первую оценку стоимости шахматных фигур Король =200, Ферзь =9, Ладья = 5, Слон = 3, Koнь =3, Пешка =1. Как ни странно, именно эта простая оценка определила развитие шахматного программирования на следуюшие 70 лет. Также Шеннон предугадал разделение шахматных программ на быстрые (brute force) и умные (clever). Быстрые программы полностью перебирают все возможные варианты на определенную глубину, оценивают позицию с помощью простой оценочной функции (вроде соотношения материала) и выбирают наилучший ход с помощью принципа минимакса. Умные программы используют более сложные алгоритмы и варьируют глубину перебора примерно так же, как это делает человек. Созданием подобного алгоритма в последние годы жизни занимался 6-й чемпион мира Михаил Ботвинник. Однако без особого успеха, как и многие другие создатели умных программ. Ибо в третьем своем предсказании Шеннон ошибся умные программы постоянно терпели неудачу в борьбе с быстрыми. Причина в том, что перебор очень хорошо параллелится и оптимизируется. А простая оценка Шеннона оказалась достаточно устойчивой и робастной. Ибо как известно шахматистам, любой позиционный перевес рано или поздно трансформируется в материальный. В то время как принципы оценки позиции поддаются формализации гораздо хуже. Они требуют громоздких последовательных вычислений и плохо оптимизируются. В результате с ростом производительности компьютеров быстрые программы стали доминировать. Так сформировался mainstream компьютерных шахмат, разительно отличающийся от человеческих перебор на определенную глубину с использованием aльфа-бета отсечения (и еще кой-каких эвристик) и оценка позиции по Шеннону. Также программы стали активно развивать и использовать дебютные (когда игра еще не ушла далеко от начальной позиции) и эндшпильные (когда количество фигур мало и дерево вариантов может быть просчитано полностью) базы. А производительность компьютеров все время росла, и программисты тоже не сидели без дела, постоянно оптимизируя движки. 11 мая 1997 года произошло эпохальное событие компьютер Deep Blue победил чемпиона мира Гарри Каспарова в матче из 6 партий.
image
Сразу после этого IBM прикрыла этот ни разу не дешевый проект. Специально для Deep Blue были созданы чипы, ускоряющие шахматные расчеты! Впрочем, и без них превосходство компьютера над человеком уже было очевидным. Deep Fritz, Deep Junior, Rybka, Komodo, Stockfish стали беспощадно громить ведущих гроссмейстеров, даже давая им материал вперед Между собой они, впрочем, играли с переменным успехом результаты чемпионатов мира среди программ можно найти тут.
Все изменилось, когда создатели AlphaZero, победив чемпиона мира по игре в го Ли Седоля, взялись наконец за шахматы. Результат оказался феноменален после 4 часов игры с самим собой AlphaZero разгромил StockFish, выиграв 28 партий и сведя вничью 72. Через год DeepMind провел более чистый эксперимент, разрешив Stockfish пользоваться дебютной и эндшпильной книгами. И все равно результат +155 -6 =839 не оставляет сомнений в том, кто является сильнейшим игроком в мире на данный момент.
Давайте разбираться, как устроено это новое чудо. (Для желающих поковыряться в python-скриптах есть уже целая книга). Основной алгоритм поиск по деревьям Монте Карло. Это тоже, конечно, перебор, что роднит AlphaZero c другими шахматными программами. Но слово Монте Карло не должно вводить в заблуждение поиск управляется нейронной сетью(для го была 80-слойная, здесь не знаю какая) и является узконаправленным. АlphaZero обрезает дерево перебора, исходя из позиционных соображений, так же как это делает человек! По сравнению со Stockfish Alphazero перебирает в почти в 1000 раз меньше вариантов. Она лопатит гораздо меньше мусора, но наиболее вероятные сильнейшие варианты просчитывает глубже и точнее. Поэтому выигрывает даже имея меньше времени или на более слабом железе. Ну и самое главное АlphaZero изучала шахматы исключительно на собственном опыте. У нее не было никакой априорной информации. Ее понимание не испорчено Шенноновской оценкой. У неё своё уникальное понимание видение шахмат и стиль игры, зачастую игнорирующий материальное соотношение (как молодой Таль!).

Какие выводы мы можем сделать из этого замечательного эксперимента?
1. Он решительно опровергает все соображения о выхолащивании шахмат. Самое появление игрока, который играет в невиданном до сих пор стиле и демонстрирует тотальное превосходство над конкурентами свидетельствует о том, что возможности игры еще далеко не исчерпаны.
2. Показывает насколько мало все же мы знаем о шахматах. За 4 часа тренировки искусственный интеллект сумел понять об игре много больше чем мы (тупые белковые) за полторы тысячи лет! Пытаясь как то осознать этот феномен я нашел такую аналогию наше знание о шахматах похоже на разложение функции в степенной ряд (типа Тейлора-Маклорена) вблизи нуля. Где нулевой точкой является материальное равновесие. Применимость такого представления падает по мере удаления от материального равенства. В то время как AlphaZero видит всю картину(функцию) целиком.
3. Ставит главный вопрос что же дальше? Конечно можно идти экстенсивным путем увеличивая время тренировки, используя все более мощное железо, оптимизируя софт и т.п. Но мне кажется, что гораздо важнее другое направление. Для AlphaZero натренированная нейронная сетка это все лишь набор чисел коэффициентов. Как мы можем извлечь из этого набора чисел новое знание об игре? В каком то смысле мы должны решить задачу, сродни той, которую решил Стейниц полтора века назад. Описать знание об игре, используя человеческий язык. И сделать его доступным для изучения другими людьми. Это именно то с чем я столкнулся при оптимизации нейронных сеток и называю обратной задачей искусственного интеллекта. А решать ее нам придется во многих областях. И если мы не сумеем призрак SkyNet станет чуть менее далеким и чуть более зловещим Ну а пока я был бы благодарен за ссылки, статьи и идеи как подступиться к этой проблеме.

PS. А партии вы все же посмотрите. Я получил ни с чем не сравнимое удовольствие.
Подробнее..

Шахматный телепорт

11.05.2021 14:17:08 | Автор: admin

Если зайти на какой-нибудь шахматный сайт типа личес, то там можно обнаружить помимо обычных шахмат шахматы с альтернативными правилами. Например, давно известна игра Шахматы Фишера. Так же очень популярны CrazyHouse, King of the hill, Horde и другие. Сегодня, хочу представить вашему вниманию новую шахматную игру Teleport. Кому интересно добро пожаловать под кат)

Проектируем Teleport

Предпосылка На данный момент шахматы с классическими правилами очень хорошо исследованы. Созданы огромные базы известных типовых позиций. Какие-то позиции хорошие, какие-то плохие. Профессиональных игроки заучивают такие позиции, чтобы усилить игру. В итоге может возникнуть ситуация, что побеждает такой игрок, кто лучше запомнил, как ходить в первоначальных позициях. Но хотелось бы, чтобы победил игрок, который быстрее и лучше мыслит за доской, чтобы изученные варианты меньше влияли на победу.Как же это сделать?Да просто нужно усложнить шахматы. В классических шахматах, чтобы сделать ход, у вас есть два варианта. Вы можете пойти либо на пустую клетку, либо срубить фигуру противника.Для игры Teleport предлагаю третий вариант. Можно пойти на клетку где стоит ваша фигура, в этом случае происходит так называемый Телепорт, и фигуры меняются местами. Вот пример, как это делается: Ходим Конь f3-g5

Конь и слон меняются местами. В данной ситуации первым в руку мы должны взять коня, потому что конь f3 может пойти на поле g5, а слон не может встать на f3. Значит инициатором телепорта будет конь.

Чего же мы добиваемся этим правилом? Количество возможных ходов возрастает, а значит возрастает сложность игры. Позиции становятся сложнее, анализировать их трудней. Игра становится зрелищней и интересней.Но продолжим проектировать игру. Например, возникает вопрос, должны ли телепортироваться пешки?

Вот ситуация: Ход белых. Ладья h8-h2 и пешка стремительно проходит в ферзи. Хорош ли такой маневр в целом для игры?

Думаю, да, очень хорош. В данной ситуации еще 1-2 хода назад черные вели, рассчитывали на победу, но вдруг ситуация в корне меняется. И это прекрасно.Далее.

Если пешка может телепортироваться, значит может произойти такая ситуация: Ладья первым же ходом делает телепорт h1-h2, и пешка становится на первую линию горизонтали. В том что пешка очутилась на первой линии, нет ничего страшного. Затем пешка сможет двигаться дальше. И она пойдет так: с поля h1-h2 пешка сдвинется на одну клетку, и снова очутится на второй линии. А со второй линии, как известно пешка может идти на третью линию или перепрыгнуть третью и сразу встать на четвертую. Здесь ничего не меняется.

Еще момент. Можно ли в этой позиции сделать телепорт?

Пешка ходит только в перед, по диагонали она только рубит. Рубить свою фигуру нельзя, значит пешкой по диагонали телепорт не возможен. Ладья ходит по вертикали и горизонтали, и значит в данной ситуации телепорт ладьей не возможен. В общем то это очевидно, но это надо проговорить.А как на счет короля, должен ли король делать телепорт?Рассмотрим позицию. Ход белых. Здесь численное преимущество белых. Если белый король находился бы в безопасности, исход был бы очевиден белые бы победили. Если мы разрешим королю телепортироваться (Слон a8-h1), то король убежит, а вместе с ним убежит зрелищность и накал страстей).

Было бы здорово, если бы в этой ситуации победили бы черные? Поэтому принимаем решение король телепортироваться не может!Просто с большим количеством фигур, легче организовать телепорт для короля, а значит мат в начале и середине игры будет поставить сложно. И это идет в разрез с тем, что мы хотим сделать игру зрелищной.Вот еще пример. Белые ставят детский мат.

Но если бы, король мог телепортироваться (Ферзь d8-e8), тогда бы король улизнул, и детского мата не произошло. В этом случае, зрелищность теряется.

ИТАК

Окончательные правила

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

Немного задачХод черных. Мат в 1 ход. Ну правда же красиво?

Белые начинают и выигрывают. Мат в 1 ход.

Белые начинают и выигрывают в 2 хода.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru