Share
Embed
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
Transcript
63 slides
-
Agenda • OTP app • Phoenix (web interface, real-time) •
Demo • Observer demo -
# mix.exs def application do [ applications: [:logger, :gproc], mod:
{TicTacToe, []} ] end defp deps do [ {:gproc, "0.5.0"} ] end -
# 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 -
# 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 -
# 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) -
# 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 -
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 -
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 ) -
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 -
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 -
# lib/tic_tac_toe/game.ex defp empty?(%{x: nil, o: nil}) do true end
defp empty?(%{x: _, o: _}) do false end -
Play (Put Symbol) # lib/tic_tac_toe/game.ex def handle_call({:put, _, _}, _,
%{finished: true} = state) do {:reply, :finished, state} end -
# 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 -
# 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 -
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 -
# 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 -
# 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 -
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 -
Add TicTacToe dependency # mix.exs def application do [ applications:
[ # ... :tic_tac_toe ] ] end defp deps do [ # ... {:tic_tac_toe, path: "../tic_tac_toe"} ] end -
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 -
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 -
# 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 -
# 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> -
# 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 -
# 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) } -
# 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 }) -
# 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 -
# web/channels/game_socket.ex def handle_info({:after_join, game_state}, socket) do broadcast! socket, "new_player",
game_state {:noreply, socket} end -
# web/static/game.js gameChannel.on("new_player", (resp) => { if (resp.x && resp.o)
{ // show game } else { // show waiting for player message } }) -
# web/static/game.js game.addEventListener("click", (e) => { e.preventDefault() let index =
e.target.getAttribute("data-index") gameChannel.push("put", {index: index}) }) -
# 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