Skip to content
Permalink
Branch: master
Commits on Jul 4, 2019
  1. Add Game.legal?/2 to handle to ko rule

    jeffkreeftmeijer committed Jun 25, 2019
    Aside from allowing the player to undo and redo moves, keeping history
    allows us to implement Go's ko rule.
    
    > A play is illegal if it would have the effect (after all steps of the
    > play have been completed) of creating a position that has occurred
    > previously in the game.
    
    Go prevents players from making moves that revert the board to a
    previous state. In practice, this happens when a stone is captured, and
    the newly added stone is immediately taken by the other player.
    
    Currently, we validate moves in the `State.legal?/2` function, which
    checks if a position is empty, and makes sure placing a stone on that
    position has liberties.
    
    To implement the ko rule, we need the history of game states, meaning we
    need access to the `Game` struct. To do that, we add a function named
    `Game.legal?/2`.
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          # ...
    
          def legal?(game, position) do
            State.legal?(Game.state(game), position) and not repeated_state?(game, position)
          end
    
          defp repeated_state?(game, position) do
            %Game{history: [%State{positions: tentative_positions} | history]} =
              Game.place(game, position)
    
            Enum.any?(history, fn %State{positions: positions} ->
              positions == tentative_positions
            end)
          end
    
          # ...
        end
    
    Our new function takes the game struct as its first argument, and the
    position a new stone is places as the second. It calls `State.legal?/2`
    to make sure the already-implemented rules are satisfied. Then, it makes
    sure the new state hasn't already happened using `repeated_state?/2`, a
    private function that places the stone and compares the new state to the
    history list.
    
    Finally, we'll update the template to switch from using `State.legal?/2`
    directly to using `Game.legal?/2`, which takes the game history in
    account.
    
        # lib/hayago_web/templates/game/index.html.leex
        # ...
    
        <div class="board <%= @state.current %>">
          <%= for {value, index} <- Enum.with_index(@state.positions) do %>
            <%= if Hayago.Game.legal?(@GAMe, index) do %>
              <button phx-click="place" phx-value="<%= index %>" class="<%= value %>"></button>
            <% else %>
              <button class="<%= value %>" disabled="disabled"></button>
            <% end %>
          <% end %>
        </div>
    
        # ...
  2. Disable undo and redo buttons when not applicable

    jeffkreeftmeijer committed Jun 24, 2019
    Finally, we need to make sure the undo and redo buttons are disabled
    when there are no moves to undo or redo. To do that, we'll implement a
    function named `history?/2`, which takes a game struct and an index and
    returns wether that index is part of the game's history.
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          # ...
    
          def history?(%Game{history: history}, index) when index >= 0 and length(history) > index do
            true
          end
    
          def history?(_game, _index), do: false
        end
    
    Our function checks if the requested index is above 0 to make sure the
    game doesn't allow jumping forward when there are no moves done yet. Then,
    it checks if the length of the history list is higher than the index, to
    make sure we don't overshoot the list when undoing moves.
    
    With our new function in place, we can check if the game can undo and
    redo to the previous and next move before rendering the button. If it
    can't it will render a disabled version.
    
        # lib/hayago_web/templates/game/index.html.leex
        <div class="history">
          <%= if Hayago.Game.history?(@GAMe, @game.index + 1) do %>
            <button phx-click="jump" phx-value="<%= @game.index + 1%>">Undo</button>
          <% else %>
            <button disabled="disabled">Undo</button>
          <% end %>
    
          <%= if Hayago.Game.history?(@GAMe, @game.index - 1) do %>
            <button phx-click="jump" phx-value="<%= @game.index - 1%>">Redo</button>
          <% else %>
            <button disabled="disabled">Redo</button>
          <% end %>
        </div>
    
    Trying our game again now, we'll see that we can't undo further than the
    list of previous moves, and we can't redo into the future.
  3. Slice history before prepending new states

    jeffkreeftmeijer committed Jun 24, 2019
    If we open our browser and navigate to https://localhost:4000, we can
    see that the undo and redo buttons work. After placing a few stones, we
    can click the undo button to get the last one removed, and the redo
    button to get it back again.
    
    However, if we undo a move and try to place a stone, we notice that the
    new stone doesn't get added to the board. Instead, the stone we just
    removed by pressing the undo button reappears.
    
    It turns out we have one more step to take before we can place a new
    stone after pressing the undo button. Let's break down what's happening.
    
    1. We place a stone on the board, which prepends a new state to the
       history list.
    2. We press the undo button to increase the history index to 1,
       bringing us back to our initial state, giving us an empty board
       again.
    3. We try to place a new stone, which prepends a new state to the
       history list.
    
    The new state is the first in the list, meaning its index is 0, but the
    Game index is still 1. That index belongs to the move we've undone by
    pressing the undo button. Essentially, each of our newly added stones
    are delayed by one move.
    
    To fix this, we need to slice any undone states from the list whenever we
    add a new state by placing a new stone.
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          # ...
    
          def place(%Game{history: history, index: index} = game, position) do
            new_state =
              game
              |> Game.state()
              |> State.place(position)
    
            %{game | history: [new_state | Enum.slice(history, index..-1)], index: 0}
          end
    
          # ...
        end
    
    In the new version of our `place/2` function, we use `Enum.slice/2` to
    drop the undone moves from the history list. We'll also reset our game's
    index attribute to 0, which makes sure our newly added stones
    always immediately appear.
  4. Add undo and redo buttons

    jeffkreeftmeijer committed Jun 24, 2019
    Now that our game module keeps a history of moves, and we can jump
    between them, let's add buttons to undo and redo moves while playing the
    game.
    
        # lib/hayago_web/templates/game/index.html.leex
        # ...
    
        <div class="history">
          <button phx-click="jump" phx-value="<%= @game.index + 1%>">Undo</button>
          <button phx-click="jump" phx-value="<%= @game.index - 1%>">Redo</button>
        </div>
    
    First, we'll add two more buttons. We'll use "jump" as the value for
    both buttons' `phx-click` attribute, which is the name for a function
    we'll implement in the `GameLive` module shortly.
    
    The `phx-value` attribute is used to pass the history index we'd like to
    jump to. The history list is reversed as new moves are prepended to the
    history list. So to undo a move, we'll increase the current index by 1,
    and we'll decrease it by 1 to redo.
    
    In the `GameLive` module, we'll handle the jump event by calling
    `Game.jump/2` with the current game and the passed index to get a new
    game struct we'll assign on the socket. Like before, we update the state
    assign for convenience.
    
        # lib/hayago_web/live/game_live.ex
        defmodule HayagoWeb.GameLive do
          # ...
    
          def handle_event("jump", destination, %{assigns: %{game: game}} = socket) do
            new_game = Game.jump(game, String.to_integer(destination))
            {:noreply, assign(socket, game: new_game, state: Game.state(new_game))}
          end
        end
  5. Add Game.jump/2

    jeffkreeftmeijer committed Jun 24, 2019
    To jump to a different index, we'll add a convenience function called
    `jump/2`, which overwrites a `Game` struct's `:index attribute.
    
        defmodule Hayago.Game do
          # ...
    
          def jump(game, destination) do
            %{game | index: destination}
          end
        end
  6. Add :index attribute to Game

    jeffkreeftmeijer committed Jun 24, 2019
    Although we're now keeping a history of game states, all functions in
    out `Game` module still only use the first state in the list. As each
    new state is prepended to the history list, the first in the list is
    always the latest state.
    
    To allow jumping back and forward through we'll add an attribute named
    `:index` to the `Game` struct, which defaults to 0.
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          alias Hayago.{Game, State}
          defstruct history: [%State{}], index: 0
    
          # ...
        end
    
    By having an index, we can jump back to a previous state, without having
    to drop states from the front of the list. Instead, we'll update our
    `state/1` function to take the index into account
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          # ...
    
          def state(%Game{history: history, index: index}) do
            Enum.at(history, index)
          end
    
          # ...
        end
    
    Now, if we have a game with a three-state history, setting the `:index`
    attribute to 1 will return the second state in the list, essentially
    reverting back to that state.
  7. Add Game struct to maintain state history

    jeffkreeftmeijer committed Jun 19, 2019
    To allow players to undo and redo moves, the game needs to keep a
    history of the moves that are made since it started. Currently, it keeps
    one `State` struct, which is accessible as `@State` in the live view.
    
        # lib/hayago_web/templates/game/index.html.leex
        <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>
    
    Whenever a move is made by clicking one of the invisible buttons on the
    board, the `GameLive` module handles the event. It uses `State.place/2`
    to create a new state struct and assigns that as the new state to update
    the view.
    
        # lib/hayago_web/live/game_live.ex
        defmodule HayagoWeb.GameLive do
          # ...
    
          def handle_event("place", index, %{assigns: assigns} = socket) do
            new_game = Game.place(assigns.game, String.to_integer(index))
            {:noreply, assign(socket, game: new_game, state: Game.state(new_game))}
          end
        end
    
    Because of this implementation, there's currently no way to jump back in
    history, as the state is replaced for each move.
    
    To retain history, we'll add a struct named `Game`, which keeps a
    history of states in its `:history` attribute. When the game starts, it
    has a single, empty state in its history list, to represent the empty
    board.
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          alias Hayago.{Game, State}
          defstruct history: [%State{}]
    
          # ...
        end
    
    We'll add a convenience function to get the current state from a game.
    Since each newly added state is prepended to the history list, we can
    take the list's head to get the current state.
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          # ...
    
          def state(%Game{history: [state | _]}) do
            state
          end
    
          # ...
        end
    
    Finally, we'll implement a `place/2` function to the `Game` module. It
    uses `State.place/2` to create a new state struct, and prepends that to
    the history list.
    
        # lib/hayago/game.ex
        defmodule Hayago.Game do
          # ...
    
          def place(%Game{history: [state | _] = history} = game, position) do
            %{game | history: [State.place(state, position) | history]}
          end
        end
    
    Now, let's use the new game struct in the `GameLive` module. The
    `mount/2` function is used to set up the game. Instead of creating a
    state struct and assigning it directly to the socket, we'll create a new
    game struct, and assign that to the `:game` assign.
    
    The `:state` assign is taken from the game struct for convenience, to
    prevent having to call to `Game.state/1` from the view or template.
    
        # lib/hayago_web/live/game_live.ex
        defmodule HayagoWeb.GameLive do
          alias Hayago.Game
          use Phoenix.LiveView
    
          # ...
    
          def mount(_session, socket) do
            game = %Game{}
            {:ok, assign(socket, game: game, state: Game.state(game))}
          end
    
          # ...
        end
    
    When placing a stone, we'll now call the `Game.place/2` function to
    update the current state while retaining the history list. Again, we use
    `Game.state/1` on the updated game to preload the state.
    
        # lib/hayago_web/live/game_live.ex
        defmodule HayagoWeb.GameLive do
          alias Hayago.Game
          use Phoenix.LiveView
    
          # ...
    
          def handle_event("place", index, %{assigns: assigns} = socket) do
            new_game = Game.place(assigns.game, String.to_integer(index))
            {:noreply, assign(socket, game: new_game, state: Game.state(new_game))}
          end
        end
    
    If we try our game again, we'll notice nothing visible has changed.
    While we keep history of all moves, we don't currently do anything with
    the history.
  8. Show captures on index.leex

    jeffkreeftmeijer committed Jun 12, 2019
    Whenever a stone is captured, a counter is incremented in `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 to make sure the list isn't empty when looping over it.
  9. Disable illegal positions in index.html.leex

    jeffkreeftmeijer committed Jun 12, 2019
    To prevent illegal moves, we'll render a disabled button for every
    position that either has a stone on it already, or ones where a newly
    placed stone would be captured immediately because it has no liberties.
    
    To make that work, we'll use `State.legal/2`, which takes the current
    state and an index and returns wether the current player can place a
    stone there.
    
        <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'll render the same button as
    before. If not, we'll render a disabled button.
  10. Place stones on the board with State.place/2

    jeffkreeftmeijer committed Jun 11, 2019
    To place stones on the board, `State` implements a `place/2` function
    that takes a `State` struct, and an index. It will use that index to
    replace 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 a `phx-click` and `phx-value` attribute 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 hanfdle 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.
    
        # 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
  11. Add GameLive, GameView and a .leex template

    jeffkreeftmeijer committed Jun 11, 2019
    We'll start out with 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 a `render/1` and a `mount/2` callback function.
    
        # 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 is tasked with setting up the assings in the
    `socket` to set the initial state for the view. We use it to create a
    new state, and we add it to the socket assigns to make it available in
    the template.
    
    Next, we'll add a template to render the board. We'll name it
    `index.html.leex`,to make it *Live EEx* template. Although similar to
    regular EEx templates, the live EEx can track changes 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 draw 81 empty `<button>` elements in a `<div>`, 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 CSS to show the
    stone you're about to place when you hover over a position.
    
    Finally, we'll 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
  12. Update README

    jeffkreeftmeijer committed Jul 4, 2019
Commits on Jun 24, 2019
  1. Add State to describe game states

    jeffkreeftmeijer committed Jun 12, 2019
    In our game, the `State` module describes the state of the game. It
    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 new state automatically creates a list of 81 `nil` values as its
    positions, as the board is empty and has 9 by 9 positions. The current
    player is black, as the player with the black stones is the first to
    move.
    
    The `State` module exposes two functions. First is `place/2`, which
    places a new stone on the board. If the new stone steals all of another
    stone's liberties, the captured stone is removed from the board
    automatically.
    
    The `legal?/2` function checks if a move is legal by checking if the
    position is already occupied by another stone, and trying the move using
    the `place/2` function to make sure it's not immediately captured.
  2. Install Phoenix LiveView according to its README

    jeffkreeftmeijer committed Jun 11, 2019
    Phoenix doesn't come with LiveView preinstalled just yet, so we'll
    install it according to the install guide in the [LiveView
    README](https://github.com/phoenixframework/phoenix_live_view/tree/108c96aca0245fa673307f0e1a617f4b0704e981#readme).
  3. Style the board, add background SVG

    jeffkreeftmeijer committed Jun 11, 2019
    The board's markup consists of a `<div>` with a list of transperant
    `<button>` elements to represent the points on the board. The board
    itself is drawn in an SVG and used as a background image for the main
    `<div>`.
  4. Remove generated files and layout elements

    jeffkreeftmeijer committed Jun 11, 2019
    - Remove generated Page controller, view and template
    - Remove generated header from application layout
    - Remove generated phoenix.css
    - Remove generated Hayago module
  5. $ mix phx.new hayago --no-ecto

    jeffkreeftmeijer committed Jun 11, 2019
    Generate a Phoenix application. Since we're not planning on storing
    games in a database, we'll skip Ecto for now. We can always add it by
    running the generator on this directory again later.
You can’t perform that action at this time.