Building a Live View for the Islands Game

Hello alchemists!

I’ve finished reading the Functional Web Development book by Lance Halvorsen where we build a game of islands (similar to a battleship game). I’ve been interested in live view so I thought building a live view interface for the game would be a pretty cool project.

This is my second medium-sized project in Phoenix. The first was a JSON API for a job interview, which is a much more straightforward project.

My current implementation works:

  • A user can register with an email and password and get access to the lobby. User auth is managed by Pow
  • While in the lobby, a player can start a game
  • Another player in the lobby can select the first player’s game. This starts the game for both players
  • Both player’s can set their islands on the board
  • As soon as both players have their islands set, they can guess coordinates in alternating turns. Hit and missed guesses are shown in the opponent’s board. The opponent’s hits are shown in the player’s board.
  • When a player wins the game, he can see that he won. The loser also sees he lost.

While this all works, the code organization is quite unruly. This is all managed by a live view that manages two channel topics:

  • The Lobby topic. In this channel I have messages about new players joining the lobby and creating games. I’m using the Phoenix.Presence module for this together with multiple handle_info() clauses for displaying the open games in the live view.
  • The Game topic. In this channel I exchange messages between the players (setting islands, guessing coordinates, winning the game etc.)

The live view also manages user events from the browser with multiple handle_event() clauses.

The actual game is managed by the GameEngine. The GameEngine is a separate application (another repo actually) with it’s own supervisor. This application manages the game state and decides what’s possible and when.

Finally, to my question: how can I organize the code so that it’s more manageable? My current live view is responsible for handling user events in handle_event() clauses, dispatching messages with PubSub and handling them in handle_info() clauses, presence diffing, handling two different topics and managing a medium sized list of assigns. I’ve tried using a nested live view for the player’s boards but I don’t think it helped much (the same problems persist, just in a smaller scale). This complexity shunned me away from testing and now I have a big fragile mess in my hands :wink: . Maybe I’m juggling too many balls at once :thinking:

I’ll post the github repo here by the end of day if anyone wants to read the code. I’m a bit ashamed by it but I also want to sharpen my skills :smile:

2 Likes

In any kind of LiveView application you end up with the handle_event being called, with all kinds of different events. Cleaning this up is indeed a bit of a challenge.

One thing that helped me when I started using LiveView, is the realization that pattern matching allows you to introduce multiple layers of abstraction that makes it easier to understand which parts of the system are responsible for which events. If you have an event like “player/board/position/change” then the handle_event could look like:

def handle_event(event_binary, params, context) do
  event_binary
  |> String.split("/")
  |> dispatch_event(params, context)
end

def dispatch_event(["player" | event]], params, context) do
  PlayerLogic.handle_event(event, params, context)
end

def dispatch_event(["chat" | event]], params, context) do
  LobbyLogic.handle_event(event, params, context)
end

Now obviously this example is somewhat opinionated, but the essence is that nothing prevents you from picking your event names in such a way that you can quickly dispatch them to other, more implementation-level parts of your application, such that your main handle_event function does not get too long.

My other suggestion for you is to model your game (both the creating->joining->playing->finishing of new game sessions as well as the process of playing a single game session) as finite state machines. Drawing them out as state diagrams might make it more clear when to create/alter/finish their respective processes.
Personally I dislike using the assigns for anything other than (a) plain settings that are only relevant for that specific connection and (b) possibly some sort of identifier so we can find the player’s current game again. For everything else I like to use a GenServer that only allows the changes that are allowed in the game.

Using handle_event -> PubSub -> handle_info is a way to create seeming separation between the LiveView application and the GameEngine application, in the sense that it is possible to drive your GameEngine application from other sources. However, neither of these extremes is good:

  • Sending the handle_event parameters 1:1 through PubSub. This will make it impossible for other implementations to drive the GameEngine unless they also 1:1 mimic how LiveView’s events work.
  • Create custom handler implementations for each of the events that might be sent to handle_event. This is just a very large amount of work, for no apparent benefit, since you are still hard-coding how an event looks like in two places.

I’d therefore suggest to take the events coming in through LiveView, normalize them somewhat (using for instance the event splitting detailed above, and possibly making sure that the assigns follow a certain specified structure) and maybe prevent blatantly wrong/non-existent events, and then send all of these on.

If you find that you need to handle two Phoenix topics in the same location, then they probably better replaced by only a single topic.


These are a couple of loose, but sort of related thoughts. Maybe they help. If you have more questions, please ask!

And I’d love to see your code. There is no reason to be ashamed for any code that you think might be improved, especially if you want to learn from it! :+1:

2 Likes

Thank you for your thoughts! Glad to know organizing events is a challenge and that I’m not alone in this.

  • Dispatching the events with namespaced events and pattern matching seems like a good idea! I’ll definitely experiment with this one.
  • The Engine already uses a finite state machine. In this case it’s pretty simple because it uses a struct and pattern matching to enforce that certain actions (function clauses) are only possible in a given state. How would I do that in a live view (GenServer) context?
  • I’m using the assigns basically for keeping all of the relevant state for the live view. If the live view process terminates, the current assigns are kept in an ETS table and keyed by the current user (this is handled in a terminate() callback). When the live view mounts, it checks if the current user has some old state and spins up the game for him by updating the socket with the assigns. I’m not quite sure I understand your recommendation in this regard.
  • I’m using two different channel topics for one simple reason: all authenticated users need to join the lobby topic so they can be aware of new games created by other users. When a player starts a new game, he joins a game topic so he can be ready as soon as another player joins his game. Only the 2 players should be present in a given game topic, since only they are interested in the messages relevant to the current game.
  • I’m also not sure I follow your recommendation in regard to the handle_event -> PubSub -> handle_info flow.

Here’s the relevant repos if anyone wants to take a look:

islands_engine is declared as a file dependency in islands_interface's mix.exs. Cloning both projects side by side should make it work.

@Qqwy I’ll work on your tips tonight. Didn’t have time yesterday, unfortunately.

Maybe @lance has some valuable input? That would be awesome :slight_smile:

2 Likes

I’ve marked @Qqwy answer as the solution as it’s given me plenty of food for thought and got me pretty far!

  • I’ve created an EventHandler module for dispatching the LiveView events to scoped handlers (LobbyHandler, BoardHandler etc.). I’ve used defdelegate on the LiveView to handle all events in this module
  • The handlers receive a GameContext struct, created by the EventHanlder based on the assigns present in the socket. This way I can pattern match the necessary keys on function heads
  • The handlers return some new state based on the event and a list of pubsub events to be dispatched
  • The EventHandler dispatches those events to a PubSub dispatcher. This module takes care of subscribing to channels and broadcasting messages.
  • A Cache module was created to interact with the ETS backed cache
  • I’ve killed the child LiveView :crossed_swords:
  • All of these changes made testing much easier. For example, the scoped EventHandlers are pretty easy to test as they are almost pure (aside from interacting with the GameEngine).

I still have a lot of work to do, but I’m pretty confident I’m in the right track now. Thanks again for the help @Qqwy!

3 Likes