Add pagination menus
This commit is contained in:
parent
e0058fedfb
commit
06f1ca7603
@ -86,10 +86,7 @@ defmodule Chessh.PlayerSession do
|
|||||||
"Player #{player.username} has #{length(expired_sessions)} expired sessions - attempting to close them"
|
"Player #{player.username} has #{length(expired_sessions)} expired sessions - attempting to close them"
|
||||||
)
|
)
|
||||||
|
|
||||||
Enum.map(expired_sessions, fn session_id ->
|
Enum.map(expired_sessions, &close_session/1)
|
||||||
:syn.publish(:player_sessions, {:session, session_id}, :session_closed)
|
|
||||||
end)
|
|
||||||
|
|
||||||
Repo.delete_all(from(p in PlayerSession, where: p.id in ^expired_sessions))
|
Repo.delete_all(from(p in PlayerSession, where: p.id in ^expired_sessions))
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -112,8 +109,13 @@ defmodule Chessh.PlayerSession do
|
|||||||
def close_all_player_sessions(player) do
|
def close_all_player_sessions(player) do
|
||||||
player_sessions = Repo.all(from(p in PlayerSession, where: p.player_id == ^player.id))
|
player_sessions = Repo.all(from(p in PlayerSession, where: p.player_id == ^player.id))
|
||||||
|
|
||||||
Enum.map(player_sessions, fn session ->
|
Enum.map(player_sessions, &close_session(&1.id))
|
||||||
:syn.publish(:player_sessions, {:session, session.id}, :session_closed)
|
end
|
||||||
end)
|
|
||||||
|
defp close_session(id) do
|
||||||
|
case :syn_backbone.get_table_name(:syn_pg_by_name, :player_sessions) do
|
||||||
|
:undefined -> nil
|
||||||
|
_ -> :syn.publish(:player_sessions, {:session, id}, :session_closed)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -25,8 +25,8 @@ defmodule Chessh.SSH.Client do
|
|||||||
def init([%State{player_session: player_session} = state]) do
|
def init([%State{player_session: player_session} = state]) do
|
||||||
send(
|
send(
|
||||||
self(),
|
self(),
|
||||||
{:set_screen_process, Chessh.SSH.Client.Menu,
|
{:set_screen_process, Chessh.SSH.Client.MainMenu,
|
||||||
%Chessh.SSH.Client.Menu.State{player_session: player_session}}
|
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
|
@ -171,6 +171,11 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
{:noreply, new_state}
|
{:noreply, new_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info(x, state) do
|
||||||
|
Logger.debug("unknown message in game process #{inspect(x)}")
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
def input(
|
def input(
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
@ -2,7 +2,6 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
alias IO.ANSI
|
alias IO.ANSI
|
||||||
alias Chessh.{Utils, Player}
|
alias Chessh.{Utils, Player}
|
||||||
alias Chessh.SSH.Client.Game
|
alias Chessh.SSH.Client.Game
|
||||||
require Logger
|
|
||||||
|
|
||||||
@chess_board_height 8
|
@chess_board_height 8
|
||||||
@chess_board_width 8
|
@chess_board_width 8
|
||||||
@ -64,36 +63,11 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
height: _height,
|
height: _height,
|
||||||
highlighted: highlighted,
|
highlighted: highlighted,
|
||||||
flipped: flipped,
|
flipped: flipped,
|
||||||
game: %Chessh.Game{
|
game: %Chessh.Game{} = game
|
||||||
dark_player: %Player{username: dark_player},
|
|
||||||
light_player: %Player{username: light_player},
|
|
||||||
turn: turn,
|
|
||||||
status: status,
|
|
||||||
winner: winner
|
|
||||||
}
|
|
||||||
}) do
|
}) do
|
||||||
rendered = [
|
rendered = [
|
||||||
Enum.join(
|
ANSI.clear_line(),
|
||||||
[
|
make_status_line(game, true)
|
||||||
ANSI.clear_line(),
|
|
||||||
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,
|
|
||||||
ANSI.default_color()
|
|
||||||
],
|
|
||||||
""
|
|
||||||
)
|
|
||||||
| draw_board(
|
| draw_board(
|
||||||
fen,
|
fen,
|
||||||
{@tile_width, @tile_height},
|
{@tile_width, @tile_height},
|
||||||
@ -111,6 +85,77 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def make_status_line(
|
||||||
|
%Chessh.Game{
|
||||||
|
light_player: light_player
|
||||||
|
} = game,
|
||||||
|
fancy
|
||||||
|
)
|
||||||
|
when is_nil(light_player),
|
||||||
|
do:
|
||||||
|
make_status_line(
|
||||||
|
%Chessh.Game{game | light_player: %Player{username: "(no opponent)"}},
|
||||||
|
fancy
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_status_line(
|
||||||
|
%Chessh.Game{
|
||||||
|
dark_player: dark_player
|
||||||
|
} = game,
|
||||||
|
fancy
|
||||||
|
)
|
||||||
|
when is_nil(dark_player),
|
||||||
|
do:
|
||||||
|
make_status_line(
|
||||||
|
%Chessh.Game{game | dark_player: %Player{username: "(no opponent)"}},
|
||||||
|
fancy
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_status_line(
|
||||||
|
%Chessh.Game{
|
||||||
|
id: game_id,
|
||||||
|
dark_player: %Player{username: dark_player},
|
||||||
|
light_player: %Player{username: light_player},
|
||||||
|
turn: turn,
|
||||||
|
status: status,
|
||||||
|
winner: winner,
|
||||||
|
moves: moves
|
||||||
|
},
|
||||||
|
fancy
|
||||||
|
) do
|
||||||
|
Enum.join(
|
||||||
|
[
|
||||||
|
if(fancy,
|
||||||
|
do: ANSI.clear_line(),
|
||||||
|
else: ""
|
||||||
|
),
|
||||||
|
"Game #{game_id} - ",
|
||||||
|
if(fancy,
|
||||||
|
do: ANSI.format_fragment([@light_piece_color, light_player]),
|
||||||
|
else: "#{light_player} (L)"
|
||||||
|
),
|
||||||
|
"#{if fancy, do: ANSI.default_color(), else: ""} --vs-- ",
|
||||||
|
if(fancy,
|
||||||
|
do: ANSI.format_fragment([@dark_piece_color, dark_player]),
|
||||||
|
else: "#{dark_player} (D)"
|
||||||
|
),
|
||||||
|
if(fancy, do: ANSI.default_color(), else: ""),
|
||||||
|
case status do
|
||||||
|
:continue ->
|
||||||
|
", #{moves} moves, #{ANSI.format_fragment([if(fancy, do: if(turn == :light, do: @light_piece_color, else: @dark_piece_color), else: ""), if(turn == :dark, do: dark_player, else: light_player)])} to move"
|
||||||
|
|
||||||
|
:draw ->
|
||||||
|
"ended in a draw after #{moves} moves"
|
||||||
|
|
||||||
|
:winner ->
|
||||||
|
", #{ANSI.format_fragment([if(fancy, do: if(winner == :light, do: @light_piece_color, else: @dark_piece_color), else: ""), if(winner == :dark, do: dark_player, else: light_player)])} won after #{moves} moves!"
|
||||||
|
end,
|
||||||
|
if(fancy, do: ANSI.default_color(), else: "")
|
||||||
|
],
|
||||||
|
""
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp draw_board(
|
defp draw_board(
|
||||||
fen,
|
fen,
|
||||||
{tile_width, tile_height} = tile_dims,
|
{tile_width, tile_height} = tile_dims,
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
defmodule Chessh.SSH.Client.Menu do
|
|
||||||
alias IO.ANSI
|
|
||||||
alias Chessh.{Utils, Repo, Game}
|
|
||||||
import Ecto.Query
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
defmodule State do
|
|
||||||
defstruct client_pid: nil,
|
|
||||||
selected: 0,
|
|
||||||
player_session: nil,
|
|
||||||
options: []
|
|
||||||
end
|
|
||||||
|
|
||||||
@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"
|
|
||||||
use Chessh.SSH.Client.Screen
|
|
||||||
|
|
||||||
def init([%State{} = state | _]) do
|
|
||||||
{:ok, %State{state | options: options(state)}}
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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) ++
|
|
||||||
[
|
|
||||||
{"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))
|
|
||||||
}
|
|
||||||
|
|
||||||
:down ->
|
|
||||||
%State{state | selected: Utils.wrap_around(selected, 1, length(options))}
|
|
||||||
|
|
||||||
:return ->
|
|
||||||
{_option, {module, state}} = Enum.at(options, selected)
|
|
||||||
send(client_pid, {:set_screen_process, module, state})
|
|
||||||
state
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
if !(action == :return) do
|
|
||||||
send(client_pid, {:send_to_ssh, render_state(width, height, new_state)})
|
|
||||||
end
|
|
||||||
|
|
||||||
new_state
|
|
||||||
end
|
|
||||||
|
|
||||||
def render(width, height, %State{client_pid: client_pid} = state) do
|
|
||||||
send(client_pid, {:send_to_ssh, render_state(width, height, state)})
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
defp render_state(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
%State{options: options, selected: selected}
|
|
||||||
) do
|
|
||||||
logo_lines = String.split(@logo, "\n")
|
|
||||||
{logo_width, logo_height} = Utils.text_dim(@logo)
|
|
||||||
{y, x} = Utils.center_rect({logo_width, logo_height + length(logo_lines)}, {width, height})
|
|
||||||
|
|
||||||
[ANSI.clear()] ++
|
|
||||||
Enum.flat_map(
|
|
||||||
Enum.zip(1..length(logo_lines), logo_lines),
|
|
||||||
fn {i, line} ->
|
|
||||||
[
|
|
||||||
ANSI.cursor(y + i, x),
|
|
||||||
line
|
|
||||||
]
|
|
||||||
end
|
|
||||||
) ++
|
|
||||||
Enum.flat_map(
|
|
||||||
Enum.zip(0..(length(options) - 1), options),
|
|
||||||
fn {i, {option, _}} ->
|
|
||||||
[
|
|
||||||
ANSI.cursor(y + length(logo_lines) + i + 1, x),
|
|
||||||
if(i == selected,
|
|
||||||
do:
|
|
||||||
ANSI.format_fragment(
|
|
||||||
[:light_cyan, :bright, "> #{option} <", :reset],
|
|
||||||
true
|
|
||||||
),
|
|
||||||
else: option
|
|
||||||
)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
) ++ [ANSI.home()]
|
|
||||||
end
|
|
||||||
end
|
|
24
lib/chessh/ssh/client/menus/game_selector.ex
Normal file
24
lib/chessh/ssh/client/menus/game_selector.ex
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
defmodule Chessh.SSH.Client.GameSelector do
|
||||||
|
import Ecto.Query
|
||||||
|
alias Chessh.Repo
|
||||||
|
|
||||||
|
def paginate_ish_query(query, current_id, direction) do
|
||||||
|
sorted_query =
|
||||||
|
if direction == :desc,
|
||||||
|
do: from(g in query, order_by: [desc: g.id]),
|
||||||
|
else: from(g in query, order_by: [asc: g.id])
|
||||||
|
|
||||||
|
results =
|
||||||
|
if !is_nil(current_id) do
|
||||||
|
if direction == :desc,
|
||||||
|
do: from(g in sorted_query, where: g.id < ^current_id),
|
||||||
|
else: from(g in sorted_query, where: g.id > ^current_id)
|
||||||
|
else
|
||||||
|
sorted_query
|
||||||
|
end
|
||||||
|
|> Repo.all()
|
||||||
|
|> Repo.preload([:light_player, :dark_player])
|
||||||
|
|
||||||
|
if direction == :desc, do: results, else: Enum.reverse(results)
|
||||||
|
end
|
||||||
|
end
|
45
lib/chessh/ssh/client/menus/main_menu.ex
Normal file
45
lib/chessh/ssh/client/menus/main_menu.ex
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
defmodule Chessh.SSH.Client.MainMenu do
|
||||||
|
alias IO.ANSI
|
||||||
|
alias Chessh.PlayerSession
|
||||||
|
|
||||||
|
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
|
||||||
|
MMMMMMMMMMM MMMMMMMMMMM MMMMMMMMMMMM" |> String.split("\n")
|
||||||
|
@logo_cols @logo |> Enum.map(&String.length/1) |> Enum.max()
|
||||||
|
|
||||||
|
use Chessh.SSH.Client.SelectPaginatePoller
|
||||||
|
|
||||||
|
def dynamic_options(), do: false
|
||||||
|
def tick_delay_ms(), do: 1000
|
||||||
|
def max_displayed_options(), do: 4
|
||||||
|
def max_box_cols(), do: @logo_cols
|
||||||
|
def title(), do: @logo
|
||||||
|
|
||||||
|
def initial_options(%State{player_session: %PlayerSession{} = player_session}) do
|
||||||
|
[
|
||||||
|
{"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}}}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_process_tuple(selected, _state) do
|
||||||
|
selected
|
||||||
|
end
|
||||||
|
end
|
85
lib/chessh/ssh/client/menus/select_current_game.ex
Normal file
85
lib/chessh/ssh/client/menus/select_current_game.ex
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
defmodule Chessh.SSH.Client.SelectCurrentGame 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: 10
|
||||||
|
def title(), do: ["-- Current 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 make_process_tuple(selected_id, %State{
|
||||||
|
player_session: player_session
|
||||||
|
}) do
|
||||||
|
game = Repo.get(Game, selected_id)
|
||||||
|
|
||||||
|
{Chessh.SSH.Client.Game,
|
||||||
|
%Chessh.SSH.Client.Game.State{
|
||||||
|
player_session: player_session,
|
||||||
|
game: game
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
87
lib/chessh/ssh/client/menus/select_joinable_game.ex
Normal file
87
lib/chessh/ssh/client/menus/select_joinable_game.ex
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
defmodule Chessh.SSH.Client.SelectJoinableGame do
|
||||||
|
alias Chessh.{Utils, Repo, Game, PlayerSession}
|
||||||
|
alias Chessh.SSH.Client.GameSelector
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
use Chessh.SSH.Client.SelectPaginatePoller
|
||||||
|
|
||||||
|
def refresh_options_ms(), do: 4000
|
||||||
|
def max_displayed_options(), do: 1
|
||||||
|
def title(), do: ["-- Joinable Games --"]
|
||||||
|
def dynamic_options(), do: true
|
||||||
|
|
||||||
|
def get_player_joinable_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do
|
||||||
|
GameSelector.paginate_ish_query(
|
||||||
|
Game
|
||||||
|
|> where([g], g.status == :continue)
|
||||||
|
|> where(
|
||||||
|
[g],
|
||||||
|
(is_nil(g.dark_player_id) or g.dark_player_id != ^player_id) and
|
||||||
|
(is_nil(g.light_player_id) or g.light_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_joinable_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_joinable_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_joinable_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 make_process_tuple(selected_id, %State{
|
||||||
|
player_session: player_session
|
||||||
|
}) do
|
||||||
|
game = Repo.get(Game, selected_id)
|
||||||
|
|
||||||
|
{Chessh.SSH.Client.Game,
|
||||||
|
%Chessh.SSH.Client.Game.State{
|
||||||
|
player_session: player_session,
|
||||||
|
game: game
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
263
lib/chessh/ssh/client/menus/select_paginate_poller.ex
Normal file
263
lib/chessh/ssh/client/menus/select_paginate_poller.ex
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
defmodule Chessh.SSH.Client.SelectPaginatePoller do
|
||||||
|
@callback dynamic_options() :: boolean()
|
||||||
|
|
||||||
|
@callback tick_delay_ms() :: integer()
|
||||||
|
@callback max_displayed_options() :: integer()
|
||||||
|
@callback max_box_cols() :: integer()
|
||||||
|
@callback make_process_tuple(selected :: any(), state :: any()) ::
|
||||||
|
{module :: module(), state :: any()}
|
||||||
|
|
||||||
|
@callback initial_options(state :: any()) ::
|
||||||
|
[{line :: any(), selected :: any()}]
|
||||||
|
|
||||||
|
@callback refresh_options_ms() :: integer()
|
||||||
|
@callback refresh_options(state :: any()) ::
|
||||||
|
[{line :: any(), selected :: any()}]
|
||||||
|
@callback next_page_options(state :: any()) ::
|
||||||
|
[{line :: any(), selected :: any()}]
|
||||||
|
@callback previous_page_options(state :: any()) ::
|
||||||
|
[{line :: any(), selected :: any()}]
|
||||||
|
|
||||||
|
@callback title() :: [any()]
|
||||||
|
|
||||||
|
defmodule State do
|
||||||
|
defstruct client_pid: nil,
|
||||||
|
selected_option_idx: 0,
|
||||||
|
player_session: nil,
|
||||||
|
options: [],
|
||||||
|
tick: 0,
|
||||||
|
cursor: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
defmacro __using__(_) do
|
||||||
|
quote do
|
||||||
|
@behaviour Chessh.SSH.Client.SelectPaginatePoller
|
||||||
|
use Chessh.SSH.Client.Screen
|
||||||
|
|
||||||
|
alias IO.ANSI
|
||||||
|
alias Chessh.{Utils, PlayerSession}
|
||||||
|
alias Chessh.SSH.Client.SelectPaginatePoller.State
|
||||||
|
alias Chessh.SSH.Client.SelectPaginatePoller
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def init([%State{} = state | _]) do
|
||||||
|
if dynamic_options() do
|
||||||
|
Process.send_after(self(), :refresh_options, refresh_options_ms())
|
||||||
|
end
|
||||||
|
|
||||||
|
Process.send_after(self(), :tick, tick_delay_ms())
|
||||||
|
|
||||||
|
{:ok, %State{state | options: initial_options(state)}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(
|
||||||
|
:refresh_options,
|
||||||
|
%State{
|
||||||
|
selected_option_idx: selected_option_idx,
|
||||||
|
tick: tick,
|
||||||
|
client_pid: client_pid
|
||||||
|
} = state
|
||||||
|
) do
|
||||||
|
if dynamic_options() do
|
||||||
|
options = refresh_options(state)
|
||||||
|
Process.send_after(self(), :refresh_options, refresh_options_ms())
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
%State{
|
||||||
|
state
|
||||||
|
| selected_option_idx: min(selected_option_idx, length(options) - 1),
|
||||||
|
options: options
|
||||||
|
}}
|
||||||
|
else
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(
|
||||||
|
:tick,
|
||||||
|
%State{
|
||||||
|
tick: tick,
|
||||||
|
client_pid: client_pid
|
||||||
|
} = state
|
||||||
|
) do
|
||||||
|
Process.send_after(self(), :tick, tick_delay_ms())
|
||||||
|
|
||||||
|
if client_pid do
|
||||||
|
send(client_pid, :refresh)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, %State{state | tick: tick + 1}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(
|
||||||
|
x,
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
Logger.debug("unknown message in pagination poller - #{inspect(x)}")
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
%State{
|
||||||
|
client_pid: client_pid
|
||||||
|
} = state
|
||||||
|
) do
|
||||||
|
send(
|
||||||
|
client_pid,
|
||||||
|
{:send_to_ssh, render_state(width, height, state)}
|
||||||
|
)
|
||||||
|
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
|
def input(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
action,
|
||||||
|
%State{
|
||||||
|
client_pid: client_pid,
|
||||||
|
options: options,
|
||||||
|
selected_option_idx: selected_option_idx
|
||||||
|
} = state
|
||||||
|
) do
|
||||||
|
max_item = min(length(options), max_displayed_options())
|
||||||
|
|
||||||
|
new_state =
|
||||||
|
if(max_item > 0,
|
||||||
|
do:
|
||||||
|
case action do
|
||||||
|
:up ->
|
||||||
|
%State{
|
||||||
|
state
|
||||||
|
| selected_option_idx: Utils.wrap_around(selected_option_idx, -1, max_item),
|
||||||
|
tick: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:down ->
|
||||||
|
%State{
|
||||||
|
state
|
||||||
|
| selected_option_idx: Utils.wrap_around(selected_option_idx, 1, max_item),
|
||||||
|
tick: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:left ->
|
||||||
|
if dynamic_options(),
|
||||||
|
do: %State{
|
||||||
|
state
|
||||||
|
| options: previous_page_options(state),
|
||||||
|
selected_option_idx: 0,
|
||||||
|
tick: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:right ->
|
||||||
|
if dynamic_options(),
|
||||||
|
do: %State{
|
||||||
|
state
|
||||||
|
| options: next_page_options(state),
|
||||||
|
selected_option_idx: 0,
|
||||||
|
tick: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
:return ->
|
||||||
|
{_, selected} = Enum.at(options, selected_option_idx)
|
||||||
|
{module, state} = make_process_tuple(selected, state)
|
||||||
|
send(client_pid, {:set_screen_process, module, state})
|
||||||
|
state
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
) || state
|
||||||
|
|
||||||
|
if !(action == :return) do
|
||||||
|
render(width, height, new_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
new_state
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_state(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
%State{} = state
|
||||||
|
) do
|
||||||
|
lines =
|
||||||
|
title() ++
|
||||||
|
[""] ++
|
||||||
|
render_lines(width, height, state) ++
|
||||||
|
if dynamic_options(), do: ["", "<= Previous | Next =>"], else: []
|
||||||
|
|
||||||
|
{y, x} = Utils.center_rect({min(width, max_box_cols()), length(lines)}, {width, height})
|
||||||
|
|
||||||
|
[ANSI.clear()] ++
|
||||||
|
Enum.flat_map(
|
||||||
|
Enum.zip(0..(length(lines) - 1), lines),
|
||||||
|
fn {i, line} ->
|
||||||
|
[
|
||||||
|
ANSI.cursor(y + i, x),
|
||||||
|
line
|
||||||
|
]
|
||||||
|
end
|
||||||
|
) ++ [ANSI.home()]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_lines(width, _height, %State{
|
||||||
|
tick: tick,
|
||||||
|
options: options,
|
||||||
|
selected_option_idx: selected_option_idx
|
||||||
|
}) do
|
||||||
|
if options && length(options) > 0 do
|
||||||
|
Enum.map(
|
||||||
|
Enum.zip(0..(max_displayed_options() - 1), options),
|
||||||
|
fn {i, {line, _}} ->
|
||||||
|
box_cols = min(max_box_cols(), width)
|
||||||
|
linelen = String.length(line)
|
||||||
|
|
||||||
|
line =
|
||||||
|
if linelen > box_cols do
|
||||||
|
delta = max(box_cols - 3 - 1, 0)
|
||||||
|
overflow = linelen - delta
|
||||||
|
start = if i == selected_option_idx, do: rem(tick, overflow), else: 0
|
||||||
|
"#{String.slice(line, start..(start + delta))}..."
|
||||||
|
else
|
||||||
|
line
|
||||||
|
end
|
||||||
|
|
||||||
|
if i == selected_option_idx do
|
||||||
|
ANSI.format_fragment(
|
||||||
|
[:light_cyan, :bright, "> #{line} <", :reset],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
else
|
||||||
|
line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
else
|
||||||
|
["Looks like there's nothing here.", "Use Ctrl+b to go back"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_options_ms(), do: 3000
|
||||||
|
def next_page_options(%State{options: options}), do: options
|
||||||
|
def previous_page_options(%State{options: options}), do: options
|
||||||
|
def refresh_options(%State{options: options}), do: options
|
||||||
|
|
||||||
|
def tick_delay_ms(), do: 1000
|
||||||
|
def max_displayed_options(), do: 10
|
||||||
|
def max_box_cols(), do: 90
|
||||||
|
|
||||||
|
defoverridable refresh_options_ms: 0,
|
||||||
|
next_page_options: 1,
|
||||||
|
previous_page_options: 1,
|
||||||
|
refresh_options: 1,
|
||||||
|
tick_delay_ms: 0,
|
||||||
|
max_displayed_options: 0,
|
||||||
|
max_box_cols: 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user