2013-09-03 6 views
10

Hier Spielzeug Problem ist:Darstellen Karte Einschränkungen als ADT

A (roguelike) 2D-Karte besteht aus quadratischen Zellen, die jeweils ein Material (Stein oder Luft).

Jede Zelle hat vier Grenzen (N, S, E und W). Jede Grenze wird von zwei Zellen geteilt.

Eine Grenze kann optional ein "Wandmerkmal" nur enthalten, wenn eine Seite Felsen und die andere Luft ist.

(Wall Merkmale könnten Hebel, Bilder, Buttons, etc. sein)

Was Algebraische Datentyp Design nur einen Platz zum Speichern einer Wand Funktion haben könnte, wenn eine Seite ist Felsen und die andere Luft? d.h. die Datenstruktur kann kein Wandmerkmal an einer Grenze zwischen zwei Luftzellen oder zwei Gesteinszellen darstellen.

Ein Ansatz, den ich ausprobiert habe ist XORing ein Schachbrett-Muster über die Zelle Werte, Änderungen rückgängig und nicht verändert.

Ich mache mich ständig über die Tatsache, dass es mehrere gleichwertige Routen zwischen den Zellen gibt - SSW ist das gleiche wie SWS (die 1D-Version dieser Frage ist trivial).

(I erkennen, dass die ADT Darstellung besonders nicht sein 'abfragbar'.)


Aktualisierung mit fehlgeschlagen Versuch:

Rufen Sie die Ost-Grenzen E und die Süd-Grenzen S. jede Grenze Let sei entweder Same oder Diff Feature. Das Problem bei diesem Ansatz ist, dass es inkonsequent Routen existieren, wie läßt:

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

Gibt es einen mathematischen Namen zu sagen, dass verschiedene Wege zum gleichen Gesamt aggregieren müssen?

Man könnte sagen, dass Same 1 war und Diff -1 und das Produkt entlang jeder Route zwischen zwei beliebigen Zellen gleich sein muss (entweder 1 oder -1).

+1

Ist die Karte endlich? Sollte es vordefiniert sein? – Nicolas

+0

Ich bin froh, dass die Karte endlich, unendlich oder ein Torus ist, was immer du auch machen kannst. – fadedbee

+0

Es kann vordefiniert sein, aber die Struktur sollte strukturell nicht in der Lage sein, ein "Merkmal" in der Grenze zwischen zwei Zellen desselben Typs zu halten. – fadedbee

Antwort

6

Ich habe keine Ahnung, ob dies überhaupt mit traditionellen ADTs möglich ist, aber Sie können es mit GADTs tun. Dies hat eine Karte unendlich in einer Dimension, und endlich in den anderen:

{-# 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 
+0

Diese Antwort ist nicht ganz das, was ich gesucht habe, aber es scheint die nächste zu sein. – fadedbee

+0

Ja, es ist ein bisschen brutal-force-ish. Aber es funktioniert. – leftaroundabout

2

Hier ist, was ich kommen würde mit (wenn ich die Anforderungen richtig zu verstehen):

{-# 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) 
-} 

mir einige Variablen vom Typ erraten hier und dort entfernt werden können.

Wrap einige Dinge in Maybe für endliche Karten.

+0

Diese Lösung scheint nicht zu skalieren. Ihr 'Tile'-Datentyp unterstützt fünf Zellen in einer Kreuzform, aber wie würden Sie einen analogen Datentyp für eine Form entwerfen, die mehrere Pfade von der Startzelle zu einer anderen Zelle hat - zum Beispiel scheint es schwierig, ein Analog zu implementieren von 'Tile' sogar für etwas so einfaches wie ein 2x2 Raster von Zellen. –

+0

Den Knoten binden? :) – copumpkin

+0

Ja, ich schätze, ich habe es vermasselt, zwischen Tile und Cell zu unterscheiden. Ich habe nicht getestet, aber ich bin ziemlich sicher, dass 1 (rekursiv) Typ sein könnte. Sobald das erledigt ist, können Sie beliebige Karten von Zellen darstellen. –

2

Meine Version ist ähnlich das, was Nicolas tat, aber ich bin ein Verweis auf die benachbarte Zelle in Boundary einen verfahrbaren Diagramm zu machen. Meine Datentypen sind

