Managing 2 states in 1 GenServer

I’m using GenServer to create the state for the card game, “War”. It’s a single player game where the cards are randomly divided between the computer and user. Then the cards are compared one at a time and the higher value card “wins”. Since the user is dealt half the cards and the computer must have the other half I’m initializing the game when the user visits the GameController#new action.

Now I have the state of the game, two lists of cards, user_cards & computer_cards, but I can’t figure out how to compare the cards one by one, moving the lower value to the opposite list.

If i just wanted to run through the cards I would do something like:

  def handle_call({:take_card}, _from, [card | rest]) do
    {:reply, card, rest}
  end

But this won’t work if I’m storing both the user cards and computer cards together in one state.

How can I manage the game in such a way where I can run through both lists, comparing the values of each card and moving the lower card to the higher card’s list?

Maybe you can keep both states in a tuple and compare them with some recursive function?

def handle_call({:take_card}, _from, {computer_cards, player_cards}) do
  compare(computer_cards, player_cards, [])
  # ...
end

defp compare([], [], acc), do: :lists.reverse(acc)
defp compare([computer_card | computer_cards], [player_card | player_cards], acc)
    when computer_card > player_card do
  compare(computer_cards, player_cards, [:computer_wins | acc])
end
defp compare([computer_card | computer_cards], [player_card | player_cards], acc)
    when computer_card < player_card do
  compare(computer_cards, player_cards, [:player_wins | acc])
end
2 Likes

This is what I have so far:

def init(:ok) do
  {:ok, load()}
end

def load() do
  deck = Deck.new
  cards = Enum.take_random(deck, 26)
  comp = deck -- cards
 %Game{
   user_cards: cards |> Enum.map(&to_tuple/1),
   status: "in progress",
   computer_cards: comp |> Enum.map(&to_tuple/1),
     }
end

This gives me a shuffled deck of cards, dealt between the user and computer. This code is called when the user navigates to the GameController#new action:

  def new(conn, _params) do
    changeset =
      conn.assigns[:current_user]
      |> Ecto.build_assoc(:games)
      |> GamePlay.change_game()
      War.GamePlay.Game.start_game()
    game =
      Server.read(War.GamePlay.Server)

    render(conn, "new.html", changeset: changeset, game: game)
  end

It should be loading the association of the user but it isn’t. In any event, I suppose there should be a button in the UI that allows the user to flip to the next card, which would call the Server#compare_cards function. Since I have the state all jumbled together in a struct it’s difficult to isolate the first card from each list.

Am I going about this the wrong way? Should each list ( the user cards, the computers cards) be separated into different processes?

My recommendation is No. Part of the reason is that you want the state of the cards to always be internally consistent and if you’re using separate processes to manage the states of various decks then you will run into difficulty keeping everything consistent at all times. Instead I would have a single struct that encapsulates the entire game state for a single game of war. Then you would have multiple instances of each game state, probably one for each user that is currently playing.

I’d highly recommend reading the article “To spawn, or not to spawn?”, it goes into great detail, and even features a card game (a slightly simplified version of blackjack). The code is also available on github.

3 Likes

That is a great article. I actually have it bookmarked as I think it’s a wonderful explanation of processes. The one difference I notice is I don’t have a Hand module because I didn’t think I needed one. I’m still having a mental roadblock as to what I’m missing.

Taking a look in iex>

