Представление ограничений карты как ADT

Вот проблема с игрушкой:

2D-карта (roguelike) состоит из квадратных ячеек, каждая из которых имеет материал (камень или воздух).

Каждая ячейка имеет четыре границы (N, S, E и W). Каждая граница разделена на две ячейки.

Граница может дополнительно содержать "элемент стены", только если одна сторона является скалой, а другая - воздухом.

(Элементы стены могут быть рычагами, рисунками, кнопками и т. Д.)

Какая конструкция типа алгебраических данных может иметь место для хранения элемента стены, только когда одна сторона скала, а другая - воздух? т.е. структура данных не может представлять элемент стены на границе между двумя воздушными ячейками или двумя каменными ячейками.

Один из подходов, которые я попробовал, - это XOR-структура шахматной доски над значениями ячеек, изменение изменений и их изменение.

Я продолжаю зацикливаться на том факте, что между ячейками существует несколько эквивалентных маршрутов - SSW - это то же самое, что SWS (1D-версия этого вопроса тривиальна).

(Я признаю, что представление ADT не будет особенно "запрашиваемым".)


Обновление с неудачной попыткой:

Назовите восточные границы E и южные границы S. Пусть каждая граница Same или же Diff Feature, Проблема с этим подходом состоит в том, что он допускает несовместимые маршруты, такие как:

E<0,0> Same
S<1,0> Same
S<0,0> Same
E<0,1> Diff

Есть ли математическое название, чтобы сказать, что разные маршруты должны объединяться в одну и ту же сумму?

Вы могли бы сказать, что Same был 1, а Diff был -1, и что продукт на каждом маршруте между любыми двумя ячейками должен быть равен (1 или -1).

3 ответа

Решение

Я понятия не имею, возможно ли это вообще с традиционными ADT, но вы можете сделать это с помощью GADT. Это карта бесконечная в одном измерении и конечная в другом:

