Игра перемешивается согласно правилам, поэтому функцию shuffle мы поселим в модуле Game. А функ-
ция readInt это скорее элемент взаимодействия с пользователем, ведь в ней мы распознаём число в строчном
ответе, она останется в модуле Loop.
Проверим работает ли наша программа:
*Loop> :r
[1 of 2] Compiling Game
( Game. hs, interpreted )
[2 of 2] Compiling Loop
( Loop. hs, interpreted )
Ok, modules loaded: Game, Loop.
*Loop>
Работает! Можно спускаться по дереву выражения ниже. Сейчас нам предстоит написать одну из самых
сложных функций, это функция gameLoop.
13.2 Пятнашки
Цикл игры
Функция цикла игры принимает текущую позицию. При этом у нас два варианта. Возможно игра пришла
в конечное положение (isGameOver) и мы можем сообщить игроку о том, что он победил (showResults), если
это не так, то мы покажем текущее положение (showGame), спросим ход (askForMove) и среагируем на ход
(reactOnMove).
-- в модуль Loop
gameLoop :: Game -> IO ()
gameLoop game
| isGameOver game
= showResults game >> setup >>= gameLoop
| otherwise
= showGame game >> askForMove >>= reactOnMove game
showResults :: Game -> IO ()
showResults = un
showGame :: Game -> IO ()
showGame = un
Пятнашки | 205
askForMove :: IO Query
askForMove = un
reactOnMove :: Game -> Query -> IO ()
reactOnMove = un
-- в модуль Game
isGameOver :: Game -> Bool
isGameOver = un
Как определить закончилась игра или нет это скорее дело модуля Game. Все остальные функции принадле-
жат модулю Loop. Функция askForMove возвращает реплику пользователя и тут же направляет её в функцию
reactOnMove. Функции showGame и showResults ничего не возвращают, они только меняют состояния экрана.
После того как игра закончится мы предложим игроку начать новую.
Обратите внимание на то, как даже не дав определение функции, мы всё же очерчиваем её смысл в
объявлении типа. Так посмотрев на функцию askForMove и сопоставив тип с именем, мы можем понять, что
эта функция предназначена для запроса значения типа Query, для запроса реплики пользователя. А по типу
функции showGame мы можем понять, что она проводит какой-то побочный эффект, судя по имени что-то
показывает, из типа видно что показывает значение типа Game или текущую позицию.
Отображение позиции
Определим функции отображения результата и позиции. Когда игра закончится мы покажем итоговое
положение и объявим результат.
showResults :: Game -> IO ()
showResults g = showGame g >> putStrLn ”Игра окончена.”
Теперь определим функцию showGame. Если тип Game является экземпляром класса Show, то определение
окажется совсем простым:
-- в модуль Loop
showGame :: Game -> IO ()
showGame = putStrLn . show
-- в модуль Game
instance Show Game where
show = un
Реакция на реплики пользователя
Теперь нужно определить функции askForMove и reactOnMove. Первая функция требует установить про-
токол реплик пользователя, в каком виде он будет набирать значения типа Query. Нам пока лень об этом
думать и мы перейдём к функции reactOnMove. Вспомним её тип:
reactOnMove :: Game -> Query -> IO ()
Функция принимает текущее положение и запрос пользователя. И ничего не возвращает, она продолжает
игру. В любом случае в этой функции будет сопоставление с образцом по запросам пользователя так что
можно написать:
reactOnMove :: Game -> Query -> IO ()
reactOnMove game query = case query of
Quit
->
NewGame n
->
Play
m
->
Рассмотрим каждый из случаев. В первом случае пользователь говорит, что ему надоело и он уже наиг-
рался. Чтож попрощаемся и вернём значение единичного типа.
206 | Глава 13: Поиграем
...
Quit
-> quit
...
quit :: IO ()
quit = putStrLn ”До встречи.” >> return ()
В следующем варианте пользователь хочет начать всё заново. Так начнём!
NewGame n
-> gameLoop =<< shuffle n
Мы вызвали функцию перемешивания shuffle с заданным уровнем сложности. И рекурсивно вызвали
цикл игры с новой позицией. Всё началось по новой. В третьей альтернативе пользователь делает ход, на это
мы должны обновить позицию запустить цикл игры с новым значением:
-- в модуль Loop
Play
m
-> gameLoop $ move m game
-- в модуль Game
move :: Move -> Game -> Game
move = un
Функция move обновляет согласно правилам текущую позицию. Соберём все определения вместе:
reactOnMove :: Game -> Query -> IO ()
reactOnMove game query = case query of
Quit
-> quit
NewGame n
-> gameLoop =<< shuffle n
Play
m
-> gameLoop $ move m game
Слушаем игрока
Теперь всё же вернёмся к функции askForMove, научимся слушать пользователя. Сначала мы скажем
какую-нибудь вводную фразу, предложение ходить (showAsk) затем запросим строку стандартной функцией
getLine, потом нам нужно будет распознать (parseQuery) в строке значение типа Query. Если распознать его