What's your approach to domain modelling?

Hi all,

Let’s assume we are all coding a poker game as a purely functional library (=> no processes at this stage). If you don’t feel comfortable with poker, any other familiar multiplayer game will do.

How would you model it?
How would you design user interactions?
How would you validate user actions?

Thanks

So at some point will there be processes? In that case, have you already had a look at The Erlangelist: To spawn, or not to spawn?. It looks at bridge for the purposes of the article - though as it uses processes it may not address your exact concerns (I also don’t recall any validations).

Yes, read it again this morning. It’s blackjack not bridge! :stuck_out_tongue:

It’s interesting but taking many shortcuts, notably regarding validations. I wonder how he would handle validation of user actions. In BlackJack, there are few possible moves and they are not complex, therefore there is not much to validate. This is my problem with his example.

Once you put your finger into verifying that every user action is possible and authorised, your code becomes a mess. For example, in a game where players select a card from their hand:

when Player P select card C in game G
-> has G started?
-> is G still playable (not game over) ?
-> does G have the min number of players?
-> does P own card C?
-> has P already selected a card before C?

That’s what just happened to me on a side project. Therefore I’m starting to think either:

  • separation of concern, another module should validate (accept or reject with a descriptive error) user actions;
  • defining a finite state machine;
  • not validate anything and let cheaters cheat, hackers hack, processes crash, and good people lose :smiley:

Of course, the second action is not acceptable.

This might help You

http://blog.tokafish.com/playing-poker-with-elixir-part-1/

Of course it is blackjack - serves me right for posting before my first coffee in the morning. :rolling_eyes:

Well, Saša Jurić already mentions :gen_statem and there is even the concept of communicating finite-state machines. However rather than coding the validation as an FSM straight away, it may make more sense to capture it in a state diagram first. With the states clearly identified a “purely functional implementation” could break the validation process into two phases:

  • State identification.
  • Validation based on identified state.

Interesting and thorough example but there it seems the author is using processes for everything and letting them crash when an unauthorised action happens.
Unfortunately, that is not what I’m curious about here.

The game logic is separated from genserver in the example

So to first do note something about structuring with processes: In multiplayer games, there usually is something the player can interact with directly/frequently, and other things that the player only interacts with sporadically (or not at all).

For a game like poker, every table/round is nearly completely isolated: The dealt cards and player actions in one game do not at all matter for another game that is happening at the same time.


And now for the ṕurely functional library’:

One round of poker should definitely be its own, purely functional piece of logic. I think it works very well as a finite state machine. Building a finite state machine in a functional language is not that difficult (However, I personally think it is easier in a strongly typed functional language because you are forced to be more explicit in the different states, how/which transitions (can) happen, and that all states are handled when reading out something from the state machine).

The state machine should make sure that only actions that are allowed at this time can be performed; So ‘player 3’ is not allowed to bet/raise/call until ‘player 2’ has done so. Matching a ‘user’ to ‘player 3 in this round’ is something that should probably exist outside of the state machine, though.

I’d personally strongly advise to separate the state machine from the GenServer that runs it as a process. This both makes it easier to test the thing, as well as making it more explicit where the boundaries of logic vs. communications are.

I have not used :gen_statem before (But I did dabble with the older :gen_fsm which I did not particularly like), but I’d advise to just build the state machine and the transitioning logic myself, rather than using a leaky abstraction layer on top of it.

@Qqwy I agree with everything you said. This is where I would appreciate a type system with algebraic data types. Without it, I feel a bit overwhelmed by the number of repetitions in the production code (validate this, validate that, …), and by the number of tests to write as well. So for one bit of game logic, I end up writing tons of validation code. It is currently mixed in the actual logic and that looks messy. That’s why I thought of an FSM to reject obviously impossible actions (like starting a game when it has already started). However, it won’t help with finer grain issues like “player 3 is trying to play before player 2” or “player A is playing a card that he doesn’t own”.
Not sure I’m making progress here.

As a state blob that I pass around. I do exactly this in the languages I’ve implemented in Elixir, just passing around an env struct, threading it through every function.

Well in purely functional you cannot get user input other than before your code runs, so I’d do it via the repl with them calling the function with the current state along with their action as a kind of ‘event input’.

In reality you need some way to get user actions at runtime, a Haskell IO monad can do that functionally (by blackboxing using input and pretending it has been there since the start even if it has not) or via event passing between purely functional processes or so.

Depends entirely on what you want to validate, but you’d do it in that above process function.

This is not actually that hard. Given a language with Sum types (or tagged tuples on the BEAM) I’d just have each action be part of that sum type that they pass in with the state, and via simple pattern matching you only handle what is valid for a given state/event mix. With matchers it is trivial to just work on the valid actions and crash on the invalid (with perhaps a single wrapper to give pretty errors).

Likewise, I just implemented an FSM on gen_server anytime I needed a FSM, just by pattern matching states and such.

1 Like

@OvermindDL1 so instead of drowning in validations, you would just code the happy path and crash for all players of this party?

Crash back to the wrapper, which then returns the original untouched state and the error.

1 Like

What about using tuples as a poor man’s Either, returning an error tuple in a catch all function clause and using the approach you described here?

Exactly. :slight_smile:

Thanks for recommending Exceptional. I already had a look at it not so long ago but without context I didn’t give it enough importance.

That helped me quite a bit this time and put on a better track too. Essentially, I refactored a bit my code from something like this:

def user_action(state, param) do
  cond do
    validate_param() ->
      {:error, :reason1}
    validate_state() ->
      {:error, :reason2}
    ...
    true ->
      # the actual logic
end

to

def user_action(state, param) do
  param
  |> validate_param()
  ~> validate_state()
  ~> do_user_action()
end

where validate_xxx function return either the param unchanged or an exception (return, not raise). You could say that it’s not much but the code is much more readable that way. Validators are defined as simple functions and I can now define them outside my module if I need to.

1 Like

Now, back to the original topic, what is your usual approach: top down, bottom up? do you represent everything with structs?

Top-down and bottom-up both work well, and in practice which one I pick depends on the problem (and often I sort of do both at the same time and meet in the middle).
I do not represent everything with structs. In this example, I’d define a %Card{} (with a suit and a rank), a %Player{} (with the amount of money bet thus far, their hand and possibly some other meta-info to link it to an external user) and a %Game{} (which is the high-level State Machine representation, containing a queue of players, the current round of the game, the shuffled deck of hidden cards, and the visible cards that form the street/river or what it is called again in Poker).

Most other things are either atoms (as “poor man’s sum types”) or integers.

I’ve had decent success with a module per state and module-defined state, managed by (in this case) a GenServer that basically just forwards calls to state.current_state_module.do_something(state.module_state). For example, my Raft library has a module for “Leader”, “Follower” and “Candidate” and functions transition_from in each one to handle state transitions (with pattern matching on the old state - there is no transition_from(:follower, ...) in the Leader module so that’s automatically an invalid state transition.

It’s simple to test (I usually want to write code in a mix of top-down and bottom-up, mostly guided by tests).

(also, I need to write log compaction for that library so I can call it mostly done ;-))