diff --git a/lib/chessh/schema/game.ex b/lib/chessh/schema/game.ex index 55b9ea4..868fec8 100644 --- a/lib/chessh/schema/game.ex +++ b/lib/chessh/schema/game.ex @@ -12,6 +12,8 @@ defmodule Chessh.Game do field(:winner, Ecto.Enum, values: [:light, :dark, :none], default: :none) field(:status, Ecto.Enum, values: [:continue, :draw, :winner], default: :continue) + field(:game_moves, :string) + belongs_to(:light_player, Player, foreign_key: :light_player_id) belongs_to(:dark_player, Player, foreign_key: :dark_player_id) @@ -31,7 +33,8 @@ defmodule Chessh.Game do :last_move, :light_player_id, :dark_player_id, - :discord_thread_id + :discord_thread_id, + :game_moves ]) end end diff --git a/lib/chessh/ssh/client/game/game.ex b/lib/chessh/ssh/client/game/game.ex index da2bd99..cd641f0 100644 --- a/lib/chessh/ssh/client/game/game.ex +++ b/lib/chessh/ssh/client/game/game.ex @@ -12,8 +12,6 @@ defmodule Chessh.SSH.Client.Game do game: nil, client_pid: nil, binbo_pid: nil, - width: 0, - height: 0, flipped: false, color: nil, player_session: nil @@ -25,7 +23,6 @@ defmodule Chessh.SSH.Client.Game 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) @@ -177,11 +174,9 @@ defmodule Chessh.SSH.Client.Game do flipped: player_color == :dark }) - send( - client_pid, - {:send_to_ssh, [Utils.clear_codes() | Renderer.render_board_state(new_state)]} - ) - + # Clear screen and do initial render + send(client_pid, {:send_to_ssh, Utils.clear_codes()}) + render(new_state) {:ok, new_state} end @@ -227,8 +222,8 @@ defmodule Chessh.SSH.Client.Game do end def input( - width, - height, + _width, + _height, action, %State{ move_from: move_from, @@ -300,8 +295,6 @@ defmodule Chessh.SSH.Client.Game do state | cursor: new_cursor, move_from: new_move_from, - width: width, - height: height, flipped: if(action == "f", do: !flipped, else: flipped) }) @@ -349,13 +342,7 @@ defmodule Chessh.SSH.Client.Game do end end - send(client_pid, {:send_to_ssh, Renderer.render_board_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, Renderer.render_board_state(new_state)}) + render(new_state) new_state end @@ -370,7 +357,7 @@ defmodule Chessh.SSH.Client.Game do from, to, %State{ - game: %Game{id: game_id, turn: turn}, + game: %Game{game_moves: game_moves, id: game_id, turn: turn}, binbo_pid: binbo_pid, flipped: flipped, color: turn @@ -404,7 +391,8 @@ defmodule Chessh.SSH.Client.Game do fen: fen, moves: game.moves + 1, turn: if(game.turn == :dark, do: :light, else: :dark), - last_move: attempted_move + last_move: attempted_move, + game_moves: if(game_moves, do: game_moves <> " ", else: "") <> attempted_move }, changeset_from_status(status) ) @@ -498,4 +486,11 @@ defmodule Chessh.SSH.Client.Game do end |> Map.merge(extra_highlights) end + + def render(_width, _height, %State{} = state), do: render(state) + + def render(%State{client_pid: client_pid} = state) do + send(client_pid, {:send_to_ssh, Renderer.render_board_state(state)}) + state + end end diff --git a/lib/chessh/ssh/client/game/previous_game.ex b/lib/chessh/ssh/client/game/previous_game.ex new file mode 100644 index 0000000..38172fd --- /dev/null +++ b/lib/chessh/ssh/client/game/previous_game.ex @@ -0,0 +1,121 @@ +defmodule Chessh.SSH.Client.PreviousGame do + @start_fen "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + + alias Chessh.{Game, Utils} + alias Chessh.SSH.Client.Game.Renderer + alias IO.ANSI + + require Logger + + defmodule State do + defstruct move_fens: %{}, + move_idx: 0, + binbo_pid: nil, + game: %Game{}, + client_pid: nil, + flipped: false + end + + use Chessh.SSH.Client.Screen + + def init([ + %State{ + client_pid: client_pid, + game: %Game{ + game_moves: game_moves + } + } = state + ]) do + {:ok, binbo_pid} = :binbo.new_server() + :binbo.new_game(binbo_pid, @start_fen) + + {move_fens, _moves} = + game_moves + |> String.trim() + |> String.split(" ") + |> Enum.reduce({%{"0" => @start_fen}, 1}, fn move, {move_idx_fen_map, curr_turn} -> + {:ok, _status} = :binbo.move(binbo_pid, move) + {:ok, fen} = :binbo.get_fen(binbo_pid) + + {Map.put(move_idx_fen_map, "#{curr_turn}", fen), curr_turn + 1} + end) + + new_state = %State{ + state + | binbo_pid: binbo_pid, + move_fens: move_fens + } + + send(client_pid, {:send_to_ssh, Utils.clear_codes()}) + render(new_state) + + {:ok, new_state} + end + + def input( + _width, + _height, + action, + %State{ + move_idx: move_idx, + flipped: flipped, + game: %Game{ + moves: num_moves + } + } = state + ) do + new_move_idx = + case action do + :left -> + Utils.wrap_around(move_idx, -1, num_moves) + + :right -> + Utils.wrap_around(move_idx, 1, num_moves) + + _ -> + move_idx + end + + new_state = %State{ + state + | move_idx: new_move_idx, + flipped: if(action == "f", do: !flipped, else: flipped) + } + + render(new_state) + new_state + end + + def render( + %State{ + flipped: flipped, + client_pid: client_pid, + move_fens: move_fens, + move_idx: move_idx, + game: %Game{id: game_id, moves: total_moves} + } = state + ) do + {:ok, fen} = Map.fetch(move_fens, "#{move_idx}") + + lines = + ["Game #{game_id} | Move #{move_idx} / #{total_moves}"] ++ + Renderer.draw_board(fen, flipped) ++ + ["<- previous | next ->"] + + send( + client_pid, + {:send_to_ssh, + [ANSI.home()] ++ + Enum.map( + Enum.zip(1..length(lines), lines), + fn {i, line} -> + [ANSI.cursor(i, 0), ANSI.clear_line(), line] + end + )} + ) + + state + end + + def render(_width, _height, %State{} = state), do: render(state) +end diff --git a/lib/chessh/ssh/client/game/renderer.ex b/lib/chessh/ssh/client/game/renderer.ex index c7d3a96..b9071a9 100644 --- a/lib/chessh/ssh/client/game/renderer.ex +++ b/lib/chessh/ssh/client/game/renderer.ex @@ -69,8 +69,6 @@ defmodule Chessh.SSH.Client.Game.Renderer do end def render_board_state(%Game.State{ - width: _width, - height: _height, highlighted: highlighted, flipped: flipped, game: @@ -169,12 +167,18 @@ defmodule Chessh.SSH.Client.Game.Renderer do ) end - defp draw_board( - fen, - {tile_width, tile_height} = tile_dims, - highlights, - flipped - ) do + def draw_board( + fen, + flipped + ), + do: draw_board(fen, {@tile_width, @tile_height}, %{}, flipped) + + def draw_board( + fen, + {tile_width, tile_height} = tile_dims, + highlights, + flipped + ) do board_coord_to_piece_art = make_board_coordinate_to_piece_art_map(fen) tile_rows = make_board_tiles(tile_dims) diff --git a/lib/chessh/ssh/client/menus/main_menu.ex b/lib/chessh/ssh/client/menus/main_menu.ex index 09aea14..ee4b976 100644 --- a/lib/chessh/ssh/client/menus/main_menu.ex +++ b/lib/chessh/ssh/client/menus/main_menu.ex @@ -4,13 +4,13 @@ defmodule Chessh.SSH.Client.MainMenu do require Logger - @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 + @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" |> String.split("\n") @logo_cols @logo |> Enum.map(&String.length/1) |> Enum.max() @@ -18,24 +18,27 @@ defmodule Chessh.SSH.Client.MainMenu do def dynamic_options(), do: false def tick_delay_ms(), do: 1000 - def max_displayed_options(), do: 4 + def max_displayed_options(), do: 5 def max_box_cols(), do: @logo_cols def title(), do: @logo ++ ["- Connected on: #{System.get_env("NODE_ID")}"] def initial_options(%State{player_session: %PlayerSession{} = player_session}) do [ + {"My Current Games", + {Chessh.SSH.Client.SelectCurrentGame, + %Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}}, + {"Joinable Games (lobby)", + {Chessh.SSH.Client.SelectJoinableGame, + %Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}}, + {"Previous Games", + {Chessh.SSH.Client.SelectPreviousGame, + %Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}}, {"Start A Game (Light)", {Chessh.SSH.Client.Game, %Chessh.SSH.Client.Game.State{player_session: player_session, color: :light}}}, {"Start A Game (Dark)", {Chessh.SSH.Client.Game, - %Chessh.SSH.Client.Game.State{player_session: player_session, color: :dark}}}, - {"Current Games", - {Chessh.SSH.Client.SelectCurrentGame, - %Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}}, - {"Joinable Games (lobby)", - {Chessh.SSH.Client.SelectJoinableGame, - %Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}} + %Chessh.SSH.Client.Game.State{player_session: player_session, color: :dark}}} ] end diff --git a/lib/chessh/ssh/client/menus/select_previous_game.ex b/lib/chessh/ssh/client/menus/select_previous_game.ex new file mode 100644 index 0000000..5f55c3d --- /dev/null +++ b/lib/chessh/ssh/client/menus/select_previous_game.ex @@ -0,0 +1,102 @@ +defmodule Chessh.SSH.Client.SelectPreviousGame do + alias Chessh.{Utils, Repo, Game, PlayerSession} + alias Chessh.SSH.Client.GameSelector + import Ecto.Query + require Logger + + use Chessh.SSH.Client.SelectPaginatePoller + + def refresh_options_ms(), do: 4000 + def max_displayed_options(), do: 5 + def title(), do: ["-- Previous Games --"] + def dynamic_options(), do: true + + def get_player_sorted_current_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do + GameSelector.paginate_ish_query( + Game + |> where([g], g.status != :continue) + |> where([g], g.light_player_id == ^player_id or g.dark_player_id == ^player_id) + |> limit(^max_displayed_options()), + current_id, + direction + ) + end + + def format_game_selection_tuple(%Game{id: game_id} = game) do + {Chessh.SSH.Client.Game.Renderer.make_status_line(game, false), game_id} + end + + def next_page_options(%State{ + options: options, + player_session: %PlayerSession{player_id: player_id} + }) do + {_label, previous_last_game_id} = List.last(options) + next_games = get_player_sorted_current_games_with_id(player_id, previous_last_game_id, :desc) + + if length(next_games) > 0, + do: + next_games + |> Enum.map(&format_game_selection_tuple/1), + else: options + end + + def previous_page_options(%State{ + options: options, + player_session: %PlayerSession{player_id: player_id} + }) do + {_label, previous_first_game_id} = List.first(options) + + previous_games = + get_player_sorted_current_games_with_id(player_id, previous_first_game_id, :asc) + + if length(previous_games) > 0, + do: + previous_games + |> Enum.map(&format_game_selection_tuple/1), + else: options + end + + def initial_options(%State{player_session: %PlayerSession{player_id: player_id}}) do + get_player_sorted_current_games_with_id(player_id) + |> Enum.map(&format_game_selection_tuple/1) + end + + def refresh_options(%State{options: options}) do + from(g in Game, + where: g.id in ^Enum.map(options, fn {_, id} -> id end), + order_by: [desc: g.id] + ) + |> Repo.all() + |> Repo.preload([:light_player, :dark_player]) + |> Enum.map(&format_game_selection_tuple/1) + end + + def refresh_options(%State{ + options: options, + player_session: %PlayerSession{player_id: player_id} + }) do + previous_last_game_id = + case List.last(options) do + {_label, id} -> id + _ -> 0 + end + + current_screen_games = + get_player_sorted_current_games_with_id(player_id, previous_last_game_id - 1, :asc) + + if !is_nil(current_screen_games) && length(current_screen_games), + do: + current_screen_games + |> Enum.map(&format_game_selection_tuple/1), + else: options + end + + def make_process_tuple(selected_id, _state) do + game = Repo.get(Game, selected_id) + + {Chessh.SSH.Client.PreviousGame, + %Chessh.SSH.Client.PreviousGame.State{ + game: game + }} + end +end diff --git a/mix.exs b/mix.exs index e977912..430f0a8 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,7 @@ defmodule Chessh.MixProject do def application do [ mod: {Chessh.Application, []}, - extra_applications: [:logger, :crypto, :syn, :ssh, :plug_cowboy, :inets, :ssl] + extra_applications: [:logger, :crypto, :syn, :ssh, :plug_cowboy, :inets, :ssl, :binbo] ] end diff --git a/priv/repo/migrations/20230304031125_add_move_history.exs b/priv/repo/migrations/20230304031125_add_move_history.exs new file mode 100644 index 0000000..ce361b6 --- /dev/null +++ b/priv/repo/migrations/20230304031125_add_move_history.exs @@ -0,0 +1,9 @@ +defmodule Chessh.Repo.Migrations.AddMoveHistory do + use Ecto.Migration + + def change do + alter table(:games) do + add(:game_moves, :string, size: 1024) + end + end +end