Skip to content

Commit

Permalink
Multiplayer MVP
Browse files Browse the repository at this point in the history
Still a lot of work to get this polished, but multiplayer now works.

Still need anti-cheat for modifying the game timers, and to disable the undo and redo buttons.
  • Loading branch information
zaccnz committed May 26, 2023
1 parent 3299444 commit 5a147b5
Show file tree
Hide file tree
Showing 15 changed files with 2,975 additions and 426 deletions.
1,816 changes: 1,803 additions & 13 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"chess.js": "^1.0.0-beta.5",
"firebase": "^9.22.0",
"fscreen": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
70 changes: 49 additions & 21 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { HashRouter, Route, Routes } from 'react-router-dom';
import { BrowserRouter, HashRouter, Route, Routes } from 'react-router-dom';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { Chess } from './components/Chess';
Expand All @@ -11,6 +11,7 @@ import { SettingsProvider } from './providers/SettingsProvider';
import { ThemeProvider } from './providers/ThemeProvider';
import { GlobalStyles } from './theme/global';
import { ChessProvider } from './providers/ChessProvider';
import { LobbyProvider } from './providers/LobbyProvider';

const Container = styled.div`
max-width: 1000px;
Expand All @@ -23,34 +24,61 @@ const Container = styled.div`
background: ${props => props.theme.colors.background};
`;

const useHashRouter: boolean = true;
const Router = useHashRouter ? HashRouter : BrowserRouter;

function App(): JSX.Element {
const [settingsOpen, setSettingsOpen] = useState(false);

const onClickSettings = () => setSettingsOpen(v => !v);

return (
<SettingsProvider>
<ChessProvider>
<ThemeProvider>
<HashRouter>
<GlobalStyles />
<ThemeProvider>
<Router>
<GlobalStyles />

<Container className='App'>
<Header onClickSettings={onClickSettings} />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/game/:id" element={<Chess />} />
<Route path="/game" element={<Chess />} />
<Route path="/lobby" element={<Lobby />} />
</Routes>
<Footer />
{
settingsOpen && <Settings onClickSettings={onClickSettings} />
}
</Container>
</HashRouter>
</ThemeProvider>
</ChessProvider>
<Container className='App'>
<Header onClickSettings={onClickSettings} />
<Routes>
<Route path="/" element={
<Home />
} />
<Route path="/game/bot" element={
<ChessProvider>
<Chess type='bot' />
</ChessProvider>
} />
<Route path="/game/:id" element={
<LobbyProvider>
<ChessProvider>
<Chess type='online' />
</ChessProvider>
</LobbyProvider>
} />
<Route path="/game" element={
<ChessProvider>
<Chess type="local" />
</ChessProvider>
} />
<Route path="/lobby/:id" element={
<LobbyProvider>
<Lobby />
</LobbyProvider>
} />
<Route path="/lobby" element={
<LobbyProvider>
<Lobby />
</LobbyProvider>
} />
</Routes>
<Footer />
{
settingsOpen && <Settings onClickSettings={onClickSettings} />
}
</Container>
</Router>
</ThemeProvider>
</SettingsProvider>
);
}
Expand Down
27 changes: 18 additions & 9 deletions src/components/Chess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Fullscreen } from '../util/Fullscreen';
import { useParams } from 'react-router-dom';
import { useChessContext } from '../providers/ChessProvider';
import { SettingsContext } from '@/providers/SettingsProvider';
import { LobbyContext } from '@/providers/LobbyProvider';

const ChessContainer = styled.div<{ fullscreen: boolean }>`
${props => props.fullscreen && `display: flex;
Expand Down Expand Up @@ -55,30 +56,35 @@ const BoardContainer = styled.div`
aspect-ratio: 1;
`;

export const Chess: React.FC = () => {
interface ChessProps {
type: 'local' | 'bot' | 'online'
}

export const Chess: React.FC<ChessProps> = ({ type }) => {
const [fullscreen, setIsFullscreen] = useState(false);
const [connecting, setIsConnecting] = useState(false);
const [connectState, setConnectState] = useState('');
const { hasLoaded } = useContext(SettingsContext);
const { id } = useParams();
const { StartNewGame } = useChessContext();
const navigate = useNavigate();

const lobby = useContext(LobbyContext);

useEffect(() => {
if (!hasLoaded) {
return;
}

if (id === 'bot') {
if (type === 'bot') {
StartNewGame({ player_white: 'local', player_black: 'bot', positions: 'default' });
} else if (!id || id.length === 0) {
} else if (type === 'local') {
StartNewGame({ player_white: 'local', player_black: 'local', positions: 'default' });
} else {
setIsConnecting(true);
// connect to multiplayer session (if we can)
if (lobby?.type === 'ready') {
lobby.Connect(id ?? '', (error) => {
alert(error);
});
}
}
}, [hasLoaded]);
}, [hasLoaded, lobby?.type]);

const toggleFullscreen = () => {
setIsFullscreen(b => !b);
Expand All @@ -92,6 +98,9 @@ export const Chess: React.FC = () => {
return (
<Fullscreen isFullscreen={fullscreen}>
<ChessContainer fullscreen={fullscreen}>
{
lobby && lobby.type !== 'ingame' && <p>connecting...</p>
}
<GameContainer fullscreen={fullscreen}>
<BoardContainer>
<Chessboard />
Expand Down
28 changes: 27 additions & 1 deletion src/components/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef, useState } from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';

Expand All @@ -19,6 +19,7 @@ const HomeButtonContainer = styled.div`
justify-content: space-around;
max-width: 200px;
margin: 0 auto;
align-items: center;
`;

const HomeButton = styled(Link)`
Expand All @@ -31,7 +32,18 @@ const HomeButton = styled(Link)`
text-align: center;
`;

const HomeInput = styled.input`
font-size: 2em;
width: 140px;
text-align: center;
padding: 10px;
border-radius: 10px;
`;

export const Home: React.FC = () => {
const idRef = useRef<HTMLInputElement>(null);
const [id, setId] = useState('');

return (
<HomeContainer>
<HomeHeader>create a game</HomeHeader>
Expand All @@ -40,6 +52,20 @@ export const Home: React.FC = () => {
<HomeButton to="/game/bot">bot</HomeButton>
<HomeButton to="/lobby">online</HomeButton>
</HomeButtonContainer>
<HomeHeader>join game</HomeHeader>
<HomeButtonContainer>
<HomeInput
placeholder='lobby id'
maxLength={6}
value={id}
ref={idRef}
type="text"
onChange={() => {
setId(idRef.current?.value ?? '');
}}
/>
<HomeButton to={`/lobby/${id}`}>go</HomeButton>
</HomeButtonContainer>
<HomeParagraph>
play chess against a local player, a bot, or an online player.
</HomeParagraph>
Expand Down
135 changes: 131 additions & 4 deletions src/components/Lobby.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,136 @@
import React from 'react';
import { useLobbyContext } from '@/providers/LobbyProvider';
import { SettingsContext } from '@/providers/SettingsProvider';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';

const LobbyContainer = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`;

const LobbyHeader = styled.div`
display: flex;
align-items: center;
`;

const LobbyHeaderText = styled.h1`
color: ${props => props.theme.colors.text};
flex: 1;
font-size: 3em;
`;

const LobbyHeaderID = styled.input`
font-size: 2em;
width: 140px;
text-align: center;
`;

const LobbyHeaderCopyID = styled.button`
font-size: 2em;
`;

const LobbyDetails = styled.div`
`;

const LobbyControls = styled.div`
`;

const LobbyButton = styled.button`
font-size: 2em;
`;

const LobbyText = styled.p`
color: ${props => props.theme.colors.text};
`;

const LobbyError = styled.h1`
color: red;
`;

export const Lobby: React.FC = () => {
const lobby = useLobbyContext();
const { id } = useParams();
const [error, setError] = useState<string | undefined>();
const navigate = useNavigate();
const { hasLoaded } = useContext(SettingsContext);

useEffect(() => {
if (lobby.type === 'loading') return;
if (lobby.type === 'lobby') {
if (!id) {
navigate(`/lobby/${lobby.lobby.lobbyId}/`);
window.location.reload();
}
return;
}
if (lobby.type === 'ingame') {
navigate(`/game/${lobby.lobby.lobbyId}/`);
return;
}
if (!hasLoaded) return;

if (id) {
lobby.Connect(id, setError);
} else {
lobby.Create(setError);
}
}, [lobby.type, id, hasLoaded]);

return (
<div>
<h1>multiplayer has not been implemented yet</h1>
</div>
<LobbyContainer>
<LobbyHeader>
<LobbyHeaderText>lobby</LobbyHeaderText>
{
lobby.type === 'lobby' && (
<>
<LobbyHeaderID value={lobby.lobby.lobbyId} type="text" disabled />
<LobbyHeaderCopyID
onClick={() => {
navigator.clipboard.writeText(window.location.href);
}}
>
copy
</LobbyHeaderCopyID>
</>
)
}
</LobbyHeader>
{
error ? (
<>
<LobbyError>{error}</LobbyError>
</>
) :
lobby.type === 'lobby' ? (<>
<LobbyDetails>
<LobbyText>white: {lobby.lobby.players.w.name}</LobbyText>
{
lobby.lobby.players.b && <LobbyText>black: {lobby.lobby.players.b.name}</LobbyText>
}
<LobbyText>spectators:{' '}
{
(lobby && lobby.lobby.spectators && lobby.lobby.spectators.length > 0) ?
lobby.lobby.spectators.map(spectator => <span key={spectator.uid}>{spectator.name}</span>)
: <span>none</span>
}
</LobbyText>
<LobbyText>game length: 5 minutes (change in settings)</LobbyText>
</LobbyDetails>
{
lobby.lobby.hostUid === lobby.uid && (
<LobbyControls
onClick={lobby.Start}
>
<LobbyButton>start game</LobbyButton>
</LobbyControls>
)
}
</>) : (
<LobbyText>{id ? 'connecting to' : 'creating'} lobby...</LobbyText>
)
}
</LobbyContainer >
);
};
8 changes: 5 additions & 3 deletions src/components/game/Chessboard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useContext } from 'react';
import styled, { DefaultTheme } from 'styled-components';
import { SquareToXY, XYtoSquare, useChessContext } from '../../providers/ChessProvider';
import { Error } from '../../util/Error';
import { ChessPiece } from './ChessPiece';
import { Color, PieceSymbol, Square } from 'chess.js';
import { pieceToFilename, pieceToName, pieceToString } from '@/game/piece';
import { LobbyContext } from '@/providers/LobbyProvider';

interface MoveProps {
grid_x: number,
Expand Down Expand Up @@ -134,11 +135,12 @@ interface GridPosition {
}

export const Chessboard: React.FC = () => {
const { board, turn, PotentialMoves, MakeMove, Promote } = useChessContext();
const { state: { board, turn }, PotentialMoves, MakeMove, Promote } = useChessContext();
const [selected, setSelected] = useState<GridPosition | null>(null);
const boardRef = useRef<HTMLDivElement>(null);
const [moveError, setMoveError] = useState('');
const [promotion, setPromotion] = useState<{ from: Square, to: Square } | undefined>(undefined);
const lobby = useContext(LobbyContext);

const onTouchMove = (e: TouchEvent) => {
if (!e.target || !boardRef.current) return;
Expand Down Expand Up @@ -237,7 +239,7 @@ export const Chessboard: React.FC = () => {
pixels_to_grid={pixelsToGrid}
grid_to_pixels={gridToPixels}
on_select_change={(selected) => selected ? setSelected({ grid_x: v.x, grid_y: v.y }) : setSelected(null)}
can_click={v.team === turn}
can_click={(v.team === turn && (!lobby || (lobby.type === 'ingame' && lobby.lobby.players[turn]?.uid === lobby.uid))) ?? false}
/>
)
}
Expand Down
Loading

0 comments on commit 5a147b5

Please sign in to comment.