summaryrefslogtreecommitdiff
path: root/presentation/tic_tac_toe.exs
diff options
context:
space:
mode:
Diffstat (limited to 'presentation/tic_tac_toe.exs')
-rw-r--r--presentation/tic_tac_toe.exs355
1 files changed, 355 insertions, 0 deletions
diff --git a/presentation/tic_tac_toe.exs b/presentation/tic_tac_toe.exs
new file mode 100644
index 0000000..e05bed4
--- /dev/null
+++ b/presentation/tic_tac_toe.exs
@@ -0,0 +1,355 @@
+defmodule Generator do
+ def gen_reference() do
+ min = String.to_integer("100000", 36)
+ max = String.to_integer("ZZZZZZ", 36)
+
+ max
+ |> Kernel.-(min)
+ |> :rand.uniform()
+ |> Kernel.+(min)
+ |> Integer.to_string(36)
+ end
+end
+
+defmodule TicTacToe.GameManager do
+ use GenServer
+
+ defmodule State do
+ defstruct games: %{},
+ joinable_games: [],
+ player_games: %{}
+ end
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, %{
+ pid: nil
+ })
+ end
+
+ def init(_) do
+ {:ok, %State{}}
+ end
+
+ defp create_board(), do: Enum.map(0..2, fn _ -> Enum.map(0..2, fn _ -> :empty end) end)
+
+ defp create_game(game_id, player) do
+ %{
+ x: player,
+ o: nil,
+ board: create_board()
+ }
+ end
+
+ def handle_info(
+ {:join, %{client_pid: client_pid, username: username, player_id: connection_id} = player},
+ %State{player_games: player_games, games: games, joinable_games: joinable_games} = state
+ ) do
+ if length(joinable_games) == 0 do
+ game_id = Generator.gen_reference()
+ send(client_pid, {:join_game, game_id})
+
+ {:ok,
+ %State{
+ state
+ | games: Map.put(games, game_id, create_game(game_id, player)),
+ joinable_games: joinable_games ++ [game_id],
+ player_games: Map.put(player_games, player_id, game_id)
+ }}
+ else
+ [joining_game_id | rest] = joinable_games
+ game = Map.get(games, joining_game_id)
+ send(game.x.client_pid, :player_joined)
+ send(client_pid, {:join_game, game_id})
+
+ {:ok,
+ %State{
+ state
+ | games: Map.put(games, game_id, %{game | o: player}),
+ joinable_games: rest,
+ connection_games: Map.put(player_games, connection_id, game_id)
+ }}
+ end
+ end
+end
+
+defmodule TicTacToe.SSHDaemon do
+ @port 4000
+ @key_dir "/tmp/keys"
+ use GenServer
+ require Logger
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, %{
+ pid: nil
+ })
+ end
+
+ def init(state) do
+ send(self(), :start)
+
+ {:ok, state}
+ end
+
+ def handle_info(:start, state) do
+ game_manager_pid =
+ case GenServer.start_link(TicTacToe.GameManager, [%{}]) do
+ {:ok, game_manager_pid} ->
+ game_manager_pid
+
+ _ ->
+ nil
+ end
+
+ case :ssh.daemon(
+ @port,
+ system_dir: @key_dir,
+ ssh_cli:
+ {TicTacToe.SSHListener,
+ [
+ %TicTacToe.SSHListener.State{
+ game_manager_pid: game_manager_pid
+ }
+ ]},
+ disconnectfun: &on_disconnect/1,
+ id_string: :random,
+ parallel_login: true,
+ max_sessions: 1_000,
+ subsystems: [],
+ no_auth_needed: true
+ ) do
+ {:ok, pid} ->
+ Logger.info("SSH server started on port #{port}, on #{inspect(pid)}")
+
+ Process.link(pid)
+
+ {:noreply, %{state | pid: pid, game_manager_pid: game_manager_pid}, :hibernate}
+
+ {:error, err} ->
+ raise inspect(err)
+ end
+
+ {:noreply, state}
+ end
+
+ def handle_info(_, state), do: {:noreply, state}
+
+ defp on_disconnect(_reason) do
+ Logger.info("#{inspect(self())} disconnected")
+ end
+end
+
+defmodule TicTacToe.SSHListener do
+ alias Chessh.SSH.Client
+
+ alias IO.ANSI
+
+ require Logger
+
+ @behaviour :ssh_server_channel
+ @session_closed_message [
+ ANSI.clear(),
+ ["This session has been closed"]
+ ]
+
+ defmodule State do
+ defstruct channel_id: nil,
+ client_pid: nil,
+ game_manager_pid: nil,
+ connection_ref: nil
+ end
+
+ def init([%State{} = init_state]) do
+ {:ok, init_state}
+ end
+
+ def handle_msg({:ssh_channel_up, channel_id, connection_ref}, %State{} = state) do
+ Logger.debug("SSH channel up #{inspect(:ssh.connection_info(connection_ref))}")
+
+ username =
+ :ssh.connection_info(connection_ref)
+ |> Keyword.fetch!(:user)
+ |> String.Chars.to_string()
+
+ {:ok,
+ %State{
+ state
+ | channel_id: channel_id,
+ connection_ref: connection_ref,
+ player: %{
+ id: Generator.gen_reference(),
+ username: username
+ }
+ }}
+ end
+
+ def handle_msg(
+ {:EXIT, client_pid, _reason},
+ %State{client_pid: client_pid, channel_id: channel_id} = state
+ ) do
+ send(client_pid, :quit)
+ {:stop, channel_id, state}
+ end
+
+ def handle_msg(
+ {:send_data, data},
+ %State{connection_ref: connection_ref, channel_id: channel_id} = state
+ ) do
+ :ssh_connection.send(connection_ref, channel_id, data)
+ {:ok, state}
+ end
+
+ def handle_msg(
+ :session_closed,
+ %State{connection_ref: connection_ref, channel_id: channel_id} = state
+ ) do
+ :ssh_connection.send(connection_ref, channel_id, @session_closed_message)
+ {:stop, channel_id, state}
+ end
+
+ def handle_msg(msg, term) do
+ Logger.debug("Unknown msg #{inspect(msg)}, #{inspect(term)}")
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, _connection_handler, {:data, _channel_id, _type, data}},
+ %State{client_pid: client_pid} = state
+ ) do
+ send(client_pid, {:data, data})
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, connection_handler,
+ {:pty, channel_id, want_reply?, {_term, _width, _height, _pixwidth, _pixheight, _opts}}},
+ %State{} = state
+ ) do
+ Logger.debug("#{inspect(state.player_session)} has requested a PTY")
+ :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, connection_handler, {:env, channel_id, want_reply?, var, value}},
+ state
+ ) do
+ :ssh_connection.reply_request(connection_handler, want_reply?, :failure, channel_id)
+
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, _connection_handler,
+ {:window_change, _channel_id, _width, _height, _pixwidth, _pixheight}},
+ %State{client_pid: client_pid} = state
+ ) do
+ {:ok, state}
+ end
+
+ def handle_ssh_msg(
+ {:ssh_cm, connection_handler, {:shell, channel_id, want_reply?}},
+ %State{player: player} = state
+ ) do
+ :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
+
+ {:ok, client_pid} =
+ GenServer.start_link(Client, [
+ %Client.State{
+ tui_pid: self(),
+ player: player
+ }
+ ])
+
+ send(client_pid, :refresh)
+ {:ok, %State{state | client_pid: client_pid}}
+ end
+
+ def handle_ssh_msg(
+ msg,
+ %State{channel_id: channel_id} = state
+ ) do
+ Logger.debug("UNKOWN MESSAGE #{inspect(msg)}")
+ # {:stop, channel_id, state}
+ {:ok, state}
+ end
+
+ def terminate(_reason, _state) do
+ :ok
+ end
+end
+
+defmodule TicTacToe.Client do
+ alias IO.ANSI
+ use GenServer
+
+ @clear_codes [
+ ANSI.clear(),
+ ANSI.home()
+ ]
+
+ defmodule State do
+ defstruct tui_pid: nil,
+ game_manager_pid: nil,
+ player: %{},
+ game_id: nil
+ end
+
+ @impl true
+ def init([%State{game_manager_pid: game_manager_pid, player: player} = state]) do
+ player = %{
+ player
+ | client_pid: self()
+ }
+
+ send(game_manager_pid, {:join, player})
+
+ {:ok,
+ %State{
+ player: player
+ }}
+ end
+
+ @impl true
+ def handle_info(:quit, %State{} = state) do
+ {:stop, :normal, state}
+ end
+
+ @impl true
+ def handle_info({:join_game, game_id}, %State{} = state) do
+ state = %State{state | game_id: game_id}
+ render(state)
+ {:stop, :normal, state}
+ end
+
+ def handle(
+ {:data, data},
+ %State{} = state
+ ) do
+ case keymap(data) do
+ :quit ->
+ {:stop, :normal, state}
+ end
+ end
+
+ def handle(
+ :player_joined,
+ %State{} = state
+ ) do
+ render(state)
+ {:noreply, state}
+ end
+
+ defp render(%State{
+ tui_pid: tui_pid
+ }) do
+ send(tui_pid, {:send_data, ["Testing"]})
+ end
+
+ def keymap(key) do
+ case key do
+ # Exit keys - C-c and C-d
+ <<3>> -> :quit
+ <<4>> -> :quit
+ x -> x
+ end
+ end
+end