haskell-notes

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з двух элементов, это перемещения фишки и пустышки.

Если же фишка за пределами доски, то мы возвращаем исходное положение.

Перемешиваем фишки

Игра начинается с такого положения, в котором все фишки перемешаны. Но перемешивать фишки про-

извольным образом было бы не честно, поскольку известно, что в пятнашках половина расстановок не при-

водит к выигрышу. Поэтому мы будем перемешивать так: мы стартуем из начального положения и делаем

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162