Entities and Stores

By decentralising references between objects and instead storing some kind of ID and some kind of Map ID Object, we can avoid mutable state. Hooray!

Entities

newtype Entity = Entity
  { unID :: Int
  } deriving stock   (Show, Generic)
    deriving newtype (Eq, Num, Read, Bounded, Hashable, Enum, Ord, Real, Integral)

A newtype wrapper around Int. Yup, that's about it. One nice feature is that we can, under the assumption that nobody does something strange (like turning a Thing into a Room), determine whether a given Entity refers to a Thing or a Room by whether we generated the ID by counting up or down:

isThing ::
  (HasID a)
  => a
  -> Bool
isThing a = getID a >= 0

isRoom ::
  (HasID a)
  => a
  -> Bool
isRoom = not . isThing

It's also nice to have a way to always get an Entity from a construct:

class HasID n where
  getID :: n -> Entity

instance HasID Entity where
  getID = id

instance Display Entity where
  display (Entity i) = "ID: " <> show i

We also then reserve a few IDs for the 'default' objects which we never want to see at runtime, but need at construction time to avoid unnecessary Maybes.

defaultVoidID :: Entity
defaultVoidID = Entity (-1)

defaultNothingID :: Entity
defaultNothingID = Entity 0

defaultPlayerID :: Entity
defaultPlayerID = Entity 1

Stores

A Store is a map from Entitys to as. Usually this is some flavour of Object wm d, but we can also use Store (Entity, Payload) for relations and things like that. Of course, since I've refactored the direct link of a Map-based store to a specific interpretation of the world's effects, this seems slightly out of place. But it's fairly obvious as the go-to implementation so it may as well stay here.

-- import qualified Data.EnumMap.Strict as EM
newtype Store a = Store
  { unStore :: EM.EnumMap Entity a
  } deriving stock   (Show, Generic)
    deriving newtype (Eq, Ord, Read)

emptyStore :: Store a
emptyStore = Store EM.empty

EnumMap and Optics

EnumMap (and its sibling EnumSet) are nice convenient newtype wrappers around IntMap, but they're not quite cut out for a) further newtype wrappers around them, and b) the instances for nice Lens/Optics things.

First let's define our own alterF for EnumMap and then another for a newtype wrapper...

alterEMF :: 
  (Functor f, Enum k)
  => (Maybe a -> f (Maybe a))
  -> k
  -> EM.EnumMap k a 
  -> f (EM.EnumMap k a)
alterEMF upd k m = EM.intMapToEnumMap <$> IM.alterF upd (fromEnum k) (EM.enumMapToIntMap m)

alterNewtypeEMF :: 
  (Functor f, Enum k)
  => (Maybe a -> f (Maybe a))
  -> k
  -> (nt -> EM.EnumMap k a)
  -> (EM.EnumMap k a -> nt)
  -> nt
  -> f nt
alterNewtypeEMF upd k unwrap wrap' m = wrap' <$> alterEMF upd k (unwrap m)

Which we can now use for a nice and tidy At instance.

instance At (Store a) where
  at k = lensVL $ \f -> alterNewtypeEMF f k unStore Store

Finally, we choose the obvious instantiations for the associated index types.

type instance IxValue (Store a) = a
type instance Index (Store a) = Entity
instance Ixed (Store a)

Which means we can now write use our lenses as someStore ^? at someEntity rather than someStore ^? coercedTo @(EnumMap Entity a) % to unStore % at someEntity, or some other verbose beast.