несколько ходов произвольным образом. Количество ходов определяет сложность игры:
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).
Теперь определим функцию shuffle1. Мы делаем ход в текущей позиции, который мы выбрали случай-
ным образом из списка доступных ходов. Выбором случайного элемента из списка, будет заниматься функция
randomElem, а функция nextMoves будет возвращать список доступных ходов для данного положения:
shuffle1 :: Game -> IO Game
shuffle1 g = flip move g (randomElem $ nextMoves g)
randomElem :: [a] -> IO a
randomElem = un
nextMoves :: Game -> [Move]
nextMoves = un
Нам осталось определить всего две функции, и всё готово для игры. Определим выбор случайного эле-
мента из списка:
import System.Random
…
randomElem :: [a] -> IO a
randomElem xs = (xs !! ) randomRIO (0, length xs — 1)
Мы генерируем случайное число в диапазоне индексов списка и затем извлекаем элемент. Теперь функ-
ция определения ходов в текущем положении:
nextMoves g = filter (within . moveEmptyTo . orient) allMoves
where moveEmptyTo v = shift v (emtyField g)
allMoves = [Up, Down, Left, Right]
Мы выполняем схожие операции с теми, что были в функции move. Мы фильтруем из списка всех ходов
те, что выводят пустую фишку за пределы доски.
212 | Глава 13: Поиграем
Отображение положения
Я немного поторопился, нам осталась ещё одна функция. Это отображение позиции. Я не буду подробно
останавливаться на теле функции, скажу лишь то, что она составляет строку так как это показано в коммен-
тарии к функции.
—
+—-+—-+—-+—-+
—
|
1 |
2 |
3 |
4 |
—
+—-+—-+—-+—-+
—
|
5 |
6 |
7 |
8 |
—
+—-+—-+—-+—-+
—
|
9 | 10 | 11 | 12 |
—
+—-+—-+—-+—-+
—
| 13 | 14 | 15 |
|
—
+—-+—-+—-+—-+
—
instance Show Game where
show (Game _ board) = ”n” ++ space ++ line ++
(foldr (a b -> a ++ space ++ line ++ b) ”n” $ map column [0 .. 3])
where post id = showLabel $ board ! id
showLabel n
= cell $ show $ case n of
15 -> 0
n
-> n+1
cell ”0”
= ”
”
cell [x]
= ’ ’:’ ’: x :’ ’:[]
cell [a,b] = ’ ’: a : b :’ ’:[]
line = ”+—-+—-+—-+—-+n”
nums = ((space ++ ”|”) ++ ) . foldr (a b -> a ++ ”|” ++ b) ”n” .
map post
column i = nums $ map (x -> (i, x)) [0 .. 3]
space = ”t”
Теперь мы можем загрузить модуль Loop в интерпретатор и набрать play. Немного отвлечёмся и поигра-
ем.
Prelude> :l Loop
[1 of 2] Compiling Game
( Game. hs, interpreted )
[2 of 2] Compiling Loop
( Loop. hs, interpreted )
Ok, modules loaded: Loop, Game.
*Loop> play
Привет! Это игра пятнашки
+—-+—-+—-+—-+
|
1 |
2 |
3 |
4 |
+—-+—-+—-+—-+
|
5 |
6 |
7 |
8 |
+—-+—-+—-+—-+
|
9 | 10 | 11 | 12 |
+—-+—-+—-+—-+
| 13 | 14 | 15 |
|
+—-+—-+—-+—-+
Возможные ходы пустой клетки:
left
или l
— налево
right
или r
— направо
up
или u
— вверх
down
или d
— вниз
Другие действия:
new int
или n int — начать новую игру, int — целое число,
указывающее на сложность
quit
или q
— выход из игры
Начнём новую игру?
Укажите сложность (положительное целое число):
5
+—-+—-+—-+—-+
|
1 |
2 |
3 |
4 |
+—-+—-+—-+—-+
|
5 |
6 |
7 |
8 |
+—-+—-+—-+—-+
Пятнашки | 213
|
9 |
| 10 | 11 |
+—-+—-+—-+—-+
| 13 | 14 | 15 | 12 |
+—-+—-+—-+—-+
Ваш ход:
r
+—-+—-+—-+—-+
|
1 |
2 |
3 |
4 |
+—-+—-+—-+—-+
|
5 |
6 |
7 |
8 |
+—-+—-+—-+—-+
|
9 | 10 |
| 11 |
+—-+—-+—-+—-+
| 13 | 14 | 15 | 12 |
+—-+—-+—-+—-+
Ваш ход:
r
+—-+—-+—-+—-+
|
1 |
2 |
3 |
4 |
+—-+—-+—-+—-+
|
5 |
6 |
7 |
8 |
+—-+—-+—-+—-+
|
9 | 10 | 11 |
|
+—-+—-+—-+—-+
| 13 | 14 | 15 | 12 |
+—-+—-+—-+—-+
Ваш ход:
d
+—-+—-+—-+—-+
|
1 |
2 |
3 |
4 |
+—-+—-+—-+—-+
|
5 |
6 |
7 |
8 |
+—-+—-+—-+—-+
|
9 | 10 | 11 | 12 |
+—-+—-+—-+—-+
| 13 | 14 | 15 |
|
+—-+—-+—-+—-+
Игра окончена.
Ураа, получилось. Мы так долго писали программу, проверяя лишь типы, и в самом конце, когда мы
закончили определение, всё работает. Конечно не всё работает так гладко, я уже написал эту программу и
объясняю готовое решение, но когда общая схема программы утряслась, возможные ошибки определяются
на раз. Мы могли вызвать отображение позиции не в том порядке или забыть проверку конца игры, всё это
несколько строчек изменений.
Самые неприятные ошибки происходят, когда в середине выясняется, что мы ошиблись с типами. Типы,
которые мы выбрали не могут описать явление, возможно мы не можем делать какие-то операции, которые
нам, как неожиданно выяснилось, очень нужны. Это значит, что нужно менять каркас. Менять каркас, это
значит сносить весь дом и строить новый. Возможно разрушения окажутся локальными, мы строим не дом,
а город. И сносить придётся не всё, а несколько кварталов. Но это тоже большие перемены. Поэтому шаг