hpr2703 :: Fog of war in Yesod based game
How to implement fog of war system in turn based web game
Hosted by Tuula on Wednesday, 2018-12-12 is flagged as Clean and is released under a CC-BY-SA license.
haskell, yesod.
(Be the first).
The show is available on the Internet Archive at: https://archive.org/details/hpr2703
Listen in ogg,
spx,
or mp3 format. Play now:
Duration: 00:25:15
Haskell.
A series looking into the Haskell (programming language)
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 single collated report
-- Resulting report will have facts from the possibly partially empty reports
-- If a fact is not present for a given field, Nothing is left there
collateReport :: (Monoid a, ReportTransform b a) => [b] -> a
collateReport reports = mconcat (map fromReport reports)
For reports of multiple entities is bit more complex, as they need to be sorted first, but the code is similarly general:
-- | Combine list of reports and form a list of collated reports
-- Each reported entity is given their own report
collateReports :: (Grouped b, Monoid a, ReportTransform b a) => [b] -> [a]
collateReports [] = []
collateReports s@(x:_) = collateReport itemsOfKind : collateReports restOfItems
where split = span (sameGroup x) s
itemsOfKind = fst split
restOfItems = snd split
Final step is to either render reports as HTML or send them as JSON back to client. For JSON case we need one more type class instance (ToJSON) that can be automatically generated. After that handler function can be defined. After authenticating the user and checking that they are member of a faction, reports of specific planet (defined by its primary key) are retrieved from database, collated, turned into JSON and sent back to client:
$(deriveJSON defaultOptions {fieldLabelModifier = drop 3} ''CollatedPlanetReport)
getApiPlanetR :: Key Planet -> Handler Value
getApiPlanetR planetId = do
(_, _, fId) <- apiRequireFaction
loadedPlanetReports <- runDB $ selectList [ PlanetReportPlanetId ==. planetId
, PlanetReportFactionId ==. fId ] [ Asc PlanetReportDate ]
let planetReport = collateReport $ map entityVal loadedPlanetReports :: CollatedPlanetReport
return $ toJSON planetReport
For those interested seeing some code, source is available at https://github.com/Tuula/deep-sky/ (https://github.com/Tuula/deep-sky/tree/baa0807dd36b61fd02174b17c10013862af4ec18 is situation before lots of Elm related changes that I mentioned in passing in the previous episode)