356 lines
7.9 KiB
Elixir
356 lines
7.9 KiB
Elixir
|
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
|