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.

Ventsislav Nikolov

August 15, 2016
Tweet

More Decks by Ventsislav Nikolov

Other Decks in Programming

Transcript

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

    {TicTacToe, []} ] end defp deps do [ {:gproc, "0.5.0"} ] end
  2. # 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
  3. # 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
  4. # 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)
  5. # 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
  6. 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
  7. 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 )
  8. 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
  9. 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
  10. Play (Put Symbol) # lib/tic_tac_toe/game.ex def handle_call({:put, _, _}, _,

    %{finished: true} = state) do {:reply, :finished, state} end
  11. # 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
  12. # 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
  13. 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
  14. # 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
  15. # 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
  16. 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
  17. Add TicTacToe dependency # mix.exs def application do [ applications:

    [ # ... :tic_tac_toe ] ] end defp deps do [ # ... {:tic_tac_toe, path: "../tic_tac_toe"} ] end
  18. 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
  19. 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
  20. # 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
  21. # 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>
  22. # 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
  23. # 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) }
  24. # 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 })
  25. # 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
  26. # web/static/game.js gameChannel.on("new_player", (resp) => { if (resp.x && resp.o)

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

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