commit
3ff2b657a3
@ -20,6 +20,7 @@
|
|||||||
"mancala.js": "^0.0.2-beta.3",
|
"mancala.js": "^0.0.2-beta.3",
|
||||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-router-dom": "6",
|
||||||
"styled-jsx": "^5.0.2",
|
"styled-jsx": "^5.0.2",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { initContext } from './context/context';
|
|
||||||
initContext();
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
const container = document.getElementById('main');
|
const container = document.getElementById('main');
|
||||||
const root = createRoot(container!);
|
const root = createRoot(container!);
|
||||||
import Home from './routes/Home';
|
import MancalaApp from './MancalaApp';
|
||||||
root.render(<Home/>);
|
root.render(<MancalaApp/>);
|
||||||
101
src/MancalaApp.tsx
Normal file
101
src/MancalaApp.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent, useState } from 'react';
|
||||||
|
import {
|
||||||
|
BrowserRouter,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
} from "react-router-dom";
|
||||||
|
import FloatingPanel from './components/FloatingPanel';
|
||||||
|
import GamePage from './routes/GamePage';
|
||||||
|
import Home from './routes/Home';
|
||||||
|
|
||||||
|
import { initContext } from './context/context';
|
||||||
|
import { RTMTWS } from './rtmt/rtmt_websocket';
|
||||||
|
import { getColorByBrightness } from './util/ColorUtil';
|
||||||
|
import { ConnectionState } from './models/ConnectionState';
|
||||||
|
import { Theme } from './theme/Theme';
|
||||||
|
import LobyPage from './routes/LobyPage';
|
||||||
|
const context = initContext();
|
||||||
|
|
||||||
|
const MancalaApp: FunctionComponent = () => {
|
||||||
|
|
||||||
|
const [userKey, setUserKey] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const [connectionState, setConnetionState] = useState<ConnectionState>("connecting");
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState<Theme>(context.themeManager.theme);
|
||||||
|
|
||||||
|
const onConnectionDone = () => {
|
||||||
|
setConnetionState("connected");
|
||||||
|
};
|
||||||
|
const onConnectionLost = () => {
|
||||||
|
connectToServer("reconnecting");
|
||||||
|
};
|
||||||
|
const onConnectionError = (event: Event) => {
|
||||||
|
setConnetionState("error");
|
||||||
|
};
|
||||||
|
const connectToServer = async (connectionState: ConnectionState) => {
|
||||||
|
setConnetionState(connectionState);
|
||||||
|
const userKey = await context.userKeyStore.getUserKey();
|
||||||
|
setUserKey(userKey);
|
||||||
|
const rtmtws = context.rtmt as RTMTWS;
|
||||||
|
if (rtmtws) {
|
||||||
|
rtmtws.initWebSocket(
|
||||||
|
userKey,
|
||||||
|
onConnectionDone,
|
||||||
|
onConnectionLost,
|
||||||
|
onConnectionError
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error("context.rtmt is not RTMTWS");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
React.useEffect(() => {
|
||||||
|
connectToServer("connecting");
|
||||||
|
context.themeManager.onThemeChange = (theme: Theme) => {
|
||||||
|
setTheme(theme);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
// todo: dispose rtmt.dispose
|
||||||
|
//context.rtmt.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showConnectionState = connectionState != "connected";
|
||||||
|
const floatingPanelColor = context.themeManager.theme.boardColor;
|
||||||
|
const connectionStateText = () => {
|
||||||
|
const map: { [key: string]: string } = {
|
||||||
|
connecting: context.texts.Connecting,
|
||||||
|
connected: context.texts.Connected,
|
||||||
|
error: context.texts.CannotConnect,
|
||||||
|
reconnecting: context.texts.ConnectingAgain,
|
||||||
|
};
|
||||||
|
return map[connectionState];
|
||||||
|
};
|
||||||
|
const textColorOnBoard = getColorByBrightness(
|
||||||
|
context.themeManager.theme.boardColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Home context={context} theme={theme} userKey={userKey} />} />
|
||||||
|
<Route path="/" >
|
||||||
|
<Route path="game" >
|
||||||
|
<Route path=":gameId" element={<GamePage context={context} theme={theme} userKey={userKey} connectionState={connectionState} />} ></Route>
|
||||||
|
</Route>
|
||||||
|
<Route path="loby" element={<LobyPage context={context} theme={theme} userKey={userKey} />}>
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
<FloatingPanel context={context} color={floatingPanelColor} visible={showConnectionState}>
|
||||||
|
<span style={{ color: textColorOnBoard }}>{connectionStateText()}</span>
|
||||||
|
</FloatingPanel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MancalaApp;
|
||||||
@ -42,7 +42,7 @@ export default class PitAnimator {
|
|||||||
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||||
}
|
}
|
||||||
|
|
||||||
public setUpdatedGame(game: MancalaGame, forceClear = false) {
|
public setUpdatedGame(game: MancalaGame) {
|
||||||
this.resetAnimationState();
|
this.resetAnimationState();
|
||||||
if (!this.game) {
|
if (!this.game) {
|
||||||
this.setNewGame(game);
|
this.setNewGame(game);
|
||||||
@ -68,7 +68,7 @@ export default class PitAnimator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAnimate() {
|
onAnimate() {
|
||||||
if(!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return;
|
if (!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return;
|
||||||
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
|
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
|
||||||
this.clearCurrentInterval();
|
this.clearCurrentInterval();
|
||||||
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||||
@ -92,7 +92,7 @@ export default class PitAnimator {
|
|||||||
boardViewModel: BoardViewModel,
|
boardViewModel: BoardViewModel,
|
||||||
gameStep: GameStep
|
gameStep: GameStep
|
||||||
) {
|
) {
|
||||||
if(!this.currentHistoryItem || !this.game) return;
|
if (!this.currentHistoryItem || !this.game) return;
|
||||||
const pitViewModel = boardViewModel.pits[index];
|
const pitViewModel = boardViewModel.pits[index];
|
||||||
if (this.animationIndex === 0) {
|
if (this.animationIndex === 0) {
|
||||||
//This is one stone move case, TODO: beautify it later
|
//This is one stone move case, TODO: beautify it later
|
||||||
|
|||||||
28
src/components/LoadingComponent.tsx
Normal file
28
src/components/LoadingComponent.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { Context } from '../context/context';
|
||||||
|
import { LoadingState } from '../models/LoadingState';
|
||||||
|
import { getColorByBrightness } from '../util/ColorUtil';
|
||||||
|
import CircularPanel from './CircularPanel';
|
||||||
|
|
||||||
|
const LoadingComponent: FunctionComponent<{ context: Context, loadingState: LoadingState<any> }> = ({ context, loadingState }) => {
|
||||||
|
if (loadingState.isUnset() || loadingState.isLoaded()) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
if (loadingState.isLoading() || loadingState.isError()) {
|
||||||
|
const textColorOnBoard = getColorByBrightness(
|
||||||
|
context.themeManager.theme.boardColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
const text = loadingState.isLoading() ? context.texts.Loading +"..." : loadingState.errorMessage;
|
||||||
|
return (
|
||||||
|
<CircularPanel color={context.themeManager.theme.boardColor}>
|
||||||
|
<h4 style={{ margin: "0", color: textColorOnBoard }}>{`${text}`}</h4>
|
||||||
|
</CircularPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadingComponent;
|
||||||
15
src/const/config.ts
Normal file
15
src/const/config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const useLocalServer = false;
|
||||||
|
|
||||||
|
export type Server = {
|
||||||
|
serverAdress: string;
|
||||||
|
wsServerAdress: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const server: Server = useLocalServer ? {
|
||||||
|
serverAdress: "http://localhost:5000",
|
||||||
|
wsServerAdress: "ws://localhost:5000",
|
||||||
|
} : {
|
||||||
|
serverAdress: "https://segin.one/mancala-backend-beta",
|
||||||
|
wsServerAdress: "wss://segin.one/mancala-backend-beta/",
|
||||||
|
};
|
||||||
|
|
||||||
@ -22,6 +22,8 @@ export type Texts = {
|
|||||||
PleaseWait : string,
|
PleaseWait : string,
|
||||||
GameDraw : string,
|
GameDraw : string,
|
||||||
Anonymous: string,
|
Anonymous: string,
|
||||||
|
GameNotFound: string,
|
||||||
|
Loading: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EnUs: Texts = {
|
export const EnUs: Texts = {
|
||||||
@ -47,6 +49,8 @@ export const EnUs: Texts = {
|
|||||||
PleaseWait : "Please Wait",
|
PleaseWait : "Please Wait",
|
||||||
GameDraw : "Draw",
|
GameDraw : "Draw",
|
||||||
Anonymous: "Anonymous",
|
Anonymous: "Anonymous",
|
||||||
|
GameNotFound: "Game Not Found",
|
||||||
|
Loading: "Loading",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TrTr: Texts = {
|
export const TrTr: Texts = {
|
||||||
@ -71,5 +75,7 @@ export const TrTr: Texts = {
|
|||||||
SearchingOpponent: "Rakip Aranıyor",
|
SearchingOpponent: "Rakip Aranıyor",
|
||||||
PleaseWait: "Lütfen Bekleyin",
|
PleaseWait: "Lütfen Bekleyin",
|
||||||
GameDraw : "Berabere",
|
GameDraw : "Berabere",
|
||||||
Anonymous: "Anonim"
|
Anonymous: "Anonim",
|
||||||
|
GameNotFound: "Oyun Bulunamadı",
|
||||||
|
Loading: "Yükleniyor",
|
||||||
}
|
}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
|
import { server } from "../const/config";
|
||||||
import { Texts, TrTr } from "../const/texts";
|
import { Texts, TrTr } from "../const/texts";
|
||||||
import { RTMT } from "../rtmt/rtmt";
|
import { RTMT } from "../rtmt/rtmt";
|
||||||
import { RTMTWS } from "../rtmt/rtmt_websocket";
|
import { RTMTWS } from "../rtmt/rtmt_websocket";
|
||||||
import { UserKeyStore, UserKeyStoreImpl } from "../store/key_store";
|
import { HttpServiceImpl } from "../service/HttpService";
|
||||||
|
import { GameStore, GameStoreImpl } from "../store/GameStore";
|
||||||
|
import { UserKeyStore, UserKeyStoreImpl } from "../store/KeyStore";
|
||||||
import ThemeManager from "../theme/ThemeManager";
|
import ThemeManager from "../theme/ThemeManager";
|
||||||
|
|
||||||
export type Context = {
|
export type Context = {
|
||||||
@ -9,11 +12,14 @@ export type Context = {
|
|||||||
userKeyStore: UserKeyStore;
|
userKeyStore: UserKeyStore;
|
||||||
texts: Texts;
|
texts: Texts;
|
||||||
themeManager: ThemeManager;
|
themeManager: ThemeManager;
|
||||||
|
gameStore: GameStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initContext = () => {
|
export const initContext = (): Context => {
|
||||||
const rtmt = new RTMTWS();
|
const rtmt = new RTMTWS();
|
||||||
const userKeyStore = new UserKeyStoreImpl();
|
const httpService = new HttpServiceImpl(server.serverAdress);
|
||||||
|
const userKeyStore = new UserKeyStoreImpl({ httpService });
|
||||||
|
const gameStore = new GameStoreImpl({ httpService });
|
||||||
const texts = TrTr;
|
const texts = TrTr;
|
||||||
const themeManager = new ThemeManager();
|
const themeManager = new ThemeManager();
|
||||||
return {
|
return {
|
||||||
@ -21,7 +27,6 @@ export const initContext = () => {
|
|||||||
userKeyStore: userKeyStore,
|
userKeyStore: userKeyStore,
|
||||||
texts: texts,
|
texts: texts,
|
||||||
themeManager,
|
themeManager,
|
||||||
|
gameStore
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const context: Context = initContext();
|
|
||||||
|
|||||||
1
src/models/ConnectionState.ts
Normal file
1
src/models/ConnectionState.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
|
||||||
46
src/models/LoadingState.tsx
Normal file
46
src/models/LoadingState.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
export type LoadingStateType = "unset" | "loading" | "loaded" | "error";
|
||||||
|
|
||||||
|
export class LoadingState<T> {
|
||||||
|
state: LoadingStateType;
|
||||||
|
|
||||||
|
errorMessage?: string;
|
||||||
|
|
||||||
|
value?: T;
|
||||||
|
|
||||||
|
constructor(props: { state?: LoadingStateType, errorMessage?: string, value?: T }) {
|
||||||
|
this.state = props.state ? props.state : "unset";
|
||||||
|
this.errorMessage = props.errorMessage;
|
||||||
|
this.value = props.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Unset<T>() {
|
||||||
|
return new LoadingState<T>({ state: "unset" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Loading<T>() {
|
||||||
|
return new LoadingState<T>({ state: "loading" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Error<T>(props: { errorMessage: string }) {
|
||||||
|
const { errorMessage } = props;
|
||||||
|
return new LoadingState<T>({ state: "error", errorMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Loaded<T>(props: { value?: T }) {
|
||||||
|
const { value } = props;
|
||||||
|
return new LoadingState<T>({ state: "loaded", value });
|
||||||
|
}
|
||||||
|
|
||||||
|
public isUnset() : boolean {
|
||||||
|
return this.state === "unset";
|
||||||
|
}
|
||||||
|
public isLoading() : boolean {
|
||||||
|
return this.state === "loading";
|
||||||
|
}
|
||||||
|
public isError() : boolean {
|
||||||
|
return this.state === "error";
|
||||||
|
}
|
||||||
|
public isLoaded() : boolean {
|
||||||
|
return this.state === "loaded";
|
||||||
|
}
|
||||||
|
}
|
||||||
244
src/routes/GamePage.tsx
Normal file
244
src/routes/GamePage.tsx
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import { CommonMancalaGame, MancalaGame, Pit } from 'mancala.js';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import PitAnimator from '../animation/PitAnimator';
|
||||||
|
import BoardToolbar from '../components/board/BoardToolbar';
|
||||||
|
import BoardView from '../components/board/BoardView';
|
||||||
|
import Button from '../components/Button';
|
||||||
|
import HeaderBar from '../components/headerbar/HeaderBar';
|
||||||
|
import HeaderbarIcon from '../components/headerbar/HeaderbarIcon';
|
||||||
|
import HeaderbarTitle from '../components/headerbar/HeaderbarTitle';
|
||||||
|
import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu';
|
||||||
|
import InfoPanel from '../components/InfoPanel';
|
||||||
|
import LoadingComponent from '../components/LoadingComponent';
|
||||||
|
import PageContainer from '../components/PageContainer';
|
||||||
|
import Row from '../components/Row';
|
||||||
|
import UserStatus from '../components/UserStatus';
|
||||||
|
import { channel_on_game_update, channel_on_game_crashed, channel_on_game_user_leave, channel_on_user_connection_change, channel_leave_game, channel_game_move } from '../const/channel_names';
|
||||||
|
import { Context } from '../context/context';
|
||||||
|
import useWindowDimensions from '../hooks/useWindowDimensions';
|
||||||
|
import { ConnectionState } from '../models/ConnectionState';
|
||||||
|
import { GameMove } from '../models/GameMove';
|
||||||
|
import { LoadingState } from '../models/LoadingState';
|
||||||
|
import { UserConnectionInfo } from '../models/UserConnectionInfo';
|
||||||
|
import { Theme } from '../theme/Theme';
|
||||||
|
import { getColorByBrightness } from '../util/ColorUtil';
|
||||||
|
import BoardViewModel from '../viewmodel/BoardViewModel';
|
||||||
|
import Center from '../components/Center';
|
||||||
|
|
||||||
|
const GamePage: FunctionComponent<{
|
||||||
|
context: Context,
|
||||||
|
userKey?: string,
|
||||||
|
theme: Theme,
|
||||||
|
connectionState: ConnectionState
|
||||||
|
}> = ({ context, userKey, theme, connectionState }) => {
|
||||||
|
let params = useParams<{ gameId: string }>();
|
||||||
|
|
||||||
|
const [game, setGame] = useState<MancalaGame | undefined>(undefined);
|
||||||
|
|
||||||
|
const [crashMessage, setCrashMessage] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel | undefined>(undefined);
|
||||||
|
|
||||||
|
const [boardId, setBoardId] = useState<string>("-1");
|
||||||
|
|
||||||
|
const [pitAnimator, setPitAnimator] = useState<PitAnimator | undefined>(undefined);
|
||||||
|
|
||||||
|
// It is a flag for ongoing action such as send game move.
|
||||||
|
// We have to block future actions if there is an ongoing action.
|
||||||
|
const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [isOpponentOnline, setIsOpponentOnline] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { height, width } = useWindowDimensions();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [gameLoadingState, setLoadingStateGame] = useState<LoadingState<MancalaGame>>(LoadingState.Unset());
|
||||||
|
|
||||||
|
const onGameUpdate = (pitAnimator: PitAnimator, message: Object) => {
|
||||||
|
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||||
|
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
||||||
|
setGame(mancalaGame);
|
||||||
|
pitAnimator.setUpdatedGame(mancalaGame);
|
||||||
|
setHasOngoingAction(false);
|
||||||
|
}
|
||||||
|
const onGameCrashed = (message: any) => {
|
||||||
|
const newCrashMessage = message as string;
|
||||||
|
console.error("on_game_crash");
|
||||||
|
console.error(newCrashMessage);
|
||||||
|
setCrashMessage(newCrashMessage);
|
||||||
|
}
|
||||||
|
const onGameUserLeave = (message: any) => {
|
||||||
|
const userKeyWhoLeave = message;
|
||||||
|
setUserKeyWhoLeave(userKeyWhoLeave);
|
||||||
|
setHasOngoingAction(false);
|
||||||
|
};
|
||||||
|
const onUserConnectionChange = (message: any) => {
|
||||||
|
const userConnectionInfo = message as UserConnectionInfo;
|
||||||
|
//todo: change this when implementing watch the game feature
|
||||||
|
setIsOpponentOnline(userConnectionInfo.isOnline);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenMessages = (pitAnimator: PitAnimator) : () => void => {
|
||||||
|
const _onGameUpdate = (message: object) => onGameUpdate(pitAnimator, message);
|
||||||
|
context.rtmt.listenMessage(channel_on_game_update, _onGameUpdate);
|
||||||
|
context.rtmt.listenMessage(channel_on_game_crashed, onGameCrashed);
|
||||||
|
context.rtmt.listenMessage(channel_on_game_user_leave, onGameUserLeave);
|
||||||
|
context.rtmt.listenMessage(channel_on_user_connection_change, onUserConnectionChange)
|
||||||
|
return () => {
|
||||||
|
context.rtmt.unlistenMessage(channel_on_game_update, _onGameUpdate);
|
||||||
|
context.rtmt.unlistenMessage(channel_on_game_crashed, onGameCrashed);
|
||||||
|
context.rtmt.unlistenMessage(channel_on_game_user_leave, onGameUserLeave);
|
||||||
|
context.rtmt.unlistenMessage(channel_on_user_connection_change, onUserConnectionChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
|
||||||
|
boardViewModel.id = v4();
|
||||||
|
setBoardId(boardViewModel.id);
|
||||||
|
setBoardViewModel(boardViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBoardIndex = (index: number) => {
|
||||||
|
if (!game) return -1;
|
||||||
|
if (userKey === game.player2Id) return index + game.board.pits.length / 2;
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOpponentId = () => game?.player1Id === userKey ? game?.player2Id : game?.player1Id;
|
||||||
|
|
||||||
|
const checkHasAnOngoingAction = () => hasOngoingAction;
|
||||||
|
|
||||||
|
const onLeaveGameClick = () => {
|
||||||
|
context.rtmt.sendMessage(channel_leave_game, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNewGameClick = () => {
|
||||||
|
navigate("/loby")
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPitSelect = (index: number, pit: Pit) => {
|
||||||
|
if (checkHasAnOngoingAction()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHasOngoingAction(true);
|
||||||
|
if (!boardViewModel) return;
|
||||||
|
//TODO : stoneCount comes from view model!
|
||||||
|
if (pit.stoneCount === 0) {
|
||||||
|
//TODO : warn user
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boardViewModel.pits[getBoardIndex(index)].pitColor =
|
||||||
|
context.themeManager.theme.pitSelectedColor;
|
||||||
|
updateBoardViewModel(boardViewModel);
|
||||||
|
const gameMove: GameMove = { index: index };
|
||||||
|
context.rtmt.sendMessage(channel_game_move, gameMove);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let pitAnimator: PitAnimator | undefined;
|
||||||
|
let unlistenMessages: ()=>void;
|
||||||
|
setLoadingStateGame(LoadingState.Loading())
|
||||||
|
context.gameStore.get(params.gameId!!).then((game) => {
|
||||||
|
if (game) {
|
||||||
|
setGame(game);
|
||||||
|
setHasOngoingAction(false);
|
||||||
|
pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
||||||
|
pitAnimator.setNewGame(game);
|
||||||
|
setPitAnimator(pitAnimator);
|
||||||
|
unlistenMessages = listenMessages(pitAnimator);
|
||||||
|
setLoadingStateGame(LoadingState.Loaded({ value: game }))
|
||||||
|
} else {
|
||||||
|
setLoadingStateGame(LoadingState.Error({ errorMessage: context.texts.GameNotFound }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
unlistenMessages?.();
|
||||||
|
pitAnimator?.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const textColorOnAppBar = getColorByBrightness(
|
||||||
|
context.themeManager.theme.appBarBgColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.state == "ended");
|
||||||
|
const showBoardView = game && boardViewModel && userKey && true;
|
||||||
|
const opponentUser = { id: getOpponentId() || "0", name: "Anonymous", isOnline: isOpponentOnline, isAnonymous: true };
|
||||||
|
const user = { id: userKey || "1", name: "Anonymous", isOnline: connectionState === "connected", isAnonymous: true };
|
||||||
|
|
||||||
|
const isMobile = width < 600;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer theme={theme!}>
|
||||||
|
<HeaderBar color={theme?.appBarBgColor}>
|
||||||
|
<Row>
|
||||||
|
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||||
|
<HeaderbarIcon />
|
||||||
|
</Link>
|
||||||
|
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||||
|
<HeaderbarTitle title={context.texts.Mancala} color={textColorOnAppBar} />
|
||||||
|
</Link>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
||||||
|
<Button
|
||||||
|
context={context}
|
||||||
|
color={context.themeManager.theme.pitColor}
|
||||||
|
text={renderNewGameBtn ? context.texts.NewGame : context.texts.Leave}
|
||||||
|
onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
|
||||||
|
</Row>
|
||||||
|
</HeaderBar>
|
||||||
|
<BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
|
||||||
|
<InfoPanel
|
||||||
|
style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
|
||||||
|
context={context}
|
||||||
|
game={game}
|
||||||
|
crashMessage={crashMessage}
|
||||||
|
userKey={userKey}
|
||||||
|
userKeyWhoLeave={userKeyWhoLeave} />
|
||||||
|
</BoardToolbar>
|
||||||
|
<BoardToolbar style={{ alignItems: "flex-end" }} visible={showBoardView || false}>
|
||||||
|
<UserStatus style={{
|
||||||
|
marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw",
|
||||||
|
width: isMobile ? "40vw" : "30vw"
|
||||||
|
}} context={context} layoutMode="left" user={opponentUser} visible={showBoardView || false} />
|
||||||
|
<InfoPanel
|
||||||
|
style={{
|
||||||
|
marginTop: "0.5rem", marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
context={context}
|
||||||
|
game={game}
|
||||||
|
crashMessage={crashMessage}
|
||||||
|
userKey={userKey}
|
||||||
|
userKeyWhoLeave={userKeyWhoLeave}
|
||||||
|
visible={!isMobile} />
|
||||||
|
<UserStatus style={{
|
||||||
|
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
|
||||||
|
width: isMobile ? "40vw" : "30vw"
|
||||||
|
}} context={context} layoutMode="right" user={user} visible={showBoardView || false} />
|
||||||
|
</BoardToolbar>
|
||||||
|
{showBoardView && (
|
||||||
|
<BoardView
|
||||||
|
userKey={userKey}
|
||||||
|
game={game}
|
||||||
|
boardId={boardId}
|
||||||
|
boardViewModel={boardViewModel}
|
||||||
|
context={context}
|
||||||
|
onPitSelect={onPitSelect} />
|
||||||
|
)}
|
||||||
|
<Center>
|
||||||
|
<LoadingComponent context={context} loadingState={gameLoadingState}></LoadingComponent>
|
||||||
|
</Center>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GamePage;
|
||||||
@ -1,302 +1,54 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent, useEffect, useState } from "react";
|
import { FunctionComponent, useEffect, useState } from "react";
|
||||||
import BoardView from "../components/board/BoardView";
|
|
||||||
import { context } from "../context/context";
|
|
||||||
import { RTMTWS } from "../rtmt/rtmt_websocket";
|
|
||||||
import {
|
|
||||||
channel_game_move,
|
|
||||||
channel_leave_game,
|
|
||||||
channel_on_game_crashed,
|
|
||||||
channel_on_game_start,
|
|
||||||
channel_on_game_update,
|
|
||||||
channel_on_game_user_leave,
|
|
||||||
channel_on_user_connection_change,
|
|
||||||
} from "../const/channel_names";
|
|
||||||
import InfoPanel from "../components/InfoPanel";
|
|
||||||
import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js";
|
|
||||||
import { GameMove } from "../models/GameMove";
|
|
||||||
import PitAnimator from "../animation/PitAnimator";
|
|
||||||
import BoardViewModel from "../viewmodel/BoardViewModel";
|
|
||||||
import { v4 } from "uuid";
|
|
||||||
import { getColorByBrightness } from "../util/ColorUtil";
|
import { getColorByBrightness } from "../util/ColorUtil";
|
||||||
import { Theme } from "../theme/Theme";
|
import { Theme } from "../theme/Theme";
|
||||||
import HeaderBar from "../components/headerbar/HeaderBar";
|
import HeaderBar from "../components/headerbar/HeaderBar";
|
||||||
import FloatingPanel from "../components/FloatingPanel";
|
|
||||||
import PageContainer from "../components/PageContainer";
|
import PageContainer from "../components/PageContainer";
|
||||||
import Row from "../components/Row";
|
import Row from "../components/Row";
|
||||||
import HeaderbarIcon from "../components/headerbar/HeaderbarIcon";
|
import HeaderbarIcon from "../components/headerbar/HeaderbarIcon";
|
||||||
import HeaderbarTitle from "../components/headerbar/HeaderbarTitle";
|
import HeaderbarTitle from "../components/headerbar/HeaderbarTitle";
|
||||||
import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu";
|
import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu";
|
||||||
import Button from "../components/Button";
|
import Button from "../components/Button";
|
||||||
import BoardToolbar from "../components/board/BoardToolbar";
|
import { Context } from "../context/context";
|
||||||
import UserStatus from "../components/UserStatus";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import Center from "../components/Center";
|
|
||||||
import CircularPanel from "../components/CircularPanel";
|
|
||||||
import useWindowDimensions from "../hooks/useWindowDimensions";
|
|
||||||
import { UserConnectionInfo } from "../models/UserConnectionInfo";
|
|
||||||
|
|
||||||
type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
|
const Home: FunctionComponent<{
|
||||||
|
context: Context,
|
||||||
|
userKey?: string,
|
||||||
|
theme: Theme,
|
||||||
|
}> = ({ context, userKey, theme }) => {
|
||||||
|
|
||||||
const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
const navigate = useNavigate();
|
||||||
const [userKey, setUserKey] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const [connectionState, setConnetionState] =
|
|
||||||
useState<ConnectionState>("connecting");
|
|
||||||
|
|
||||||
const [searchingOpponent, setSearchingOpponent] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [game, setGame] = useState<CommonMancalaGame | undefined>(undefined);
|
|
||||||
|
|
||||||
const [crashMessage, setCrashMessage] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel | undefined>(undefined);
|
|
||||||
|
|
||||||
const [boardId, setBoardId] = useState<string>("-1");
|
|
||||||
|
|
||||||
const [pitAnimator, setPitAnimator] = useState<PitAnimator | undefined>(undefined);
|
|
||||||
|
|
||||||
const [theme, setTheme] = useState<Theme | undefined>(undefined);
|
|
||||||
|
|
||||||
// It is a flag for ongoing action such as send game move.
|
|
||||||
// We have to block future actions if there is an ongoing action.
|
|
||||||
const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const { height, width } = useWindowDimensions();
|
|
||||||
|
|
||||||
const [isOpponentOnline, setIsOpponentOnline] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const onConnectionDone = () => {
|
|
||||||
setConnetionState("connected");
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConnectionLost = () => {
|
|
||||||
connectToServer("reconnecting");
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConnectionError = (event: Event) => {
|
|
||||||
setConnetionState("error");
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectToServer = (connectionState: ConnectionState) => {
|
|
||||||
setConnetionState(connectionState);
|
|
||||||
context.userKeyStore.getUserKey((userKey: string) => {
|
|
||||||
setUserKey(userKey);
|
|
||||||
const rtmtws = context.rtmt as RTMTWS;
|
|
||||||
if (rtmtws) {
|
|
||||||
rtmtws.initWebSocket(
|
|
||||||
userKey,
|
|
||||||
onConnectionDone,
|
|
||||||
onConnectionLost,
|
|
||||||
onConnectionError
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error("context.rtmt is not RTMTWS");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const listenMessages = (pitAnimator: PitAnimator) => {
|
|
||||||
context.rtmt.listenMessage(channel_on_game_start, (message: Object) => {
|
|
||||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
|
||||||
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
|
||||||
setSearchingOpponent(false);
|
|
||||||
setGame(mancalaGame);
|
|
||||||
pitAnimator.setNewGame(mancalaGame);
|
|
||||||
setHasOngoingAction(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.rtmt.listenMessage(channel_on_game_update, (message: Object) => {
|
|
||||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
|
||||||
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
|
||||||
setGame(mancalaGame);
|
|
||||||
pitAnimator.setUpdatedGame(mancalaGame);
|
|
||||||
setHasOngoingAction(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.rtmt.listenMessage(channel_on_game_crashed, (message: any) => {
|
|
||||||
const newCrashMessage = message as string;
|
|
||||||
console.error("on_game_crash");
|
|
||||||
console.error(newCrashMessage);
|
|
||||||
setCrashMessage(newCrashMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.rtmt.listenMessage(channel_on_game_user_leave, (message: any) => {
|
|
||||||
const userKeyWhoLeave = message;
|
|
||||||
setUserKeyWhoLeave(userKeyWhoLeave);
|
|
||||||
setHasOngoingAction(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
context.rtmt.listenMessage(channel_on_user_connection_change, (message: any) => {
|
|
||||||
const userConnectionInfo = message as UserConnectionInfo;
|
|
||||||
//todo: change this when implementing watch the game feature
|
|
||||||
setIsOpponentOnline(userConnectionInfo.isOnline);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
|
|
||||||
boardViewModel.id = v4();
|
|
||||||
setBoardId(boardViewModel.id);
|
|
||||||
setBoardViewModel(boardViewModel);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetGameState = () => {
|
|
||||||
setGame(undefined);
|
|
||||||
setCrashMessage(undefined);
|
|
||||||
setUserKeyWhoLeave(undefined);
|
|
||||||
setHasOngoingAction(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBoardIndex = (index: number) => {
|
|
||||||
if (!game) return -1;
|
|
||||||
if (userKey === game.player2Id) return index + game.board.pits.length / 2;
|
|
||||||
return index;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOpponentId = () => game?.player1Id === userKey ? game?.player2Id : game?.player1Id;
|
|
||||||
|
|
||||||
const checkHasAnOngoingAction = () => hasOngoingAction;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setTheme(context.themeManager.theme);
|
|
||||||
const pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
|
||||||
setPitAnimator(pitAnimator);
|
|
||||||
listenMessages(pitAnimator);
|
|
||||||
connectToServer("connecting");
|
|
||||||
return () => {
|
|
||||||
pitAnimator.dispose();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
context.themeManager.onThemeChange = (theme) => {
|
|
||||||
setTheme(theme);
|
|
||||||
pitAnimator && game && updateBoardViewModel(pitAnimator.getBoardViewModelFromGame(game));
|
|
||||||
};
|
|
||||||
}, [boardViewModel]);
|
|
||||||
|
|
||||||
const onNewGameClick = () => {
|
const onNewGameClick = () => {
|
||||||
resetGameState();
|
navigate("/loby")
|
||||||
setSearchingOpponent(true);
|
|
||||||
context.rtmt.sendMessage("new_game", {});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLeaveGameClick = () => {
|
|
||||||
context.rtmt.sendMessage(channel_leave_game, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPitSelect = (index: number, pit: Pit) => {
|
|
||||||
if (checkHasAnOngoingAction()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setHasOngoingAction(true);
|
|
||||||
if (!boardViewModel) return;
|
|
||||||
//TODO : stoneCount comes from view model!
|
|
||||||
if (pit.stoneCount === 0) {
|
|
||||||
//TODO : warn user
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boardViewModel.pits[getBoardIndex(index)].pitColor =
|
|
||||||
context.themeManager.theme.pitSelectedColor;
|
|
||||||
updateBoardViewModel(boardViewModel);
|
|
||||||
const gameMove: GameMove = { index: index };
|
|
||||||
context.rtmt.sendMessage(channel_game_move, gameMove);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showConnectionState = connectionState != "connected";
|
|
||||||
|
|
||||||
const connectionStateText = () => {
|
|
||||||
let map: { [key: string]: string } = {
|
|
||||||
connecting: context.texts.Connecting,
|
|
||||||
connected: context.texts.Connected,
|
|
||||||
error: context.texts.CannotConnect,
|
|
||||||
reconnecting: context.texts.ConnectingAgain,
|
|
||||||
};
|
|
||||||
|
|
||||||
return map[connectionState];
|
|
||||||
};
|
|
||||||
|
|
||||||
const textColorOnBoard = getColorByBrightness(
|
|
||||||
context.themeManager.theme.boardColor,
|
|
||||||
context.themeManager.theme.textColor,
|
|
||||||
context.themeManager.theme.textLightColor
|
|
||||||
);
|
|
||||||
const textColorOnAppBar = getColorByBrightness(
|
const textColorOnAppBar = getColorByBrightness(
|
||||||
context.themeManager.theme.appBarBgColor,
|
context.themeManager.theme.appBarBgColor,
|
||||||
context.themeManager.theme.textColor,
|
context.themeManager.theme.textColor,
|
||||||
context.themeManager.theme.textLightColor
|
context.themeManager.theme.textLightColor
|
||||||
);
|
);
|
||||||
const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.state == "ended");
|
|
||||||
const showBoardView = game && boardViewModel && userKey && true;
|
|
||||||
const opponentUser = { id: getOpponentId() || "0", name: "Anonymous", isOnline: isOpponentOnline, isAnonymous: true };
|
|
||||||
const user = { id: userKey || "1", name: "Anonymous", isOnline: connectionState === "connected", isAnonymous: true };
|
|
||||||
|
|
||||||
const isMobile = width < 600;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer theme={theme!}>
|
<PageContainer theme={theme!}>
|
||||||
<FloatingPanel context={context} color={context.themeManager.theme.boardColor} visible={showConnectionState}>
|
|
||||||
<span style={{ color: textColorOnBoard }}>{connectionStateText()}</span>
|
|
||||||
</FloatingPanel>
|
|
||||||
<HeaderBar color={theme?.appBarBgColor}>
|
<HeaderBar color={theme?.appBarBgColor}>
|
||||||
<Row>
|
<Row>
|
||||||
|
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||||
<HeaderbarIcon />
|
<HeaderbarIcon />
|
||||||
|
</Link>
|
||||||
|
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||||
<HeaderbarTitle title={context.texts.Mancala} color={textColorOnAppBar} />
|
<HeaderbarTitle title={context.texts.Mancala} color={textColorOnAppBar} />
|
||||||
|
</Link>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
||||||
<Button
|
<Button
|
||||||
context={context}
|
context={context}
|
||||||
color={context.themeManager.theme.pitColor}
|
color={context.themeManager.theme.pitColor}
|
||||||
text={renderNewGameBtn ? context.texts.NewGame : context.texts.Leave}
|
text={context.texts.NewGame}
|
||||||
onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
|
onClick={onNewGameClick} />
|
||||||
</Row>
|
</Row>
|
||||||
</HeaderBar>
|
</HeaderBar>
|
||||||
<BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
|
|
||||||
<InfoPanel
|
|
||||||
style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
|
|
||||||
context={context}
|
|
||||||
game={game}
|
|
||||||
crashMessage={crashMessage}
|
|
||||||
userKey={userKey}
|
|
||||||
userKeyWhoLeave={userKeyWhoLeave} />
|
|
||||||
</BoardToolbar>
|
|
||||||
<BoardToolbar style={{ alignItems: "flex-end" }} visible={showBoardView || false}>
|
|
||||||
<UserStatus style={{
|
|
||||||
marginBottom: "0.5rem", marginLeft: "6%", maxWidth: isMobile ? "40vw" : "30vw",
|
|
||||||
width: isMobile ? "40vw" : "30vw"
|
|
||||||
}} context={context} layoutMode="left" user={opponentUser} visible={showBoardView || false} />
|
|
||||||
<InfoPanel
|
|
||||||
style={{
|
|
||||||
marginTop: "0.5rem", marginBottom: "0.5rem",
|
|
||||||
}}
|
|
||||||
context={context}
|
|
||||||
game={game}
|
|
||||||
crashMessage={crashMessage}
|
|
||||||
userKey={userKey}
|
|
||||||
userKeyWhoLeave={userKeyWhoLeave}
|
|
||||||
visible={!isMobile} />
|
|
||||||
<UserStatus style={{
|
|
||||||
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
|
|
||||||
width: isMobile ? "40vw" : "30vw"
|
|
||||||
}} context={context} layoutMode="right" user={user} visible={showBoardView || false} />
|
|
||||||
</BoardToolbar>
|
|
||||||
{showBoardView ? (
|
|
||||||
<BoardView
|
|
||||||
userKey={userKey}
|
|
||||||
game={game}
|
|
||||||
boardId={boardId}
|
|
||||||
boardViewModel={boardViewModel}
|
|
||||||
context={context}
|
|
||||||
onPitSelect={onPitSelect} />
|
|
||||||
) : searchingOpponent && (
|
|
||||||
<Center>
|
|
||||||
<CircularPanel color={context.themeManager.theme.boardColor}>
|
|
||||||
<h4 style={{ margin: "0", color: textColorOnBoard }}>{`${context.texts.SearchingOpponent} ${context.texts.PleaseWait}...`}</h4>
|
|
||||||
</CircularPanel>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
72
src/routes/LobyPage.tsx
Normal file
72
src/routes/LobyPage.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { CommonMancalaGame, MancalaGame } from 'mancala.js';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FunctionComponent, useEffect } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import Center from '../components/Center';
|
||||||
|
import CircularPanel from '../components/CircularPanel';
|
||||||
|
import HeaderBar from '../components/headerbar/HeaderBar';
|
||||||
|
import HeaderbarIcon from '../components/headerbar/HeaderbarIcon';
|
||||||
|
import HeaderbarTitle from '../components/headerbar/HeaderbarTitle';
|
||||||
|
import ThemeSwitchMenu from '../components/headerbar/ThemeSwitchMenu';
|
||||||
|
import PageContainer from '../components/PageContainer';
|
||||||
|
import Row from '../components/Row';
|
||||||
|
import { channel_on_game_start } from '../const/channel_names';
|
||||||
|
import { Context } from '../context/context';
|
||||||
|
import { Theme } from '../theme/Theme';
|
||||||
|
import { getColorByBrightness } from '../util/ColorUtil';
|
||||||
|
|
||||||
|
const LobyPage: FunctionComponent<{
|
||||||
|
context: Context,
|
||||||
|
userKey?: string,
|
||||||
|
theme: Theme
|
||||||
|
}> = ({ context, userKey, theme }) => {
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listenMessages();
|
||||||
|
context.rtmt.sendMessage("new_game", {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const listenMessages = () => {
|
||||||
|
context.rtmt.listenMessage(channel_on_game_start, (message: Object) => {
|
||||||
|
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||||
|
navigate(`/game/${newGame.id}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const textColorOnAppBar = getColorByBrightness(
|
||||||
|
context.themeManager.theme.appBarBgColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
const textColorOnBoard = getColorByBrightness(
|
||||||
|
context.themeManager.theme.boardColor,
|
||||||
|
context.themeManager.theme.textColor,
|
||||||
|
context.themeManager.theme.textLightColor
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<PageContainer theme={theme!}>
|
||||||
|
<HeaderBar color={theme?.appBarBgColor}>
|
||||||
|
<Row>
|
||||||
|
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||||
|
<HeaderbarIcon />
|
||||||
|
</Link>
|
||||||
|
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||||
|
<HeaderbarTitle title={context.texts.Mancala} color={textColorOnAppBar} />
|
||||||
|
</Link>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
||||||
|
</Row>
|
||||||
|
</HeaderBar>
|
||||||
|
<Center>
|
||||||
|
<CircularPanel color={context.themeManager.theme.boardColor}>
|
||||||
|
<h4 style={{ margin: "0", color: textColorOnBoard }}>{`${context.texts.SearchingOpponent} ${context.texts.PleaseWait}...`}</h4>
|
||||||
|
</CircularPanel>
|
||||||
|
</Center>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LobyPage;
|
||||||
@ -4,4 +4,5 @@ export type OnMessage = (message : Object) => any
|
|||||||
export interface RTMT{
|
export interface RTMT{
|
||||||
sendMessage : (channel : string, message : Object) => any
|
sendMessage : (channel : string, message : Object) => any
|
||||||
listenMessage : (channel : string, callback : OnMessage) => any
|
listenMessage : (channel : string, callback : OnMessage) => any
|
||||||
|
unlistenMessage : (channel : string, callback : OnMessage) => any
|
||||||
}
|
}
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import { decode, encode } from "./encode_decode_message";
|
import { decode, encode } from "./encode_decode_message";
|
||||||
import { Bytes, OnMessage, RTMT } from "./rtmt";
|
import { Bytes, OnMessage, RTMT } from "./rtmt";
|
||||||
import { server } from "../service/http_service";
|
|
||||||
import { channel_ping, channel_pong } from "../const/channel_names";
|
import { channel_ping, channel_pong } from "../const/channel_names";
|
||||||
|
import { server } from "../const/config";
|
||||||
|
|
||||||
const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000;
|
const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000;
|
||||||
|
|
||||||
export class RTMTWS implements RTMT {
|
export class RTMTWS implements RTMT {
|
||||||
private messageChannels: Map<String, OnMessage>;
|
private messageChannels: Map<String, OnMessage | undefined>;
|
||||||
private ws: WebSocket;
|
private ws: WebSocket;
|
||||||
|
|
||||||
private pingTimeout?: number = undefined;
|
private pingTimeout?: number = undefined;
|
||||||
@ -69,10 +69,16 @@ export class RTMTWS implements RTMT {
|
|||||||
this.ws.send(data);
|
this.ws.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: support multible listeners
|
||||||
listenMessage(channel: string, callback: OnMessage) {
|
listenMessage(channel: string, callback: OnMessage) {
|
||||||
this.messageChannels.set(channel, callback);
|
this.messageChannels.set(channel, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: support multible listeners
|
||||||
|
unlistenMessage(channel : string, callback : OnMessage) {
|
||||||
|
this.messageChannels.set(channel, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
onWebSocketMessage(rtmt: RTMTWS, event: MessageEvent) {
|
onWebSocketMessage(rtmt: RTMTWS, event: MessageEvent) {
|
||||||
const { channel, message } = decode(event.data);
|
const { channel, message } = decode(event.data);
|
||||||
rtmt.onMessage(channel, message);
|
rtmt.onMessage(channel, message);
|
||||||
|
|||||||
23
src/service/HttpService.ts
Normal file
23
src/service/HttpService.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { server } from "../const/config";
|
||||||
|
|
||||||
|
export interface HttpService {
|
||||||
|
get: (route: string) => Promise<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpServiceImpl implements HttpService {
|
||||||
|
public serverAdress: string;
|
||||||
|
|
||||||
|
constructor(serverAdress: string) {
|
||||||
|
this.serverAdress = serverAdress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(route: string): Promise<Response> {
|
||||||
|
const url = server.serverAdress + route;
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
const response = await fetch(url, requestOptions);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,30 +0,0 @@
|
|||||||
export type Server = {
|
|
||||||
serverAdress: string;
|
|
||||||
wsServerAdress: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const server: Server = {
|
|
||||||
serverAdress: "https://segin.one/mancala-backend-beta",
|
|
||||||
wsServerAdress: "wss://segin.one/mancala-backend-beta/",
|
|
||||||
};
|
|
||||||
|
|
||||||
const useLocal = false;
|
|
||||||
|
|
||||||
if (useLocal) {
|
|
||||||
server.serverAdress = "http://localhost:5000";
|
|
||||||
server.wsServerAdress = "ws://localhost:5000";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HttpService {
|
|
||||||
get: (route: string, succes: () => any, error: () => any) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
class HttpServiceImpl implements HttpService {
|
|
||||||
public serverAdress: string;
|
|
||||||
|
|
||||||
constructor(serverAdress: string) {
|
|
||||||
this.serverAdress = serverAdress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(route: string, succes: () => any, error: () => any): any {}
|
|
||||||
}
|
|
||||||
25
src/store/GameStore.ts
Normal file
25
src/store/GameStore.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { CommonMancalaGame, MancalaGame } from "mancala.js";
|
||||||
|
import { HttpService } from "../service/HttpService";
|
||||||
|
|
||||||
|
export interface GameStore {
|
||||||
|
get(id: string): Promise<MancalaGame | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameStoreImpl implements GameStore {
|
||||||
|
httpService: HttpService;
|
||||||
|
constructor(props: { httpService: HttpService }) {
|
||||||
|
this.httpService = props.httpService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string): Promise<MancalaGame | undefined> {
|
||||||
|
try {
|
||||||
|
const response = await this.httpService.get(`/game/${id}`);
|
||||||
|
const json = await response.json();
|
||||||
|
return MancalaGame.createFromMancalaGame(json as CommonMancalaGame);
|
||||||
|
} catch (error) {
|
||||||
|
// todo check error
|
||||||
|
Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
53
src/store/KeyStore.ts
Normal file
53
src/store/KeyStore.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { HttpService } from "../service/HttpService"
|
||||||
|
|
||||||
|
const user_key = "user_key"
|
||||||
|
|
||||||
|
export interface UserKeyStore {
|
||||||
|
getUserKey: () => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserKeyStoreImpl implements UserKeyStore {
|
||||||
|
private httpService: HttpService;
|
||||||
|
private keyStoreHttp: UserKeyStore;
|
||||||
|
private keyStoreLocalStorage = new UserKeyStoreLocalStorage()
|
||||||
|
|
||||||
|
constructor(props: { httpService: HttpService }) {
|
||||||
|
this.httpService = props.httpService;
|
||||||
|
this.keyStoreHttp = new UserKeyStoreLocalHttp({ httpService: this.httpService });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUserKey(): Promise<string> {
|
||||||
|
const maybeUserKey = await this.keyStoreLocalStorage.getUserKey();
|
||||||
|
if (maybeUserKey === undefined) {
|
||||||
|
const userKey = await this.keyStoreHttp.getUserKey()
|
||||||
|
this.keyStoreLocalStorage.storeUserKey(userKey)
|
||||||
|
return Promise.resolve(userKey);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(maybeUserKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserKeyStoreLocalHttp implements UserKeyStore {
|
||||||
|
httpService: HttpService;
|
||||||
|
|
||||||
|
constructor(params: { httpService: HttpService }) {
|
||||||
|
this.httpService = params.httpService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUserKey(): Promise<string> {
|
||||||
|
const response = await this.httpService.get("/register/")
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserKeyStoreLocalStorage {
|
||||||
|
public getUserKey(): Promise<string | undefined> {
|
||||||
|
const userKey = localStorage.getItem(user_key)
|
||||||
|
return Promise.resolve(userKey === null ? undefined : userKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public storeUserKey(userKey: string): void {
|
||||||
|
localStorage.setItem(user_key, userKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { server } from "../service/http_service"
|
|
||||||
|
|
||||||
const key = "user_key"
|
|
||||||
|
|
||||||
export type OnUserKeyFound = (userKey: string) => any
|
|
||||||
|
|
||||||
export interface UserKeyStore {
|
|
||||||
getUserKey: (callback: OnUserKeyFound) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserKeyStoreImpl implements UserKeyStore {
|
|
||||||
private keyStoreHttp = new UserKeyStoreLocalHttp()
|
|
||||||
private keyStoreLocalStoreage = new UserKeyStoreLocalStoreage()
|
|
||||||
|
|
||||||
public getUserKey(callback: (userKey: string) => any): any {
|
|
||||||
this.keyStoreLocalStoreage.getUserKey((userKey)=>{
|
|
||||||
if(userKey){
|
|
||||||
callback(userKey)
|
|
||||||
}else{
|
|
||||||
this.keyStoreHttp.getUserKey((userKey)=>{
|
|
||||||
this.keyStoreLocalStoreage.storeUserKey(userKey)
|
|
||||||
callback(userKey)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserKeyStoreLocalHttp implements UserKeyStore {
|
|
||||||
public getUserKey(callback: (userKey: string) => any): any {
|
|
||||||
const url = server.serverAdress + "/register/"
|
|
||||||
const requestOptions = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
};
|
|
||||||
fetch(url, requestOptions)
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(data => callback(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserKeyStoreLocalStoreage implements UserKeyStore {
|
|
||||||
public getUserKey(callback: (userKey: string) => any): any {
|
|
||||||
const userKey = localStorage.getItem(key)
|
|
||||||
callback(userKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
public storeUserKey(userKey : string) : any {
|
|
||||||
localStorage.setItem(key, userKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
yarn.lock
31
yarn.lock
@ -171,6 +171,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf"
|
||||||
integrity sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA==
|
integrity sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA==
|
||||||
|
|
||||||
|
"@babel/runtime@^7.7.6":
|
||||||
|
version "7.18.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
|
||||||
|
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.18.6":
|
"@babel/template@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
|
||||||
@ -1446,6 +1453,13 @@ has-flag@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||||
|
|
||||||
|
history@^5.2.0:
|
||||||
|
version "5.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
|
||||||
|
integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.7.6"
|
||||||
|
|
||||||
htmlnano@^2.0.0:
|
htmlnano@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.2.tgz#3e3170941e2446a86211196d740272ebca78f878"
|
resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.2.tgz#3e3170941e2446a86211196d740272ebca78f878"
|
||||||
@ -1744,6 +1758,21 @@ react-refresh@^0.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
|
||||||
integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
|
integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
|
||||||
|
|
||||||
|
react-router-dom@6:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d"
|
||||||
|
integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==
|
||||||
|
dependencies:
|
||||||
|
history "^5.2.0"
|
||||||
|
react-router "6.3.0"
|
||||||
|
|
||||||
|
react-router@6.3.0:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
|
||||||
|
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==
|
||||||
|
dependencies:
|
||||||
|
history "^5.2.0"
|
||||||
|
|
||||||
react-transition-state@^1.1.4:
|
react-transition-state@^1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-1.1.4.tgz#113224eaa27e0ff81661305e44d5e0348cdf61ac"
|
resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-1.1.4.tgz#113224eaa27e0ff81661305e44d5e0348cdf61ac"
|
||||||
@ -1756,7 +1785,7 @@ react-transition-state@^1.1.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
regenerator-runtime@^0.13.7:
|
regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
|
||||||
version "0.13.9"
|
version "0.13.9"
|
||||||
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
|
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
|
||||||
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user