Welcome back to another Elixir Alchemy! This time, we'll discover the power of Phoenix LiveView by building an interactive game.
By rendering HTML on the server and communicating between the frontend and backend over web sockets, LiveView helps us build real-time interfaces without writing JavaScript or worrying about updating the state in the browser. By updating the state on the server side, LiveView makes sure to only update the parts of the page that need to be, resulting in fast applications that send minimal amounts of data over the wire.
To illustrate this, we’ll build our game in Phoenix, and we’ll use LiveView to make it interactive. Although tic-tac-toe is fun, we’ll go for something a bit more ambitious by building Go.
Go is an abstract strategy board game for two players, in which the aim is to surround more territory than the opponent. The game was invented in China more than 2,500 years ago and is believed to be the oldest board game continuously played to the present day.
The final result is an implementation of the Go game that allows players to take turns placing stones on the board. Players can capture each other's stones, and the game keeps track of which stones were captured.
Along the way, we’ll learn how Phoenix LiveView can help you build interactive applications without duplicating code between the frontend and the backend by keeping everything in Elixir.
Let’s Go!
The starter app is a Phoenix application with some parts already set up. It cleans up some generated files, adds styling for the Go board, pre-installs Phoenix LiveView according to the install guide in the README, and has a module that keeps track of the game’s state.
In our game, the State
module describes the state of the game. The starter app already comes with the State
module so we can focus on working with LiveView.
The State
struct keeps track of which stones are on the board in its :positions
list, and it knows which player is up next in its :current
key.
# lib/hayago/state.ex
defmodule Hayago.State do
alias Hayago.State
defstruct positions: Enum.map(1..81, fn _ -> nil end), current: :black
# ...
end
A newly created state is initialized with list of 81 nil
values as its :positions
, as the board is empty and has 9 by 9 positions. The player with the black stones is the first to move, so the :current
key is set to :black
for any new state.
The State
module exposes two functions. The first is place/2
which places a new stone on the board. If the new stone steals all the liberties of another stone, the captured stone is removed from the board automatically.
The legal?/2
function checks if a move is legal by checking if another stone already occupies the position and making sure the stone wouldn't be immediately captured.
-> If you’d like to learn more about the implementation of the State
module that we’ll use, check out the module’s documentation, which explains how it handles placing stones on the board, capturing stones and validating possible moves.
The GameLive
Module
We’ll start by rendering the board. First, we add a live view to our application to handle rendering and updating the board. It’s called GameLive
, and it has render/1
and mount/2
callback functions.
# lib/hayago_web/live/game_live.ex
defmodule HayagoWeb.GameLive do
use Phoenix.LiveView
def render(assigns) do
HayagoWeb.GameView.render("index.html", assigns)
end
def mount(_session, socket) do
{:ok, assign(socket, state: %Hayago.State{})}
end
end
The mount/2
callback sets up the assigns in the socket
to set the initial state for the view. We use it to create a new state, then add it to the socket assigns to make it available in the template.
Next, we’ll add a template to render the board. We name it index.html.leex
, making it a Live EEx template. Although similar to regular EEx templates, live templates track changes in order to send a minimal amount of data over the wire whenever the view updates.
# lib/hayago_web/templates/game/index.html.leex
<div class="board <%= @state.current %>">
<%= for _position <- @state.positions do %>
<button></button>
<% end %>
</div>
We loop over all positions in the @state
struct we assigned in the live view and an empty <button>
elements for each. The buttons are in a <div>
element, which the stylesheet automatically styles as a Go board. We’ll also add the current color as a class name to the board, to allow the stylesheet to show the stone you’re about to place when you hover over a position.
Finally, we route any requests to /
to our GameLive
module in our router:
# lib/hayago_web/router.ex
defmodule HayagoWeb.Router
# ...
scope "/", HayagoWeb do
pipe_through :browser
live "/", GameLive
end
end
If we start the Phoenix server and navigate to https://localhost:4000 in our browser, we'll see the app render an empty Go board. While we can't place stones just yet, hovering over the positions on the board shows us where a new stone would be placed.
Making Moves
To place stones on the board, State
implements a place/2
function that takes a State
struct and an index. It replaces the position that corresponds to the index with the value in the :current
key, which is either :black
or :white
, depending on which player’s turn it is.
In the template, we add phx-click
and phx-value
attributes to our buttons. These attributes tell LiveView to send an event to our GameLive
module.
# lib/hayago_web/templates/game/index.html.leex
<div class="board">
<%= for {value, index} <- Enum.with_index(@state.positions) do %>
<button phx-click="place" phx-value="<%= index %>" class="<%= value %>"></button>
<% end %>
</div>
In our live view, we handle the event by matching on the attributes we set in the template. We use the passed index to call State.place/2
, which returns a new state with the stone placed on the board. We'll return a :noreply
-tuple with the new state.
# lib/hayago_web/live/game_live.ex
defmodule HayagoWeb.GameLive do
# ...
def handle_event("place", index, %{assigns: assigns} = socket) do
new_state = State.place(assigns.state, String.to_integer(index))
{:noreply, assign(socket, state: new_state)}
end
end
By updating the state in the socket, LiveView knows to rerender the parts of the template that changed. The updated page is then compared to the rendered page to apply a minimal patch to the already-rendered page.
Back in our browser, which automatically refreshed the page after we made our changes, our project is already starting to look like a proper Go game. We can place stones on the board and even surround an enemy stone to have it removed!
Look at all the things we're not doing! We didn't have to write any code to send data to the browser, and we didn't have to worry about updating the rendered page. LiveView took care of updating the page whenever the state changed.
However, if we click the same position twice, an already placed stone is replaced by another. To prevent this from happening, we should disable the buttons that represent illegal moves.
Preventing Illegal Moves
To prevent illegal moves, we’ll render a disabled button for every position that either has a stone on it already or one where a newly placed stone has no liberties.
To make that work, we’ll use State.legal?/2
, which takes the current state and an index, and returns a value indicating whether the current player can place a stone there or not.
<div class="board <%= @state.current %>">
<%= for {value, index} <- Enum.with_index(@state.positions) do %>
<%= if Hayago.State.legal?(@state, index) do %>
<button phx-click="place" phx-value="<%= index %>" class="<%= value %>"></button>
<% else %>
<button class="<%= value %>" disabled="disabled"></button>
<% end %>
<% end %>
</div>
Because LiveView takes care of updating the page, we can add an if-statement to the template that checks if placing a stone on each position is a legal move. If it is, we render the same button as before. Otherwise, we render a disabled button.
The stylesheet makes sure not to show hovers for disabled buttons, and changes the cursor to indidcate that a stone can be placed there.
Capturing Stones
When capturing a stone, the place/2
function increments a counter in the current state's :captures
map, which holds a counter for both the black and the white stones.
For each captured stone, we’ll show a stone above the board. Since the captures are already available in the @state
struct we receive from the live view, we’ll loop over each of the counters to render a <span>
with the correct class name for the stylesheet to turn into a button.
<div class="captures">
<div>
<%= for _ <- 1..@state.captures.black, @state.captures.black > 0 do %>
<span class="black"></span>
<% end %>
</div>
<div>
<%= for _ <- 1..@state.captures.white, @state.captures.white > 0 do %>
<span class="white"></span>
<% end %>
</div>
</div>
Since we’re using a range in our list comprehension, we’ll make sure to add a filter that ensures that the list isn’t empty when looping over it.
Now, we have a way to keep track of which stones were captured, as the stones are displayed above the board.
Scoring, History and the Ko Rule
We've made some strides implementing Go, and we’ve now learned how to set up a LiveView project. We've seen that carefully updating the state is enough to make build an interface without having to worry about updating the view. Instead, we focussed on rendering a static representation of the current state, and left the work of updating the page the player sees up to LiveView. Aside from setting up our live view module, the code we wrote to show the state of the board was all done by adding logic to the template, and we didn't have to write any JavaScript ourselves.
However, there are still some things we should do. The next time we meet, we’ll add history to our game to allow players to undo moves and we’ll implement the ko rule to prevent repeating moves. Finally, we’ll add a score counter to show each player’s current score. See you then!
Top comments (0)