{-# 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) 

Ich beschloss, die Karte begrenzt zu machen, so dass jede Zelle möglicherweise oder möglicherweise nicht haben Nachbarn (also Maybe Typen für Grenzen).Der Boundary Datentyp ist , der über die Materialien der zwei angrenzenden Zellen parametrisiert ist und einen Verweis auf die Zielzelle enthält, und Wandmerkmale sind strukturell auf Grenzen beschränkt, die Zellen aus unterschiedlichem Material verbinden.

Dies ist im wesentlichen ein Digraph so zwischen jeder adjancent Zelle A und B eine Grenze vom Typ gibt es Boundary matA matB von A nach B und eine Grenze des Typs Boundary matB matA von B nach A. Dies ist für die Adjazenzrelation ermöglicht asymmetrische zu sein, aber In der Praxis können Sie in Ihrem Code festlegen, dass alle Beziehungen symmetrisch sind.

Nun ist dies alles gut und Dandy auf einer theoretischen Ebene, aber die tatsächliche Cell Graph ist ein ziemlicher Schmerz. Also, nur zum Spaß, machen wir ein DSL zur Definition der Zellen-Beziehungen zwingend und dann "binden den Knoten", um den endgültigen Graphen zu produzieren.

Da die Zellen verschiedene Typen haben, können Sie sie nicht einfach in einer temporären Liste oder Data.Map für die Knotenbindung speichern, also werde ich das vault Paket verwenden. Ein Vault ist ein typsicherer, polymorpher Container, in dem Sie beliebige Datentypen speichern und typsicher abrufen können. Verwenden Sie dazu eine Key, die typenkodiert ist. Wenn Sie beispielsweise eine Key String haben, können Sie eine String aus einer Vault abrufen, und wenn Sie eine Key Int haben, können Sie einen Int Wert abrufen.

Also, beginnen wir mit der Definition der Operationen in der 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) 

Der Connection Typ bestimmt die Himmelsrichtungen, wo wir Zellen verbinden und ist wie folgt definiert:

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 

Jetzt können wir eine einfache Testkarte mit unseren Operationen konstruieren:

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 

Obwohl wir die Funktionen noch nicht implementiert haben, können wir sehen, dass diese Art überprüft. Wenn Sie versuchen, inkonsistente Typen zu platzieren (z. B. das Verbinden derselben Kacheltypen mit einer Wandfunktion), erhalten Sie einen Typfehler.

Der konkrete Typ I für Gen verwenden werde ist

type Gen = ReaderT Vault (StateT Vault IO) 

Die Basis Monade ist IO da was man braucht neue Vault Schlüssel erstellen (wir auch ST nutzen könnten, aber dies ist etwas einfacher). Wir verwenden State Vault, um neu erstellte Zellen zu speichern und ihnen neue Grenzen hinzuzufügen, indem Sie den Tresorschlüssel verwenden, um eine Zelle eindeutig zu identifizieren und in den DSL-Vorgängen darauf Bezug zu nehmen.

Die dritte Monade im Stapel ist Reader Vault, die verwendet wird, um auf den Tresor im vollständig aufgebauten Zustand zuzugreifen. I.e. Während wir den Tresor in State erstellen, können wir Reader verwenden, um in die Zukunft zu sehen, wo der Tresor bereits alle Zellen mit ihren endgültigen Grenzen enthält. In der Praxis wird dies erreicht, indem mfix verwendet wird, um den "monadischen Fixpunkt" zu erhalten (für weitere Details siehe z. B. das Papier "Value Recursion in Monadic Computations" oder die MonadFix wiki page).

Also, unsere Karte Konstruktor ausführen können, definieren wir

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 

Hier haben wir die Stateful Berechnung ausführen und einen Wert vom Typ (a, Vault) das heißt das Ergebnis aus der Berechnung und dem Gewölbe raus, die unsere Zellen alle enthält. Über mfix können wir auf das Ergebnis zugreifen, bevor wir es berechnen, sodass wir den Ergebnisspeicher als Parameter an runReaderT übergeben können. Daher können wir innerhalb der Monade get (von MonadState) verwenden, um auf den unvollständigen Tresor zuzugreifen, der gerade erstellt wird, und ask (von MonadReader), um auf den vollständig fertiggestellten Tresor zuzugreifen.

Nun Rest der Implementierung ist einfach:

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 erstellt ein neues Gewölbe Schlüssel und verwendet sie eine neue Zelle ohne Grenzen einzufügen.

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 greift auf die „Zukunft Gewölbe“ über ask so können wir die benachbarte Zelle von dort nachschlagen und speichern sie in der Grenze.

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 ist so ziemlich das gleiche, außer dass wir die zusätzliche Wand-Funktion bieten. Wir brauchen auch die explizite Bedingung (b ~ Other a, a ~ Other b) bis zwei symmetrische Grenzen zu konstruieren.

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

startFrom ruft einfach die fertige Zelle mit dem angegebenen Schlüssel, damit wir es als Ergebnis unseres Generator zurück.

ist die komplette Beispielquelle mit zusätzlichen Show Instanzen für das Debuggen, so dass Sie diese Texte selbst ausprobieren können:

{-# 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"