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

-- 
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 [email protected].
For more options, visit https://groups.google.com/d/optout.

Reply via email to