Intro
In episode hpr2748 Writing Web Game in Haskell - Special events, I talked about how to add special events in the game. One drawback with the system presented there was that the kragii worms might attack planet that already had kragii worms present. This time we’ll look into how to prevent this. As a nice bonus, we also come up with system that can be used to record when a planet has particularly good harvest season.
Data types and Database
We need a way to represent different kinds of statuses that a planet might have. These will include things like on going kragii attack or a particularly good harvest season. And since these are will be stored in database, we are also going to use derivePersistField to generate code needed for that.
data PlanetaryStatus =
GoodHarvest
| PoorHarvest
| GoodMechanicals
| PoorMechanicals
| GoodChemicals
| PoorChemicals
| KragiiAttack
derivePersistField "PlanetaryStatus"
We could have recorded statuses as strings, but declaring a separate data type means that compiler can catch typos for us. It also makes code easier to read as PlanetaryStatus is much more informative than String or Text.
For database, we use following definition shown below in models file. It creates database table planet_status and respective Haskell data type PlanetStatus. There will be one row in database for each status that a planet has. I could have stored all statuses in a list and store that in database, effectively having one row for any planet. Now there’s one row for any planet + status combination. Choice wasn’t really based on any deep analysis, but merely a gut feeling that this feels like a good idea.
PlanetStatus json
planetId PlanetId
status PlanetaryStatus
expiration Int Maybe
deriving Show Read Eq
expiration column doesn’t have NOT NULL constraint like all other columns in the table. This is reflected in PlanetStatus record where data type of planetStatusExpiration is Maybe Int instead of Int. So some statuses will have expiration time, while others might not. I originally chose to represent time as Int instead of own data type, but I have been recently wondering if that was really a good decision.
Kragii attack, redux
Code that does actual database query looks pretty scary on a first glance and it’s rather long. First part of the code is there to query database and join several tables into the query. Second part of the code deals with counting and grouping data and eventually returning [Entity Planet] data that contains all planets that match the criteria.
-- | Load planets that are kragii attack candidates
kragiiTargetPlanets :: (MonadIO m, BackendCompatible SqlBackend backend
, PersistQueryRead backend, PersistUniqueRead backend) =>
Int -> Int -> Key Faction -> ReaderT backend m [Entity Planet]
kragiiTargetPlanets pop farms fId = do
planets <- E.select $
E.from $ (planet `E.LeftOuterJoin` population `E.LeftOuterJoin` building `E.LeftOuterJoin` status) -> do
E.on (status E.?. PlanetStatusPlanetId E.==. E.just (planet E.^. PlanetId)
E.&&. status E.?. PlanetStatusStatus E.==. E.val (Just KragiiAttack))
E.on (building E.?. BuildingPlanetId E.==. E.just (planet E.^. PlanetId))
E.on (population E.?. PlanetPopulationPlanetId E.==. E.just (planet E.^. PlanetId))
E.where_ (planet E.^. PlanetOwnerId E.==. E.val (Just fId)
E.&&. building E.?. BuildingType E.==. E.val (Just Fa