{-# LANGUAGE GADTs #-}


data Nil
type AirEnd = AirCell Nil
type RockEnd = RockCell Nil

data AirCell next
data RockCell next

data WallFeature = Lever | Picture | Buttons | Etc ()
type Wall = Maybe WallFeature


data RogueStrip contents neighbour where

  AirEnd_ngbAir :: RogueStrip AirEnd AirEnd
  AirEnd_ngbRock :: Wall -> RogueStrip AirEnd RockEnd
  RockEnd_ngbAir :: Wall -> RogueStrip RockEnd AirEnd
  RockEnd_ngbRock :: RogueStrip RockEnd RockEnd

  AirCons_nextAir_ngbAir ::
          RogueStrip          (AirCell next')           neighbourNext
       -> RogueStrip (AirCell (AirCell next')) (AirCell neighbourNext)
  AirCons_nextAir_ngbRock :: Wall ->
          RogueStrip          (AirCell next')            neighbourNext
       -> RogueStrip (AirCell (AirCell next')) (RockCell neighbourNext)
  AirCons_nextRock_ngbAir :: Wall ->
          RogueStrip          (RockCell next')           neighbourNext
       -> RogueStrip (AirCell (RockCell next')) (AirCell neighbourNext)
  AirCons_nextRock_ngbRock :: Wall -> Wall ->
          RogueStrip          (RockCell next')            neighbourNext
       -> RogueStrip (AirCell (RockCell next')) (RockCell neighbourNext)
  RockCons_nextAir_ngbAir :: Wall -> Wall ->
          RogueStrip           (AirCell next')           neighbourNext
       -> RogueStrip (RockCell (AirCell next')) (AirCell neighbourNext)
  RockCons_nextAir_ngbRock :: Wall ->
          RogueStrip           (AirCell next')            neighbourNext
       -> RogueStrip (RockCell (AirCell next')) (RockCell neighbourNext)
  RockCons_nextRock_ngbAir :: Wall ->
          RogueStrip           (RockCell next')           neighbourNext
       -> RogueStrip (RockCell (RockCell next')) (AirCell neighbourNext)
  RockCons_nextRock_ngbRock ::
          RogueStrip           (RockCell next')            neighbourNext
       -> RogueStrip (RockCell (RockCell next')) (RockCell neighbourNext)


data RogueSList topStrip where
  StripCons :: RogueStrip topStrip nextStrip -> RogueSList nextStrip
                                             -> RogueSList topStrip

data RogueMap where
  RogueMap :: RogueSList top -> RogueMap

Моя версия похожа на то, что сделал Николас, но я включаю ссылку на соседнюю ячейку в Boundary сделать пройденный граф. Мои типы данных

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}

data Material = Rock | Air

data WallFeature = Lever | Picture | Button deriving Show

type family Other (t :: Material) :: Material
type instance Other Air  = Rock
type instance Other Rock = Air

data Tile :: Material -> * where
    RockTile :: Tile Rock
    AirTile  :: Tile Air

data Cell mat where
    Cell
        :: Tile mat
        -> Maybe (Boundary mat n)
        -> Maybe (Boundary mat s)
        -> Maybe (Boundary mat e)
        -> Maybe (Boundary mat w)
        -> Cell mat

data Boundary (src :: Material) (dst :: Material) where
    Same  :: Cell mat -> Boundary mat mat
    Diff  :: WallFeature -> Cell (Other mat) -> Boundary mat (Other mat)

Я решил сделать карту ограниченной, поэтому каждая ячейка может иметь или не иметь соседей (следовательно, Maybe типы для границ). Boundary тип данных параметризован по материалам двух соседних ячеек и содержит ссылку на ячейку назначения, а элементы стен структурно ограничены границами, которые соединяют ячейки из другого материала.

По сути, это ориентированный граф, поэтому между каждой вспомогательной ячейкой A и B есть граница типа Boundary matA matB от А до В и границы типа Boundary matB matA от B до A. Это позволяет асимметричному отношению смежности, но на практике вы можете решить в своем коде сделать все отношения симметричными.

Теперь это все хорошо и теоретически, но построение фактического Cell График довольно больно. Итак, просто для забавы, давайте создадим DSL для обязательного определения отношений ячеек, а затем "свяжем узел" для создания окончательного графа.

Поскольку ячейки имеют разные типы, вы не можете просто сохранить их во временном списке или Data.Map для завязывания узлов, так что я собираюсь использовать vault пакет. Vault является полиморфным контейнером, безопасным для типов, в котором вы можете хранить любые типы данных и извлекать их безопасным для типов способом, используя Key это кодируется типом. Так, например, если у вас есть Key String Вы можете получить String из Vault и если у вас есть Key Int Вы можете получить Int значение.

Итак, начнем с определения операций в DSL.

data Gen a

new :: Tile a -> Gen (Key (Cell a))

connectSame :: Connection a a -> Key (Cell a) -> Key (Cell a) -> Gen ()

connectDiff
    :: (b ~ Other a, a ~ Other b)
    => Connection a b -> WallFeature
    -> Key (Cell a) -> Key (Cell b) -> Gen ()

startFrom :: Key (Cell a) -> Gen (Cell a)

Connection Тип определяет основные направления, где мы соединяем ячейки, и определяется следующим образом:

type Setter a b = Maybe (Boundary a b) -> Cell a -> Cell a
type Connection b a = (Setter a b, Setter b a)

north :: Setter a b
south :: Setter a b
east  :: Setter a b
west  :: Setter a b

Теперь мы можем построить простую тестовую карту, используя наши операции:

testMap :: Gen (Cell Rock)
testMap = do
    nw <- new RockTile
    ne <- new AirTile
    se <- new AirTile
    sw <- new AirTile

    connectDiff (west,east) Lever nw ne
    connectSame (north,south) ne se
    connectSame (east,west) se sw
    connectDiff (south,north) Button sw nw

    startFrom nw

Хотя мы еще не реализовали эти функции, мы видим, что это проверка типов. Кроме того, если вы попытаетесь поместить несовместимые типы (например, соединяете одинаковые типы плиток с помощью элемента стены), вы получите ошибку типа.

Тип бетона, который я собираюсь использовать для Gen является

type Gen = ReaderT Vault (StateT Vault IO)

Базовая монада IO потому что это требуется для создания нового Vault ключи (мы могли бы также использовать ST но это немного проще). Мы используем State Vault хранить вновь созданные ячейки и добавлять к ним новые границы, используя ключ хранилища, чтобы однозначно идентифицировать ячейку и ссылаться на нее в операциях DSL.

Третья монада в стеке Reader Vault который используется для доступа к хранилищу в его полностью построенном состоянии. Т.е. пока мы строим хранилище в State, мы можем использовать Reader "заглянуть в будущее", где хранилище уже содержит все ячейки с их окончательными границами. На практике это достигается с помощью mfix чтобы получить "монадическую фиксированную точку" (более подробно см., например, статью "Рекурсия значений в монадических вычислениях" или вики-страницу MonadFix).

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

import Control.Monad.State
import Control.Monad.Reader
import Data.Vault.Lazy as V

runGen :: Gen a -> IO a
runGen g = fmap fst $ mfix $ \(~(_, v)) -> runStateT (runReaderT g v) V.empty

Здесь мы запускаем вычисление с учетом состояния и получаем значение типа (a, Vault) то есть результат вычисления и хранилище, которое содержит все наши ячейки. С помощью mfix мы можем получить доступ к результату до того, как вычислим его, поэтому можем передать результирующее хранилище в качестве параметра runReaderT, Следовательно, внутри монады мы можем использовать get (от MonadState) получить доступ к незавершенному хранилищу, которое строится, и ask (от MonadReader) для доступа к полностью заполненному хранилищу.

Теперь остальная часть реализации проста:

new :: Tile a -> Gen (Key (Cell a))
new t = do
    k <- liftIO $ newKey
    modify $ V.insert k $ Cell t Nothing Nothing Nothing Nothing
    return k

new создает новый ключ хранилища и использует его для вставки новой ячейки без границ.

connectSame :: Connection a a -> Key (Cell a) -> Key (Cell a) -> Gen ()
connectSame (s2,s1) ka kb = do
    v <- ask
    let b1 = fmap Same $ V.lookup kb v
        b2 = fmap Same $ V.lookup ka v
    modify $ adjust (s1 b1) ka . adjust (s2 b2) kb

connectSame получает доступ к "будущему хранилищу" через ask так что мы можем посмотреть соседнюю ячейку оттуда и сохранить ее на границе.

connectDiff 
    :: (b ~ Other a, a ~ Other b)
    => Connection a b -> WallFeature
    -> Key (Cell a) -> Key (Cell b) -> Gen ()
connectDiff (s2, s1) wf ka kb = do
    v <- ask
    let b1 = fmap (Diff wf) $ V.lookup kb v
        b2 = fmap (Diff wf) $ V.lookup ka v
    modify $ adjust (s1 b1) ka . adjust (s2 b2) kb

connectDiff почти такой же, за исключением того, что мы предоставляем дополнительную функцию стены. Нам также нужно явное ограничение (b ~ Other a, a ~ Other b) построить две симметричные границы.

startFrom :: Key (Cell a) -> Gen (Cell a)
startFrom k = fmap (fromJust . V.lookup k) ask

startFrom просто получает заполненную ячейку с заданным ключом, чтобы мы могли вернуть ее в результате из нашего генератора.

Вот полный пример источника с дополнительными Show примеры для отладки, так что вы можете попробовать это самостоятельно:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}

import Control.Monad.State
import Control.Monad.Reader
import Data.Vault.Lazy as V
import Data.Maybe

data Material = Rock | Air

data WallFeature = Lever | Picture | Button deriving Show

type family Other (t :: Material) :: Material
type instance Other Air  = Rock
type instance Other Rock = Air

data Tile :: Material -> * where
    RockTile :: Tile Rock
    AirTile  :: Tile Air

data Cell mat where
    Cell
        :: Tile mat
        -> Maybe (Boundary mat n)
        -> Maybe (Boundary mat s)
        -> Maybe (Boundary mat e)
        -> Maybe (Boundary mat w)
        -> Cell mat

data Boundary (a :: Material) (b :: Material) where
    Same  :: Cell mat -> Boundary mat mat
    Diff  :: WallFeature -> Cell (Other mat) -> Boundary mat (Other mat)

type Gen = ReaderT Vault (StateT Vault IO)

type Setter a b = Maybe (Boundary a b) -> Cell a -> Cell a
type Connection b a = (Setter a b, Setter b a)

-- Boundary setters
north :: Setter a b
north n (Cell t _ s e w) = Cell t n s e w

south :: Setter a b
south s (Cell t n _ e w) = Cell t n s e w

east :: Setter a b
east e (Cell t n s _ w) = Cell t n s e w

west :: Setter a b
west w (Cell t n s e _) = Cell t n s e w


new :: Tile a -> Gen (Key (Cell a))
new t = do
    k <- liftIO $ newKey
    modify $ V.insert k $ Cell t Nothing Nothing Nothing Nothing
    return k

connectSame :: Connection a a -> Key (Cell a) -> Key (Cell a) -> Gen ()
connectSame (s2,s1) ka kb = do
    v <- ask
    let b1 = fmap Same $ V.lookup kb v
        b2 = fmap Same $ V.lookup ka v
    modify $ adjust (s1 b1) ka . adjust (s2 b2) kb

connectDiff
    :: (b ~ Other a, a ~ Other b)
    => Connection a b -> WallFeature
    -> Key (Cell a) -> Key (Cell b) -> Gen ()
connectDiff (s2, s1) wf ka kb = do
    v <- ask
    let b1 = fmap (Diff wf) $ V.lookup kb v
        b2 = fmap (Diff wf) $ V.lookup ka v
    modify $ adjust (s1 b1) ka . adjust (s2 b2) kb

startFrom :: Key (Cell a) -> Gen (Cell a)
startFrom k = fmap (fromJust . V.lookup k) ask

runGen :: Gen a -> IO a
runGen g = fmap fst $ mfix $ \(~(_, v)) -> runStateT (runReaderT g v) V.empty

testMap :: Gen (Cell Rock)
testMap = do
    nw <- new RockTile
    ne <- new AirTile
    se <- new AirTile
    sw <- new AirTile

    connectDiff (west,east) Lever nw ne
    connectSame (north,south) ne se
    connectSame (east,west) se sw
    connectDiff (south,north) Button sw nw

    startFrom nw

main :: IO ()
main = do
    c <- runGen testMap
    print c


-- Show Instances

instance Show (Cell mat) where
    show (Cell t n s e w)
        = unwords ["Cell", show t, show n, show s, show e, show w]

instance Show (Boundary a b) where
    show (Same _) = "<Same>"
    show (Diff wf _) = "<Diff with " ++ show wf ++ ">"

instance Show (Tile mat) where
    show RockTile = "RockTile"
    show AirTile = "AirTile"

Вот что я бы придумал (если я правильно понимаю требования):

{-# LANGUAGE GADTs, DataKinds, TypeFamilies #-}

module Features where

data CellType = Rock | Air

type family Other (c :: CellType) :: CellType
type instance Other Rock = Air
type instance Other Air = Rock

data Cell (a :: CellType) where
    RockCell :: Cell Rock
    AirCell :: Cell Air

data BoundaryType = Picture | Button

data Boundary (a :: CellType) (b :: CellType) where
    NoBoundary :: Boundary a b
    Boundary :: (b ~ Other a) => BoundaryType -> Boundary a b

data Tile m n e s w where
    Tile :: Cell m ->
            Cell n -> Boundary m n ->
            Cell e -> Boundary m e ->
            Cell s -> Boundary m s ->
            Cell w -> Boundary m w ->
            Tile m n e s w

demo :: Tile Rock Air Air Rock Air
demo = Tile RockCell
            AirCell NoBoundary
            AirCell (Boundary Picture)
            RockCell NoBoundary
            AirCell (Boundary Button)

{- Invalid: -}

demo2 = Tile RockCell
             RockCell (Boundary Picture)
             AirCell (Boundary Button)
             RockCell NoBoundary
             AirCell (Boundary Picture)

{-
 -   Couldn't match type `'Air' with `'Rock'
 -   In the third argument of `Tile', namely `(Boundary Picture)'
 -   In the expression:
 -     Tile
 -       RockCell
 -       RockCell
 -       (Boundary Picture)
 -       AirCell
 -       (Boundary Button)
 -       RockCell
 -       NoBoundary
 -       AirCell
 -       (Boundary Picture)
 -   In an equation for `demo2':
 -       demo2
 -         = Tile
 -             RockCell
 -             RockCell
 -             (Boundary Picture)
 -             AirCell
 -             (Boundary Button)
 -             RockCell
 -             NoBoundary
 -             AirCell
 -             (Boundary Picture)
 -}

Я думаю, некоторые переменные типа могут быть удалены здесь и там.

Оберните некоторые вещи в Maybe для конечных карт.

Другие вопросы по тегам