Bots #23
@ -7,6 +7,7 @@ config :chessh,
|
|||||||
ascii_chars_json_file: Path.join(Path.dirname(__DIR__), "priv/ascii_chars.json")
|
ascii_chars_json_file: Path.join(Path.dirname(__DIR__), "priv/ascii_chars.json")
|
||||||
|
|
||||||
config :chessh, RateLimits,
|
config :chessh, RateLimits,
|
||||||
|
player_bots: 2,
|
||||||
jail_timeout_ms: 5 * 60 * 1000,
|
jail_timeout_ms: 5 * 60 * 1000,
|
||||||
jail_attempt_threshold: 15,
|
jail_attempt_threshold: 15,
|
||||||
max_concurrent_user_sessions: 5,
|
max_concurrent_user_sessions: 5,
|
||||||
@ -16,7 +17,9 @@ config :chessh, RateLimits,
|
|||||||
create_game_ms: 60 * 1000,
|
create_game_ms: 60 * 1000,
|
||||||
create_game_rate: 3,
|
create_game_rate: 3,
|
||||||
discord_notification_rate: 30,
|
discord_notification_rate: 30,
|
||||||
discord_notification_rate_ms: 1000
|
discord_notification_rate_ms: 1000,
|
||||||
|
bot_redrive_rate: 1,
|
||||||
|
bot_redrive_rate_ms: 60_000
|
||||||
|
|
||||||
config :chessh, Web,
|
config :chessh, Web,
|
||||||
discord_oauth_login_url: "https://discord.com/api/oauth2/token",
|
discord_oauth_login_url: "https://discord.com/api/oauth2/token",
|
||||||
|
@ -69,7 +69,7 @@ body {
|
|||||||
.navbar {
|
.navbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
@ -7,6 +7,7 @@ import { Root } from "./root";
|
|||||||
import { Demo } from "./routes/demo";
|
import { Demo } from "./routes/demo";
|
||||||
import { Home } from "./routes/home";
|
import { Home } from "./routes/home";
|
||||||
import { Keys } from "./routes/keys";
|
import { Keys } from "./routes/keys";
|
||||||
|
import { Bots } from "./routes/bots";
|
||||||
import { ManPages } from "./routes/man_pages";
|
import { ManPages } from "./routes/man_pages";
|
||||||
import { Password } from "./routes/password";
|
import { Password } from "./routes/password";
|
||||||
import { AuthSuccessful } from "./routes/auth_successful";
|
import { AuthSuccessful } from "./routes/auth_successful";
|
||||||
@ -36,6 +37,10 @@ const router = createBrowserRouter([
|
|||||||
path: "auth-successful",
|
path: "auth-successful",
|
||||||
element: <AuthSuccessful />,
|
element: <AuthSuccessful />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "bots",
|
||||||
|
element: <Bots />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "man-pages",
|
path: "man-pages",
|
||||||
element: <ManPages />,
|
element: <ManPages />,
|
||||||
|
@ -10,18 +10,18 @@ export const Root = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="navbar">
|
|
||||||
<div className="flex-row-around">
|
<div className="flex-row-around">
|
||||||
<Link to="/home">
|
<Link to="/home">
|
||||||
<img src={logo} className="logo" alt="CheSSH Logo" />
|
<img src={logo} className="logo" alt="CheSSH Logo" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="navbar">
|
||||||
<div className="nav">
|
<div className="nav">
|
||||||
<Link className="link" to="/man-pages">
|
|
||||||
Man Pages
|
|
||||||
</Link>
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<>
|
<>
|
||||||
|
<Link className="button" onClick={signOut} to="/">
|
||||||
|
Sign Out
|
||||||
|
</Link>
|
||||||
<Link className="link" to="/home">
|
<Link className="link" to="/home">
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
@ -31,8 +31,8 @@ export const Root = () => {
|
|||||||
<Link className="link" to="/keys">
|
<Link className="link" to="/keys">
|
||||||
Keys
|
Keys
|
||||||
</Link>
|
</Link>
|
||||||
<Link className="button" onClick={signOut} to="/">
|
<Link className="link" to="/bots">
|
||||||
Sign Out
|
Bots
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -45,6 +45,10 @@ export const Root = () => {
|
|||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Link className="link" to="/man-pages">
|
||||||
|
Man Pages
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
|
194
front/src/routes/bots.jsx
Normal file
194
front/src/routes/bots.jsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import Modal from "react-modal";
|
||||||
|
import { useAuthContext } from "../context/auth_context";
|
||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
|
Modal.setAppElement("#root");
|
||||||
|
|
||||||
|
const BotButton = ({ onSave, givenBot }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState(givenBot?.name || "");
|
||||||
|
const [webhook, setWebhook] = useState(givenBot?.webhook || "");
|
||||||
|
const [errors, setErrors] = useState(null);
|
||||||
|
const [isPublic, setIsPublic] = useState(givenBot?.public || false);
|
||||||
|
|
||||||
|
const setDefaults = () => {
|
||||||
|
setName("");
|
||||||
|
setWebhook("");
|
||||||
|
setErrors(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (!givenBot) {
|
||||||
|
setDefaults();
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBot = () => {
|
||||||
|
fetch(givenBot ? `/api/player/bots/${givenBot.id}` : "/api/player/bots", {
|
||||||
|
credentials: "same-origin",
|
||||||
|
method: givenBot ? "PUT" : "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
webhook: webhook.trim(),
|
||||||
|
name: name.trim(),
|
||||||
|
public: isPublic,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => {
|
||||||
|
if (d.success) {
|
||||||
|
if (onSave) {
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
} else if (d.errors) {
|
||||||
|
if (typeof d.errors === "object") {
|
||||||
|
setErrors(
|
||||||
|
Object.keys(d.errors).map(
|
||||||
|
(field) => `${field}: ${d.errors[field].join(",")}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setErrors([d.errors]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button className="button" onClick={() => setOpen(true)}>
|
||||||
|
{givenBot ? "Update" : "+ Add"} Bot
|
||||||
|
</button>
|
||||||
|
{givenBot && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
style={{ marginLeft: "1rem" }}
|
||||||
|
className="button gold"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(givenBot?.token);
|
||||||
|
alert("Bot's token was copied to the clipboard.");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Token
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{ marginLeft: "1rem" }}
|
||||||
|
className="button red"
|
||||||
|
onClick={() =>
|
||||||
|
fetch(`/api/player/bots/${givenBot.id}/redrive`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then(({ message }) => alert(message))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Schedule Redrive
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Modal
|
||||||
|
isOpen={open}
|
||||||
|
onRequestClose={close}
|
||||||
|
className="modal"
|
||||||
|
contentLabel="Add Bot"
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: "20vw" }}>
|
||||||
|
<h3>Add Bot</h3>
|
||||||
|
<hr />
|
||||||
|
<p>Bot Name *</p>
|
||||||
|
<input
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Webhook *</p>
|
||||||
|
<input
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
value={webhook}
|
||||||
|
onChange={(e) => setWebhook(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Public *{" "}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={name}
|
||||||
|
checked={isPublic}
|
||||||
|
onChange={(e) => setIsPublic(!isPublic)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
{errors && (
|
||||||
|
<div style={{ color: "red" }}>
|
||||||
|
{errors.map((error, i) => (
|
||||||
|
<p key={i}>{error}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-end-row">
|
||||||
|
<button className="button" onClick={updateBot}>
|
||||||
|
{givenBot ? "Update" : "+ Add"}
|
||||||
|
</button>
|
||||||
|
<button className="button red" onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BotCard = ({ botStruct, onSave }) => {
|
||||||
|
const { name, token } = botStruct;
|
||||||
|
return (
|
||||||
|
<div className="key-card">
|
||||||
|
<h4>{name}</h4>
|
||||||
|
<BotButton onSave={onSave} givenBot={botStruct} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Bots = () => {
|
||||||
|
const {
|
||||||
|
player: { id: userId },
|
||||||
|
} = useAuthContext();
|
||||||
|
const [bots, setBots] = useState(null);
|
||||||
|
|
||||||
|
const refreshBots = () =>
|
||||||
|
fetch("/api/player/bots")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((bots) => setBots(bots));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) {
|
||||||
|
refreshBots();
|
||||||
|
}
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
if (bots === null) return <p>Loading...</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Bots</h1>
|
||||||
|
<BotButton onSave={refreshBots} />
|
||||||
|
|
||||||
|
<div className="key-card-collection">
|
||||||
|
{bots.length ? (
|
||||||
|
bots.map((bot) => (
|
||||||
|
<BotCard key={bot.id} onSave={refreshBots} botStruct={bot} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p>Looks like you've got no bots, try adding one!</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -14,16 +14,14 @@ const minimizeKey = (key) => {
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeyCard = ({ onDelete, props }) => {
|
const KeyCard = ({ onDelete, keyStruct: { id, name, key } }) => {
|
||||||
const { id, name, key } = props;
|
|
||||||
|
|
||||||
const deleteThisKey = () => {
|
const deleteThisKey = () => {
|
||||||
if (
|
if (
|
||||||
window.confirm(
|
window.confirm(
|
||||||
"Are you sure? This will close all your currently opened ssh sessions."
|
"Are you sure? This will close all your currently opened ssh sessions."
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
fetch(`/api/keys/${id}`, {
|
fetch(`/api/player/keys/${id}`, {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
})
|
})
|
||||||
@ -182,7 +180,7 @@ export const Keys = () => {
|
|||||||
}
|
}
|
||||||
}, [userId, refreshKeys]);
|
}, [userId, refreshKeys]);
|
||||||
|
|
||||||
if (!keys) return <p>Loading...</p>;
|
if (keys === null) return <p>Loading...</p>;
|
||||||
|
|
||||||
if (Array.isArray(keys)) {
|
if (Array.isArray(keys)) {
|
||||||
return (
|
return (
|
||||||
@ -192,7 +190,7 @@ export const Keys = () => {
|
|||||||
<div className="key-card-collection">
|
<div className="key-card-collection">
|
||||||
{keys.length ? (
|
{keys.length ? (
|
||||||
keys.map((key) => (
|
keys.map((key) => (
|
||||||
<KeyCard key={key.id} onDelete={refreshKeys} props={key} />
|
<KeyCard key={key.id} onDelete={refreshKeys} keyStruct={key} />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p>Looks like you've got no keys, try adding some!</p>
|
<p>Looks like you've got no keys, try adding some!</p>
|
||||||
|
79
lib/chessh/schema/bot.ex
Normal file
79
lib/chessh/schema/bot.ex
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
defmodule Chessh.Bot do
|
||||||
|
alias Chessh.{Player, Game, Repo}
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Query
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@derive {Jason.Encoder, only: [:id, :name, :webhook, :token, :public]}
|
||||||
|
schema "bots" do
|
||||||
|
field(:name, :string)
|
||||||
|
field(:webhook, :string)
|
||||||
|
field(:token, :string)
|
||||||
|
field(:public, :boolean, default: false)
|
||||||
|
|
||||||
|
belongs_to(:player, Player, foreign_key: :player_id)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(game, attrs) do
|
||||||
|
game
|
||||||
|
|> cast(attrs, [
|
||||||
|
:public,
|
||||||
|
:name,
|
||||||
|
:webhook,
|
||||||
|
:token,
|
||||||
|
:player_id
|
||||||
|
])
|
||||||
|
|> validate_required([:name, :webhook, :token, :public])
|
||||||
|
|> validate_format(:webhook, ~r/^https:\/\//, message: "must start with https://")
|
||||||
|
|> unique_constraint(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_game_status_message(%Game{
|
||||||
|
id: game_id,
|
||||||
|
bot: %Chessh.Bot{id: bot_id, name: bot_name},
|
||||||
|
fen: fen,
|
||||||
|
turn: turn,
|
||||||
|
last_move: last_move,
|
||||||
|
status: status,
|
||||||
|
light_player_id: light_player_id,
|
||||||
|
dark_player_id: dark_player_id
|
||||||
|
}) do
|
||||||
|
%{
|
||||||
|
bot_id: bot_id,
|
||||||
|
bot_name: bot_name,
|
||||||
|
game_id: game_id,
|
||||||
|
fen: fen,
|
||||||
|
turn: Atom.to_string(turn),
|
||||||
|
bot_turn:
|
||||||
|
(is_nil(light_player_id) && turn == :light) || (is_nil(dark_player_id) && turn == :dark),
|
||||||
|
last_move: last_move,
|
||||||
|
status: Atom.to_string(status)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def redrive_games(%Chessh.Bot{id: bot_id, webhook: webhook}) do
|
||||||
|
messages =
|
||||||
|
Repo.all(from(g in Game, where: g.bot_id == ^bot_id))
|
||||||
|
|> Repo.preload([:bot])
|
||||||
|
|> Enum.map(&make_game_status_message/1)
|
||||||
|
|
||||||
|
send_message(webhook, messages)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_update(%Game{bot: %Chessh.Bot{webhook: webhook}} = game) do
|
||||||
|
send_message(webhook, make_game_status_message(game))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_message(webhook, msg) do
|
||||||
|
:httpc.request(
|
||||||
|
:post,
|
||||||
|
{String.to_charlist(webhook), [], 'application/json', Jason.encode!(msg)},
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -1,8 +1,10 @@
|
|||||||
defmodule Chessh.Game do
|
defmodule Chessh.Game do
|
||||||
alias Chessh.Player
|
alias Chessh.{Bot, Player, Game}
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@default_fen "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||||
|
|
||||||
schema "games" do
|
schema "games" do
|
||||||
field(:fen, :string)
|
field(:fen, :string)
|
||||||
field(:moves, :integer, default: 0)
|
field(:moves, :integer, default: 0)
|
||||||
@ -17,6 +19,8 @@ defmodule Chessh.Game do
|
|||||||
belongs_to(:light_player, Player, foreign_key: :light_player_id)
|
belongs_to(:light_player, Player, foreign_key: :light_player_id)
|
||||||
belongs_to(:dark_player, Player, foreign_key: :dark_player_id)
|
belongs_to(:dark_player, Player, foreign_key: :dark_player_id)
|
||||||
|
|
||||||
|
belongs_to(:bot, Bot, foreign_key: :bot_id)
|
||||||
|
|
||||||
field(:discord_thread_id, :string)
|
field(:discord_thread_id, :string)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
@ -34,7 +38,51 @@ defmodule Chessh.Game do
|
|||||||
:light_player_id,
|
:light_player_id,
|
||||||
:dark_player_id,
|
:dark_player_id,
|
||||||
:discord_thread_id,
|
:discord_thread_id,
|
||||||
|
:bot_id,
|
||||||
:game_moves
|
:game_moves
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new_game(initial_player_color, player_id, fen \\ @default_fen) do
|
||||||
|
Game.changeset(
|
||||||
|
%Game{
|
||||||
|
fen: fen
|
||||||
|
},
|
||||||
|
if(initial_player_color == :light,
|
||||||
|
do: %{light_player_id: player_id},
|
||||||
|
else: %{dark_player_id: player_id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_with_status(%Game{} = game, move, fen, status) do
|
||||||
|
Game.changeset(
|
||||||
|
game,
|
||||||
|
%{
|
||||||
|
fen: fen,
|
||||||
|
moves: game.moves + 1,
|
||||||
|
turn: if(game.turn == :dark, do: :light, else: :dark),
|
||||||
|
last_move: move,
|
||||||
|
game_moves:
|
||||||
|
if(!is_nil(game) && game.game_moves != "", do: "#{game.game_moves} ", else: "") <> move
|
||||||
|
}
|
||||||
|
|> Map.merge(changeset_from_status(status))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset_from_status(game_status) do
|
||||||
|
case game_status do
|
||||||
|
:continue ->
|
||||||
|
%{}
|
||||||
|
|
||||||
|
{:draw, _} ->
|
||||||
|
%{status: :draw}
|
||||||
|
|
||||||
|
{:checkmate, :white_wins} ->
|
||||||
|
%{status: :winner, winner: :light}
|
||||||
|
|
||||||
|
{:checkmate, :black_wins} ->
|
||||||
|
%{status: :winner, winner: :dark}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +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}
|
alias Chessh.{Key, Game, Bot}
|
||||||
|
|
||||||
@derive {Inspect, except: [:password]}
|
@derive {Inspect, except: [:password]}
|
||||||
schema "players" do
|
schema "players" do
|
||||||
@ -17,6 +17,7 @@ defmodule Chessh.Player do
|
|||||||
has_many(:keys, Key)
|
has_many(:keys, Key)
|
||||||
has_many(:light_games, Game, foreign_key: :light_player_id, references: :id)
|
has_many(:light_games, Game, foreign_key: :light_player_id, references: :id)
|
||||||
has_many(:dark_games, Game, foreign_key: :dark_player_id, references: :id)
|
has_many(:dark_games, Game, foreign_key: :dark_player_id, references: :id)
|
||||||
|
has_many(:bots, Bot, foreign_key: :player_id, references: :id)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -156,8 +156,8 @@ defmodule Chessh.SSH.Client do
|
|||||||
# C-b
|
# C-b
|
||||||
<<2>> -> :menu
|
<<2>> -> :menu
|
||||||
# Escape
|
# Escape
|
||||||
"\e" -> :menu
|
"\e" -> :escape
|
||||||
# VIM keys, per request
|
# VIM keys
|
||||||
"k" -> :up
|
"k" -> :up
|
||||||
"j" -> :down
|
"j" -> :down
|
||||||
"h" -> :left
|
"h" -> :left
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
defmodule Chessh.SSH.Client.Game do
|
defmodule Chessh.SSH.Client.Game do
|
||||||
require Logger
|
require Logger
|
||||||
alias Chessh.{Game, Utils, Repo}
|
alias Chessh.{Game, Utils, Repo, Bot}
|
||||||
alias Chessh.SSH.Client.Game.Renderer
|
alias Chessh.SSH.Client.Game.Renderer
|
||||||
|
|
||||||
@default_fen "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
|
||||||
|
|
||||||
defmodule State do
|
defmodule State do
|
||||||
defstruct cursor: %{x: 7, y: 7},
|
defstruct cursor: %{x: 7, y: 7},
|
||||||
highlighted: %{},
|
highlighted: %{},
|
||||||
@ -69,27 +67,14 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
|
|
||||||
case Hammer.check_rate_inc(
|
case Hammer.check_rate_inc(
|
||||||
:redis,
|
:redis,
|
||||||
"player-#{state.player_session.id}-create-game-rate",
|
"player-#{state.player_session.player_id}-create-game-rate",
|
||||||
create_game_ms,
|
create_game_ms,
|
||||||
create_game_rate,
|
create_game_rate,
|
||||||
1
|
1
|
||||||
) do
|
) do
|
||||||
{:allow, _count} ->
|
{:allow, _count} ->
|
||||||
# Starting a new game
|
game = Game.new_game(color, player_session.player_id) |> Repo.insert!()
|
||||||
{:ok, %Game{id: game_id} = game} =
|
%Game{id: game_id} = game
|
||||||
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()
|
|
||||||
|
|
||||||
GenServer.cast(
|
GenServer.cast(
|
||||||
:discord_notifier,
|
:discord_notifier,
|
||||||
@ -129,13 +114,15 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
id: game_id,
|
id: game_id,
|
||||||
fen: fen,
|
fen: fen,
|
||||||
dark_player_id: dark_player_id,
|
dark_player_id: dark_player_id,
|
||||||
light_player_id: light_player_id
|
light_player_id: light_player_id,
|
||||||
|
bot_id: bot_id
|
||||||
} = game
|
} = game
|
||||||
} = state
|
} = state
|
||||||
| _
|
| _
|
||||||
]) do
|
]) do
|
||||||
maybe_changeset =
|
maybe_changeset =
|
||||||
case color do
|
if !bot_id do
|
||||||
|
case(color) do
|
||||||
:light ->
|
:light ->
|
||||||
if !light_player_id,
|
if !light_player_id,
|
||||||
do: Game.changeset(game, %{light_player_id: player_session.player_id})
|
do: Game.changeset(game, %{light_player_id: player_session.player_id})
|
||||||
@ -144,6 +131,7 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
if !dark_player_id,
|
if !dark_player_id,
|
||||||
do: Game.changeset(game, %{dark_player_id: player_session.player_id})
|
do: Game.changeset(game, %{dark_player_id: player_session.player_id})
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
{status, maybe_joined_game} =
|
{status, maybe_joined_game} =
|
||||||
if maybe_changeset do
|
if maybe_changeset do
|
||||||
@ -164,7 +152,7 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
end
|
end
|
||||||
|
|
||||||
binbo_pid = initialize_game(game_id, fen)
|
binbo_pid = initialize_game(game_id, fen)
|
||||||
game = Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player])
|
game = Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
|
|
||||||
player_color = if(game.light_player_id == player_session.player_id, do: :light, else: :dark)
|
player_color = if(game.light_player_id == player_session.player_id, do: :light, else: :dark)
|
||||||
|
|
||||||
@ -206,7 +194,7 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
}
|
}
|
||||||
end).(%State{
|
end).(%State{
|
||||||
state
|
state
|
||||||
| game: Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player])
|
| game: Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
})
|
})
|
||||||
|
|
||||||
send(client_pid, {:send_to_ssh, Renderer.render_board_state(new_state)})
|
send(client_pid, {:send_to_ssh, Renderer.render_board_state(new_state)})
|
||||||
@ -218,7 +206,7 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
:player_joined,
|
:player_joined,
|
||||||
%State{client_pid: client_pid, game: %Game{id: game_id}} = state
|
%State{client_pid: client_pid, game: %Game{id: game_id}} = state
|
||||||
) do
|
) do
|
||||||
game = Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player])
|
game = Repo.get(Game, game_id) |> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
new_state = %State{state | game: game}
|
new_state = %State{state | game: game}
|
||||||
send(client_pid, {:send_to_ssh, Renderer.render_board_state(new_state)})
|
send(client_pid, {:send_to_ssh, Renderer.render_board_state(new_state)})
|
||||||
{:noreply, new_state}
|
{:noreply, new_state}
|
||||||
@ -391,22 +379,15 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
{:ok, status} ->
|
{:ok, status} ->
|
||||||
{:ok, fen} = :binbo.get_fen(binbo_pid)
|
{:ok, fen} = :binbo.get_fen(binbo_pid)
|
||||||
|
|
||||||
{:ok, %Game{status: after_move_status}} =
|
{:ok, %Game{status: after_move_status} = game} =
|
||||||
game
|
game
|
||||||
|> Game.changeset(
|
|> Game.update_with_status(attempted_move, fen, status)
|
||||||
Map.merge(
|
|
||||||
%{
|
|
||||||
fen: fen,
|
|
||||||
moves: game.moves + 1,
|
|
||||||
turn: if(game.turn == :dark, do: :light, else: :dark),
|
|
||||||
last_move: attempted_move,
|
|
||||||
game_moves: if(game_moves, do: game_moves <> " ", else: "") <> attempted_move
|
|
||||||
},
|
|
||||||
changeset_from_status(status)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
|
|
||||||
|
if !is_nil(game.bot) do
|
||||||
|
spawn(fn -> Bot.send_update(Repo.get(Game, game.id) |> Repo.preload([:bot])) end)
|
||||||
|
end
|
||||||
|
|
||||||
:syn.publish(:games, {:game, game_id}, {:new_move, attempted_move})
|
:syn.publish(:games, {:game, game_id}, {:new_move, attempted_move})
|
||||||
|
|
||||||
if after_move_status == :continue do
|
if after_move_status == :continue do
|
||||||
@ -433,22 +414,6 @@ defmodule Chessh.SSH.Client.Game do
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
defp changeset_from_status(game_status) do
|
|
||||||
case game_status do
|
|
||||||
:continue ->
|
|
||||||
%{}
|
|
||||||
|
|
||||||
{:draw, _} ->
|
|
||||||
%{status: :draw}
|
|
||||||
|
|
||||||
{:checkmate, :white_wins} ->
|
|
||||||
%{status: :winner, winner: :light}
|
|
||||||
|
|
||||||
{:checkmate, :black_wins} ->
|
|
||||||
%{status: :winner, winner: :dark}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp make_highlight_map(
|
defp make_highlight_map(
|
||||||
%State{
|
%State{
|
||||||
game: %Game{last_move: last_move, turn: turn},
|
game: %Game{last_move: last_move, turn: turn},
|
||||||
|
@ -2,6 +2,7 @@ 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
|
||||||
@ -42,29 +43,17 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
%Game.State{
|
%Game.State{
|
||||||
game:
|
game:
|
||||||
%Chessh.Game{
|
%Chessh.Game{
|
||||||
light_player: light_player
|
light_player: light_player,
|
||||||
} = game
|
|
||||||
} = state
|
|
||||||
)
|
|
||||||
when is_nil(light_player) do
|
|
||||||
render_board_state(%Game.State{
|
|
||||||
state
|
|
||||||
| game: %Chessh.Game{game | light_player: %Player{username: "(no opponent)"}}
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_board_state(
|
|
||||||
%Game.State{
|
|
||||||
game:
|
|
||||||
%Chessh.Game{
|
|
||||||
dark_player: dark_player
|
dark_player: dark_player
|
||||||
} = game
|
} = game
|
||||||
} = state
|
} = state
|
||||||
)
|
)
|
||||||
when is_nil(dark_player) do
|
when is_nil(light_player) or is_nil(dark_player) do
|
||||||
|
{light_player, dark_player} = get_players(game)
|
||||||
|
|
||||||
render_board_state(%Game.State{
|
render_board_state(%Game.State{
|
||||||
state
|
state
|
||||||
| game: %Chessh.Game{game | dark_player: %Player{username: "(no opponent)"}}
|
| game: %Chessh.Game{game | light_player: light_player, dark_player: dark_player}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -73,9 +62,12 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
flipped: flipped,
|
flipped: flipped,
|
||||||
game:
|
game:
|
||||||
%Chessh.Game{
|
%Chessh.Game{
|
||||||
fen: fen
|
fen: fen,
|
||||||
|
light_player: light_player,
|
||||||
|
dark_player: dark_player
|
||||||
} = game
|
} = game
|
||||||
}) do
|
})
|
||||||
|
when not is_nil(light_player) and not is_nil(dark_player) do
|
||||||
rendered = [
|
rendered = [
|
||||||
ANSI.clear_line(),
|
ANSI.clear_line(),
|
||||||
make_status_line(game, true)
|
make_status_line(game, true)
|
||||||
@ -98,29 +90,19 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
|
|
||||||
def make_status_line(
|
def make_status_line(
|
||||||
%Chessh.Game{
|
%Chessh.Game{
|
||||||
light_player: light_player
|
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
|
dark_player: dark_player
|
||||||
} = game,
|
} = game,
|
||||||
fancy
|
fancy
|
||||||
)
|
)
|
||||||
when is_nil(dark_player),
|
when is_nil(light_player) or is_nil(dark_player) do
|
||||||
do:
|
{light_player, dark_player} = get_players(game)
|
||||||
|
|
||||||
make_status_line(
|
make_status_line(
|
||||||
%Chessh.Game{game | dark_player: %Player{username: "(no opponent)"}},
|
%Chessh.Game{game | light_player: light_player, dark_player: dark_player},
|
||||||
fancy
|
fancy
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def make_status_line(
|
def make_status_line(
|
||||||
%Chessh.Game{
|
%Chessh.Game{
|
||||||
@ -143,12 +125,12 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
"Game #{game_id} - ",
|
"Game #{game_id} - ",
|
||||||
if(fancy,
|
if(fancy,
|
||||||
do: ANSI.format_fragment([@light_piece_color, light_player]),
|
do: ANSI.format_fragment([@light_piece_color, light_player]),
|
||||||
else: "#{light_player} (L)"
|
else: "♔ #{light_player}"
|
||||||
),
|
),
|
||||||
"#{if fancy, do: ANSI.default_color(), else: ""} --vs-- ",
|
"#{if fancy, do: ANSI.default_color(), else: ""} --vs-- ",
|
||||||
if(fancy,
|
if(fancy,
|
||||||
do: ANSI.format_fragment([@dark_piece_color, dark_player]),
|
do: ANSI.format_fragment([@dark_piece_color, dark_player]),
|
||||||
else: "#{dark_player} (D)"
|
else: "♚ #{dark_player}"
|
||||||
),
|
),
|
||||||
if(fancy, do: ANSI.default_color(), else: ""),
|
if(fancy, do: ANSI.default_color(), else: ""),
|
||||||
case status do
|
case status do
|
||||||
@ -373,4 +355,25 @@ defmodule Chessh.SSH.Client.Game.Renderer do
|
|||||||
Map.merge(acc, pieces_map_for_this_row)
|
Map.merge(acc, pieces_map_for_this_row)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_players(
|
||||||
|
%Chessh.Game{light_player: light_player, dark_player: dark_player, bot: bot} = game
|
||||||
|
) do
|
||||||
|
case {is_nil(light_player), is_nil(dark_player), is_nil(bot)} do
|
||||||
|
{false, true, false} ->
|
||||||
|
{game.light_player, %Player{username: bot.name}}
|
||||||
|
|
||||||
|
{true, false, false} ->
|
||||||
|
{%Player{username: bot.name}, game.dark_player}
|
||||||
|
|
||||||
|
{true, false, true} ->
|
||||||
|
{%Player{username: "(no opponent)"}, game.dark_player}
|
||||||
|
|
||||||
|
{false, true, true} ->
|
||||||
|
{game.light_player, %Player{username: "(no opponent)"}}
|
||||||
|
|
||||||
|
{false, false, true} ->
|
||||||
|
{game.light_player, game.dark_player}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
42
lib/chessh/ssh/client/menus/create_game.ex
Normal file
42
lib/chessh/ssh/client/menus/create_game.ex
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
defmodule Chessh.SSH.Client.CreateGameMenu do
|
||||||
|
alias IO.ANSI
|
||||||
|
|
||||||
|
alias Chessh.PlayerSession
|
||||||
|
alias Chessh.SSH.Client.Game
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
use Chessh.SSH.Client.SelectPaginatePoller
|
||||||
|
|
||||||
|
def dynamic_options(), do: false
|
||||||
|
def tick_delay_ms(), do: 1000
|
||||||
|
def max_displayed_options(), do: 4
|
||||||
|
def title(), do: ["-- Create A New Game --"]
|
||||||
|
|
||||||
|
def initial_options(%State{player_session: %PlayerSession{} = player_session}) do
|
||||||
|
[
|
||||||
|
{"😀 vs 😀 | ⬜ White", {Game, %Game.State{player_session: player_session, color: :light}}},
|
||||||
|
{"😀 vs 😀 | ⬛ Black", {Game, %Game.State{player_session: player_session, color: :dark}}},
|
||||||
|
{"😀 vs 🤖 | ⬜ White",
|
||||||
|
{Chessh.SSH.Client.SelectBot,
|
||||||
|
%Chessh.SSH.Client.SelectPaginatePoller.State{
|
||||||
|
player_session: player_session,
|
||||||
|
extra_info: %{
|
||||||
|
color: :light
|
||||||
|
}
|
||||||
|
}}},
|
||||||
|
{"🤖 vs 😀 | ⬛ Black",
|
||||||
|
{Chessh.SSH.Client.SelectBot,
|
||||||
|
%Chessh.SSH.Client.SelectPaginatePoller.State{
|
||||||
|
player_session: player_session,
|
||||||
|
extra_info: %{
|
||||||
|
color: :dark
|
||||||
|
}
|
||||||
|
}}}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_process_tuple(selected, _state) do
|
||||||
|
selected
|
||||||
|
end
|
||||||
|
end
|
@ -1,6 +1,5 @@
|
|||||||
defmodule Chessh.SSH.Client.MainMenu do
|
defmodule Chessh.SSH.Client.MainMenu do
|
||||||
alias IO.ANSI
|
alias IO.ANSI
|
||||||
alias Chessh.PlayerSession
|
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@ -22,9 +21,12 @@ defmodule Chessh.SSH.Client.MainMenu do
|
|||||||
def max_box_cols(), do: @logo_cols
|
def max_box_cols(), do: @logo_cols
|
||||||
def title(), do: @logo ++ ["- Connected on: #{System.get_env("NODE_ID")}"]
|
def title(), do: @logo ++ ["- Connected on: #{System.get_env("NODE_ID")}"]
|
||||||
|
|
||||||
def initial_options(%State{player_session: %PlayerSession{} = player_session}) do
|
def initial_options(%State{player_session: player_session}) do
|
||||||
[
|
[
|
||||||
{"My Current Games",
|
{"Create Game",
|
||||||
|
{Chessh.SSH.Client.CreateGameMenu,
|
||||||
|
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}},
|
||||||
|
{"Current Games",
|
||||||
{Chessh.SSH.Client.SelectCurrentGame,
|
{Chessh.SSH.Client.SelectCurrentGame,
|
||||||
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}},
|
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}},
|
||||||
{"Joinable Games (lobby)",
|
{"Joinable Games (lobby)",
|
||||||
@ -32,13 +34,7 @@ defmodule Chessh.SSH.Client.MainMenu do
|
|||||||
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}},
|
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}},
|
||||||
{"Previous Games",
|
{"Previous Games",
|
||||||
{Chessh.SSH.Client.SelectPreviousGame,
|
{Chessh.SSH.Client.SelectPreviousGame,
|
||||||
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}},
|
%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}}}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
117
lib/chessh/ssh/client/menus/select_bot.ex
Normal file
117
lib/chessh/ssh/client/menus/select_bot.ex
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
defmodule Chessh.SSH.Client.SelectBot do
|
||||||
|
alias Chessh.{Utils, Bot, Repo, Game}
|
||||||
|
alias Chessh.SSH.Client.Selector
|
||||||
|
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: ["-- Select Bot To Play Against --"]
|
||||||
|
def dynamic_options(), do: true
|
||||||
|
|
||||||
|
def get_bots(player_id, current_id \\ nil, direction \\ :desc) do
|
||||||
|
Selector.paginate_ish_query(
|
||||||
|
Bot
|
||||||
|
|> where([b], b.player_id == ^player_id or b.public == true)
|
||||||
|
|> limit(^max_displayed_options()),
|
||||||
|
current_id,
|
||||||
|
direction
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_bot_tuple(%Bot{id: id, name: name}), do: {name, id}
|
||||||
|
|
||||||
|
def next_page_options(%State{
|
||||||
|
options: options,
|
||||||
|
player_session: %PlayerSession{
|
||||||
|
player_id: player_id
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
{_label, previous_last_bot_id} = List.last(options)
|
||||||
|
next_bots = get_bots(player_id, previous_last_bot_id, :desc)
|
||||||
|
|
||||||
|
if length(next_bots) > 0,
|
||||||
|
do:
|
||||||
|
next_bots
|
||||||
|
|> Enum.map(&format_bot_tuple/1),
|
||||||
|
else: options
|
||||||
|
end
|
||||||
|
|
||||||
|
def previous_page_options(%State{
|
||||||
|
options: options,
|
||||||
|
player_session: %PlayerSession{player_id: player_id}
|
||||||
|
}) do
|
||||||
|
{_label, previous_first_bot_id} = List.first(options)
|
||||||
|
|
||||||
|
previous_bots = get_bots(player_id, previous_first_bot_id, :asc)
|
||||||
|
|
||||||
|
if length(previous_bots) > 0,
|
||||||
|
do:
|
||||||
|
previous_bots
|
||||||
|
|> Enum.map(&format_bot_tuple/1),
|
||||||
|
else: options
|
||||||
|
end
|
||||||
|
|
||||||
|
def initial_options(%State{
|
||||||
|
player_session: %PlayerSession{player_id: player_id}
|
||||||
|
}) do
|
||||||
|
get_bots(player_id)
|
||||||
|
|> Enum.map(&format_bot_tuple/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_options(%State{
|
||||||
|
options: options,
|
||||||
|
player_session: %PlayerSession{player_id: player_id}
|
||||||
|
}) do
|
||||||
|
previous_last_bot_id =
|
||||||
|
case List.last(options) do
|
||||||
|
{_name, id} -> id
|
||||||
|
_ -> 1
|
||||||
|
end
|
||||||
|
|
||||||
|
current_screen_games = get_bots(player_id, previous_last_bot_id - 1, :asc)
|
||||||
|
|
||||||
|
if !is_nil(current_screen_games) && length(current_screen_games),
|
||||||
|
do:
|
||||||
|
current_screen_games
|
||||||
|
|> Enum.map(&format_bot_tuple/1),
|
||||||
|
else: options
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_process_tuple(selected_id, %State{
|
||||||
|
player_session: player_session,
|
||||||
|
extra_info: %{color: color}
|
||||||
|
}) do
|
||||||
|
[create_game_ms, create_game_rate] =
|
||||||
|
Application.get_env(:chessh, RateLimits)
|
||||||
|
|> Keyword.take([:create_game_ms, :create_game_rate])
|
||||||
|
|> Keyword.values()
|
||||||
|
|
||||||
|
case Hammer.check_rate_inc(
|
||||||
|
:redis,
|
||||||
|
"player-#{player_session.player_id}-create-game-rate",
|
||||||
|
create_game_ms,
|
||||||
|
create_game_rate,
|
||||||
|
1
|
||||||
|
) do
|
||||||
|
{:allow, _count} ->
|
||||||
|
{:ok, game} =
|
||||||
|
Game.changeset(
|
||||||
|
Game.new_game(color, player_session.player_id),
|
||||||
|
%{
|
||||||
|
bot_id: selected_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> Repo.insert()
|
||||||
|
|
||||||
|
{Chessh.SSH.Client.Game,
|
||||||
|
%Chessh.SSH.Client.Game.State{player_session: player_session, color: color, game: game}}
|
||||||
|
|
||||||
|
{:deny, _limit} ->
|
||||||
|
{Chessh.SSH.Client.MainMenu,
|
||||||
|
%Chessh.SSH.Client.SelectPaginatePoller.State{player_session: player_session}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,6 +1,6 @@
|
|||||||
defmodule Chessh.SSH.Client.SelectCurrentGame do
|
defmodule Chessh.SSH.Client.SelectCurrentGame do
|
||||||
alias Chessh.{Utils, Repo, Game, PlayerSession}
|
alias Chessh.{Utils, Repo, Game, PlayerSession}
|
||||||
alias Chessh.SSH.Client.GameSelector
|
alias Chessh.SSH.Client.Selector
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ defmodule Chessh.SSH.Client.SelectCurrentGame do
|
|||||||
def dynamic_options(), do: true
|
def dynamic_options(), do: true
|
||||||
|
|
||||||
def get_player_sorted_current_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do
|
def get_player_sorted_current_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do
|
||||||
GameSelector.paginate_ish_query(
|
Selector.paginate_ish_query(
|
||||||
Game
|
Game
|
||||||
|> where([g], g.status == :continue)
|
|> where([g], g.status == :continue)
|
||||||
|> where([g], g.light_player_id == ^player_id or g.dark_player_id == ^player_id)
|
|> where([g], g.light_player_id == ^player_id or g.dark_player_id == ^player_id)
|
||||||
@ -20,6 +20,7 @@ defmodule Chessh.SSH.Client.SelectCurrentGame do
|
|||||||
current_id,
|
current_id,
|
||||||
direction
|
direction
|
||||||
)
|
)
|
||||||
|
|> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_game_selection_tuple(%Game{id: game_id} = game) do
|
def format_game_selection_tuple(%Game{id: game_id} = game) do
|
||||||
@ -67,7 +68,7 @@ defmodule Chessh.SSH.Client.SelectCurrentGame do
|
|||||||
order_by: [desc: g.id]
|
order_by: [desc: g.id]
|
||||||
)
|
)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|> Repo.preload([:light_player, :dark_player])
|
|> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
|> Enum.map(&format_game_selection_tuple/1)
|
|> Enum.map(&format_game_selection_tuple/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ defmodule Chessh.SSH.Client.SelectCurrentGame do
|
|||||||
previous_last_game_id =
|
previous_last_game_id =
|
||||||
case List.last(options) do
|
case List.last(options) do
|
||||||
{_label, id} -> id
|
{_label, id} -> id
|
||||||
_ -> 0
|
_ -> 1
|
||||||
end
|
end
|
||||||
|
|
||||||
current_screen_games =
|
current_screen_games =
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
defmodule Chessh.SSH.Client.SelectJoinableGame do
|
defmodule Chessh.SSH.Client.SelectJoinableGame do
|
||||||
alias Chessh.{Utils, Repo, Game, PlayerSession}
|
alias Chessh.{Utils, Repo, Game, PlayerSession}
|
||||||
alias Chessh.SSH.Client.GameSelector
|
alias Chessh.SSH.Client.Selector
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
require Logger
|
||||||
|
|
||||||
use Chessh.SSH.Client.SelectPaginatePoller
|
use Chessh.SSH.Client.SelectPaginatePoller
|
||||||
|
|
||||||
@ -12,18 +13,20 @@ defmodule Chessh.SSH.Client.SelectJoinableGame do
|
|||||||
def dynamic_options(), do: true
|
def dynamic_options(), do: true
|
||||||
|
|
||||||
def get_player_joinable_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do
|
def get_player_joinable_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do
|
||||||
GameSelector.paginate_ish_query(
|
Selector.paginate_ish_query(
|
||||||
Game
|
Game
|
||||||
|> where([g], g.status == :continue)
|
|> where([g], g.status == :continue)
|
||||||
|> where(
|
|> where(
|
||||||
[g],
|
[g],
|
||||||
(is_nil(g.dark_player_id) or is_nil(g.light_player_id)) and
|
(is_nil(g.dark_player_id) or is_nil(g.light_player_id)) and
|
||||||
(g.dark_player_id != ^player_id or g.light_player_id != ^player_id)
|
(g.dark_player_id != ^player_id or g.light_player_id != ^player_id) and
|
||||||
|
is_nil(g.bot_id)
|
||||||
)
|
)
|
||||||
|> limit(^max_displayed_options()),
|
|> limit(^max_displayed_options()),
|
||||||
current_id,
|
current_id,
|
||||||
direction
|
direction
|
||||||
)
|
)
|
||||||
|
|> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_game_selection_tuple(%Game{id: game_id} = game) do
|
def format_game_selection_tuple(%Game{id: game_id} = game) do
|
||||||
@ -60,6 +63,8 @@ defmodule Chessh.SSH.Client.SelectJoinableGame do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initial_options(%State{player_session: %PlayerSession{player_id: player_id}}) do
|
def initial_options(%State{player_session: %PlayerSession{player_id: player_id}}) do
|
||||||
|
Logger.info(player_id)
|
||||||
|
|
||||||
get_player_joinable_games_with_id(player_id)
|
get_player_joinable_games_with_id(player_id)
|
||||||
|> Enum.map(&format_game_selection_tuple/1)
|
|> Enum.map(&format_game_selection_tuple/1)
|
||||||
end
|
end
|
||||||
@ -71,7 +76,7 @@ defmodule Chessh.SSH.Client.SelectJoinableGame do
|
|||||||
previous_last_game_id =
|
previous_last_game_id =
|
||||||
case List.last(options) do
|
case List.last(options) do
|
||||||
{_label, id} -> id
|
{_label, id} -> id
|
||||||
_ -> 0
|
_ -> 1
|
||||||
end
|
end
|
||||||
|
|
||||||
current_screen_games =
|
current_screen_games =
|
||||||
|
@ -26,7 +26,8 @@ defmodule Chessh.SSH.Client.SelectPaginatePoller do
|
|||||||
player_session: nil,
|
player_session: nil,
|
||||||
options: [],
|
options: [],
|
||||||
tick: 0,
|
tick: 0,
|
||||||
cursor: nil
|
cursor: nil,
|
||||||
|
extra_info: %{}
|
||||||
end
|
end
|
||||||
|
|
||||||
defmacro __using__(_) do
|
defmacro __using__(_) do
|
||||||
@ -127,8 +128,14 @@ defmodule Chessh.SSH.Client.SelectPaginatePoller do
|
|||||||
max_item = min(length(options), max_displayed_options())
|
max_item = min(length(options), max_displayed_options())
|
||||||
|
|
||||||
new_state =
|
new_state =
|
||||||
if(max_item > 0,
|
if max_item > 0 do
|
||||||
do:
|
if action == :return do
|
||||||
|
{_, selected} = Enum.at(options, selected_option_idx)
|
||||||
|
{module, state} = make_process_tuple(selected, state)
|
||||||
|
send(client_pid, {:set_screen_process, module, state})
|
||||||
|
state
|
||||||
|
else
|
||||||
|
if(max_item > 1) do
|
||||||
case action do
|
case action do
|
||||||
:up ->
|
:up ->
|
||||||
%State{
|
%State{
|
||||||
@ -162,16 +169,14 @@ defmodule Chessh.SSH.Client.SelectPaginatePoller do
|
|||||||
tick: 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
|
state
|
||||||
|
end
|
||||||
|
else
|
||||||
|
state
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
) || state
|
|
||||||
|
|
||||||
if !(action == :return) do
|
if !(action == :return) do
|
||||||
render(width, height, new_state)
|
render(width, height, new_state)
|
||||||
@ -229,7 +234,7 @@ defmodule Chessh.SSH.Client.SelectPaginatePoller do
|
|||||||
|
|
||||||
if i == selected_option_idx do
|
if i == selected_option_idx do
|
||||||
ANSI.format_fragment(
|
ANSI.format_fragment(
|
||||||
[:light_cyan, :bright, "> #{line} <", :reset],
|
[:light_cyan, :bright, "♜ #{line} ♜", :reset],
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@ -238,7 +243,7 @@ defmodule Chessh.SSH.Client.SelectPaginatePoller do
|
|||||||
end
|
end
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
["Looks like there's nothing here.", "Use Ctrl+b to go back."]
|
["Looks like there's nothing here.", "Use Ctrl+b return to the menu."]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
defmodule Chessh.SSH.Client.SelectPreviousGame do
|
defmodule Chessh.SSH.Client.SelectPreviousGame do
|
||||||
alias Chessh.{Utils, Repo, Game, PlayerSession}
|
alias Chessh.{Utils, Repo, Game, PlayerSession}
|
||||||
alias Chessh.SSH.Client.GameSelector
|
alias Chessh.SSH.Client.Selector
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ defmodule Chessh.SSH.Client.SelectPreviousGame do
|
|||||||
def dynamic_options(), do: true
|
def dynamic_options(), do: true
|
||||||
|
|
||||||
def get_player_sorted_current_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do
|
def get_player_sorted_current_games_with_id(player_id, current_id \\ nil, direction \\ :desc) do
|
||||||
GameSelector.paginate_ish_query(
|
Selector.paginate_ish_query(
|
||||||
Game
|
Game
|
||||||
|> where([g], g.status != :continue)
|
|> where([g], g.status != :continue)
|
||||||
|> where([g], g.light_player_id == ^player_id or g.dark_player_id == ^player_id)
|
|> where([g], g.light_player_id == ^player_id or g.dark_player_id == ^player_id)
|
||||||
@ -20,6 +20,7 @@ defmodule Chessh.SSH.Client.SelectPreviousGame do
|
|||||||
current_id,
|
current_id,
|
||||||
direction
|
direction
|
||||||
)
|
)
|
||||||
|
|> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_game_selection_tuple(%Game{id: game_id} = game) do
|
def format_game_selection_tuple(%Game{id: game_id} = game) do
|
||||||
@ -67,7 +68,7 @@ defmodule Chessh.SSH.Client.SelectPreviousGame do
|
|||||||
order_by: [desc: g.id]
|
order_by: [desc: g.id]
|
||||||
)
|
)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|> Repo.preload([:light_player, :dark_player])
|
|> Repo.preload([:light_player, :dark_player, :bot])
|
||||||
|> Enum.map(&format_game_selection_tuple/1)
|
|> Enum.map(&format_game_selection_tuple/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ defmodule Chessh.SSH.Client.SelectPreviousGame do
|
|||||||
previous_last_game_id =
|
previous_last_game_id =
|
||||||
case List.last(options) do
|
case List.last(options) do
|
||||||
{_label, id} -> id
|
{_label, id} -> id
|
||||||
_ -> 0
|
_ -> 1
|
||||||
end
|
end
|
||||||
|
|
||||||
current_screen_games =
|
current_screen_games =
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
defmodule Chessh.SSH.Client.GameSelector do
|
defmodule Chessh.SSH.Client.Selector do
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
alias Chessh.Repo
|
alias Chessh.Repo
|
||||||
|
|
||||||
@ -17,7 +17,6 @@ defmodule Chessh.SSH.Client.GameSelector do
|
|||||||
sorted_query
|
sorted_query
|
||||||
end
|
end
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|> Repo.preload([:light_player, :dark_player])
|
|
||||||
|
|
||||||
if direction == :desc, do: results, else: Enum.reverse(results)
|
if direction == :desc, do: results, else: Enum.reverse(results)
|
||||||
end
|
end
|
@ -1,4 +1,6 @@
|
|||||||
defmodule Chessh.Utils do
|
defmodule Chessh.Utils do
|
||||||
|
require Logger
|
||||||
|
|
||||||
@ascii_chars Application.compile_env!(:chessh, :ascii_chars_json_file)
|
@ascii_chars Application.compile_env!(:chessh, :ascii_chars_json_file)
|
||||||
|> File.read!()
|
|> File.read!()
|
||||||
|> Jason.decode!()
|
|> Jason.decode!()
|
||||||
@ -35,4 +37,8 @@ defmodule Chessh.Utils do
|
|||||||
calc = index + delta
|
calc = index + delta
|
||||||
if(calc < 0, do: length, else: 0) + rem(calc, length)
|
if(calc < 0, do: length, else: 0) + rem(calc, length)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def random_token() do
|
||||||
|
:crypto.strong_rand_bytes(16) |> Base.encode16()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
defmodule Chessh.Web.Endpoint do
|
defmodule Chessh.Web.Endpoint do
|
||||||
alias Chessh.{Player, Repo, Key, PlayerSession}
|
alias Chessh.{Player, Repo, Key, PlayerSession, Bot, Utils, Game}
|
||||||
alias Chessh.Web.Token
|
alias Chessh.Web.Token
|
||||||
use Plug.Router
|
use Plug.Router
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
@ -108,7 +108,7 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
{status, body} =
|
{status, body} =
|
||||||
case conn.body_params do
|
case conn.body_params do
|
||||||
%{"key" => key, "name" => name} ->
|
%{"key" => key, "name" => name} ->
|
||||||
if player_key_count > max_key_count do
|
if player_key_count >= max_key_count do
|
||||||
{400, %{errors: "Player has reached threshold of #{max_key_count} keys."}}
|
{400, %{errors: "Player has reached threshold of #{max_key_count} keys."}}
|
||||||
else
|
else
|
||||||
case Key.changeset(%Key{player_id: player.id}, %{key: key, name: name})
|
case Key.changeset(%Key{player_id: player.id}, %{key: key, name: name})
|
||||||
@ -164,7 +164,7 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
|> send_resp(200, Jason.encode!(keys))
|
|> send_resp(200, Jason.encode!(keys))
|
||||||
end
|
end
|
||||||
|
|
||||||
delete "/keys/:id" do
|
delete "/player/keys/:id" do
|
||||||
player = get_player_from_jwt(conn)
|
player = get_player_from_jwt(conn)
|
||||||
PlayerSession.close_all_player_sessions(player)
|
PlayerSession.close_all_player_sessions(player)
|
||||||
|
|
||||||
@ -193,6 +193,203 @@ defmodule Chessh.Web.Endpoint do
|
|||||||
|> send_resp(status, Jason.encode!(body))
|
|> send_resp(status, Jason.encode!(body))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/player/bots" do
|
||||||
|
player = get_player_from_jwt(conn)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(
|
||||||
|
200,
|
||||||
|
Jason.encode!(Repo.all(from(b in Bot, where: b.player_id == ^player.id)))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
put "/player/bots/:id" do
|
||||||
|
player = get_player_from_jwt(conn)
|
||||||
|
bot = Repo.get(Bot, conn.path_params["id"])
|
||||||
|
|
||||||
|
{status, body} =
|
||||||
|
if player.id != bot.player_id do
|
||||||
|
{403, %{errors: "Player cannot edit that bot."}}
|
||||||
|
else
|
||||||
|
case conn.body_params do
|
||||||
|
%{"webhook" => webhook, "name" => name, "public" => public} ->
|
||||||
|
case Bot.changeset(bot, %{webhook: webhook, name: name, public: public})
|
||||||
|
|> Repo.update() do
|
||||||
|
{:ok, new_bot} ->
|
||||||
|
{200,
|
||||||
|
%{
|
||||||
|
success: true,
|
||||||
|
bot: new_bot
|
||||||
|
}}
|
||||||
|
|
||||||
|
{:error, %{valid?: false} = changeset} ->
|
||||||
|
{
|
||||||
|
400,
|
||||||
|
%{
|
||||||
|
errors: format_errors(changeset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{400, %{errors: "webhook, name, publicity must all be specified"}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(
|
||||||
|
status,
|
||||||
|
Jason.encode!(body)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/player/bots/:id/redrive" do
|
||||||
|
player = get_player_from_jwt(conn)
|
||||||
|
bot = Repo.get(Bot, conn.path_params["id"])
|
||||||
|
|
||||||
|
[bot_redrive_rate, bot_redrive_rate_ms] =
|
||||||
|
Application.get_env(:chessh, RateLimits)
|
||||||
|
|> Keyword.take([
|
||||||
|
:bot_redrive_rate,
|
||||||
|
:bot_redrive_rate_ms
|
||||||
|
])
|
||||||
|
|> Keyword.values()
|
||||||
|
|
||||||
|
{status, body} =
|
||||||
|
if player.id == bot.player_id do
|
||||||
|
case Hammer.check_rate_inc(
|
||||||
|
:redis,
|
||||||
|
"bot-#{bot.id}-redrive",
|
||||||
|
bot_redrive_rate_ms,
|
||||||
|
bot_redrive_rate,
|
||||||
|
1
|
||||||
|
) do
|
||||||
|
{:allow, _count} ->
|
||||||
|
spawn(fn -> Bot.redrive_games(bot) end)
|
||||||
|
{200, %{message: "redrive rescheduled"}}
|
||||||
|
|
||||||
|
{:deny, _} ->
|
||||||
|
{429,
|
||||||
|
%{
|
||||||
|
message:
|
||||||
|
"can only redrive #{bot_redrive_rate} time(s) #{bot_redrive_rate_ms} milliseconds"
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{403, %{message: "you can't do that"}}
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(
|
||||||
|
status,
|
||||||
|
Jason.encode!(body)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/bots/games/:id/turn" do
|
||||||
|
token = conn.body_params["token"]
|
||||||
|
attempted_move = conn.body_params["attempted_move"]
|
||||||
|
|
||||||
|
bot = Repo.one(from(b in Bot, where: b.token == ^token))
|
||||||
|
game = Repo.get(Game, conn.path_params["id"])
|
||||||
|
|
||||||
|
{status, body} =
|
||||||
|
if game.bot_id == bot.id do
|
||||||
|
if (game.turn == :light && !game.light_player_id) ||
|
||||||
|
(game.turn == :dark && !game.dark_player_id) do
|
||||||
|
{:ok, binbo_pid} = :binbo.new_server()
|
||||||
|
:binbo.new_game(binbo_pid, game.fen)
|
||||||
|
|
||||||
|
case :binbo.move(binbo_pid, attempted_move) do
|
||||||
|
{:ok, status} ->
|
||||||
|
{:ok, fen} = :binbo.get_fen(binbo_pid)
|
||||||
|
|
||||||
|
{:ok, %Game{status: after_move_status} = game} =
|
||||||
|
game
|
||||||
|
|> Game.update_with_status(attempted_move, fen, status)
|
||||||
|
|> Repo.update()
|
||||||
|
|
||||||
|
:syn.publish(:games, {:game, game.id}, {:new_move, attempted_move})
|
||||||
|
|
||||||
|
{200, %{message: "success"}}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{400, %{message: "invalid move"}}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{400, %{message: "not the bot's turn"}}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{403, %{message: "unauthorized"}}
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(
|
||||||
|
status,
|
||||||
|
Jason.encode!(body)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/player/bots" do
|
||||||
|
player = get_player_from_jwt(conn)
|
||||||
|
|
||||||
|
player_bot_count =
|
||||||
|
Repo.aggregate(from(b in Bot, where: b.player_id == ^player.id), :count, :id)
|
||||||
|
|
||||||
|
max_bot_count = Application.get_env(:chessh, RateLimits)[:player_bots]
|
||||||
|
bot_token = Utils.random_token()
|
||||||
|
|
||||||
|
{status, body} =
|
||||||
|
case conn.body_params do
|
||||||
|
%{"webhook" => webhook, "name" => name, "public" => public} ->
|
||||||
|
if player_bot_count >= max_bot_count do
|
||||||
|
{400, %{errors: "Player has reached threshold of #{max_bot_count} bots."}}
|
||||||
|
else
|
||||||
|
case Bot.changeset(%Bot{player_id: player.id}, %{
|
||||||
|
token: bot_token,
|
||||||
|
webhook: webhook,
|
||||||
|
name: name,
|
||||||
|
public: public
|
||||||
|
})
|
||||||
|
|> Repo.insert() do
|
||||||
|
{:ok, new_bot} ->
|
||||||
|
{
|
||||||
|
200,
|
||||||
|
%{
|
||||||
|
success: true,
|
||||||
|
bot: new_bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:error, %{valid?: false} = changeset} ->
|
||||||
|
{
|
||||||
|
400,
|
||||||
|
%{
|
||||||
|
errors: format_errors(changeset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{
|
||||||
|
400,
|
||||||
|
%{errors: "webhook, name, publicity must all be specified"}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(
|
||||||
|
status,
|
||||||
|
Jason.encode!(body)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
match _ do
|
match _ do
|
||||||
send_resp(conn, 404, "Route undefined")
|
send_resp(conn, 404, "Route undefined")
|
||||||
end
|
end
|
||||||
|
11
priv/repo/migrations/20230529193453_add_citext.exs
Normal file
11
priv/repo/migrations/20230529193453_add_citext.exs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
defmodule Chessh.Repo.Migrations.AddCitext do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute("CREATE EXTENSION citext")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute("DROP EXTENSION citext")
|
||||||
|
end
|
||||||
|
end
|
22
priv/repo/migrations/20230529193504_add_bots.exs
Normal file
22
priv/repo/migrations/20230529193504_add_bots.exs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
defmodule Chessh.Repo.Migrations.AddBots do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:bots) do
|
||||||
|
add(:name, :citext, null: false)
|
||||||
|
add(:webhook, :string, null: false)
|
||||||
|
add(:token, :string, null: false)
|
||||||
|
add(:public, :boolean, null: false)
|
||||||
|
add(:player_id, references(:players), null: false)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create(unique_index(:bots, [:name]))
|
||||||
|
create(unique_index(:bots, [:token]))
|
||||||
|
|
||||||
|
alter table(:games) do
|
||||||
|
add(:bot_id, references(:bots), null: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user