Persistent game #5
@ -1,6 +1 @@
|
|||||||
# CheSSH
|
# CheSSH
|
||||||
|
|
||||||
Features:
|
|
||||||
- [X] SSH Key & Password authentication
|
|
||||||
- [X] Session rate limiting
|
|
||||||
- [X] Multi-node support
|
|
||||||
|
32
lib/chessh/schema/game.ex
Normal file
32
lib/chessh/schema/game.ex
Normal file
@ -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
|
@ -1,6 +1,7 @@
|
|||||||
defmodule Chessh.Player do
|
defmodule Chessh.Player do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
alias Chessh.{Key, Game}
|
||||||
|
|
||||||
@derive {Inspect, except: [:password]}
|
@derive {Inspect, except: [:password]}
|
||||||
schema "players" do
|
schema "players" do
|
||||||
@ -11,7 +12,9 @@ defmodule Chessh.Player do
|
|||||||
|
|
||||||
field(:authentications, :integer, default: 0)
|
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()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -24,8 +24,13 @@ defmodule Chessh.SSH.Client do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init([%State{} = state]) do
|
def init([%State{player_session: player_session} = state]) do
|
||||||
send(self(), {:set_screen_process, Chessh.SSH.Client.Menu, %Chessh.SSH.Client.Menu.State{}})
|
send(
|
||||||
|
self(),
|
||||||
|
{:set_screen_process, Chessh.SSH.Client.Menu,
|
||||||
|
%Chessh.SSH.Client.Menu.State{player_session: player_session}}
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -56,6 +61,11 @@ defmodule Chessh.SSH.Client do
|
|||||||
}}
|
}}
|
||||||
end
|
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
|
@impl true
|
||||||
def handle_info(:quit, %State{} = state) do
|
def handle_info(:quit, %State{} = state) do
|
||||||
{:stop, :normal, state}
|
{:stop, :normal, state}
|
||||||
@ -87,8 +97,7 @@ defmodule Chessh.SSH.Client do
|
|||||||
%State{
|
%State{
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
screen_pid: screen_pid,
|
screen_pid: screen_pid
|
||||||
screen_state_initials: [_ | rest_initial]
|
|
||||||
} = state
|
} = state
|
||||||
) do
|
) do
|
||||||
case keymap(data) do
|
case keymap(data) do
|
||||||
@ -96,10 +105,7 @@ defmodule Chessh.SSH.Client do
|
|||||||
{:stop, :normal, state}
|
{:stop, :normal, state}
|
||||||
|
|
||||||
:previous_screen ->
|
:previous_screen ->
|
||||||
[{prev_module, prev_state_initial} | _] = rest_initial
|
{:noreply, go_back_one_screen(state)}
|
||||||
send(self(), {:set_screen_process, prev_module, prev_state_initial})
|
|
||||||
|
|
||||||
{:noreply, %State{state | screen_state_initials: rest_initial}}
|
|
||||||
|
|
||||||
action ->
|
action ->
|
||||||
send(screen_pid, {:input, width, height, action})
|
send(screen_pid, {:input, width, height, action})
|
||||||
@ -181,4 +187,25 @@ defmodule Chessh.SSH.Client do
|
|||||||
send(screen_pid, {:render, width, height})
|
send(screen_pid, {:render, width, height})
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
359
lib/chessh/ssh/client/game/game.ex
Normal file
359
lib/chessh/ssh/client/game/game.ex
Normal file
@ -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
|
63
lib/chessh/ssh/client/game/promotion.ex
Normal file
63
lib/chessh/ssh/client/game/promotion.ex
Normal file
@ -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
|
@ -1,7 +1,7 @@
|
|||||||
defmodule Chessh.SSH.Client.Board.Renderer do
|
defmodule Chessh.SSH.Client.Game.Renderer do
|
||||||
alias IO.ANSI
|
alias IO.ANSI
|
||||||
alias Chessh.Utils
|
alias Chessh.{Utils, Player}
|
||||||
alias Chessh.SSH.Client.Board
|
alias Chessh.SSH.Client.Game
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@chess_board_height 8
|
@chess_board_height 8
|
||||||
@ -25,23 +25,85 @@ defmodule Chessh.SSH.Client.Board.Renderer do
|
|||||||
"#{List.to_string([?a + x])}#{@chess_board_height - y}"
|
"#{List.to_string([?a + x])}#{@chess_board_height - y}"
|
||||||
end
|
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,
|
width: _width,
|
||||||
height: _height,
|
height: _height,
|
||||||
highlighted: highlighted,
|
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
|
}) do
|
||||||
board =
|
rendered = [
|
||||||
draw_board(
|
Enum.join(
|
||||||
fen,
|
[
|
||||||
{@tile_width, @tile_height},
|
ANSI.clear_line(),
|
||||||
highlighted,
|
"Game #{game_id}: ",
|
||||||
flipped
|
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()] ++
|
[ANSI.home()] ++
|
||||||
Enum.map(
|
Enum.map(
|
||||||
Enum.zip(1..length(board), board),
|
Enum.zip(1..length(rendered), rendered),
|
||||||
fn {i, line} ->
|
fn {i, line} ->
|
||||||
[ANSI.cursor(i, 0), line]
|
[ANSI.cursor(i, 0), line]
|
||||||
end
|
end
|
@ -1,12 +1,15 @@
|
|||||||
defmodule Chessh.SSH.Client.Menu do
|
defmodule Chessh.SSH.Client.Menu do
|
||||||
alias Chessh.Utils
|
|
||||||
alias IO.ANSI
|
alias IO.ANSI
|
||||||
|
alias Chessh.{Utils, Repo, Game}
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
defmodule State do
|
defmodule State do
|
||||||
defstruct client_pid: nil,
|
defstruct client_pid: nil,
|
||||||
selected: 0
|
selected: 0,
|
||||||
|
player_session: nil,
|
||||||
|
options: []
|
||||||
end
|
end
|
||||||
|
|
||||||
@logo " Simponic's
|
@logo " Simponic's
|
||||||
@ -20,31 +23,70 @@ defmodule Chessh.SSH.Client.Menu do
|
|||||||
use Chessh.SSH.Client.Screen
|
use Chessh.SSH.Client.Screen
|
||||||
|
|
||||||
def init([%State{} = state | _]) do
|
def init([%State{} = state | _]) do
|
||||||
{:ok, state}
|
{:ok, %State{state | options: options(state)}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@options [
|
def options(%State{player_session: player_session}) do
|
||||||
{"Start A Game", {Chessh.SSH.Client.Board, %Chessh.SSH.Client.Board.State{}}},
|
current_games =
|
||||||
{"Join A Game", {}},
|
Repo.all(
|
||||||
{"My Games", {}},
|
from(g in Game,
|
||||||
{"Settings", {}},
|
where: g.light_player_id == ^player_session.player_id,
|
||||||
{"Help", {}}
|
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 =
|
new_state =
|
||||||
case(action) do
|
case(action) do
|
||||||
:up ->
|
:up ->
|
||||||
%State{
|
%State{
|
||||||
state
|
state
|
||||||
| selected: Utils.wrap_around(selected, -1, length(@options))
|
| selected: Utils.wrap_around(selected, -1, length(options))
|
||||||
}
|
}
|
||||||
|
|
||||||
:down ->
|
:down ->
|
||||||
%State{state | selected: Utils.wrap_around(selected, 1, length(@options))}
|
%State{state | selected: Utils.wrap_around(selected, 1, length(options))}
|
||||||
|
|
||||||
:return ->
|
:return ->
|
||||||
{_option, {module, state}} = Enum.at(@options, selected)
|
{_option, {module, state}} = Enum.at(options, selected)
|
||||||
send(client_pid, {:set_screen_process, module, state})
|
send(client_pid, {:set_screen_process, module, state})
|
||||||
state
|
state
|
||||||
|
|
||||||
@ -67,7 +109,7 @@ defmodule Chessh.SSH.Client.Menu do
|
|||||||
defp render_state(
|
defp render_state(
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
%State{selected: selected}
|
%State{options: options, selected: selected}
|
||||||
) do
|
) do
|
||||||
logo_lines = String.split(@logo, "\n")
|
logo_lines = String.split(@logo, "\n")
|
||||||
{logo_width, logo_height} = Utils.text_dim(@logo)
|
{logo_width, logo_height} = Utils.text_dim(@logo)
|
||||||
@ -84,7 +126,7 @@ defmodule Chessh.SSH.Client.Menu do
|
|||||||
end
|
end
|
||||||
) ++
|
) ++
|
||||||
Enum.flat_map(
|
Enum.flat_map(
|
||||||
Enum.zip(0..(length(@options) - 1), @options),
|
Enum.zip(0..(length(options) - 1), options),
|
||||||
fn {i, {option, _}} ->
|
fn {i, {option, _}} ->
|
||||||
[
|
[
|
||||||
ANSI.cursor(y + length(logo_lines) + i + 1, x),
|
ANSI.cursor(y + length(logo_lines) + i + 1, x),
|
||||||
|
@ -144,7 +144,6 @@ defmodule Chessh.SSH.Tui do
|
|||||||
{:ssh_cm, connection_handler, {:shell, channel_id, want_reply?}},
|
{:ssh_cm, connection_handler, {:shell, channel_id, want_reply?}},
|
||||||
%{width: width, height: height, player_session: player_session} = state
|
%{width: width, height: height, player_session: player_session} = state
|
||||||
) do
|
) do
|
||||||
Logger.debug("Session #{player_session.id} requested shell")
|
|
||||||
:ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
|
:ssh_connection.reply_request(connection_handler, want_reply?, :success, channel_id)
|
||||||
|
|
||||||
{:ok, client_pid} =
|
{:ok, client_pid} =
|
||||||
@ -161,47 +160,6 @@ defmodule Chessh.SSH.Tui do
|
|||||||
{:ok, %State{state | client_pid: client_pid}}
|
{:ok, %State{state | client_pid: client_pid}}
|
||||||
end
|
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(
|
def handle_ssh_msg(
|
||||||
msg,
|
msg,
|
||||||
%State{channel_id: channel_id} = state
|
%State{channel_id: channel_id} = state
|
||||||
|
19
priv/repo/migrations/20230115201050_create_games.exs
Normal file
19
priv/repo/migrations/20230115201050_create_games.exs
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user