Мы будем кодировать фишки цифрами от нуля до 14, а пустая клетка будет равна 15. Это просто согла-
шения о внутреннем представлении фишек, показывать мы их будем совсем по-другому.
С этим значением мы можем легко определить функцию определения конца игры. Нам нужно только
добавить deriving (Eq) к типу Game. Тогда функция isGameOver примет вид:
isGameOver :: Game -> Bool
isGameOver = ( == initGame)
Делаем ход
Напишем функцию:
move :: Move -> Game -> Game
Она обновляет позицию после хода. В пятнашках не во всех позициях доступны все ходы. Если пустышка
находится на краю, мы не можем вывести её за пределы доски. Это необходимо как-то учесть. Каждый ход
задаёт направление обмена фишками. Если у нас есть текущее положение пустышки и ход, то по ходу мы
можем узнать направление, а по направлению ту фишку, которая займёт место пустышки после хода. При
этом нам необходимо проверять находится ли та фишка, которую мы хотим поместить на пустое место в пре-
делах доски. Например если пустышка расположена в самом верху и мы хотим сделать ход Up (передвинуть
её ещё выше), то положение игры не должно измениться.
import Prelude hiding (Either(.. ))
newtype Vec = Vec (Int, Int)
move :: Move -> Game -> Game
move m (Game id board)
| within id’ = Game id’ $ board // updates
| otherwise
= Game id board
where id’ = shift (orient m) id
updates = [(id, board ! id’), (id’, emptyLabel)]
-- определение того, что индексы внутри доски
within :: Pos -> Bool
within (a, b) = p a && p b
where p x = x >= 0 && x <= 3
-- смещение положение по направдению
shift :: Vec -> Pos -> Pos
shift (Vec (va, vb)) (pa, pb) = (va + pa, vb + pb)
-- направление хода
orient :: Move -> Vec
orient m = Vec $ case m of
Up
-> (-1, 0)
Down
-> (1 , 0)
Left
-> (0 , -1)
Right
-> (0 , 1)
-- метка для пустой фишки
emptyLabel :: Label
emptyLabel = 15
Маленькие функции within, shift, orient, emptyLabel делают как раз то, что подписано в комментариях.
Думаю, что их определение не сложно понять. Но есть одна тонкость, поскольку в функции orient мы поль-
зуемся конструкторами Left и Right необходимо спрятать тип Either из Prelude. Мы ввели дополнительный
тип Vec для обозначения смещения, чтобы случайно не подставить вместо него индексы.
Разберёмся с функцией move. Сначала мы вычисляем положение фишки, которая пойдёт на пустое место
id’. Мы делаем это, сместив (shift) положение пустышки (id) по направлению хода (orient a).
Мы обновляем массив, который описывает доску с помощью специальной функции //. Посмотрим на её
тип:
Пятнашки | 211
(//) :: Ix i => Array i a -> [(i, a)] -> Array i a
Она принимает массив и список обновлений в этом массиве. Обновления представлены в виде пары
индекс-значение. В охранном выражении мы проверяем, если индекс перемещаемой фишки в пределах дос-
ки, то мы возвращаем новое положение, в котором пустышка уже находится в положении id’ и массив об-
новлён. Мы составляем список обновлений updates bз двух элементов, это перемещения фишки и пустышки.
Если же фишка за пределами доски, то мы возвращаем исходное положение.
Перемешиваем фишки
Игра начинается с такого положения, в котором все фишки перемешаны. Но перемешивать фишки про-
извольным образом было бы не честно, поскольку известно, что в пятнашках половина расстановок не при-
водит к выигрышу. Поэтому мы будем перемешивать так: мы стартуем из начального положения и делаем
несколько ходов произвольным образом. Количество ходов определяет сложность игры:
shuffle :: Int -> IO Game
shuffle n = (iterate (shuffle1 =<< ) $ pure initGame) !! n
shuffle1 :: Game -> IO Game
shuffle1 = un
Функция shuffle1 перемешивает фишки один раз. С помощью функции iterate мы строим список рас-
становок, которые мы получаем на каждом шаге перемешивания. В самом конце мы выбираем из списка
n-тую позицию. Обратите внимание на то, что мы не можем просто написать:
iterate shuffle1 initGame
Так у нас не совпадут типы. Для функции iterate нужно чтобы вход и выход функции имели одинаковые
типы. Поэтому мы пользуемся в функции iterate методами классов Monad и Applicative (глава 6).