Interactive Elixir (1.5.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Server.start
{:ok, #PID<0.443.0>}
iex(2)> {:ok, pid} = v(-1)
{:ok, #PID<0.443.0>}
iex(3)> Server.read pid
%War.GamePlay.Game{__meta__: #Ecto.Schema.Metadata<:built, "games">,
 computer_cards: [{2, :clubs}, {2, :spades}, {8, :spades}, {14, :clubs},
  {13, :diamonds}, {8, :diamonds}, {8, :clubs}, {10, :diamonds}, {11, :clubs},
  {2, :diamonds}, {11, :spades}, {9, :hearts}, {4, :clubs}, {4, :hearts},
  {12, :spades}, {9, :spades}, {14, :diamonds}, {3, :spades}, {5, :diamonds},
  {11, :diamonds}, {12, :clubs}, {7, :hearts}, {14, :hearts}, {7, :spades},
  {3, :clubs}, {12, :hearts}], id: nil, inserted_at: nil, status: "in progress",
 updated_at: nil,
 user: #Ecto.Association.NotLoaded<association :user is not loaded>,
 user_cards: [{3, :hearts}, {14, :spades}, {10, :clubs}, {8, :hearts},
  {6, :hearts}, {13, :clubs}, {13, :spades}, {13, :hearts}, {4, :diamonds},
  {2, :hearts}, {12, :diamonds}, {9, :diamonds}, {6, :clubs}, {3, :diamonds},
  {5, :hearts}, {10, :spades}, {5, :spades}, {4, :spades}, {6, :spades},
  {6, :diamonds}, {7, :diamonds}, {11, :hearts}, {5, :clubs}, {7, :clubs},
  {10, :hearts}, {9, :clubs}], user_id: nil, won: false}

The cards are properly dealt and stored in either user_cards or computer_cards but comparing them is proving to be more complicated than i anticipated. What would the next step be?

1 Like

Agreed, that article cleared up so many things for me when moving from an OO world to Elixir…

Break it down further. The entire game should be simple to model as a recursive function operating on a state.

How about this. The game state holds, user_cards, comp_cards and a pile of cards.

  • If both players run out of cards it is a draw
  • If one player runs out of cards the other player wins
  • If the number of card is the same, increase the pile with 3 cards each and then do another turn
  • Else, the highest number wins the two cards + the pile.
def turn(%{user_cards: [], comp_cards: []}), do: draw
def turn(%{user_cards: []}), do: comp_wins
def turn(%{comp_cards: []), do: user_wins
def turn(g = %{user_cards: [user_first|user_rest], comp_cards: [comp_first|comp_rest]}) do
    case Deck.highest_value(user_first, comp_first) do
        user_first -> # User had the highest card.
            new_state = %{g | user_cards: user_rest ++ [user_first, comp_first] ++ g.pile, 
                              comp_cards: comp_rest, pile =[]}
            turn(new_state)
       comp_first ->
              # Same as above but equal
       equal ->
             # This is war! Just pile some cards on the pile
             # and try again
            {user_to_pile, user_left} = Enum.split(user_rest, 3)
            {comp_to_pile, comp_left} = Enum.split(comp_rest, 3)
            pile = [ user_first, comp_first | g.pile ] ++ user_to_pile ++ comp_to_pile,
            new_state = %{g | user_cards: user_left, comp_cards: comp_lfet, pile = pile}
            turn(new_state)
    end
end.

Note code above not tested but should give you some idea.

You can even add an additional function header pattern matching clause to cater for the war state. I.e

 turn(%{user_card: [ uc = {_suite, val} | user_rest], comp_card: [ cc = {_suite, val} | comp_rest ]}), do ... 

This would make the comparison of the value of the first card of each players pile in the function header. Which is clearer I don’t know but it is a great feature of elixir to be able to do this.

1 Like

I’m not sure if I’m moving backwards or forwards :frowning:

This code is failing for (module Genserver is not available) failed: :badfile

  def handle_call({:battle}, _from, %Game{user_cards: user_hand, computer_cards: computer_hand} = state) do
    

    [first | rest] = user_hand
    user_card = elem(first, 0)

    [first | rest] = computer_hand
    computer_card = elem(first, 0)

    compare(computer_card, user_card)
  end

  defp compare(computer_card, user_card) when computer_card > user_card do
    IO.puts "computer card higher!"
  end

  defp compare(computer_card, user_card) when computer_card < user_card do
    IO.puts "user card higher!"
  end

Each card is a tuple with an integer and atom, like {4, :hearts}, which is why I am using enum.


I’m cheating in the UI just grabbing the first card for the user and computer with Enum.at

Is there a syntax error in the handle_call for “battle” ?? I am expecting IO.puts to the console but I’m not getting anything.

You have written Genserver instead of GenServer, that’s probably the reason for the error.

Also, it seems you forgot to deal with when the cards are equal in your guards.

1 Like

Thank you for catching the typo

So, I’m at the point where I’m comparing two cards and determining which value is higher. Here is the error I’m receiving:

[error] GenServer War.GamePlay.Server terminating
** (stop) bad return value: "computer card higher!"

After determining which card is higher, both cards should be given to the winners deck. Ideally, they should be held in a separate pile until all the cards are used, then shuffled back in.

In my “battle” call, I’m only pulling out the first card of each opponent. How can I rewrite this so that I can add the two cards to the winners pile?

  def handle_call({:battle}, _from, %Game{user_cards: user_hand, computer_cards: computer_hand} = state) do
    
    [first | rest] = user_hand
    user_card = elem(first, 0)

    [first | rest] = computer_hand
    computer_card = elem(first, 0)

    compare(computer_card, user_card)
  end



  defp compare(computer_card, user_card) when computer_card > user_card do
    "computer card higher!"
  end

  defp compare(computer_card, user_card) when computer_card < user_card do
    "user card higher!"
  end

The last line, compare(computer_card, user_card), is where I’ll need to make sure I’m including the current game state (both the user and computer hands). So, would i just use compare(computer_card, user_card, state) and handle the state in the private functions?

1 Like
  def handle_call({:battle}, _from, %Game{user_cards: user_hand, computer_cards: computer_hand} = state) do
    
    [first | rest] = user_hand
    user_card = elem(first, 0)

    [first | rest] = computer_hand
    computer_card = elem(first, 0)

    {:reply, compare(computer_card, user_card), state}
  end

Need to return {:reply, reply, state} tuple in order to reply to a call with genserver behavior.

Also consider using :battle instead of {:battle}. Putting a single value into a “tuple” seems a bit redundant to me.

1 Like

When you are using a GenServer you implement a specific behaviour. This means that you must implement the call-backs specified by the GenServer. Each call-back defined (like handle_call) must follow this contract.

The normal reply from a GenServer is {:reply, your_reply, state}
where your reply would be the result of the compare function and the state would be the Game data.

I recommend that you read up on the documentation on GenServers.

https://elixir-lang.org/getting-started/mix-otp/genserver.html
https://hexdocs.pm/elixir/GenServer.html

What inputs would compare take then? Like this?

defp compare(computer_card, user_card, state) when computer_card > user_card do

I did forget the {:reply …} part of the response. But how can I add the cards to the winners hand? I’m trying to implement something similar to the solution you proposed above but I’m stuck at the “battle” call - comparing the cards and adding the cards to the winners hand.

Using @idi527’s suggestion {:reply, compare(computer_card, user_card), state} I’m getting a function clause error.

I think it’s because I can’t do this:

  defp compare(computer_card, user_card, state) when computer_card > user_card do
    computer_cards ++ [computer_card, user_card]
  end

If computer_cards are part of the state then I’d create a list with both cards and add them back to the hand. This is not working though

What inputs would compare take then?

Same inputs as before, compare/2 should not be affected by my suggestion.

Firstly add some sort of :discard key or something to your Game struct. Then update the state.

def handle_call({:battle}, _from, %Game{user_cards: user_hand, computer_cards: computer_hand} = state) do
    
    [first | rest] = user_hand
    user_card = elem(first, 0)

    [first | rest] = computer_hand
    computer_card = elem(first, 0)

    new_state = Map.update(state, :discard, [user_card, computer_card], &[user_card, computer_card | &1])

    {:reply, compare(computer_card, user_card), new_state}
  end

These tuples you return from the handle_* functions are your way of communicating with the gen server logic. What you return as “state” is basically telling gen server to keep track of that and give that back to me the next time I’m called.

1 Like

I think you are having some problems because you are mixing two concepts. GenServers and the recursive patterns

“recursive pattern”

Because elixir is immutable you can’t change anything. That is why you need to keep passing a state around. This state is just data that you want available.

If we take a simpler example instead of cards. Lets say I want to throw a dice 100 times and store the result in some state.

in C you would do something like:


int roll_dice() {
   return rand() % 6;
}
int main() {
    int result[100];
    int i = 0;

    for (i = 0; i < 100; i++) {
        result[i] = roll_dice();
    }
}

Your state is the result array which is mutated in the for loop.

In elixir you can’t do this because everything is immutable. Instead you keep passing your state around where you need it. The normal way to do this in elixir is to use recursive functions (a good way to practice this is to implement a few of the functions in the List module on your own, for example reverse)

def roll_dice(), do: :rand.uniform(6)

def collect(0, state), do: state
def collect(count, state) do
    new_state = [ roll_dice() | state ]
    collect(count - 1, new_state)
end

def main() do
   initial_state = []
   collect(100, initial_state)
end

Above we have the collect function which is a recursive function. It keeps going until count reaches 0 when it returns the state as the result. If the count is not zero we do a dice roll and create a new state. Then we call ourselves again but we reduce the count (otherwise the function would never return) and pass in the new state.

I think a good exercise for you would be to initally implement war without a GenServer and simply use recursive functions to run through an entire game. This would help you seeing the pattern behind it all when combining it with a GenServer logic.

GenServer

The GenServer behaviour is basically a generalization of a recursive patterns for writing servers. Something along the line of

  1. spawn a process into a loop with an initial state
  2. the loop function waits for a message to be sent to it (blocking)
  3. the loop function receives the message and either
  4. calls itself recursively with an potentially updated state
  5. or, stops calling itself recursively and just return a value
# Here is our loop function which takes a state
loop(state) do
    receive do  # this is blocking so the process will wait here until it gets a message it can match
        {from, :battle} -> # We receive a message consisting of the pid that sent us the message and the actual message
           new_state = do_war_game_logic(state)
           # Send something back to the caller (from)
           send(from, :ok)
           # Keep looping but with new state
           loop(new_state)
        _ ->
           # received an unknown message, lets quite but simply stop
           # calling ourselves recursively and return instead
            {:error, :unknown_msg, :quitting}
    end
         
   # Spawn our process
   pid = spawn(fn -> loop(%Game{}) end)

   # End send the :battle command to it.
   send(pid, {self(), :battle})   
end

Hopefully I haven’t confused things more (which is likely).

4 Likes

Thank you for taking the time to explain the answer @cmkarlsson. It hasn’t made anything more confusing I understand exactly what you’re saying. I almost have a working version now. I don’t think I should create a loop function though because the user should only see two cards at a time, his top card and the computers top card.

Then there would be a button to trigger a “next_card” event, which would bring two more cards on the screen with a message like, “you’ve won”, or “you’ve lost”.

That is great! And your screenshot of the game looks really good. Best of luck!

Not in the final version. I understand that you want to use the GenServer (or a process) to wait for use input before showing the next card. This suggestion was more so that you can see that the game logic and the GenServer are two separate things. By having a “pure” (no messages or anything) game first it is easy to incorporate this into a gen server.

2 Likes