Duality of the universe: there's true state of the universe used in simulation and there's state the the players perceive. These most likely will always be in conflict. One possible solution is to separate these completely. Perform simulation in one system and record what players see in other.
For every type of entity in the game, there's two sets of data: real and reported. Reports are tied to time and faction. Examples are given for planets. Thus, we have Planet, PlanetReport and CollatedPlanetReport. First is the real entity, second is report of that entity tied in time and faction. Third one is aggregated information a faction has of given entity. In database two first ones are:
Planet json
name Text
position Int
starSystemId StarSystemId
ownerId FactionId Maybe
gravity Double
SystemPosition starSystemId position
deriving Show
PlanetReport json
planetId PlanetId
ownerId FactionId Maybe
starSystemId StarSystemId
name Text Maybe
position Int Maybe
gravity Double Maybe
factionId FactionId
date Int
deriving Show
Third one is defined as a datatype:
data CollatedPlanetReport = CollatedPlanetReport
{ cprPlanetId :: Key Planet
, cprSystemId :: Key StarSystem
, cprOwnerId :: Maybe (Key Faction)
, cprName :: Maybe Text
, cprPosition :: Maybe Int
, cprGravity :: Maybe Double
, cprDate :: Int
} deriving Show
Data from database need to be transformed before working on it. Usually it's 1:1 mapping, but sometimes it makes sense to enrich it (turning IDs into names for example). For this we use ReportTransform type class:
-- | Class to transform a report stored in db to respective collated report
class ReportTransform a b where
fromReport :: a -> b
instance ReportTransform PlanetReport CollatedPlanetReport where
fromReport report =
CollatedPlanetReport (planetReportPlanetId report)
(planetReportStarSystemId report)
(planetReportOwnerId report)
(planetReportName report)
(planetReportPosition report)
(planetReportGravity report)
(planetReportDate report)
To easily combine bunch of collated reports together, we define instances
of semigroup and monoid for collated report data.
Semigroup defines an associative binary operation (<>) and monoid defines a zero or empty item (mempty). My explanation about Monoid and Semigroup were a bit rambling, so maybe have a look at https://wiki.haskell.org/Monoid which explains it in detail.
instance Semigroup CollatedPlanetReport where
(<>) a b = CollatedPlanetReport (cprPlanetId a)
(cprSystemId a)
(cprOwnerId a <|> cprOwnerId b)
(cprName a <|> cprName b)
(cprPosition a <|> cprPosition b)
(cprGravity a <|> cprGravity b)
(max (cprDate a) (cprDate b))
instance Monoid CollatedPlanetReport where
mempty = CollatedPlanetReport (toSqlKey 0) (toSqlKey 0) Nothing Nothing Nothing Nothing 0
In some cases there might be a list of collated reports that are about different entities of same type (several reports for every planet in solar system). For those cases, we need a way to tell what reports belong together:
-- | Class to indicate if two reports are about same entity
class Grouped a where
sameGroup :: a -> a -> Bool
instance Grouped PlanetReport where
sameGroup a b =
planetReportPlanetId a == planetReportPlanetId b
After this, processing a list of reports for same entity is short amount of very general code:
-- | Combine list of reports and form a sin