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
— смещение положение по направдению
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з двух элементов, это перемещения фишки и пустышки.
Если же фишка за пределами доски, то мы возвращаем исходное положение.
Перемешиваем фишки
Игра начинается с такого положения, в котором все фишки перемешаны. Но перемешивать фишки про-
извольным образом было бы не честно, поскольку известно, что в пятнашках половина расстановок не при-
водит к выигрышу. Поэтому мы будем перемешивать так: мы стартуем из начального положения и делаем