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"
args = ["build", "--release"]
dependencies = ["build-ui"]
[tasks.run-release]
command = "cargo"
args = ["run", "--release"]
dependencies = ["build-ui"]

View file

@ -8,7 +8,7 @@
</head>
<body>
<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>
</body>

View file

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

View file

@ -2,24 +2,30 @@ lockfileVersion: 5.4
specifiers:
'@fontsource/noto-sans-symbols-2': ^4.5.10
'@fontsource/ubuntu': ^4.5.11
'@solid-primitives/websocket': ^0.3.3
'@types/node': ^18.11.7
'@types/uuid': ^8.3.4
solid-devtools: ^0.20.1
solid-js: ^1.6.0
typescript: ^4.8.4
uuid: ^9.0.0
vite: ^3.2.1
vite-plugin-solid: ^2.3.10
zod: ^3.19.1
dependencies:
'@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-devtools: 0.20.1_solid-js@1.6.0+vite@3.2.1
solid-js: 1.6.0
uuid: 9.0.0
zod: 3.19.1
devDependencies:
'@types/node': 18.11.7
'@types/uuid': 8.3.4
typescript: 4.8.4
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==}
dev: false
/@fontsource/ubuntu/4.5.11:
resolution: {integrity: sha512-c+B8A8r31hJLk8Ek7Svoz4GwjtInhc60LFM6XTr5TKtZbcct8TPNxJuKX4+hnqoWgcvy8s0Gj4mde4TcK/SgDQ==}
dev: false
/@jridgewell/gen-mapping/0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'}
@ -566,6 +576,10 @@ packages:
resolution: {integrity: sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==}
dev: true
/@types/uuid/8.3.4:
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
dev: true
/ansi-styles/3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@ -1025,6 +1039,11 @@ packages:
escalade: 3.1.1
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:
resolution: {integrity: sha512-5jMF+QFk2TQaPLlDl7LvZZ99D4kO1X+rD9LR78p9sx9O+XisVSQaHFPLrCsyW/lXuBwub+ox/pNaZdCUZJwd3Q==}
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 { wsUrl } from './constants';
import Spinner from './components/Spinner';
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';
interface Props {
@ -13,29 +13,29 @@ interface Props {
const App: Component<Props> = (props) => {
const [board, setBoard] = createSignal<BoardData>(Array(64).fill(null));
const [possibleMoves, setPossibleMoves] = createSignal<Move[]>([]);
const [side, setSide] = createSignal<Side | null>(null);
function handleEvent(e: MessageEvent<string>) {
const data = JSON.parse(e.data);
const event = ServerChessEvent.parse(data);
if (event.event === 'BoardUpdate') {
console.log(event.data.board);
setBoard(event.data.board);
} else if (event.event === 'PossibleMoves') {
console.log(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);
function joinGame() {
connect();
setSide(null);
setBoard(Array(64).fill(null));
setPossibleMoves([]);
}
function makeMove(move: Move) {
send(JSON.stringify({
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}>
<Welcome gameId={props.gameId} joinGame={() => connect()} />
<Welcome gameId={props.gameId} joinGame={() => joinGame()} />
</Match>
<Match when={state() === WebSocket.CONNECTING}>
<Spinner />
</Match>
<Match when={state() === WebSocket.OPEN}>
<Show when={side() !== null} fallback={<>
<h1>Waiting for opponent...</h1>
<Spinner />
</>}>
<Board board={board} moves={possibleMoves} makeMove={makeMove} />
</Show>
</Match>
</Switch>
};

View file

@ -1,10 +1,12 @@
.button {
width: 100%;
font-family: Ubuntu, sans-serif;
font-size: 1.35rem;
padding: 8px;
border-radius: 8px;
border: 1px solid black;
background-color: #ddd;
border: 2px solid var(--accent);
background-color: var(--accent-dim);
color: white;
}
.button:hover {
@ -14,3 +16,10 @@
.button:active {
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 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) => {
return <div class="spinner" style={{
'--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 Button from "./Button";
import Button, { ButtonContainer } from "./Button";
interface Props {
gameId: string;
@ -7,16 +7,29 @@ interface Props {
}
const Welcome: Component<Props> = (props) => {
console.log(props.gameId);
return <main>
function shareInvite() {
navigator.share({
url: window.location.href,
});
}
return <>
<h1>Welcome</h1>
<div>
Game ID: <pre>{props.gameId}</pre>
</div>
<section>
<h2>Game ID:</h2>
<pre>{props.gameId}</pre>
</section>
<ButtonContainer>
<Button onClick={() => shareInvite()}>
Share invite
</Button>
<Button onClick={() => props.joinGame()}>
Join
</Button>
</main>;
</ButtonContainer>
</>;
}
export default Welcome;

View file

@ -1,8 +1,8 @@
// const WS_BASE = 'ws://localhost:3000/ws/';
export function wsUrl(id: string) {
if (import.meta.env.WS_BASE) {
return import.meta.env.WS_BASE + id;
if (import.meta.env.VITE_WS_BASE) {
return import.meta.env.VITE_WS_BASE + id;
} else {
const loc = window.location;
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 const StartGameEvent = z.object({
side: Side,
});
export type StartGameEvent = z.infer<typeof StartGameEvent>;
export const ServerChessEvent = z.discriminatedUnion("event", [
z.object({ event: z.literal('BoardUpdate'), data: BoardUpdateEvent }),
z.object({ event: z.literal('PossibleMoves'), data: PossibleMovesEvent }),
z.object({ event: z.literal('StartGame'), data: StartGameEvent })
]);
export type ServerChessEvent = z.infer<typeof ServerChessEvent>;

View file

@ -4,11 +4,20 @@ import 'solid-devtools';
import App from './App';
import './main.css';
import '@fontsource/ubuntu';
import "@fontsource/noto-sans-symbols-2";
import { v4 as uuidv4 } from 'uuid';
import Button from './components/Button';
const search = new URLSearchParams(window.location.search);
if (search.has('game_id')) {
render(() => <App gameId={search.get('game_id')!} />, document.getElementById('root') as HTMLElement);
} else {
console.error('no game id');
const root = document.getElementById('root')!;
function newGame() {
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 {
box-sizing: border-box;
}
@ -5,6 +16,10 @@ html {
html, body {
padding: 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%;
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 crate::{
chess::{mv::generate_legal, Side},
chess::{mv::generate_legal},
constants::START_FEN,
player::{IncomingPlayerEvent, OutgoingPlayerEvent, Player},
prelude::Board,
prelude::*,
};
#[derive(Actor, Default)]
@ -80,6 +80,7 @@ impl Handler<JoinGame> for ChessGame {
None
} else {
self.black = Some(join_game.player);
self.send_game_start();
self.broadcast_new_board();
self.send_possible_moves();
Some(Side::Black)
@ -128,18 +129,28 @@ impl ChessGame {
}
}
fn broadcast_new_board(&self) {
fn send_game_start(&self) {
// TODO: Handle players disconnecting
tokio::spawn(self.white.send(OutgoingPlayerEvent::BoardUpdate {
board: self.board.board.to_vec(),
}));
tokio::spawn(self.white.send(OutgoingPlayerEvent::StartGame { side: White }));
if let Some(black) = &self.black {
tokio::spawn(black.send(OutgoingPlayerEvent::BoardUpdate {
board: self.board.board.to_vec(),
}));
tokio::spawn(black.send(OutgoingPlayerEvent::StartGame { side: Black }));
}
}
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) {
let (current, other) = match &self.board.to_move {
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::Piece;
pub use crate::chess::Side;
pub use crate::chess::PieceType::*;
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")]
pub enum OutgoingPlayerEvent {
StartGame { side: Side },
BoardUpdate { board: Vec<Option<Piece>> },
PossibleMoves { moves: Vec<Move> },
}

View file

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