feat: lots of design improvements and bugfixes

This commit is contained in:
Ashhhleyyy 2022-11-01 10:00:17 +00:00
parent 15d3d71715
commit a2a6ea7bc3
Signed by: ash
GPG key ID: 83B789081A0878FB
17 changed files with 160 additions and 45 deletions

View file

@ -6,3 +6,8 @@ args = ["build"]
command = "cargo" command = "cargo"
args = ["build", "--release"] args = ["build", "--release"]
dependencies = ["build-ui"] dependencies = ["build-ui"]
[tasks.run-release]
command = "cargo"
args = ["run", "--release"]
dependencies = ["build-ui"]

View file

@ -8,7 +8,7 @@
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <main id="root"></main>
<script src="/src-web/index.tsx" type="module"></script> <script src="/src-web/index.tsx" type="module"></script>
</body> </body>

View file

@ -13,15 +13,18 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/node": "^18.11.7", "@types/node": "^18.11.7",
"@types/uuid": "^8.3.4",
"typescript": "^4.8.4", "typescript": "^4.8.4",
"vite": "^3.2.1", "vite": "^3.2.1",
"vite-plugin-solid": "^2.3.10" "vite-plugin-solid": "^2.3.10"
}, },
"dependencies": { "dependencies": {
"@fontsource/noto-sans-symbols-2": "^4.5.10", "@fontsource/noto-sans-symbols-2": "^4.5.10",
"@fontsource/ubuntu": "^4.5.11",
"@solid-primitives/websocket": "^0.3.3", "@solid-primitives/websocket": "^0.3.3",
"solid-devtools": "^0.20.1", "solid-devtools": "^0.20.1",
"solid-js": "^1.6.0", "solid-js": "^1.6.0",
"uuid": "^9.0.0",
"zod": "^3.19.1" "zod": "^3.19.1"
} }
} }

View file

@ -2,24 +2,30 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@fontsource/noto-sans-symbols-2': ^4.5.10 '@fontsource/noto-sans-symbols-2': ^4.5.10
'@fontsource/ubuntu': ^4.5.11
'@solid-primitives/websocket': ^0.3.3 '@solid-primitives/websocket': ^0.3.3
'@types/node': ^18.11.7 '@types/node': ^18.11.7
'@types/uuid': ^8.3.4
solid-devtools: ^0.20.1 solid-devtools: ^0.20.1
solid-js: ^1.6.0 solid-js: ^1.6.0
typescript: ^4.8.4 typescript: ^4.8.4
uuid: ^9.0.0
vite: ^3.2.1 vite: ^3.2.1
vite-plugin-solid: ^2.3.10 vite-plugin-solid: ^2.3.10
zod: ^3.19.1 zod: ^3.19.1
dependencies: dependencies:
'@fontsource/noto-sans-symbols-2': 4.5.10 '@fontsource/noto-sans-symbols-2': 4.5.10
'@fontsource/ubuntu': 4.5.11
'@solid-primitives/websocket': 0.3.3_solid-js@1.6.0 '@solid-primitives/websocket': 0.3.3_solid-js@1.6.0
solid-devtools: 0.20.1_solid-js@1.6.0+vite@3.2.1 solid-devtools: 0.20.1_solid-js@1.6.0+vite@3.2.1
solid-js: 1.6.0 solid-js: 1.6.0
uuid: 9.0.0
zod: 3.19.1 zod: 3.19.1
devDependencies: devDependencies:
'@types/node': 18.11.7 '@types/node': 18.11.7
'@types/uuid': 8.3.4
typescript: 4.8.4 typescript: 4.8.4
vite: 3.2.1 vite: 3.2.1
vite-plugin-solid: 2.3.10_solid-js@1.6.0+vite@3.2.1 vite-plugin-solid: 2.3.10_solid-js@1.6.0+vite@3.2.1
@ -335,6 +341,10 @@ packages:
resolution: {integrity: sha512-3sDgCDm3zNijKoQ4UVSg5PgGk2Drp9QIF73RKZzfiZ4Ksgsu6h4XXng2i+f6cXHpSaP2CEOLgTu7EwH298djMg==} resolution: {integrity: sha512-3sDgCDm3zNijKoQ4UVSg5PgGk2Drp9QIF73RKZzfiZ4Ksgsu6h4XXng2i+f6cXHpSaP2CEOLgTu7EwH298djMg==}
dev: false dev: false
/@fontsource/ubuntu/4.5.11:
resolution: {integrity: sha512-c+B8A8r31hJLk8Ek7Svoz4GwjtInhc60LFM6XTr5TKtZbcct8TPNxJuKX4+hnqoWgcvy8s0Gj4mde4TcK/SgDQ==}
dev: false
/@jridgewell/gen-mapping/0.1.1: /@jridgewell/gen-mapping/0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@ -566,6 +576,10 @@ packages:
resolution: {integrity: sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==} resolution: {integrity: sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==}
dev: true dev: true
/@types/uuid/8.3.4:
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
dev: true
/ansi-styles/3.2.1: /ansi-styles/3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -1025,6 +1039,11 @@ packages:
escalade: 3.1.1 escalade: 3.1.1
picocolors: 1.0.0 picocolors: 1.0.0
/uuid/9.0.0:
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
hasBin: true
dev: false
/vite-plugin-solid/2.3.10_solid-js@1.6.0+vite@3.2.1: /vite-plugin-solid/2.3.10_solid-js@1.6.0+vite@3.2.1:
resolution: {integrity: sha512-5jMF+QFk2TQaPLlDl7LvZZ99D4kO1X+rD9LR78p9sx9O+XisVSQaHFPLrCsyW/lXuBwub+ox/pNaZdCUZJwd3Q==} resolution: {integrity: sha512-5jMF+QFk2TQaPLlDl7LvZZ99D4kO1X+rD9LR78p9sx9O+XisVSQaHFPLrCsyW/lXuBwub+ox/pNaZdCUZJwd3Q==}
peerDependencies: peerDependencies:

