summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLizzy Hunt <elizabeth.hunt@simponic.xyz>2023-03-13 14:13:11 -0600
committerGitHub <noreply@github.com>2023-03-13 14:13:11 -0600
commita1f01d4a2b1ce2a167b7fec1506f3970bf13ea79 (patch)
tree3eb1a991569da428a77155b6e6bb601d00c3eb98
parent3cb88323c22e72f36d82ae5ada384311123eae67 (diff)
downloadchessh-a1f01d4a2b1ce2a167b7fec1506f3970bf13ea79.tar.gz
chessh-a1f01d4a2b1ce2a167b7fec1506f3970bf13ea79.zip
Move history (#18)
* store move history * Add previous game viewer
-rw-r--r--lib/chessh/schema/game.ex5
-rw-r--r--lib/chessh/ssh/client/game/game.ex37
-rw-r--r--lib/chessh/ssh/client/game/previous_game.ex121
-rw-r--r--lib/chessh/ssh/client/game/renderer.ex20
-rw-r--r--lib/chessh/ssh/client/menus/main_menu.ex33
-rw-r--r--lib/chessh/ssh/client/menus/select_previous_game.ex102
-rw-r--r--mix.exs2
-rw-r--r--priv/repo/migrations/20230304031125_add_move_history.exs9
8 files changed, 283 insertions, 46 deletions
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