haskell-notes

imported from ‘Prelude’ at Loop. hs:1:811

(and originally defined in Data.Either’)

or ‘Game.Left’,

imported from ‘Game’ at Loop. hs:5:111

(and originally defined at Game. hs:10:2528)

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

— смещение положение по направдению

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

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

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

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

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

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

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

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 будет возвращать список доступных ходов для данного положения:

Читай продолжение на следующей странице
Добавить комментарии