Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

928a8437ce8b4c0545c9b0c4284f16bf?s=128

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

  2. Ventsislav Nikolov @ventsislaf

  3. None
  4. Agenda • OTP app • Phoenix (web interface, real-time) •

    Demo • Observer demo
  5. None
  6. None
  7. Processes

  8. msg 1 msg 2 msg 3 mailbox

  9. OTP (Open Telecom Platform)

  10. Supervision Tree

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

  12. <code>

  13. $ mix new tic_tac_toe --sup

  14. $ vim

  15. # mix.exs def application do [ applications: [:logger, :gproc], mod:

    {TicTacToe, []} ] end defp deps do [ {:gproc, "0.5.0"} ] end
  16. $ mix deps.get

  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
  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
  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)
  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
  21. None
  22. Events (Messages)

  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
  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 )
  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
  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
  27. # lib/tic_tac_toe/game.ex defp empty?(%{x: nil, o: nil}) do true end

    defp empty?(%{x: _, o: _}) do false end
  28. Play (Put Symbol) # lib/tic_tac_toe/game.ex def handle_call({:put, _, _}, _,

    %{finished: true} = state) do {:reply, :finished, state} end
  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
  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
  31. # lib/tic_tac_toe/game.ex def handle_call({:put, _symbol, _pos}, _form, state) do {:reply,

    :cheat, state} end
  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
  33. Game Board

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

    -> val end) end
  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
  38. None
  39. [info] Sent 200 in 260µs (on my laptop in development

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

  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
  42. Router

  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
  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
  45. Controllers

  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
  47. Views

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

  49. Templates

  50. # web/templates/new.html.eex <div class="form"> <%= form_for @conn, player_path(@conn, :create), [as:

    :player], fn(f) -> %> <div class="input-group"> <%= text_input f, :name, placeholder: "Enter your name", autofocus: true, class: "form-control" %> <span class="input-group-btn"> <%= submit "Enter", class: "btn btn-primary" %> </span> </div> <% end %> </div>
  51. Sockets

  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
  53. Channels

  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) }
  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 })
  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
  57. # web/channels/game_socket.ex def handle_info({:after_join, game_state}, socket) do broadcast! socket, "new_player",

    game_state {:noreply, socket} end
  58. # web/static/game.js gameChannel.on("new_player", (resp) => { if (resp.x && resp.o)

    { // show game } else { // show waiting for player message } })
  59. # web/static/game.js game.addEventListener("click", (e) => { e.preventDefault() let index =

    e.target.getAttribute("data-index") gameChannel.push("put", {index: index}) })
  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
  61. Demo

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

  63. THANK YOU