Prelude> :l Loop
[1 of 2] Compiling Game
( Game. hs, interpreted )
[2 of 2] Compiling Loop
( Loop. hs, interpreted )
Loop. hs:46:28:
Ambiguous occurrence ‘Left’
It could refer to either ‘Prelude.Left’,
imported from ‘Prelude’ at Loop. hs:1:8-11
(and originally defined in ‘Data.Either’)
or ‘Game.Left’,
imported from ‘Game’ at Loop. hs:5:1-11
(and originally defined at Game. hs:10:25-28)
Loop. hs:47:28:
Ambiguous occurrence ‘Left’
...
...
Failed, modules loaded: Game.
*Game>
По ошибкам видно, что произошёл конфликт имён. Конструкторы Left и Right уже определены в Prelude.
Это конструкторы типа Either. Давайте скроем их, добавим в модуль такую строчку:
import Prelude hiding (Either(.. ))
Пятнашки | 209
Теперь проверим:
*Game> :r
[2 of 2] Compiling Loop
( Loop. hs, interpreted )
Ok, modules loaded: Game, Loop.
*Loop>
Всё работает, можно двигаться дальше.
Последние штрихи
В модуле Loop нам осталось определить несколько маленьких функций. Поиск по слову un говорит нам
о том, что осталось определить функции “
greetings
:: IO ()
readInt
:: String -> Maybe Int
showAsk
:: IO ()
Самая простая это функция showAsk, она приглашает игрока сделать ход:
showAsk :: IO ()
showAsk = putStrLn ”Ваш ход: ”
Теперь функция распознавания целого числа:
import Data.Char (isDigit)
...
readInt :: String -> Maybe Int
readInt n
| all isDigit n = Just $ read n
| otherwise
= Nothing
В первой альтернативе мы с помощью стандартной функции isDigit :: Char -> Bool проверяем, что
строка состоит из одних только чисел. Если все символы числа, то мы пользуемся функцией из модуля Read
и читаем целое число, иначе возвращаем Nothing.
Последняя функция, это функция приветствия. Когда игрок входит в игру он сталкивается с её результа-
тами. Определим её так:
-- в модуль Loop
greetings :: IO ()
greetings = putStrLn ”Привет! Это игра пятнашки” >>
showGame initGame >>
remindMoves
-- в модуль Game
initGame :: Game
initGame = un
Сначала мы приветствуем игрока, затем показываем состояние (initGame), к которому ему нужно стре-
миться, и напоминаем как делаются ходы. На этом определении мы раскрыли все выражения в модуле Loop,
нам остался лишь модуль Game.
Правила игры
Определим модуль Game, но мы будем определять его не с чистого листа. Те функции, которые нам нуж-
ны уже определились в ходе описания диалога с пользователем. Нам нужно уметь составлять начальное
состояние initGame, уметь составлять перемешанное состояние игры shuffle, нам нужно уметь реагиро-
вать на ходы move, определять какая позиция является выигрышной isGameOver и уметь показывать фишки
в красивом виде. Приступим!
initGame
:: Game
shuffle
:: Int -> IO Game
isGameOver
:: Game -> Bool
move
:: Move -> Game -> Game
instance Show Game where
show = un
Таков наш план.
210 | Глава 13: Поиграем
Начальное состояние
Начнём с самой простой функции, составим начальное состояние:
initGame :: Game
initGame = Game (3, 3) $ listArray ((0, 0), (3, 3)) $ [0 .. 15]
Мы будем кодировать фишки цифрами от нуля до 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