View file

@ -1,9 +1,9 @@
import { Component, createEffect, createSignal, Match, Switch } from 'solid-js'; import { Component, createSignal, Match, Show, Switch } from 'solid-js';
import createWebsocket from '@solid-primitives/websocket'; import createWebsocket from '@solid-primitives/websocket';
import { wsUrl } from './constants'; import { wsUrl } from './constants';
import Spinner from './components/Spinner'; import Spinner from './components/Spinner';
import Welcome from './components/Welcome'; import Welcome from './components/Welcome';
import { Board as BoardData, Move, ServerChessEvent } from './events'; import { Board as BoardData, Move, ServerChessEvent, Side } from './events';
import Board from './components/Board'; import Board from './components/Board';
interface Props { interface Props {
@ -13,29 +13,29 @@ interface Props {
const App: Component<Props> = (props) => { const App: Component<Props> = (props) => {
const [board, setBoard] = createSignal<BoardData>(Array(64).fill(null)); const [board, setBoard] = createSignal<BoardData>(Array(64).fill(null));
const [possibleMoves, setPossibleMoves] = createSignal<Move[]>([]); const [possibleMoves, setPossibleMoves] = createSignal<Move[]>([]);
const [side, setSide] = createSignal<Side | null>(null);
function handleEvent(e: MessageEvent<string>) { function handleEvent(e: MessageEvent<string>) {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
const event = ServerChessEvent.parse(data); const event = ServerChessEvent.parse(data);
if (event.event === 'BoardUpdate') { if (event.event === 'BoardUpdate') {
console.log(event.data.board);
setBoard(event.data.board); setBoard(event.data.board);
} else if (event.event === 'PossibleMoves') { } else if (event.event === 'PossibleMoves') {
console.log(event.data.moves);
setPossibleMoves(event.data.moves); setPossibleMoves(event.data.moves);
} else if (event.event === 'StartGame') {
setSide(event.data.side);
} }
} }
createEffect(() => {
console.log('board is now', board());
});
createEffect(() => {
console.log('moves is now', possibleMoves());
});
const [connect, disconnect, send, state, socket] = createWebsocket(wsUrl(props.gameId), handleEvent, console.error); const [connect, disconnect, send, state, socket] = createWebsocket(wsUrl(props.gameId), handleEvent, console.error);
function joinGame() {
connect();
setSide(null);
setBoard(Array(64).fill(null));
setPossibleMoves([]);
}
function makeMove(move: Move) { function makeMove(move: Move) {
send(JSON.stringify({ send(JSON.stringify({
event: 'MakeMove', event: 'MakeMove',
@ -45,15 +45,20 @@ const App: Component<Props> = (props) => {
})); }));
} }
return <Switch fallback={<><h1>Hello, World!</h1><Spinner /></>}> return <Switch fallback={<Spinner />}>
<Match when={state() === WebSocket.CLOSED}> <Match when={state() === WebSocket.CLOSED}>
<Welcome gameId={props.gameId} joinGame={() => connect()} /> <Welcome gameId={props.gameId} joinGame={() => joinGame()} />
</Match> </Match>
<Match when={state() === WebSocket.CONNECTING}> <Match when={state() === WebSocket.CONNECTING}>
<Spinner /> <Spinner />
</Match> </Match>
<Match when={state() === WebSocket.OPEN}> <Match when={state() === WebSocket.OPEN}>
<Board board={board} moves={possibleMoves} makeMove={makeMove} /> <Show when={side() !== null} fallback={<>
<h1>Waiting for opponent...</h1>
<Spinner />
</>}>
<Board board={board} moves={possibleMoves} makeMove={makeMove} />
</Show>
</Match> </Match>
</Switch> </Switch>
}; };

View file

@ -1,10 +1,12 @@
.button { .button {
width: 100%; width: 100%;
font-family: Ubuntu, sans-serif;
font-size: 1.35rem; font-size: 1.35rem;
padding: 8px; padding: 8px;
border-radius: 8px; border-radius: 8px;
border: 1px solid black; border: 2px solid var(--accent);
background-color: #ddd; background-color: var(--accent-dim);
color: white;
} }
.button:hover { .button:hover {
@ -14,3 +16,10 @@
.button:active { .button:active {
filter: brightness(.8); filter: brightness(.8);
} }
.button-container {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
}

View file

@ -14,3 +14,11 @@ const Button: Component<Props> = (props) => {
} }
export default Button; export default Button;
export const ButtonContainer: Component<{ children: JSX.Element }> = (props) => {
const c = children(() => props.children);
return <div class="button-container">
{c()}
</div>
}

View file

@ -9,7 +9,7 @@ interface Props {
const Spinner: Component<Props> = (props) => { const Spinner: Component<Props> = (props) => {
return <div class="spinner" style={{ return <div class="spinner" style={{
'--spinner-size': props.size ?? '32px', '--spinner-size': props.size ?? '32px',
'--spinner-colour': props.colour ?? '#f9027a', '--spinner-colour': props.colour ?? 'var(--accent)',
}} /> }} />
} }

View file

@ -1,5 +1,5 @@
import { Component } from "solid-js"; import { Component } from "solid-js";
import Button from "./Button"; import Button, { ButtonContainer } from "./Button";
interface Props { interface Props {
gameId: string; gameId: string;
@ -7,16 +7,29 @@ interface Props {
} }
const Welcome: Component<Props> = (props) => { const Welcome: Component<Props> = (props) => {
console.log(props.gameId); function shareInvite() {
return <main> navigator.share({
url: window.location.href,
});
}
return <>
<h1>Welcome</h1> <h1>Welcome</h1>
<div> <section>
Game ID: <pre>{props.gameId}</pre> <h2>Game ID:</h2>
</div> <pre>{props.gameId}</pre>
<Button onClick={() => props.joinGame()}> </section>
Join
</Button> <ButtonContainer>
</main>; <Button onClick={() => shareInvite()}>
Share invite
</Button>
<Button onClick={() => props.joinGame()}>
Join
</Button>
</ButtonContainer>
</>;
} }
export default Welcome; export default Welcome;

View file

@ -1,8 +1,8 @@
// const WS_BASE = 'ws://localhost:3000/ws/'; // const WS_BASE = 'ws://localhost:3000/ws/';
export function wsUrl(id: string) { export function wsUrl(id: string) {
if (import.meta.env.WS_BASE) { if (import.meta.env.VITE_WS_BASE) {
return import.meta.env.WS_BASE + id; return import.meta.env.VITE_WS_BASE + id;
} else { } else {
const loc = window.location; const loc = window.location;
let newUri = loc.protocol === "https:" ? "wss://" : "ws://"; let newUri = loc.protocol === "https:" ? "wss://" : "ws://";

View file

@ -51,9 +51,16 @@ export const PossibleMovesEvent = z.object({
export type PossibleMovesEvent = z.infer<typeof PossibleMovesEvent>; export type PossibleMovesEvent = z.infer<typeof PossibleMovesEvent>;
export const StartGameEvent = z.object({
side: Side,
});
export type StartGameEvent = z.infer<typeof StartGameEvent>;
export const ServerChessEvent = z.discriminatedUnion("event", [ export const ServerChessEvent = z.discriminatedUnion("event", [
z.object({ event: z.literal('BoardUpdate'), data: BoardUpdateEvent }), z.object({ event: z.literal('BoardUpdate'), data: BoardUpdateEvent }),
z.object({ event: z.literal('PossibleMoves'), data: PossibleMovesEvent }), z.object({ event: z.literal('PossibleMoves'), data: PossibleMovesEvent }),
z.object({ event: z.literal('StartGame'), data: StartGameEvent })
]); ]);
export type ServerChessEvent = z.infer<typeof ServerChessEvent>; export type ServerChessEvent = z.infer<typeof ServerChessEvent>;

View file

@ -4,11 +4,20 @@ import 'solid-devtools';
import App from './App'; import App from './App';
import './main.css'; import './main.css';
import '@fontsource/ubuntu';
import "@fontsource/noto-sans-symbols-2"; import "@fontsource/noto-sans-symbols-2";
import { v4 as uuidv4 } from 'uuid';
import Button from './components/Button';
const search = new URLSearchParams(window.location.search); const search = new URLSearchParams(window.location.search);
if (search.has('game_id')) { const root = document.getElementById('root')!;
render(() => <App gameId={search.get('game_id')!} />, document.getElementById('root') as HTMLElement);
} else { function newGame() {
console.error('no game id'); window.location.search = '?game_id=' + uuidv4();
}
if (search.has('game_id')) {
render(() => <App gameId={search.get('game_id')!} />, root);
} else {
render(() => <Button onClick={newGame}>New game</Button>, root);
} }

View file

@ -1,3 +1,14 @@
:root {
--background: #13092b;
--background-transparent: #13092baa;
--background-2: #090f2b;
--foreground: #ddd;
--foreground-bright: #fff;
--foreground-dim: #aaa;
--accent: #f9027a;
--accent-dim: hsl(331, 50%, 49%);
}
html { html {
box-sizing: border-box; box-sizing: border-box;
} }
@ -5,6 +16,10 @@ html {
html, body { html, body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: Ubuntu, sans-serif;
font-size: 16px;
background-color: var(--background);
color: var(--foreground);
} }
* { * {
@ -17,3 +32,10 @@ body {
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
} }
#root {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

View file

@ -4,10 +4,10 @@ use uuid::Uuid;
use xtra::{prelude::*, WeakAddress}; use xtra::{prelude::*, WeakAddress};
use crate::{ use crate::{
chess::{mv::generate_legal, Side}, chess::{mv::generate_legal},
constants::START_FEN, constants::START_FEN,
player::{IncomingPlayerEvent, OutgoingPlayerEvent, Player}, player::{IncomingPlayerEvent, OutgoingPlayerEvent, Player},
prelude::Board, prelude::*,
}; };
#[derive(Actor, Default)] #[derive(Actor, Default)]
@ -80,6 +80,7 @@ impl Handler<JoinGame> for ChessGame {
None None
} else { } else {
self.black = Some(join_game.player); self.black = Some(join_game.player);
self.send_game_start();
self.broadcast_new_board(); self.broadcast_new_board();
self.send_possible_moves(); self.send_possible_moves();
Some(Side::Black) Some(Side::Black)
@ -128,18 +129,28 @@ impl ChessGame {
} }
} }
fn broadcast_new_board(&self) { fn send_game_start(&self) {
// TODO: Handle players disconnecting // TODO: Handle players disconnecting
tokio::spawn(self.white.send(OutgoingPlayerEvent::BoardUpdate { tokio::spawn(self.white.send(OutgoingPlayerEvent::StartGame { side: White }));
board: self.board.board.to_vec(),
}));
if let Some(black) = &self.black { if let Some(black) = &self.black {
tokio::spawn(black.send(OutgoingPlayerEvent::BoardUpdate { tokio::spawn(black.send(OutgoingPlayerEvent::StartGame { side: Black }));
board: self.board.board.to_vec(),
}));
} }
} }
fn broadcast(&self, message: OutgoingPlayerEvent) {
// TODO: Handle players disconnecting
tokio::spawn(self.white.send(message.clone()));
if let Some(black) = &self.black {
tokio::spawn(black.send(message));
}
}
fn broadcast_new_board(&self) {
self.broadcast(OutgoingPlayerEvent::BoardUpdate {
board: self.board.board.to_vec(),
});
}
fn send_possible_moves(&self) { fn send_possible_moves(&self) {
let (current, other) = match &self.board.to_move { let (current, other) = match &self.board.to_move {
Side::Black => (self.black.clone().unwrap(), self.white.clone()), Side::Black => (self.black.clone().unwrap(), self.white.clone()),

View file

@ -11,6 +11,7 @@ pub mod prelude {
pub use crate::chess::Board; pub use crate::chess::Board;
pub use crate::chess::Piece; pub use crate::chess::Piece;
pub use crate::chess::Side;
pub use crate::chess::PieceType::*; pub use crate::chess::PieceType::*;
pub use crate::chess::Side::*; pub use crate::chess::Side::*;

View file

@ -25,9 +25,10 @@ impl Player {
} }
} }
#[derive(Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(tag = "event", content = "data")] #[serde(tag = "event", content = "data")]
pub enum OutgoingPlayerEvent { pub enum OutgoingPlayerEvent {
StartGame { side: Side },
BoardUpdate { board: Vec<Option<Piece>> }, BoardUpdate { board: Vec<Option<Piece>> },
PossibleMoves { moves: Vec<Move> }, PossibleMoves { moves: Vec<Move> },
} }

View file

@ -1,3 +1,5 @@
#[cfg(not(debug_assertions))]
use axum::http::uri::Uri;
use axum::{ use axum::{
extract::{ extract::{
ws::{Message, WebSocket}, ws::{Message, WebSocket},