$30 off During Our Annual Pro Sale. View Details »

Building Multiplayer Real-Time Game with Elixir and Phoenix

Building Multiplayer Real-Time Game with Elixir and Phoenix

Showing how to build multiplayer tic-tac-toe game step by step.

Ventsislav Nikolov

August 15, 2016
Tweet

More Decks by Ventsislav Nikolov

Other Decks in Programming

Transcript

  1. Building Multiplayer Real-Time
    Game with Elixir and Phoenix
    VarnaConf 2016

    View Slide

  2. Ventsislav Nikolov
    @ventsislaf

    View Slide

  3. View Slide

  4. Agenda
    • OTP app
    • Phoenix (web interface, real-time)
    • Demo
    • Observer demo

    View Slide

  5. View Slide

  6. View Slide

  7. Processes

    View Slide

  8. msg 1
    msg 2
    msg 3
    mailbox

    View Slide

  9. OTP
    (Open Telecom Platform)

    View Slide

  10. Supervision Tree

    View Slide

  11. Supervisor
    (:one_for_one)
    GameSupervisor
    (:simple_one_for_one)
    Registry
    Game Game

    View Slide


  12. View Slide

  13. $ mix new tic_tac_toe --sup

    View Slide

  14. $ vim

    View Slide

  15. # mix.exs
    def application do
    [
    applications: [:logger, :gproc],
    mod: {TicTacToe, []}
    ]
    end
    defp deps do
    [
    {:gproc, "0.5.0"}
    ]
    end

    View Slide

  16. $ mix deps.get

    View Slide

  17. # lib/tic_tac_toe/game_supervisor.ex
    defmodule TicTacToe.GameSupervisor do
    use Supervisor
    def start_link do
    Supervisor.start_link(__MODULE__, nil, name: __MODULE__)
    end
    def start_child(name) do
    Supervisor.start_child(__MODULE__, [name])
    end
    def init(_) do
    children = [
    worker(TicTacToe.Game, [], restart: :temporary)
    ]
    supervise(children, strategy: :simple_one_for_one)
    end
    end

    View Slide

  18. # lib/tic_tac_toe/registry.ex
    defmodule TicTacToe.Registry do
    use GenServer
    def start_link do
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)
    end
    def init(_) do
    {:ok, nil}
    end
    end

    View Slide

  19. # lib/tic_tac_toe/registry.ex
    def game_process(name) do
    case TicTacToe.Game.whereis(name) do
    :undefined ->
    GenServer.call(__MODULE__, {:game_process, name})
    pid ->
    pid
    end
    end
    def handle_call({:game_process, name}, _from, state) do
    game_pid = case TicTacToe.Game.whereis(name) do
    :undefined ->
    {:ok, pid} = TicTacToe.GameSupervisor.start_child(name)
    pid
    pid ->
    pid
    end
    API (client process)
    Callback (server process)

    View Slide

  20. # lib/tic_tac_toe.ex
    defmodule TicTacToe do
    use Application
    def start(_type, _args) do
    import Supervisor.Spec, warn: false
    children = [
    supervisor(TicTacToe.GameSupervisor, []),
    worker(TicTacToe.Registry, [])
    ]
    opts = [strategy: :one_for_one, name: TicTacToe.Supervisor]
    Supervisor.start_link(children, opts)
    end
    end

    View Slide

  21. View Slide

  22. Events (Messages)

    View Slide

  23. Start Game
    # lib/tic_tac_toe/game.ex
    defmodule TicTacToe.Game do
    use GenServer
    def start_link(name) do
    GenServer.start_link(__MODULE__, nil, name: via_tuple(name))
    end
    end

    View Slide

  24. Game State
    # lib/tic_tac_toe/game.ex
    @initial_score %{x: 0, ties: 0, o: 0}
    defstruct(
    board: %TicTacToe.Board{},
    x: nil,
    o: nil,
    first: :x,
    next: :x,
    score: @initial_score,
    finished: false
    )

    View Slide

  25. Join Game
    # lib/tic_tac_toe/game.ex
    def handle_call({:join, player}, _form, %{x: nil} = state) do
    new_state = %{state | x: player}
    {:reply, {:ok, :x, new_state}, new_state}
    end
    def handle_call({:join, player}, _form, %{o: nil} = state) do
    new_state = %{state | o: player}
    {:reply, {:ok, :o, new_state}, new_state}
    end
    def handle_call({:join, _player}, _from, state) do
    {:reply, :error, state}
    end

    View Slide

  26. Leave Game
    # lib/tic_tac_toe/game.ex
    def handle_call({:leave, symbol}, _from, state) do
    new_state =
    state
    |> remove_player(symbol)
    |> reset_score()
    |> reset_board()
    if empty?(new_state) do
    {:stop, :normal, new_state}
    else
    {:reply, new_state, new_state}
    end
    end

    View Slide

  27. # lib/tic_tac_toe/game.ex
    defp empty?(%{x: nil, o: nil}) do
    true
    end
    defp empty?(%{x: _, o: _}) do
    false
    end

    View Slide

  28. Play (Put Symbol)
    # lib/tic_tac_toe/game.ex
    def handle_call({:put, _, _}, _, %{finished: true} = state) do
    {:reply, :finished, state}
    end

    View Slide

  29. # lib/tic_tac_toe/game.ex
    def handle_call({:put, symbol, position}, _, %{next: symbol} =
    state) do
    case TicTacToe.Board.put(state.board, symbol, position) do
    {:ok, board} ->
    state = %{state | board: board}
    cond do
    winner = TicTacToe.Board.winner(board) ->
    new_state = finish_game(state, winner)
    {:reply, {:winner, winner, new_state}, new_state}
    TicTacToe.Board.full?(board) ->
    new_state = finish_game(state, :ties)
    {:reply, {:draw, new_state}, new_state}
    true ->
    new_state = next_turn(state)
    {:reply, {:ok, new_state}, new_state}
    end
    :error ->
    {:reply, :retry, state}
    end
    end

    View Slide

  30. # lib/tic_tac_toe/game.ex
    defp finish_game(state, symbol) do
    score = Map.update!(state.score, symbol, &(&1 + 1))
    %{state | score: score, finished: true}
    end
    defp next_turn(%{next: :x} = state) do
    %{state | next: :o}
    end
    defp next_turn(%{next: :o} = state) do
    %{state | next: :x}
    end

    View Slide

  31. # lib/tic_tac_toe/game.ex
    def handle_call({:put, _symbol, _pos}, _form, state) do
    {:reply, :cheat, state}
    end

    View Slide

  32. New Round
    # lib/tic_tac_toe/game.ex
    def handle_call(:new_round, _from, state) do
    new_state =
    %{state | board: %TicTacToe.Board{}, finished: false}
    |> next_round()
    {:reply, new_state, new_state}
    end
    defp next_round(%{first: :x} = state) do
    %{state | first: :o, next: :o}
    end
    defp next_round(%{first: :o} = state) do
    %{state | first: :x, next: :x}
    end

    View Slide

  33. Game Board

    View Slide

  34. # lib/tic_tac_toe/board.ex
    defmodule TicTacToe.Board do
    defstruct data: [nil, nil, nil,
    nil, nil, nil,
    nil, nil, nil]
    @symbols [:x, :o]
    end
    Board Data

    View Slide

  35. # lib/tic_tac_toe/board.ex
    def put(board, symbol, pos) when symbol in @symbols do
    case Enum.at(board.data, pos) do
    nil ->
    data = List.replace_at(board.data, pos, symbol)
    {:ok, %TicTacToe.Board{board | data: data}}
    _ ->
    :error
    end
    end
    def put(_board, _symbol, _pos) do
    :error
    end
    Put Symbol

    View Slide

  36. Full Board?
    # lib/tic_tac_toe/board.ex
    def full?(%TicTacToe.Board{data: data}) do
    Enum.all?(data, fn(val) -> val end)
    end

    View Slide

  37. Winner?
    # lib/tic_tac_toe/board.ex
    def winner(%TicTacToe.Board{data: data}) do
    do_winner(data)
    end
    defp do_winner([
    s, s, s,
    _, _, _,
    _, _, _
    ]) when s in @symbols, do: s
    # ...
    defp do_winner(_), do: nil

    View Slide

  38. View Slide

  39. [info] Sent 200 in 260µs
    (on my laptop in development mode)

    View Slide

  40. $ mix phoenix.new demo --no-ecto

    View Slide

  41. Add TicTacToe dependency
    # mix.exs
    def application do
    [
    applications: [
    # ...
    :tic_tac_toe
    ]
    ]
    end
    defp deps do
    [
    # ...
    {:tic_tac_toe, path: "../tic_tac_toe"}
    ]
    end

    View Slide

  42. Router

    View Slide

  43. Plug & Pipelines
    # web/router.ex
    pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug Demo.Auth
    end

    View Slide

  44. Scopes
    # web/router.ex
    scope "/", Demo do
    pipe_through :browser
    get "/", PlayerController, :new
    resources "/players", PlayerController,
    only: [:create, :delete]
    end
    scope "/", Demo do
    pipe_through [:browser, :authenticate_player]
    resources "/games", GameController,
    only: [:new, :create, :show], param: "name"
    end

    View Slide

  45. Controllers

    View Slide

  46. # web/controllers/player_controller.ex
    defmodule Demo.PlayerController do
    use Demo.Web, :controller
    plug :scrub_params, "player" when action in [:create]
    def new(conn, _params) do
    render conn, "new.html"
    end
    def create(conn, %{"player" => player_params}) do
    player = Map.get(player_params, "name", "Anonymous")
    conn
    |> Demo.Auth.login(player)
    |> redirect(to: game_path(conn, :new))
    end
    def delete(conn, _params) do
    conn
    |> Demo.Auth.logout()
    |> redirect(to: player_path(conn, :new))
    end
    end

    View Slide

  47. Views

    View Slide

  48. # web/views/player_view.ex
    defmodule Demo.PlayerView do
    use Demo.Web, :view
    end

    View Slide

  49. Templates

    View Slide

  50. # web/templates/new.html.eex

    <%= form_for @conn, player_path(@conn, :create),
    [as: :player], fn(f) -> %>

    <%= text_input f, :name, placeholder: "Enter your name",
    autofocus: true, class: "form-control" %>

    <%= submit "Enter", class: "btn btn-primary" %>


    <% end %>

    View Slide

  51. Sockets

    View Slide

  52. # web/channels/user_socket.ex
    defmodule Demo.UserSocket do
    use Phoenix.Socket
    channel "game:*", Demo.GameChannel
    def connect(%{"player" => player}, socket) do
    {:ok, assign(socket, :player, player)}
    end
    end

    View Slide

  53. Channels

    View Slide

  54. # web/static/app.js
    import {Socket} from "phoenix"
    import Game from "./game"
    let socket = new Socket(
    "/socket",
    {params: {player: window.currentPlayer}}
    )
    let element = document.getElementById("game")
    if (element) {
    Game.init(socket)
    }

    View Slide

  55. # web/static/game.js
    socket.connect()
    let gameName = game.getAttribute("data-name")
    let gameChannel = socket.channel("game:" + gameName)
    gameChannel.params.player = window.currentPlayer
    gameChannel.join()
    .receive("ok", resp => {
    // noop
    })
    .receive("error", reason => {
    // show game is full message
    })

    View Slide

  56. # web/channels/game_socket.ex
    defmodule Demo.GameChannel do
    use Phoenix.Channel
    def join("game:" <> name, _params, socket) do
    game = TicTacToe.Registry.game_process(name)
    case TicTacToe.Game.join(game, socket.assigns.player) do
    {:ok, symbol, game_state} ->
    send self(), {:after_join, game_state}
    socket =
    socket
    |> assign(:game, name)
    |> assign(:symbol, symbol)
    {:ok, game_state, socket}
    :error ->
    {:error, %{reason: "full game"}}
    end
    end
    end

    View Slide

  57. # web/channels/game_socket.ex
    def handle_info({:after_join, game_state}, socket) do
    broadcast! socket, "new_player", game_state
    {:noreply, socket}
    end

    View Slide

  58. # web/static/game.js
    gameChannel.on("new_player", (resp) => {
    if (resp.x && resp.o) {
    // show game
    } else {
    // show waiting for player message
    }
    })

    View Slide

  59. # web/static/game.js
    game.addEventListener("click", (e) => {
    e.preventDefault()
    let index = e.target.getAttribute("data-index")
    gameChannel.push("put", {index: index})
    })

    View Slide

  60. # web/channels/game_channel.ex
    def handle_in("put", %{"index" => index}, socket) do
    game = TicTacToe.Registry.game_process(socket.assigns.game)
    case TicTacToe.Game.put(game, socket.assigns.symbol,
    String.to_integer(index)) do
    {:ok, game_state} ->
    broadcast! socket, "update_board", game_state
    {:draw, game_state} ->
    broadcast! socket, "finish_game", game_state
    {:winner, _symbol, game_state} ->
    broadcast! socket, "finish_game", game_state
    _ ->
    :ok
    end
    {:noreply, socket}
    end

    View Slide

  61. Demo

    View Slide

  62. https://github.com/ventsislaf/talks/tree/master/
    multiplayer_tictactoe

    View Slide

  63. THANK YOU

    View Slide