Skip to content

Instantly share code, notes, and snippets.

@mmmries
Last active September 16, 2015 13:18
Show Gist options
  • Save mmmries/eb34b6fe595e177f00ac to your computer and use it in GitHub Desktop.
Save mmmries/eb34b6fe595e177f00ac to your computer and use it in GitHub Desktop.
Tic Tac Toe Client Workshop

Intro

games.riesd.com hosts online games that have an open API so you can easily write a game client and have it play against an opponent. The the site is written in elixir and uses phoenix and channels.

Opponents can be:

  • A human using the web interface
  • A hosted AI
  • Some other program using a websocket

Tonight we are going to write an elixir program that opens a websocket and plays a game of tic-tac-toe.

Start the Project

We'll start the project by running mix new toelixir. This will create a skeleton mix project which we will use. cd into the project directory and run mix test to make sure that your project is working correctly. You should see one test pass. 🎉⚡💥

We will use an erlang library to handle our websockets for us. websocket_client is an excellent erlang project for making websocket connections. It hasn't been published to hex.pm yet so we will use it directly from github. Update your mix.exs file to have use this dependency as seen below.

def deps do
  [
    {:websocket_client, git: "https://github.com/sanmiguel/websocket_client.git", branch: "master"},
  ]
end

Now run mix deps.get and mix compile to download the erlang library and compile it in your project.

Translating Erlang to Elixir

The example from websocket_client's README file is in erlang. So to start our project we will copy and paste the example code into our lib/toelixir.ex file and start translating it into Elixir. This example connects to a public websocket server that just echoes back whatever you send to it.

If you get behind during this part of the workshop you can grab a copy of the finished translation from this gist. See the 02.toelixir.ex file below.

Now startup an iex session with the command iex -S mix and you can run this code like this:

