Modeling players in a board game

Hi,

I’m trying to build a board game and I’m not sure how to model some basic things like the players state.

I want to have a variable number of players, so I’m using a list (even though there can be only 2, 3 or 4; is that already a bad idea?).

Where should I store the information of the “current player”?

  • If I have an Integer currentPlayer field in my model, it might be out of bounds, and I have to do a bunch of Array.fromList/Array.toList/Array.get/Array.set to make it work with the players field.
  • If I have a Bool (or some kind of state) in each Player, there might be several players “currently playing”.

Any input appreciated!

It’s really a fascinating language and paradigm to use, as difficult as it can be sometimes, it’s a great feeling to wrap my head around some new concepts!

PS: Also using Arrays force me to use a bunch of Maybe.withDefault emptyPlayer (emptyRow/emptyCell in the board view). It doesn’t seem like the best option, but should I really use an 225 lines long union type for modeling a 15×15 grid?

I just thought of another option. Maybe it’s silly for some reason, please tell me!

What if I use some kind of Queue for storing the players?

  • When a player starts playing, I can “pop” the first element of players into currentPlayer (now a Player or Maybe Player field), removing it from players at the same time.
  • Then when I want to move on to the next player, I push the current one to the end of players, and I repeat the previous operation.

I was just thinking about the queue possibility and your messaged popped in :smile:
I’m no expert but this is my take:

Simple approach?

You have two separate types which are “depending” on each other for their constraints – so you will probably end up having to deal with the “bad state” (a Maybe somewhere). I’d focus on data structures that would make the rest of the operations easier.

e.g.

type alias Player = { ... }
type alias Model =
  { activePlayer : Int
  , players : Dict Int Player
  }

getActivePlayer : Model -> Maybe Player
getActivePlayer model = Dict.get model.activePlayer model.players

I’d try to handle the Maybe possibility as high up in the logic as I could (I could then display the bad state warning – then I’d have the defined activePlayer available everywhere else.

The important part is that all approaches would result in a Maybe. In my opinion you would have to just be careful about other problems like data duplication.

Another approach? More verbose but safer

I tried to go a bit crazy here while thinking about a way that no bad state would ever be possible. With a strategy more similar to the one below It would be impossible to define a bad state – but you would have to write quite a few wrappers / unwrappers to handle the different custom types.

type TwoPlayerGame
  = TwoPlayerFirstPlayer Player Player
  | TwoPlayerSecondPlayer Player Player

type ThreePlayerGame
  = ThreePlayerFirstPlayer Player Player Player
  | ThreePlayerSecondPlayer Player Player Player
  | ThreePlayerThirdPlayer Player Player Player

...

type Game
  = Game TwoPlayerGame
  | Game ThreePlayerGame
  | Game FourPlayerGame
1 Like

Another option would be to have a fixed list of players (which is in the order they play their turns) and a count for the turn number. The turn number then would determine which of the players in your list has his turn…

Another option is:

{ currentPlayer: Player
, otherPlayers: List Player
}

This would require reshuffling the list each time the current player changes (removing the new current player and adding the old current player), but it eliminates the edge cases you’re talking about. This was inspired by the “list zipper” data structure.

2 Likes

Regarding the question about using arrays, it’s a tradeoff. If your game can only be one size, you can use a long product type, but it will likely lead to more complex code. The strategy I would probably use in this situation is to use Maybe.map rather than Maybe.withDefault and then if we wind up with Nothing, surface that as an illegal move.

{ currentPlayer: Player
, otherPlayers: List Player
}

I really like this approach! It’s basically a non-empty list so it removes the Maybe from the picture in a much cleaner way. :100:

You could also use Skinney/elm-deque:

import Deque

{ currentPlayer: Player
, otherPlayers: Deque Player
}

The advantage of that being that you can push the currentPlayer onto the back, and take the next from the front, without walking the whole list. Not that it will make much performance difference with just 4 players, but I do like the convenience of using Deque for a FIFO.

I’d go for a custom type, even if it is more verbose.

type Players
    = OnePlayer Player
    | TwoPlayers Player Player
    | ...

Maybe an Int is okay to store whose turn it is, because you can use turn |> modBy amountOfPlayers.

type alias Game =
    { players : Players
    , turn : Int
    }

I guess all approaches have tradeoffs, I personally like to make impossible states impossible, but sometimes it’s worth giving up some guarantees :slight_smile:

Thanks for your answer! Hmm yeah I imagine the verbose option would be the only way to make an impossible state impossible.

I didn’t think of using a Dict structure, I have to see if it would make my life easier in some places. But maybe I thought I needed the sequential nature of lists and arrays for this purpose.

Yes! I had this idea just after posting my initial post. @rupert also suggests this but using a Deque, I thought about using a Queue [EDIT: I guess a Deque is also a Queue actually!]. I’ll look into these options!

And this! I guess a reasonably detailed custom type might be another valid solution.

Thanks all for your ideas! And for confirming that it isn’t always possible (in a reasonable way) to make invalid states impossible…

1 Like

It seems like other folks have mostly covered it, but I’m just going to point out:

You’re using a list but then occasionally going back and forth from an Array. Is there a reason why you shouldn’t just use an array in your model?

1 Like

Good point! If I’m using an Int to navigate the data, it makes much more sense to use an Array. If I’m using a technique like currentPlayer/otherPlayers with a Queue, then I don’t need an Int. (I might still assign Into to the players to help display their names at a consistent place or something)

Thanks!

Just a thought, if you create an opaque type:

type Players
    = Players
       { current : Player
       , all : Dict Int Player
       }

and expose only the API the game needs to create the players, access the current player, and move to the next player etc, then you can play around with the data structure for all players until you decide which of the suggested methods you prefer, and it won’t affect the rest of your game code if you start with a Dict but then try Deque for example.

Looks interesting, I’m not familiar with this kind of construct. Could you show me a very simple example of how to use a type like this?

An opaque type is a type that doesn’t expose it’s internal data structure, and so the only way to access it is with an API that is exposed. This results in you being able to change the internals without necessarily changing the API. This makes your codebase more robust to change.

Creating an opaque type

module Players exposing (Players, current, next)

type Players
    = Players
         { current : Player
         , all : Dict Int Player
         }

type alias Player =
    { name : String 
    , number : Int
    }

current : Players -> Player
current (Players players) = -- notice how you need to unwrap the type
    players.current

next : Players -> Player
next (Players players) =
    -- get the next player
    -- if you change the data structure, you only need to change
    -- the code in this function the rest of your codebase is unaffected
              

Now you simply import the Players module and use the exposed functions to access the internals.

2 Likes

Ok nice! I started changing my code to use a Queue, and I guess I could use that in order to hide the inner structure of my queue.

1 Like

I’m just curious. What’s the board game you’re building?

Scrabble! The goal is to build a local multiplayer (same computer) game first, and then maybe (if I make it this far already!) implement remote multiplayer. I’m not sure how this will go considering I’ll have to rewrite all of the game logic in some backend language and I will want to have a similar language as Elm for the server. I’m a little bit afraid to learn Haskell…