Этим утром я понял, что написал сообщение в блоге после того, как заставил непревзойденный компьютерный проигрыватель работать на всех языках, на которых я играл в крестики-нолики (доказательство: Первое использование минимакса, Минимакс в Java, «Негамакс в Рубин"). Теперь, когда у меня есть рабочее решение на Clojure, пришло время опубликовать его в блоге.

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

Эта функция вызывается с помощью функции choose-space (строка 42) с переданным состоянием доски. Состояние доски представляет собой хэш-карту с размером доски (т. е. количеством строк/столбцов) и списком ходов в последовательном порядке. доска с нулевым индексом (поэтому позиция 0 — это верхний левый угол, позиция 4 — середина, позиция 8 — нижний правый угол), например. { :size 3 :board [4] } (подробнее).

choose-space вызывает negamax и передает board-state, начальное depth из 0, начальное colour из 1 и marker компьютера (X или O). Этот маркер определяется состоянием доски при начальном вызове функции — при четном количестве ходов (0 считается четным) это X, при нечетном количестве ходов — O. В данной реализации X всегда идет первым.

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

Если игра не окончена, вызывается функция score-spaces. Это занимает каждое доступное место на доске и эмулирует ход туда. Затем он снова рекурсивно вызывает функцию negamax.

После завершения рекурсивного вызова, предполагая, что глубина не равна нулю, negamax возвращает наивысший из возможных результатов (при условии, что игрок, против которого он играет, также непобедим. Это negamax-score. В Clojure есть встроенная функция под названием zipma, которая принимает два списка и сшивает их вместе в карту, где первый список является ключом, а второй — значением. В этом случае первый список — это список доступных пространств, а второй — соответствующий негамакс баллы.

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

На самом деле я начал с алгоритма Minimax, но потом подумал об Alpha Beta Pruning и решил переписать его. На самом деле это было безболезненно, и мои тесты на удивление остались зелеными. Между ними не слишком большая разница.

По моему опыту в ruby, сокращение альфа-бета значительно проще в Negamax, но не в Clojure. В Clojure нет очевидного способа вернуться из вызова функции досрочно, как в некоторых других языках, просто вставив оператор return.

Мои наставники дали мне понять, что если я использую reduce и reduced, я могу делать именно то, что хочу, так что теперь мне просто нужно найти время, чтобы сделать это. Я очень рад, что у меня это получилось, и не могу дождаться, когда это заработает с Alpha Beta Pruning!