iex(1)> {:ok, pid} = Toelixir.start_link
{:ok, #PID<0.127.0>}
connected! time to send the first message
Received text! "message 1" (2)
Received text! "hello, this is message 2" (3)
Received text! "hello, this is message 3" (4)
Received text! "hello, this is message 4" (5)
iex(2)>

Run The Websocket From IEX

Now that we have a proof of concept under our belts we can see what sorts of things we can do with this websocket_client library. Let's use this knowledge to write up a super basic websocket client to connect to the game server and allow us to send erlang messages that in turn get forwarded to the game server. This way we can play a game of tic tac toe from iex.

  • If you get behind during this part of the workshop you can grab a copy of the completed code from this gist. See the 03.toelixir.ex file below.*

Now we can talk to our websocket from iex. This is a good time to review the tic-tac-toe tutorial. Below you can see an example of a full game played out over iex.

iex(1)> {:ok, pid} = Toelixir.start_link
{:ok, #PID<0.127.0>}
connected! time to send the first message
iex(2)> join = %{topic: "tictactoe:1", event: "phx_join", ref: 1, payload: %{token: "me", name: "me"}}
%{event: "phx_join", payload: %{name: "me", token: "me"}, ref: 1,
  topic: "tictactoe:1"}
disconnected because {:remote, :closed}
connected! time to send the first message
iex(3)> send pid, {:send, join}
sending %{event: "phx_join", payload: %{name: "me", token: "me"}, ref: 1, topic: "tictactoe:1"}
{:send,
 %{event: "phx_join", payload: %{name: "me", token: "me"}, ref: 1,
   topic: "tictactoe:1"}}
Received text! "{\"topic\":\"tictactoe:1\",\"ref\":1,\"payload\":{\"status\":\"ok\",\"response\":{\"role\":\"O\"}},\"event\":\"phx_reply\"}" (1)
Received text! "{\"topic\":\"tictactoe:1\",\"ref\":null,\"payload\":{\"winner\":null,\"whose_turn\":\"X\",\"board\":[null,null,null,null,null,null,null,null,null]},\"event\":\"state\"}" (1)
Received text! "{\"topic\":\"tictactoe:1\",\"ref\":null,\"payload\":{\"winner\":null,\"whose_turn\":\"O\",\"board\":[null,null,null,null,null,null,null,null,\"X\"]},\"event\":\"state\"}" (1)
iex(4)> send pid, {:send, %{topic: "tictactoe:1", event: "move", ref: 2, payload: %{token: "me", square: 0}}}
sending %{event: "move", payload: %{square: 0, token: "me"}, ref: 2, topic: "tictactoe:1"}
{:send,
 %{event: "move", payload: %{square: 0, token: "me"}, ref: 2,
   topic: "tictactoe:1"}}
Received text! "{\"topic\":\"tictactoe:1\",\"ref\":null,\"payload\":{\"winner\":null,\"whose_turn\":\"X\",\"board\":[\"O\",null,null,null,null,null,null,null,\"X\"]},\"event\":\"state\"}" (1)
Received text! "{\"topic\":\"tictactoe:1\",\"ref\":null,\"payload\":{\"winner\":null,\"whose_turn\":\"O\",\"board\":[\"O\",null,\"X\",null,null,null,null,null,\"X\"]},\"event\":\"state\"}" (1)
iex(5)> send pid, {:send, %{topic: "tictactoe:1", event: "move", ref: 2, payload: %{token: "me", square: 6}}}
sending %{event: "move", payload: %{square: 6, token: "me"}, ref: 2, topic: "tictactoe:1"}
{:send,
 %{event: "move", payload: %{square: 6, token: "me"}, ref: 2,
   topic: "tictactoe:1"}}
Received text! "{\"topic\":\"tictactoe:1\",\"ref\":null,\"payload\":{\"winner\":null,\"whose_turn\":\"X\",\"board\":[\"O\",null,\"X\",null,null,null,\"O\",null,\"X\"]},\"event\":\"state\"}" (1)
Received text! "{\"topic\":\"tictactoe:1\",\"ref\":null,\"payload\":{\"winner\":\"X\",\"whose_turn\":null,\"board\":[\"O\",null,\"X\",null,null,\"X\",\"O\",null,\"X\"]},\"event\":\"game_over\"}" (1)
iex(6)>

Automating The Gameplay

Next step is to automate the sending of the messages. We'll leave the hardcoded token, topic and name for now. And we'll just focus on triggering the different behaviors at the right time.

  • If you get behind during this part of the workshop you can grab a copy of the completed code from this gist. See the 04.toelixir.ex file below.*

Go Make It Your Own

From here you should be able to improve the game client on your own. Here are a few suggestions:

  • Pass in topic, token, name and ai as arguments to the start_link function
  • Improve the strategy for picking which square to play
  • Write a mix task so we can kick off the game directly from the console

I'll leave a few browser windows open on the projector watching games utex1, utex2...

Feel free to join those games and do battle against the other attendees.

# Translated Erlang example to Elixir
defmodule Toelixir do
@behaviour :websocket_client
def start_link do
:crypto.start()
:ssl.start()
:websocket_client.start_link('wss://echo.websocket.org', __MODULE__, [])
end
def init([]), do: {:once, 2}
def onconnect(_wsreq, state) do
IO.puts "connected!"
:websocket_client.cast(self, {:text, "message 1"})
{:ok, state}
end
def ondisconnect({:remote, :closed}, state) do
IO.puts "disconnected by the remote"
{:reconnect, state}
end
def websocket_handle({:pong, _}, _conn, state) do
IO.puts "received PONG"
{:ok, state}
end
def websocket_handle({:text, msg}, _conn, 5) do
IO.puts "Received msg #{msg}"
{:close, "", 'done'}
end
def websocket_handle({:text, msg}, _conn, state) do
IO.puts "Received msg #{msg}"
:timer.sleep(1000)
{:reply, {:text, "hello, this is message ##{state}"}, state + 1}
end
def websocket_info(:start, _conn, state) do
IO.puts "recevied erlang message"
{:reply, {:text, "erlang message received"}, state}
end
def websocket_terminate(reason, _conn, state) do
IO.puts "Websocket closed in state #{inspect state} wih reason #{inspect state}"
:ok
end
end
# A websocket process that can be controlled from an IEX session
defmodule Toelixir do
@behaviour :websocket_client
def start_link do
:crypto.start()
:ssl.start()
:websocket_client.start_link('ws://games.riesd.com/socket/websocket?vsn=1.0.0', __MODULE__, [])
end
# Callbacks
def init([]), do: {:once, 1}
def onconnect(_wsreq, state) do
IO.puts "connected!"
{:ok, state}
end
def ondisconnect(reason, state) do
IO.puts "disconnected because #{inspect reason}"
{:reconnect, state}
end
def websocket_handle({:text, msg}, _conn, state) do
IO.puts "Received text! #{inspect msg} (#{state})"
{:ok, state}
end
def websocket_info({:send, msg}, _connstate, state) do
IO.puts "sending #{inspect msg}"
{:reply, {:text, Poison.encode!(msg)}, state}
end
def websocket_terminate(reason, _connstate, state) do
IO.puts "Websocket closed #{inspect reason}"
IO.inspect state
:ok
end
end
# A random automtated player
defmodule Toelixir do
@behaviour :websocket_client
def start_link do
start_link('ws://games.riesd.com/socket/websocket?vsn=1.0.0')
end
def start_link(uri) do
:crypto.start()
:ssl.start()
:websocket_client.start_link(uri, __MODULE__, [])
end
# Callbacks
def init([]) do
:random.seed(:erlang.timestamp())
{:once, %{role: nil, token: "me", name: "me", topic: "tictactoe:1"}}
end
def onconnect(_wsreq, state) do
IO.puts "connected! time to send the first message"
send_join_request(state)
{:ok, state}
end
def ondisconnect(reason, state) do
IO.puts "disconnected because #{inspect reason}"
{:reconnect, state}
end
def websocket_handle({:text, msg}, _conn, state) do
IO.puts "Recevied: #{msg}"
IO.puts "State: #{inspect state}"
msg = Poison.decode!(msg)
case msg do
%{"event" => "phx_reply", "payload" => %{"status" => "ok", "response" => %{"role" => assigned_role}}} ->
{:ok, %{state | role: assigned_role}}
%{"event" => "state", "payload" => payload} ->
make_a_move(state, payload)
{:ok, state}
%{"event" => "game_over", "payload" => payload} ->
IO.puts "the game is over man!"
IO.inspect(payload)
{:close, "game_over", state}
_ ->
{:ok, state}
end
end
def websocket_info({:send, msg}, _connstate, state) do
msg = Poison.encode!(msg)
IO.puts "sending: #{msg}"
{:reply, {:text, msg}, state}
end
def websocket_terminate(reason, _connstate, state) do
IO.puts "Websocket closed #{inspect reason}"
IO.inspect state
:ok
end
# Private Methods
defp make_a_move(%{token: token, topic: topic}, %{"board" => board}) do
square = pick_square_to_play(board)
msg = %{topic: topic, event: "move", ref: 2, payload: %{token: token, square: square}}
send self, {:send, msg}
end
defp send_join_request(%{token: token, name: name, topic: topic}) do
msg = %{topic: topic, event: "phx_join", ref: 1, payload: %{token: token, name: name}}
send self, {:send, msg}
end
def pick_square_to_play(board) do
playable_squares(board) |> Enum.shuffle |> List.first
end
def playable_squares(board) do
Enum.with_index(board)
|> Enum.filter(fn({nil, _idx}) -> true
(_) -> false end)
|> Enum.map fn({nil,idx}) -> idx end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment