Move history (#18)

* store move history

* Add previous game viewer
This commit is contained in:
Elizabeth Hunt 2023-03-13 14:13:11 -06:00 committed by GitHub
parent 3cb88323c2
commit a1f01d4a2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 283 additions and 46 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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