10

这是一个玩具问题:

(roguelike)2D 地图由方形单元组成,每个单元都有一种材料(岩石或空气)。

每个单元格有四个边界(N、S、E 和 W)。每个边界由两个单元共享。

仅当一侧是岩石而另一侧是空气时,边界才可以选择包含“墙特征”。

(墙壁特征可以是杠杆、图片、按钮等)

只有当一侧是岩石而另一侧是空气时,什么代数数据类型设计才能有一个存储墙壁特征的地方?即数据结构不能表示两个空气单元或两个岩石单元之间边界上的壁特征。

我尝试过的一种方法是在单元格值上对棋盘模式进行异或运算,反转更改和未更改。

我一直对细胞之间存在多条等效路线这一事实感到困惑 - SSW 与 SWS 相同(这个问题的 1D 版本是微不足道的)。

(我认识到 ADT 表示不会特别“可查询”。)


尝试失败更新:

称东边界 E 和南边界 S。让每个边界为SameDiff Feature。这种方法的问题在于它允许存在不一致的路由,例如:

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

是否有一个数学名称可以表示不同的路线必须聚合到相同的总数?

您可以说 Same 为 1 而 Diff 为 -1,并且沿任何两个单元格之间的每条路径的乘积必须相等(1 或 -1)。

4

3 回答 3

6

我不知道这对于传统的 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
于 2013-09-03T16:19:35.013 回答
2

这是我想出的(如果我正确理解了要求):

{-# 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为有限地图包装一些东西。

于 2013-09-03T16:00:39.543 回答
2

我的版本与 Nicolas 所做的类似,但我在其中包含了对相邻单元格的引用以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从 A 到 B 的类型边界和一个Boundary matB matA从 B 到 A 的类型边界。这允许邻接关系是不对称的,但实际上,您可以决定在您的代码中使所有关系对称。

现在从理论上讲,这一切都很好而且很花哨,但是构建实际的 Cell图表是相当痛苦的。因此,为了好玩,让我们创建一个 DSL 来强制定义单元格关系,然后“打结”以生成最终图。

由于单元格有不同的类型,您不能简单地将它们存储在临时列表中或Data.Map用于打结,因此我将使用该vault包。AVault是一个类型安全的多态容器,您可以在其中存储任何类型的数据并使用Key类型编码的 a 以类型安全的方式检索它们。因此,例如,如果您有 a ,则可以从 aKey String中检索 a StringVault如果您有 a 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)

基本 monad 是IO因为它是创建新Vault密钥所必需的(我们也可以使用ST,但这有点简单)。我们使用State Vault存储新创建的单元格并为其添加新边界,使用 vault-key 唯一标识一个单元格并在 DSL 操作中引用它。

堆栈中的第三个 monadReader Vault用于访问处于完全构造状态的库。即,当我们在 中构建保险库时State,我们可以使用Reader“展望未来”,其中保险库已经包含所有具有最终边界的单元格。在实践中,这是通过使用mfix获取“monadic 固定点”来实现的(有关更多详细信息,请参见例如论文“Value Recursion in Monadic Computations”MonadFix wiki 页面)。

因此,要运行我们的地图构造函数,我们定义

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. 因此,在 monad 内部,我们可以使用get(from MonadState) 访问正在构建的不完整库,并使用ask(from 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"
于 2013-09-04T07:44:48.597 回答