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
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
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…
{ 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.
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.
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.
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!
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)
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.
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.
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…