diff --git a/config/config.exs b/config/config.exs index a12949b..2e20647 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,13 +8,14 @@ config :chessh, ecto_repos: [Chessh.Repo], key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"), port: 42_069, - max_sessions: 255 + max_sessions: 255, + ascii_chars_json_file: Path.join(Path.dirname(__DIR__), "priv/ascii_chars.json") config :chessh, RateLimits, jail_timeout_ms: 5 * 60 * 1000, jail_attempt_threshold: 15, max_concurrent_user_sessions: 5, - player_session_message_burst_ms: 3_000, - player_session_message_burst_rate: 15 + player_session_message_burst_ms: 1_000, + player_session_message_burst_rate: 8 import_config "#{config_env()}.exs" diff --git a/lib/chessh/ssh/client.ex b/lib/chessh/ssh/client.ex index 4eceb38..dfa222c 100644 --- a/lib/chessh/ssh/client.ex +++ b/lib/chessh/ssh/client.ex @@ -1,6 +1,6 @@ defmodule Chessh.SSH.Client do alias IO.ANSI - + alias Chessh.SSH.Client.Menu require Logger use GenServer @@ -26,7 +26,7 @@ defmodule Chessh.SSH.Client do height: 0, player_session: nil, buffer: [], - state_stack: [{&Chessh.SSH.Client.Menu.render/2, []}] + state_stack: [{Menu, %Menu.State{}}] end @impl true @@ -61,13 +61,21 @@ defmodule Chessh.SSH.Client do end end - def handle({:data, data}, %State{tui_pid: tui_pid} = state) do - new_state = - keymap(data) - |> keypress(state) + def handle( + {:data, data}, + %State{tui_pid: tui_pid, state_stack: [{module, _screen_state} | _tail]} = state + ) do + action = keymap(data) - send(tui_pid, {:send_data, render(new_state)}) - {:noreply, new_state} + if action == :quit do + {:stop, :normal, state} + else + new_state = module.handle_input(action, state) + + send(tui_pid, {:send_data, render(new_state)}) + + {:noreply, new_state} + end end def handle({:resize, {width, height}}, %State{tui_pid: tui_pid} = state) do @@ -80,18 +88,6 @@ defmodule Chessh.SSH.Client do {:noreply, new_state} end - def keypress(:up, state), do: state - def keypress(:right, state), do: state - def keypress(:down, state), do: state - def keypress(:left, state), do: state - - def keypress(:quit, state) do - send(self(), :quit) - state - end - - def keypress(_, state), do: state - def keymap(key) do case key do # Exit keys - C-c and C-d @@ -106,17 +102,18 @@ defmodule Chessh.SSH.Client do end end - @spec terminal_size_allowed(any, any) :: boolean def terminal_size_allowed(width, height) do Enum.member?(@min_terminal_width..@max_terminal_width, width) && Enum.member?(@min_terminal_height..@max_terminal_height, height) end - defp render(%{width: width, height: height, state_stack: [{render_fn, args} | _tail]} = state) do + defp render( + %State{width: width, height: height, state_stack: [{module, _screen_state}]} = state + ) do if terminal_size_allowed(width, height) do [ @clear_codes ++ - render_fn.(state, args) + module.render(state) ] else @terminal_bad_dim_msg diff --git a/lib/chessh/ssh/renderers/menu.ex b/lib/chessh/ssh/renderers/menu.ex deleted file mode 100644 index c3c3646..0000000 --- a/lib/chessh/ssh/renderers/menu.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule Chessh.SSH.Client.Menu do - alias Chessh.SSH.Client.State - alias Chessh.Utils - - alias IO.ANSI - - @logo " Simponic's - - dP MP\"\"\"\"\"\"`MM MP\"\"\"\"\"\"`MM M\"\"MMMMM\"\"MM - 88 M mmmmm..M M mmmmm..M M MMMMM MM -.d8888b. 88d888b. .d8888b. M. `YM M. `YM M `M -88' `\"\" 88' `88 88ooood8 MMMMMMM. M MMMMMMM. M M MMMMM MM -88. ... 88 88 88. ... M. .MMM' M M. .MMM' M M MMMMM MM -`88888P' dP dP `88888P' Mb. .dM Mb. .dM M MMMMM MM - MMMMMMMMMMM MMMMMMMMMMM MMMMMMMMMMMM" - - def render( - %State{width: width, height: height, state_stack: [_current_state | _tail]} = _state, - _args - ) do - {logo_width, logo_height} = Utils.text_dim(@logo) - - split = String.split(@logo, "\n") - - Enum.flat_map( - Enum.zip(0..(length(split) - 1), split), - fn {i, x} -> - [ - ANSI.cursor(div(height - logo_height, 2) + i, div(width - logo_width, 2)), - "#{x}\n" - ] - end - ) - end -end diff --git a/lib/chessh/ssh/screens/board.ex b/lib/chessh/ssh/screens/board.ex new file mode 100644 index 0000000..7b22052 --- /dev/null +++ b/lib/chessh/ssh/screens/board.ex @@ -0,0 +1,15 @@ +defmodule Chessh.SSH.Client.Board do + use Chessh.SSH.Client.Screen + alias Chessh.SSH.Client.State + + def render(%State{} = _state) do + @ascii_chars["pieces"]["white"]["knight"] + end + + def handle_input(action, state) do + case action do + "q" -> state + _ -> state + end + end +end diff --git a/lib/chessh/ssh/screens/menu.ex b/lib/chessh/ssh/screens/menu.ex new file mode 100644 index 0000000..22aba85 --- /dev/null +++ b/lib/chessh/ssh/screens/menu.ex @@ -0,0 +1,79 @@ +defmodule Chessh.SSH.Client.Menu do + alias Chessh.SSH.Client + alias Chessh.Utils + alias IO.ANSI + + require Logger + + defmodule State do + defstruct y: 0, + x: 0 + end + + use Chessh.SSH.Client.Screen + + @logo " Simponic's + + dP MP\"\"\"\"\"\"`MM MP\"\"\"\"\"\"`MM M\"\"MMMMM\"\"MM + 88 M mmmmm..M M mmmmm..M M MMMMM MM +.d8888b. 88d888b. .d8888b. M. `YM M. `YM M `M +88' `\"\" 88' `88 88ooood8 MMMMMMM. M MMMMMMM. M M MMMMM MM +88. ... 88 88 88. ... M. .MMM' M M. .MMM' M M MMMMM MM +`88888P' dP dP `88888P' Mb. .dM Mb. .dM M MMMMM MM + MMMMMMMMMMM MMMMMMMMMMM MMMMMMMMMMMM" + + def render(%Client.State{ + width: width, + height: height, + state_stack: [{_this_module, %State{y: y, x: x}} | _tail] + }) do + {logo_width, logo_height} = Utils.text_dim(@logo) + + split = String.split(@logo, "\n") + + Enum.flat_map( + Enum.zip(0..(length(split) - 1), split), + fn {i, line} -> + [ + ANSI.cursor(div(height - logo_height, 2) + i + y, div(width - logo_width, 2) + x), + "#{line}\n" + ] + end + ) + end + + def handle_input( + data, + %Client.State{state_stack: [{this_module, %State{y: y, x: x} = screen_state} | tail]} = + state + ) do + case data do + :left -> + %Client.State{ + state + | state_stack: [{this_module, %State{screen_state | x: x - 1}} | tail] + } + + :right -> + %Client.State{ + state + | state_stack: [{this_module, %State{screen_state | x: x + 1}} | tail] + } + + :up -> + %Client.State{ + state + | state_stack: [{this_module, %State{screen_state | y: y - 1}} | tail] + } + + :down -> + %Client.State{ + state + | state_stack: [{this_module, %State{screen_state | y: y + 1}} | tail] + } + + _ -> + state + end + end +end diff --git a/lib/chessh/ssh/screens/screen.ex b/lib/chessh/ssh/screens/screen.ex new file mode 100644 index 0000000..be6e00e --- /dev/null +++ b/lib/chessh/ssh/screens/screen.ex @@ -0,0 +1,15 @@ +defmodule Chessh.SSH.Client.Screen do + @callback render(state :: Chessh.SSH.Client.State.t() | any()) :: any() + @callback handle_input(action :: any(), state :: Chessh.SSH.Client.State.t()) :: + Chessh.SSH.Client.State.t() + + defmacro __using__(_) do + quote do + @behaviour Chessh.SSH.Client.Screen + + @ascii_chars Application.compile_env!(:chessh, :ascii_chars_json_file) + |> File.read!() + |> Jason.decode!() + end + end +end diff --git a/mix.exs b/mix.exs index 6066333..fd5a4b8 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,8 @@ defmodule Chessh.MixProject do {:postgrex, "~> 0.16.5"}, {:bcrypt_elixir, "~> 3.0"}, {:hammer, "~> 6.1"}, - {:syn, "~> 3.3"} + {:syn, "~> 3.3"}, + {:jason, "~> 1.3"} ] end diff --git a/mix.lock b/mix.lock index c376f97..48fe5e1 100644 --- a/mix.lock +++ b/mix.lock @@ -11,6 +11,7 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "esshd": {:hex, :esshd, "0.2.1", "cded6a329c32bc3b3c15828bcd34203227bbef310db3c39a6f3c55cf5b29cd34", [:mix], [], "hexpm", "b058b56af53aba1c23522d72a3c39ab7f302e509af1c0ba1a748f00d93053c4d"}, "hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, "syn": {:hex, :syn, "3.3.0", "4684a909efdfea35ce75a9662fc523e4a8a4e8169a3df275e4de4fa63f99c486", [:rebar3], [], "hexpm", "e58ee447bc1094bdd21bf0acc102b1fbf99541a508cd48060bf783c245eaf7d6"}, diff --git a/priv/ascii_chars.json b/priv/ascii_chars.json new file mode 100644 index 0000000..e50be22 --- /dev/null +++ b/priv/ascii_chars.json @@ -0,0 +1,184 @@ +{ + "letters": { + "a": [ + " ", + " /\\ ", + "/--\\" + ], + "b": [ + " __ ", + "|__)", + "|__)" + ], + "c": [ + " __ ", + "/ ", + "\\__ " + ], + "d": [ + " __ ", + "| \\", + "|__/" + ], + "e": [ + " __ ", + "|_ ", + "|__ " + ], + "f": [ + " __ ", + "|_ ", + "| " + ], + "g": [ + " __ ", + "/ _ ", + "\\__\\" + ], + "h": [ + "| |", + "|__|", + "| |" + ] + }, + "numbers": { + "0": [ + " _ ", + "| |", + "|_|" + ], + "1": [ + " ", + " /|", + " |" + ], + "2": [ + " _ ", + " )", + " /_" + ], + "3": [ + " _ ", + " _)", + " _)" + ], + "4": [ + " .", + " /|", + "'-|" + ], + "5": [ + " _ ", + "|_ ", + " _)" + ], + "6": [ + " ", + " / ", + "(_)" + ], + "7": [ + " __", + " /", + " / " + ], + "8": [ + " _ ", + "(_)", + "(_)" + ], + "9": [ + " _ ", + "(_)", + " )" + ] + }, + "pieces": { + "white": { + "rook": [ + " ", + " [`'`'] ", + " | | ", + " |__| ", + " " + ], + "knight": [ + " _ _ ", + " \\` '/ ", + " (o o) ", + " \\ / \\", + " ^ " + ], + "queen": [ + " /\\+/\\ ", + " /(o o)\\ ", + " (_) ", + " " + ], + "king": [ + " ", + " |`+'| ", + " (o o) ", + " (_) ", + " " + ], + "bishop": [ + " ", + " |v| ", + " (0 o) ", + " (_) ", + " " + ], + "pawn": [ + " _ ", + " ( ) ", + " | | ", + " |_| ", + " " + ] + }, + "black": { + "rook": [ + " ", + " [`'`'] ", + " |::| ", + " |::| ", + " " + ], + "knight": [ + " _ _ ", + " \\`.'/ ", + " (o:o) ", + " \\:/:\\", + " ^ " + ], + "queen": [ + " /\\+/\\ ", + " /(o:o)\\ ", + " (:) ", + " " + ], + "king": [ + " ", + " |`+'| ", + " (o:o) ", + " (:) ", + " " + ], + "bishop": [ + " ", + " |v| ", + " (o:o) ", + " (:) ", + " " + ], + "pawn": [ + " _ ", + " (:) ", + " |:| ", + " |_| ", + " " + ] + } + } +}