Stefan Matthias Aust
2017-04-21 21:37:58 UTC
Hi there!
Just for fun, I started to learn Elm. I wonder how to idiomatically solve
the following problems.
I decided to create a little 4X strategy game. Currently, I'm tinkering
with the best representation of such a game and how to implement game
commands that update the model.
Here's a simplified run down: The game has stars and players. Each star has
planets. Planets have properties like size, population, industry, defense,
etc. Planets can be owned by players. Spaceships (organized as fleets (aka
taskforces aka tfs) can move between stars. They belong to players. One
kind of ship is the transport and it can be used to colonize unoccupied
planets.
I refer stars, planets, players and fleets by number (aka `no`) because
(this is my current understanding) I have no other way to refer to records
and I need a way for the user to enter commands (I intent to eventually
create a retro text mode interface). So a command looks like `BuildDefense
TeamNo StarNo PlanetNo Amount` or `LandTransports TeamNo TfNo StarNo
PlanetNo Amount`. All that types are aliases for `Int`.
This is an excerpt from type definitions:
Game = { stars : List Star, teams : List Team }
Star = { no: StarNo, planets : List Planet }
Planet = { no : PlanetNo, defense : Int }
Command = BuildDefense ... | LandTransports ...
*My first question*: Is there any way to create a constrained type like
"something that has a `no` field?
Right now, I have a lot of nearly identical code like this:
findStar : StarNo -> Game -> Maybe Star
findStar no game =
List.head (List.filter (\star -> star.no == no) game.stars)
findPlanet : PlanetNo -> Star -> Maybe Planet
findPlanet no star =
List.head (List.filter (\planet -> planet.no == no) star.planets)
In other languages, I could create some kind of protocol or interface or
category and then declare `Star` or `Planet` to be conforming. Then I could
implement common operations for conforming types.
I also didn't find some kind of "find first" operation in the standard
library (which is frankly surprisingly small). It's easy enough to
implement but why do I need to?
find : (a -> Bool) -> List a -> Maybe a
find test list =
case list of
head :: tail ->
if test (head) then
Just head
else
find test tail
other ->
Nothing
Because I like terse code, I used the much more compact although less
efficient variant.
Also, mainly for esthetic reasons, I'd prefer to extend the `List` module
so that my function reads `List.find` because if I have to use `List.map`
and `Maybe.map` and `Result.map` and `Random.map` everywhere, I'd stay
consistent.
Sidenote, would be using `<|` more idiomatic?
findPlanet no star =
List.head <| List.filter (\planet -> planet.no == no) star.planets
*Second question*: Is there an easy way to replace an element in a list?
As I have to recreate the whole game if I change something, I crafted a lot
of helper function:
planet
|> addDefense amount
|> subtractResources amount * defenseCost
|> updatePlanetInStar star
|> updateStarInGame game
|> Ok
This ready nicely.
The `updatePlanetInStar` and `updateStarInGame` functions are however very
verbose:
updatePlanetInStar : Star -> Planet -> Star
updatePlanetInStar star planet =
{ star
| planets =
List.map
(\p ->
if p.no == planet.no then
planet
else
p
)
star.planets
}
Again, I'm unable to abstract it further.
Sidenote: I'd love to use
{ game | stars[x].planets[y] = planet }
and let the compiler deal with all that stupid boilerplate code. I realize
that for this to work, I'd have to use `Array` (a rather unsupported type)
instead of `List` but that would be fine. That way, I could alias my `no`
to indices. By the way, do I really had to create my own `nth` function for
Lists?
*Third question*. When implementing `BuildDefense`, I need to convert the
no's to records and this could fail. Instead of working with `Maybe`, I use
a `Result Error a` type and `Error` is a sum type that contains all the
errors that could occur. However, I get some really ugly chainingâŠ
buildDefense : TeamNo -> StarNo -> PlanetNo -> Int -> Game -> Result Error
Game
buildDefense teamNo starNo planetNo amount game =
findTeamR teamNo game
|> Result.andThen
(\team ->
findStarR starNo game
|> Result.andThen
(\star ->
findPlanetR planetNo star
|> Result.andThen
(\planet ->
if amount < 1 then
Err InvalidAmount
else if planet.owner /= team.no then
Err NotYourPlanet
else if planet.resources < amount *
defenseCost then
Err NotEnoughResources
else
planet
|> addDefense amount
|> subtractResources
(amount * defenseCost)
|> updatePlanet star
|> updateStar game
|> Ok
)
)
)
How am I supposed to write this "the Elm way"?
In other languages, I'd probably use exceptions. And I don't want to invert
the control flow by creating tiny help functions where I'm currently using
anonymous functions. I want to read the control from from top to bottom.
BTW, `findTeamR` is `findTeam`, wrapped in `Result.fromMaybe InvalidTeam`.
And while I could use `Result.map2` to resolve team and star in parallel, I
cannot do this for the planet which is dependent on the star and therefore
I didn't bother.
Here's my final code snippet:
landTransports : TeamNo -> TfNo -> StarNo -> PlanetNo -> Int -> Game ->
Result Error Game
landTransports teamNo tfNo starNo planetNo amount game =
findTeamAndTfR teamNo tfNo game
|> Result.andThen
(\( team, tf ) ->
findStarAndPlanetR starNo planetNo game
|> Result.andThen
(\( star, planet ) ->
if amount < 1 || amount > tf.ships.transports
then
Err InvalidAmount
else if tf.dest /= star.no || tf.eta /= 0 then
Err TfNotAtStar
else if (planet.owner /= team.no) &&
(planet.owner /= noTeam) then
Err PlanetNotYours
else if planet.population + amount >
planet.size then
Err TooMuchPopulation
else
let
game1 =
tf
|> substractShips { noShips |
transports = amount }
|> updateTfInTeam team
|> updateTeamInGame game
game2 =
planet
|> addPopulation amount
|> setOwner team.no
|> updatePlanetInStar star
|> updateStarIngame game1
in
Ok game2
)
)
I'm really sorry for this long mail :-)
Stefan
Just for fun, I started to learn Elm. I wonder how to idiomatically solve
the following problems.
I decided to create a little 4X strategy game. Currently, I'm tinkering
with the best representation of such a game and how to implement game
commands that update the model.
Here's a simplified run down: The game has stars and players. Each star has
planets. Planets have properties like size, population, industry, defense,
etc. Planets can be owned by players. Spaceships (organized as fleets (aka
taskforces aka tfs) can move between stars. They belong to players. One
kind of ship is the transport and it can be used to colonize unoccupied
planets.
I refer stars, planets, players and fleets by number (aka `no`) because
(this is my current understanding) I have no other way to refer to records
and I need a way for the user to enter commands (I intent to eventually
create a retro text mode interface). So a command looks like `BuildDefense
TeamNo StarNo PlanetNo Amount` or `LandTransports TeamNo TfNo StarNo
PlanetNo Amount`. All that types are aliases for `Int`.
This is an excerpt from type definitions:
Game = { stars : List Star, teams : List Team }
Star = { no: StarNo, planets : List Planet }
Planet = { no : PlanetNo, defense : Int }
Command = BuildDefense ... | LandTransports ...
*My first question*: Is there any way to create a constrained type like
"something that has a `no` field?
Right now, I have a lot of nearly identical code like this:
findStar : StarNo -> Game -> Maybe Star
findStar no game =
List.head (List.filter (\star -> star.no == no) game.stars)
findPlanet : PlanetNo -> Star -> Maybe Planet
findPlanet no star =
List.head (List.filter (\planet -> planet.no == no) star.planets)
In other languages, I could create some kind of protocol or interface or
category and then declare `Star` or `Planet` to be conforming. Then I could
implement common operations for conforming types.
I also didn't find some kind of "find first" operation in the standard
library (which is frankly surprisingly small). It's easy enough to
implement but why do I need to?
find : (a -> Bool) -> List a -> Maybe a
find test list =
case list of
head :: tail ->
if test (head) then
Just head
else
find test tail
other ->
Nothing
Because I like terse code, I used the much more compact although less
efficient variant.
Also, mainly for esthetic reasons, I'd prefer to extend the `List` module
so that my function reads `List.find` because if I have to use `List.map`
and `Maybe.map` and `Result.map` and `Random.map` everywhere, I'd stay
consistent.
Sidenote, would be using `<|` more idiomatic?
findPlanet no star =
List.head <| List.filter (\planet -> planet.no == no) star.planets
*Second question*: Is there an easy way to replace an element in a list?
As I have to recreate the whole game if I change something, I crafted a lot
of helper function:
planet
|> addDefense amount
|> subtractResources amount * defenseCost
|> updatePlanetInStar star
|> updateStarInGame game
|> Ok
This ready nicely.
The `updatePlanetInStar` and `updateStarInGame` functions are however very
verbose:
updatePlanetInStar : Star -> Planet -> Star
updatePlanetInStar star planet =
{ star
| planets =
List.map
(\p ->
if p.no == planet.no then
planet
else
p
)
star.planets
}
Again, I'm unable to abstract it further.
Sidenote: I'd love to use
{ game | stars[x].planets[y] = planet }
and let the compiler deal with all that stupid boilerplate code. I realize
that for this to work, I'd have to use `Array` (a rather unsupported type)
instead of `List` but that would be fine. That way, I could alias my `no`
to indices. By the way, do I really had to create my own `nth` function for
Lists?
*Third question*. When implementing `BuildDefense`, I need to convert the
no's to records and this could fail. Instead of working with `Maybe`, I use
a `Result Error a` type and `Error` is a sum type that contains all the
errors that could occur. However, I get some really ugly chainingâŠ
buildDefense : TeamNo -> StarNo -> PlanetNo -> Int -> Game -> Result Error
Game
buildDefense teamNo starNo planetNo amount game =
findTeamR teamNo game
|> Result.andThen
(\team ->
findStarR starNo game
|> Result.andThen
(\star ->
findPlanetR planetNo star
|> Result.andThen
(\planet ->
if amount < 1 then
Err InvalidAmount
else if planet.owner /= team.no then
Err NotYourPlanet
else if planet.resources < amount *
defenseCost then
Err NotEnoughResources
else
planet
|> addDefense amount
|> subtractResources
(amount * defenseCost)
|> updatePlanet star
|> updateStar game
|> Ok
)
)
)
How am I supposed to write this "the Elm way"?
In other languages, I'd probably use exceptions. And I don't want to invert
the control flow by creating tiny help functions where I'm currently using
anonymous functions. I want to read the control from from top to bottom.
BTW, `findTeamR` is `findTeam`, wrapped in `Result.fromMaybe InvalidTeam`.
And while I could use `Result.map2` to resolve team and star in parallel, I
cannot do this for the planet which is dependent on the star and therefore
I didn't bother.
Here's my final code snippet:
landTransports : TeamNo -> TfNo -> StarNo -> PlanetNo -> Int -> Game ->
Result Error Game
landTransports teamNo tfNo starNo planetNo amount game =
findTeamAndTfR teamNo tfNo game
|> Result.andThen
(\( team, tf ) ->
findStarAndPlanetR starNo planetNo game
|> Result.andThen
(\( star, planet ) ->
if amount < 1 || amount > tf.ships.transports
then
Err InvalidAmount
else if tf.dest /= star.no || tf.eta /= 0 then
Err TfNotAtStar
else if (planet.owner /= team.no) &&
(planet.owner /= noTeam) then
Err PlanetNotYours
else if planet.population + amount >
planet.size then
Err TooMuchPopulation
else
let
game1 =
tf
|> substractShips { noShips |
transports = amount }
|> updateTfInTeam team
|> updateTeamInGame game
game2 =
planet
|> addPopulation amount
|> setOwner team.no
|> updatePlanetInStar star
|> updateStarIngame game1
in
Ok game2
)
)
I'm really sorry for this long mail :-)
Stefan
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.