From bdf99b4ee989df1813745e1dfd2983689b09ca85 Mon Sep 17 00:00:00 2001 From: Logan Hunt Date: Tue, 17 Jan 2023 14:00:18 -0700 Subject: [PATCH] Persistent game (#5) * Remove unnecessary server in board * Initial persistent games * Remove done chessh README stuff, warning issue * Show current players and move * Add promotion * Merge default changeset on all status --- README.md | 5 - lib/chessh/schema/game.ex | 32 ++ lib/chessh/schema/player.ex | 5 +- lib/chessh/ssh/client/board/board.ex | 123 ------ lib/chessh/ssh/client/board/server.ex | 19 - lib/chessh/ssh/client/client.ex | 43 ++- lib/chessh/ssh/client/game/game.ex | 359 ++++++++++++++++++ lib/chessh/ssh/client/game/promotion.ex | 63 +++ .../ssh/client/{board => game}/renderer.ex | 86 ++++- lib/chessh/ssh/client/menu.ex | 74 +++- lib/chessh/ssh/tui.ex | 42 -- .../20230115201050_create_games.exs | 19 + 12 files changed, 644 insertions(+), 226 deletions(-) create mode 100644 lib/chessh/schema/game.ex delete mode 100644 lib/chessh/ssh/client/board/board.ex delete mode 100644 lib/chessh/ssh/client/board/server.ex create mode 100644 lib/chessh/ssh/client/game/game.ex create mode 100644 lib/chessh/ssh/client/game/promotion.ex rename lib/chessh/ssh/client/{board => game}/renderer.ex (77%) create mode 100644 priv/repo/migrations/20230115201050_create_games.exs diff --git a/README.md b/README.md index 2949303..3bd6fb1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1 @@ # CheSSH - -Features: -- [X] SSH Key & Password authentication -- [X] Session rate limiting -- [X] Multi-node support diff --git a/lib/chessh/schema/game.ex b/lib/chessh/schema/game.ex new file mode 100644 index 0000000..b6ff327 --- /dev/null +++ b/lib/chessh/schema/game.ex @@ -0,0 +1,32 @@ +defmodule Chessh.Game do + alias Chessh.Player + use Ecto.Schema + import Ecto.Changeset + + schema "games" do + field(:fen, :string) + field(:moves, :integer, default: 0) + + field(:turn, Ecto.Enum, values: [:light, :dark], default: :light) + field(:winner, Ecto.Enum, values: [:light, :dark, :none], default: :none) + field(:status, Ecto.Enum, values: [:continue, :draw, :winner], default: :continue) + + belongs_to(:light_player, Player, foreign_key: :light_player_id) + belongs_to(:dark_player, Player, foreign_key: :dark_player_id) + + timestamps() + end + + def changeset(game, attrs) do + game + |> cast(attrs, [ + :fen, + :moves, + :turn, + :winner, + :status, + :light_player_id, + :dark_player_id + ]) + end +end diff --git a/lib/chessh/schema/player.ex b/lib/chessh/schema/player.ex index 8eaffee..074ea4e 100644 --- a/lib/chessh/schema/player.ex +++ b/lib/chessh/schema/player.ex @@ -1,6 +1,7 @@ defmodule Chessh.Player do use Ecto.Schema import Ecto.Changeset + alias Chessh.{Key, Game} @derive {Inspect, except: [:password]} schema "players" do @@ -11,7 +12,9 @@ defmodule Chessh.Player do field(:authentications, :integer, default: 0) - has_many(:keys, Chessh.Key) + has_many(:keys, Key) + has_many(:light_games, Game, foreign_key: :light_player_id, references: :id) + has_many(:dark_games, Game, foreign_key: :dark_player_id, references: :id) timestamps() end diff --git a/lib/chessh/ssh/client/board/board.ex b/lib/chessh/ssh/client/board/board.ex deleted file mode 100644 index cbbade5..0000000 --- a/lib/chessh/ssh/client/board/board.ex +++ /dev/null @@ -1,123 +0,0 @@ -defmodule Chessh.SSH.Client.Board do - require Logger - alias Chessh.Utils - alias Chessh.SSH.Client.Board.Renderer - - defmodule State do - defstruct cursor: %{x: 7, y: 7}, - highlighted: %{}, - move_from: nil, - game_id: nil, - client_pid: nil, - binbo_pid: nil, - width: 0, - height: 0, - flipped: false - end - - use Chessh.SSH.Client.Screen - - def init([%State{client_pid: client_pid, game_id: game_id} = state | _]) do - :syn.add_node_to_scopes([:games]) - :ok = :syn.join(:games, {:game, game_id}, self()) - - :binbo.start() - {:ok, binbo_pid} = :binbo.new_server() - :binbo.new_game(binbo_pid) - - send(client_pid, {:send_to_ssh, Utils.clear_codes()}) - - {:ok, %State{state | binbo_pid: binbo_pid}} - end - - def handle_info({:new_move, move}, %State{binbo_pid: binbo_pid, client_pid: client_pid} = state) do - case :binbo.move(binbo_pid, move) do - {:ok, :continue} -> - send(client_pid, {:send_to_ssh, render_state(state)}) - - _ -> - nil - end - - {:noreply, state} - end - - def input( - width, - height, - action, - %State{ - move_from: move_from, - game_id: game_id, - cursor: %{x: cursor_x, y: cursor_y} = cursor, - client_pid: client_pid, - flipped: flipped - } = state - ) do - new_cursor = - case action do - :left -> %{y: cursor_y, x: Utils.wrap_around(cursor_x, -1, Renderer.chess_board_width())} - :right -> %{y: cursor_y, x: Utils.wrap_around(cursor_x, 1, Renderer.chess_board_width())} - :down -> %{y: Utils.wrap_around(cursor_y, 1, Renderer.chess_board_height()), x: cursor_x} - :up -> %{y: Utils.wrap_around(cursor_y, -1, Renderer.chess_board_height()), x: cursor_x} - _ -> cursor - end - - {new_move_from, move_to} = - if action == :return do - coords = {new_cursor.y, new_cursor.x} - - case move_from do - nil -> {coords, nil} - _ -> {nil, coords} - end - else - {move_from, nil} - end - - # TODO: Check move here, then publish new move, subscribers get from DB instead - if move_from && move_to do - attempted_move = - if flipped, - do: - "#{Renderer.to_chess_coord(flip(move_from))}#{Renderer.to_chess_coord(flip(move_to))}", - else: "#{Renderer.to_chess_coord(move_from)}#{Renderer.to_chess_coord(move_to)}" - - :syn.publish(:games, {:game, game_id}, {:new_move, attempted_move}) - end - - new_state = %State{ - state - | cursor: new_cursor, - move_from: new_move_from, - highlighted: %{ - {new_cursor.y, new_cursor.x} => Renderer.to_select_background(), - new_move_from => Renderer.from_select_background() - }, - width: width, - height: height, - flipped: if(action == "f", do: !flipped, else: flipped) - } - - send(client_pid, {:send_to_ssh, render_state(new_state)}) - new_state - end - - def render(width, height, %State{client_pid: client_pid} = state) do - send(client_pid, {:send_to_ssh, render_state(state)}) - %State{state | width: width, height: height} - end - - def flip({y, x}), - do: {Renderer.chess_board_height() - 1 - y, Renderer.chess_board_width() - 1 - x} - - defp render_state( - %State{ - binbo_pid: binbo_pid - } = state - ) do - {:ok, fen} = :binbo.get_fen(binbo_pid) - - Renderer.render_board_state(fen, state) - end -end diff --git a/lib/chessh/ssh/client/board/server.ex b/lib/chessh/ssh/client/board/server.ex deleted file mode 100644 index 2e480e7..0000000 --- a/lib/chessh/ssh/client/board/server.ex +++ /dev/null @@ -1,19 +0,0 @@ -# defmodule Chessh.SSH.Client.Board.Server do -# use GenServer -# -# defmodule State do -# defstruct game_id: nil, -# binbo_pid: nil -# end -# -# def init([%State{game_id: game_id} = state]) do -# {:ok, binbo_pid} = GenServer.start_link(:binbo, []) -# -# :syn.join(:games, {:game, game_id}) -# {:ok, state} -# end -# -# def handle_cast({:new_move, attempted_move}, %State{game_id: game_id} = state) do -# {:no_reply, state} -# end -# end diff --git a/lib/chessh/ssh/client/client.ex b/lib/chessh/ssh/client/client.ex index da9779b..72bbb66 100644 --- a/lib/chessh/ssh/client/client.ex +++ b/lib/chessh/ssh/client/client.ex @@ -24,8 +24,13 @@ defmodule Chessh.SSH.Client do end @impl true - def init([%State{} = state]) do - send(self(), {:set_screen_process, Chessh.SSH.Client.Menu, %Chessh.SSH.Client.Menu.State{}}) + def init([%State{player_session: player_session} = state]) do + send( + self(), + {:set_screen_process, Chessh.SSH.Client.Menu, + %Chessh.SSH.Client.Menu.State{player_session: player_session}} + ) + {:ok, state} end @@ -56,6 +61,11 @@ defmodule Chessh.SSH.Client do }} end + @impl true + def handle_info({:go_back_one_screen, previous_state}, %State{} = state) do + {:noreply, go_back_one_screen(state, previous_state)} + end + @impl true def handle_info(:quit, %State{} = state) do {:stop, :normal, state} @@ -87,8 +97,7 @@ defmodule Chessh.SSH.Client do %State{ width: width, height: height, - screen_pid: screen_pid, - screen_state_initials: [_ | rest_initial] + screen_pid: screen_pid } = state ) do case keymap(data) do @@ -96,10 +105,7 @@ defmodule Chessh.SSH.Client do {:stop, :normal, state} :previous_screen -> - [{prev_module, prev_state_initial} | _] = rest_initial - send(self(), {:set_screen_process, prev_module, prev_state_initial}) - - {:noreply, %State{state | screen_state_initials: rest_initial}} + {:noreply, go_back_one_screen(state)} action -> send(screen_pid, {:input, width, height, action}) @@ -181,4 +187,25 @@ defmodule Chessh.SSH.Client do send(screen_pid, {:render, width, height}) end end + + defp go_back_one_screen( + %State{ + screen_state_initials: [_ | rest_initial] + } = state, + previous_state + ) do + [{prev_module, prev_state_initial} | _] = rest_initial + + send( + self(), + {:set_screen_process, prev_module, + if(is_nil(previous_state), do: prev_state_initial, else: previous_state)} + ) + + %State{state | screen_state_initials: rest_initial} + end + + defp go_back_one_screen(%State{} = state) do + go_back_one_screen(state, nil) + end end diff --git a/lib/chessh/ssh/client/game/game.ex b/lib/chessh/ssh/client/game/game.ex new file mode 100644 index 0000000..2ee6dca --- /dev/null +++ b/lib/chessh/ssh/client/game/game.ex @@ -0,0 +1,359 @@ +defmodule Chessh.SSH.Client.Game do + require Logger + alias Chessh.{Game, Utils, Repo} + alias Chessh.SSH.Client.Game.Renderer + + @default_fen "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + + defmodule State do + defstruct cursor: %{x: 7, y: 7}, + highlighted: %{}, + move_from: nil, + game: nil, + client_pid: nil, + binbo_pid: nil, + width: 0, + height: 0, + flipped: false, + color: nil, + player_session: nil + end + + use Chessh.SSH.Client.Screen + + defp initialize_game(game_id, fen) do + :syn.add_node_to_scopes([:games]) + :ok = :syn.join(:games, {:game, game_id}, self()) + + :binbo.start() + {:ok, binbo_pid} = :binbo.new_server() + :binbo.new_game(binbo_pid, fen) + + binbo_pid + end + + def init([ + %State{ + color: color, + game: %Game{dark_player_id: dark_player_id, light_player_id: light_player_id} + } = state + | tail + ]) + when is_nil(color) do + new_state = + case {is_nil(dark_player_id), is_nil(light_player_id)} do + {true, false} -> %State{state | color: :dark} + {false, true} -> %State{state | color: :light} + {_, _} -> %State{state | color: Enum.random([:light, :dark])} + end + + init([new_state | tail]) + end + + def init([ + %State{ + player_session: player_session, + color: color, + client_pid: client_pid, + game: + %Game{ + id: game_id, + fen: fen, + dark_player_id: dark_player_id, + light_player_id: light_player_id + } = game + } = state + | _ + ]) do + maybe_changeset = + case color do + :light -> + if !light_player_id, + do: Game.changeset(game, %{light_player_id: player_session.player_id}) + + :dark -> + if !dark_player_id, + do: Game.changeset(game, %{dark_player_id: player_session.player_id}) + end + + {status, maybe_joined_game} = + if maybe_changeset do + maybe_changeset + |> Repo.update() + else + {:undefined, nil} + end + + if status == :ok && maybe_joined_game do + :syn.publish(:games, {:game, game_id}, :player_joined) + end + + binbo_pid = initialize_game(game_id, fen) + send(client_pid, {:send_to_ssh, Utils.clear_codes()}) + + new_game = Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player]) + + new_state = %State{ + state + | binbo_pid: binbo_pid, + color: if(new_game.light_player_id == player_session.player_id, do: :light, else: :dark), + game: new_game + } + + {:ok, new_state} + end + + def init([ + %State{player_session: player_session, color: color, client_pid: client_pid, game: nil} = + state + | _ + ]) do + {:ok, %Game{id: game_id, fen: fen}} = + Game.changeset( + %Game{}, + Map.merge( + if(color == :light, + do: %{light_player_id: player_session.player_id}, + else: %{dark_player_id: player_session.player_id} + ), + %{ + fen: @default_fen + } + ) + ) + |> Repo.insert() + + binbo_pid = initialize_game(game_id, fen) + send(client_pid, {:send_to_ssh, Utils.clear_codes()}) + + {:ok, + %State{ + state + | game: Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player]), + binbo_pid: binbo_pid + }} + end + + def handle_info( + {:new_move, move}, + %State{game: %Game{id: game_id}, client_pid: client_pid, binbo_pid: binbo_pid} = state + ) do + :binbo.move(binbo_pid, move) + + new_state = %State{ + state + | game: Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player]) + } + + send(client_pid, {:send_to_ssh, render_state(new_state)}) + + {:noreply, new_state} + end + + def handle_info( + :player_joined, + %State{client_pid: client_pid, game: %Game{id: game_id}} = state + ) do + game = Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player]) + new_state = %State{state | game: game} + send(client_pid, {:send_to_ssh, render_state(new_state)}) + {:noreply, new_state} + end + + def input( + width, + height, + action, + %State{ + move_from: move_from, + cursor: %{x: cursor_x, y: cursor_y} = cursor, + client_pid: client_pid, + flipped: flipped, + binbo_pid: binbo_pid + } = state + ) do + new_cursor = + case action do + :left -> %{y: cursor_y, x: Utils.wrap_around(cursor_x, -1, Renderer.chess_board_width())} + :right -> %{y: cursor_y, x: Utils.wrap_around(cursor_x, 1, Renderer.chess_board_width())} + :down -> %{y: Utils.wrap_around(cursor_y, 1, Renderer.chess_board_height()), x: cursor_x} + :up -> %{y: Utils.wrap_around(cursor_y, -1, Renderer.chess_board_height()), x: cursor_x} + _ -> cursor + end + + {new_move_from, move_to} = + if action == :return do + coords = {new_cursor.y, new_cursor.x} + + case move_from do + nil -> {coords, nil} + _ -> {nil, coords} + end + else + {move_from, nil} + end + + new_state = %State{ + state + | cursor: new_cursor, + move_from: new_move_from, + highlighted: %{ + {new_cursor.y, new_cursor.x} => Renderer.to_select_background(), + new_move_from => Renderer.from_select_background() + }, + width: width, + height: height, + flipped: if(action == "f", do: !flipped, else: flipped) + } + + if move_from && move_to do + maybe_flipped_from = if flipped, do: flip(move_from), else: move_from + maybe_flipped_to = if flipped, do: flip(move_to), else: move_to + + piece_type = + :binbo_position.get_piece( + :binbo_board.notation_to_index(Renderer.to_chess_coord(maybe_flipped_from)), + :binbo.game_state(binbo_pid) + ) + + promotion_possible = + case piece_type do + 1 -> + # Light pawn + {y, _} = maybe_flipped_to + y == 0 + + 17 -> + # Dark pawn + {y, _} = maybe_flipped_to + y == Renderer.chess_board_height() - 1 + + _ -> + false + end + + if promotion_possible do + send( + client_pid, + {:set_screen_process, Chessh.SSH.Client.Game.PromotionScreen, + %Chessh.SSH.Client.Game.PromotionScreen.State{ + client_pid: client_pid, + game_pid: self(), + game_state: new_state + }} + ) + + receive do + {:promotion, promotion} -> + attempt_move(move_from, move_to, state, promotion) + end + else + attempt_move(move_from, move_to, state) + end + end + + send(client_pid, {:send_to_ssh, render_state(new_state)}) + new_state + end + + def render(width, height, %State{client_pid: client_pid} = state) do + new_state = %State{state | width: width, height: height} + send(client_pid, {:send_to_ssh, render_state(new_state)}) + new_state + end + + defp attempt_move( + from, + to, + %State{} = state + ), + do: attempt_move(from, to, state, nil) + + defp attempt_move( + from, + to, + %State{ + game: %Game{id: game_id, turn: turn}, + binbo_pid: binbo_pid, + flipped: flipped, + color: turn + }, + promotion + ) do + attempted_move = + if(flipped, + do: "#{Renderer.to_chess_coord(flip(from))}#{Renderer.to_chess_coord(flip(to))}", + else: "#{Renderer.to_chess_coord(from)}#{Renderer.to_chess_coord(to)}" + ) <> + if(promotion, do: promotion, else: "") + + game = Repo.get(Game, game_id) + + case :binbo.move( + binbo_pid, + attempted_move + ) do + {:ok, status} -> + {:ok, fen} = :binbo.get_fen(binbo_pid) + + default_changeset = %{ + fen: fen, + moves: game.moves + 1, + turn: if(game.turn == :dark, do: :light, else: :dark) + } + + case status do + :continue -> + {:ok, _new_game} = + Game.changeset( + game, + default_changeset + ) + |> Repo.update() + + {:draw, _} -> + Game.changeset( + game, + Map.merge(default_changeset, %{status: :draw}) + ) + |> Repo.update() + + {:checkmate, :white_wins} -> + Game.changeset( + game, + Map.merge(default_changeset, %{status: :winner, winner: :light}) + ) + |> Repo.update() + + {:checkmate, :black_wins} -> + Game.changeset( + game, + Map.merge(default_changeset, %{status: :winner, winner: :dark}) + ) + |> Repo.update() + end + + :syn.publish(:games, {:game, game_id}, {:new_move, attempted_move}) + + x -> + Logger.debug(inspect(x)) + nil + end + end + + defp attempt_move(_, _, _, _) do + Logger.debug("No matching clause for move attempt - must be illegal?") + nil + end + + defp flip({y, x}), + do: {Renderer.chess_board_height() - 1 - y, Renderer.chess_board_width() - 1 - x} + + defp render_state( + %State{ + game: %Game{fen: fen} + } = state + ) do + Renderer.render_board_state(fen, state) + end +end diff --git a/lib/chessh/ssh/client/game/promotion.ex b/lib/chessh/ssh/client/game/promotion.ex new file mode 100644 index 0000000..c4cece6 --- /dev/null +++ b/lib/chessh/ssh/client/game/promotion.ex @@ -0,0 +1,63 @@ +defmodule Chessh.SSH.Client.Game.PromotionScreen do + alias Chessh.Utils + alias Chessh.SSH.Client.Game + alias IO.ANSI + + defmodule State do + defstruct game_pid: nil, + client_pid: nil, + game_state: nil + end + + use Chessh.SSH.Client.Screen + + @promotion_screen Utils.clear_codes() ++ + [ + "Press the key associated to the piece you'd like to promote", + " 'q' - queen", + " 'r' - rook", + " 'n' - knight", + " 'b' - bishop" + ] + + def init([%State{} = state | _]) do + {:ok, state} + end + + def render(_, _, %State{client_pid: client_pid} = state) do + rendered = + Enum.flat_map( + Enum.zip(0..(length(@promotion_screen) - 1), @promotion_screen), + fn {i, promotion} -> + [ + ANSI.cursor(i, 0), + promotion + ] + end + ) ++ [ANSI.home()] + + send( + client_pid, + {:send_to_ssh, rendered} + ) + + state + end + + def input( + _, + _, + action, + %State{client_pid: client_pid, game_pid: game_pid, game_state: %Game.State{} = game_state} = + state + ) do + promotion = if Enum.member?(["q", "b", "n", "r"], action), do: action, else: nil + + if promotion do + send(client_pid, {:go_back_one_screen, game_state}) + send(game_pid, {:promotion, promotion}) + end + + state + end +end diff --git a/lib/chessh/ssh/client/board/renderer.ex b/lib/chessh/ssh/client/game/renderer.ex similarity index 77% rename from lib/chessh/ssh/client/board/renderer.ex rename to lib/chessh/ssh/client/game/renderer.ex index fa8648f..f8cf689 100644 --- a/lib/chessh/ssh/client/board/renderer.ex +++ b/lib/chessh/ssh/client/game/renderer.ex @@ -1,7 +1,7 @@ -defmodule Chessh.SSH.Client.Board.Renderer do +defmodule Chessh.SSH.Client.Game.Renderer do alias IO.ANSI - alias Chessh.Utils - alias Chessh.SSH.Client.Board + alias Chessh.{Utils, Player} + alias Chessh.SSH.Client.Game require Logger @chess_board_height 8 @@ -25,23 +25,85 @@ defmodule Chessh.SSH.Client.Board.Renderer do "#{List.to_string([?a + x])}#{@chess_board_height - y}" end - def render_board_state(fen, %Board.State{ + def render_board_state( + fen, + %Game.State{ + game: + %Chessh.Game{ + light_player: light_player + } = game + } = state + ) + when is_nil(light_player) do + render_board_state(fen, %Game.State{ + state + | game: %Chessh.Game{game | light_player: %Player{username: "(no opponent)"}} + }) + end + + def render_board_state( + fen, + %Game.State{ + game: + %Chessh.Game{ + dark_player: dark_player + } = game + } = state + ) + when is_nil(dark_player) do + render_board_state(fen, %Game.State{ + state + | game: %Chessh.Game{game | dark_player: %Player{username: "(no opponent)"}} + }) + end + + def render_board_state(fen, %Game.State{ width: _width, height: _height, highlighted: highlighted, - flipped: flipped + flipped: flipped, + game: %Chessh.Game{ + id: game_id, + dark_player: %Player{username: dark_player}, + light_player: %Player{username: light_player}, + turn: turn, + status: status, + winner: winner + } }) do - board = - draw_board( - fen, - {@tile_width, @tile_height}, - highlighted, - flipped + rendered = [ + Enum.join( + [ + ANSI.clear_line(), + "Game #{game_id}: ", + ANSI.format_fragment([@light_piece_color, light_player]), + "#{ANSI.default_color()} --vs-- ", + ANSI.format_fragment([@dark_piece_color, dark_player]), + ANSI.default_color(), + case status do + :continue -> + ", #{ANSI.format_fragment([if(turn == :light, do: @light_piece_color, else: @dark_piece_color), if(turn == :dark, do: dark_player, else: light_player)])} to move" + + :draw -> + "ended in a draw" + + :winner -> + ", #{ANSI.format_fragment([if(winner == :light, do: @light_piece_color, else: @dark_piece_color), if(winner == :dark, do: dark_player, else: light_player)])} won!" + end + ], + "" ) + | draw_board( + fen, + {@tile_width, @tile_height}, + highlighted, + flipped + ) + ] [ANSI.home()] ++ Enum.map( - Enum.zip(1..length(board), board), + Enum.zip(1..length(rendered), rendered), fn {i, line} -> [ANSI.cursor(i, 0), line] end diff --git a/lib/chessh/ssh/client/menu.ex b/lib/chessh/ssh/client/menu.ex index 946928e..6c96cc2 100644 --- a/lib/chessh/ssh/client/menu.ex +++ b/lib/chessh/ssh/client/menu.ex @@ -1,12 +1,15 @@ defmodule Chessh.SSH.Client.Menu do - alias Chessh.Utils alias IO.ANSI + alias Chessh.{Utils, Repo, Game} + import Ecto.Query require Logger defmodule State do defstruct client_pid: nil, - selected: 0 + selected: 0, + player_session: nil, + options: [] end @logo " Simponic's @@ -20,31 +23,70 @@ defmodule Chessh.SSH.Client.Menu do use Chessh.SSH.Client.Screen def init([%State{} = state | _]) do - {:ok, state} + {:ok, %State{state | options: options(state)}} end - @options [ - {"Start A Game", {Chessh.SSH.Client.Board, %Chessh.SSH.Client.Board.State{}}}, - {"Join A Game", {}}, - {"My Games", {}}, - {"Settings", {}}, - {"Help", {}} - ] + def options(%State{player_session: player_session}) do + current_games = + Repo.all( + from(g in Game, + where: g.light_player_id == ^player_session.player_id, + or_where: g.dark_player_id == ^player_session.player_id, + where: g.status == :continue + ) + ) - def input(width, height, action, %State{client_pid: client_pid, selected: selected} = state) do + joinable_games = + Repo.all( + from(g in Game, + where: is_nil(g.light_player_id), + or_where: is_nil(g.dark_player_id) + ) + ) + + [ + {"Start A Game As Light", + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{player_session: player_session, color: :light}}}, + {"Start A Game As Dark", + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{player_session: player_session, color: :dark}}} + ] ++ + Enum.map(current_games, fn game -> + {"Current Game - #{game.id}", + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{player_session: player_session, game: game}}} + end) ++ + Enum.map(joinable_games, fn game -> + {"Joinable Game - #{game.id}", + {Chessh.SSH.Client.Game, + %Chessh.SSH.Client.Game.State{player_session: player_session, game: game}}} + end) ++ + [ + {"Settings", {}}, + {"Help", {}} + ] + end + + def input( + width, + height, + action, + %State{options: options, client_pid: client_pid, selected: selected} = state + ) do new_state = case(action) do :up -> %State{ state - | selected: Utils.wrap_around(selected, -1, length(@options)) + | selected: Utils.wrap_around(selected, -1, length(options)) } :down -> - %State{state | selected: Utils.wrap_around(selected, 1, length(@options))} + %State{state | selected: Utils.wrap_around(selected, 1, length(options))} :return -> - {_option, {module, state}} = Enum.at(@options, selected) + {_option, {module, state}} = Enum.at(options, selected) send(client_pid, {:set_screen_process, module, state}) state @@ -67,7 +109,7 @@ defmodule Chessh.SSH.Client.Menu do defp render_state( width, height, - %State{selected: selected} + %State{options: options, selected: selected} ) do logo_lines = String.split(@logo, "\n") {logo_width, logo_height} = Utils.text_dim(@logo) @@ -84,7 +126,7 @@ defmodule Chessh.SSH.Client.Menu do end ) ++ Enum.flat_map( - Enum.zip(0..(length(@options) - 1), @options), + Enum.zip(0..(length(options) - 1), options), fn {i, {option, _}} -> [ ANSI.cursor(y + length(logo_lines) + i + 1, x), diff --git a/lib/chessh/ssh/tui.ex b/lib/chessh/ssh/tui.ex index 1838fb8..483d882 100644 --- a/lib/chessh/ssh/tui.ex +++ b/lib/chessh/ssh/tui.ex @@ -144,7 +144,6 @@ defmodule Chessh.SSH.Tui do {:ssh_cm, connection_handler, {:shell, channel_id, want_reply?}}, %{width: width, height: height, player_session: player_session} = state ) do - Logger.debug("Session #{player_session.id} requested shell") :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id) {:ok, client_pid} = @@ -161,47 +160,6 @@ defmodule Chessh.SSH.Tui do {:ok, %State{state | client_pid: client_pid}} end - def handle_ssh_msg( - {:ssh_cm, connection_handler, {:exec, channel_id, want_reply?, cmd}}, - %State{} = state - ) do - :ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id) - Logger.debug("EXEC #{cmd}") - {:ok, state} - end - - def handle_ssh_msg( - {:ssh_cm, _connection_handler, {:eof, _channel_id}}, - %State{} = state - ) do - Logger.debug("EOF") - {:ok, state} - end - - def handle_ssh_msg( - {:ssh_cm, _connection_handler, {:signal, _channel_id, signal}}, - %State{} = state - ) do - Logger.debug("SIGNAL #{signal}") - {:ok, state} - end - - def handle_ssh_msg( - {:ssh_cm, _connection_handler, {:exit_signal, channel_id, signal, err, lang}}, - %State{} = state - ) do - Logger.debug("EXIT SIGNAL #{signal} #{err} #{lang}") - {:stop, channel_id, state} - end - - def handle_ssh_msg( - {:ssh_cm, _connection_handler, {:exit_STATUS, channel_id, status}}, - %State{} = state - ) do - Logger.debug("EXIT STATUS #{status}") - {:stop, channel_id, state} - end - def handle_ssh_msg( msg, %State{channel_id: channel_id} = state diff --git a/priv/repo/migrations/20230115201050_create_games.exs b/priv/repo/migrations/20230115201050_create_games.exs new file mode 100644 index 0000000..418d1de --- /dev/null +++ b/priv/repo/migrations/20230115201050_create_games.exs @@ -0,0 +1,19 @@ +defmodule Chessh.Repo.Migrations.CreateGames do + use Ecto.Migration + + def change do + create table(:games) do + add(:fen, :string) + add(:moves, :integer, default: 0) + + add(:turn, :string) + add(:winner, :string) + add(:status, :string) + + add(:light_player_id, references(:players), null: true) + add(:dark_player_id, references(:players), null: true) + + timestamps() + end + end +end