diff --git a/package.json b/package.json
index 9781d67..0567cb1 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"mancala.js": "^0.0.2-beta.3",
"react": "^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",
"uuid": "^8.3.2"
},
diff --git a/src/App.tsx b/src/App.tsx
index 5edb8b9..d079d3d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,9 +1,6 @@
-import { initContext } from './context/context';
-initContext();
-
import * as React from 'react';
import { createRoot } from 'react-dom/client';
const container = document.getElementById('main');
const root = createRoot(container!);
-import Home from './routes/Home';
-root.render();
\ No newline at end of file
+import MancalaApp from './MancalaApp';
+root.render();
\ No newline at end of file
diff --git a/src/MancalaApp.tsx b/src/MancalaApp.tsx
new file mode 100644
index 0000000..2329499
--- /dev/null
+++ b/src/MancalaApp.tsx
@@ -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(undefined);
+
+ const [connectionState, setConnetionState] = useState("connecting");
+
+ const [theme, setTheme] = useState(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 (
+ <>
+
+
+ } />
+
+
+ } >
+
+ }>
+
+
+
+
+
+ {connectionStateText()}
+
+ >
+ );
+}
+
+export default MancalaApp;
\ No newline at end of file
diff --git a/src/animation/PitAnimator.ts b/src/animation/PitAnimator.ts
index 38cf540..2f4d442 100644
--- a/src/animation/PitAnimator.ts
+++ b/src/animation/PitAnimator.ts
@@ -42,7 +42,7 @@ export default class PitAnimator {
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
}
- public setUpdatedGame(game: MancalaGame, forceClear = false) {
+ public setUpdatedGame(game: MancalaGame) {
this.resetAnimationState();
if (!this.game) {
this.setNewGame(game);
@@ -68,7 +68,7 @@ export default class PitAnimator {
}
onAnimate() {
- if(!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return;
+ if (!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return;
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
this.clearCurrentInterval();
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
@@ -92,7 +92,7 @@ export default class PitAnimator {
boardViewModel: BoardViewModel,
gameStep: GameStep
) {
- if(!this.currentHistoryItem || !this.game) return;
+ if (!this.currentHistoryItem || !this.game) return;
const pitViewModel = boardViewModel.pits[index];
if (this.animationIndex === 0) {
//This is one stone move case, TODO: beautify it later
diff --git a/src/components/LoadingComponent.tsx b/src/components/LoadingComponent.tsx
new file mode 100644
index 0000000..3c9495e
--- /dev/null
+++ b/src/components/LoadingComponent.tsx
@@ -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 }> = ({ 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 (
+
+ {`${text}`}
+
+ );
+ }
+ return <>>
+}
+
+export default LoadingComponent;
\ No newline at end of file
diff --git a/src/const/config.ts b/src/const/config.ts
new file mode 100644
index 0000000..95fea6b
--- /dev/null
+++ b/src/const/config.ts
@@ -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/",
+};
+
diff --git a/src/const/texts.ts b/src/const/texts.ts
index b9b1133..ac2de91 100644
--- a/src/const/texts.ts
+++ b/src/const/texts.ts
@@ -22,6 +22,8 @@ export type Texts = {
PleaseWait : string,
GameDraw : string,
Anonymous: string,
+ GameNotFound: string,
+ Loading: string,
}
export const EnUs: Texts = {
@@ -47,6 +49,8 @@ export const EnUs: Texts = {
PleaseWait : "Please Wait",
GameDraw : "Draw",
Anonymous: "Anonymous",
+ GameNotFound: "Game Not Found",
+ Loading: "Loading",
}
export const TrTr: Texts = {
@@ -71,5 +75,7 @@ export const TrTr: Texts = {
SearchingOpponent: "Rakip Aranıyor",
PleaseWait: "Lütfen Bekleyin",
GameDraw : "Berabere",
- Anonymous: "Anonim"
+ Anonymous: "Anonim",
+ GameNotFound: "Oyun Bulunamadı",
+ Loading: "Yükleniyor",
}
\ No newline at end of file
diff --git a/src/context/context.tsx b/src/context/context.tsx
index 28d2485..ee35337 100644
--- a/src/context/context.tsx
+++ b/src/context/context.tsx
@@ -1,7 +1,10 @@
+import { server } from "../const/config";
import { Texts, TrTr } from "../const/texts";
import { RTMT } from "../rtmt/rtmt";
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";
export type Context = {
@@ -9,11 +12,14 @@ export type Context = {
userKeyStore: UserKeyStore;
texts: Texts;
themeManager: ThemeManager;
+ gameStore: GameStore;
};
-export const initContext = () => {
+export const initContext = (): Context => {
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 themeManager = new ThemeManager();
return {
@@ -21,7 +27,6 @@ export const initContext = () => {
userKeyStore: userKeyStore,
texts: texts,
themeManager,
+ gameStore
};
};
-
-export const context: Context = initContext();
diff --git a/src/models/ConnectionState.ts b/src/models/ConnectionState.ts
new file mode 100644
index 0000000..5010944
--- /dev/null
+++ b/src/models/ConnectionState.ts
@@ -0,0 +1 @@
+export type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
\ No newline at end of file
diff --git a/src/models/LoadingState.tsx b/src/models/LoadingState.tsx
new file mode 100644
index 0000000..dfe1e4e
--- /dev/null
+++ b/src/models/LoadingState.tsx
@@ -0,0 +1,46 @@
+export type LoadingStateType = "unset" | "loading" | "loaded" | "error";
+
+export class LoadingState {
+ 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() {
+ return new LoadingState({ state: "unset" });
+ }
+
+ public static Loading() {
+ return new LoadingState({ state: "loading" });
+ }
+
+ public static Error(props: { errorMessage: string }) {
+ const { errorMessage } = props;
+ return new LoadingState({ state: "error", errorMessage });
+ }
+
+ public static Loaded(props: { value?: T }) {
+ const { value } = props;
+ return new LoadingState({ 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";
+ }
+}
\ No newline at end of file
diff --git a/src/routes/GamePage.tsx b/src/routes/GamePage.tsx
new file mode 100644
index 0000000..36df155
--- /dev/null
+++ b/src/routes/GamePage.tsx
@@ -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(undefined);
+
+ const [crashMessage, setCrashMessage] = useState(undefined);
+
+ const [userKeyWhoLeave, setUserKeyWhoLeave] = useState(undefined);
+
+ const [boardViewModel, setBoardViewModel] = useState(undefined);
+
+ const [boardId, setBoardId] = useState("-1");
+
+ const [pitAnimator, setPitAnimator] = useState(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(false);
+
+ const [isOpponentOnline, setIsOpponentOnline] = useState(false);
+
+ const { height, width } = useWindowDimensions();
+
+ const navigate = useNavigate();
+
+ const [gameLoadingState, setLoadingStateGame] = useState>(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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showBoardView && (
+
+ )}
+
+
+
+
+ );
+}
+
+export default GamePage;
\ No newline at end of file
diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx
index 0485672..c37b345 100644
--- a/src/routes/Home.tsx
+++ b/src/routes/Home.tsx
@@ -1,302 +1,54 @@
import * as React 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 { Theme } from "../theme/Theme";
import HeaderBar from "../components/headerbar/HeaderBar";
-import FloatingPanel from "../components/FloatingPanel";
import PageContainer from "../components/PageContainer";
import Row from "../components/Row";
import HeaderbarIcon from "../components/headerbar/HeaderbarIcon";
import HeaderbarTitle from "../components/headerbar/HeaderbarTitle";
import ThemeSwitchMenu from "../components/headerbar/ThemeSwitchMenu";
import Button from "../components/Button";
-import BoardToolbar from "../components/board/BoardToolbar";
-import UserStatus from "../components/UserStatus";
-import Center from "../components/Center";
-import CircularPanel from "../components/CircularPanel";
-import useWindowDimensions from "../hooks/useWindowDimensions";
-import { UserConnectionInfo } from "../models/UserConnectionInfo";
+import { Context } from "../context/context";
+import { Link, useNavigate } from "react-router-dom";
-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 [userKey, setUserKey] = useState(undefined);
-
- const [connectionState, setConnetionState] =
- useState("connecting");
-
- const [searchingOpponent, setSearchingOpponent] = useState(false);
-
- const [game, setGame] = useState(undefined);
-
- const [crashMessage, setCrashMessage] = useState(undefined);
-
- const [userKeyWhoLeave, setUserKeyWhoLeave] = useState(undefined);
-
- const [boardViewModel, setBoardViewModel] = useState(undefined);
-
- const [boardId, setBoardId] = useState("-1");
-
- const [pitAnimator, setPitAnimator] = useState(undefined);
-
- const [theme, setTheme] = useState(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(false);
-
- const { height, width } = useWindowDimensions();
-
- const [isOpponentOnline, setIsOpponentOnline] = useState(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 navigate = useNavigate();
const onNewGameClick = () => {
- resetGameState();
- setSearchingOpponent(true);
- context.rtmt.sendMessage("new_game", {});
+ navigate("/loby")
};
- 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(
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 (
-
- {connectionStateText()}
-
-
-
+
+
+
+
+
+
+ text={context.texts.NewGame}
+ onClick={onNewGameClick} />
-
-
-
-
-
-
-
-
- {showBoardView ? (
-
- ) : searchingOpponent && (
-
-
- {`${context.texts.SearchingOpponent} ${context.texts.PleaseWait}...`}
-
-
- )}
);
};
diff --git a/src/routes/LobyPage.tsx b/src/routes/LobyPage.tsx
new file mode 100644
index 0000000..c20ae89
--- /dev/null
+++ b/src/routes/LobyPage.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {`${context.texts.SearchingOpponent} ${context.texts.PleaseWait}...`}
+
+
+
+ );
+}
+
+export default LobyPage;
\ No newline at end of file
diff --git a/src/rtmt/rtmt.ts b/src/rtmt/rtmt.ts
index efb6b70..bc04659 100644
--- a/src/rtmt/rtmt.ts
+++ b/src/rtmt/rtmt.ts
@@ -4,4 +4,5 @@ export type OnMessage = (message : Object) => any
export interface RTMT{
sendMessage : (channel : string, message : Object) => any
listenMessage : (channel : string, callback : OnMessage) => any
+ unlistenMessage : (channel : string, callback : OnMessage) => any
}
\ No newline at end of file
diff --git a/src/rtmt/rtmt_websocket.ts b/src/rtmt/rtmt_websocket.ts
index 3f93536..180d3c2 100644
--- a/src/rtmt/rtmt_websocket.ts
+++ b/src/rtmt/rtmt_websocket.ts
@@ -1,12 +1,12 @@
import { decode, encode } from "./encode_decode_message";
import { Bytes, OnMessage, RTMT } from "./rtmt";
-import { server } from "../service/http_service";
import { channel_ping, channel_pong } from "../const/channel_names";
+import { server } from "../const/config";
const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000;
export class RTMTWS implements RTMT {
- private messageChannels: Map;
+ private messageChannels: Map;
private ws: WebSocket;
private pingTimeout?: number = undefined;
@@ -69,10 +69,16 @@ export class RTMTWS implements RTMT {
this.ws.send(data);
}
+ // todo: support multible listeners
listenMessage(channel: string, callback: OnMessage) {
this.messageChannels.set(channel, callback);
}
+ // todo: support multible listeners
+ unlistenMessage(channel : string, callback : OnMessage) {
+ this.messageChannels.set(channel, undefined);
+ }
+
onWebSocketMessage(rtmt: RTMTWS, event: MessageEvent) {
const { channel, message } = decode(event.data);
rtmt.onMessage(channel, message);
diff --git a/src/service/HttpService.ts b/src/service/HttpService.ts
new file mode 100644
index 0000000..c07c2f7
--- /dev/null
+++ b/src/service/HttpService.ts
@@ -0,0 +1,23 @@
+import { server } from "../const/config";
+
+export interface HttpService {
+ get: (route: string) => Promise;
+}
+
+export class HttpServiceImpl implements HttpService {
+ public serverAdress: string;
+
+ constructor(serverAdress: string) {
+ this.serverAdress = serverAdress;
+ }
+
+ public async get(route: string): Promise {
+ const url = server.serverAdress + route;
+ const requestOptions = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+ const response = await fetch(url, requestOptions);
+ return response;
+ }
+}
diff --git a/src/service/http_service.ts b/src/service/http_service.ts
deleted file mode 100644
index fd7d5eb..0000000
--- a/src/service/http_service.ts
+++ /dev/null
@@ -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 {}
-}
diff --git a/src/store/GameStore.ts b/src/store/GameStore.ts
new file mode 100644
index 0000000..66f4c6c
--- /dev/null
+++ b/src/store/GameStore.ts
@@ -0,0 +1,25 @@
+import { CommonMancalaGame, MancalaGame } from "mancala.js";
+import { HttpService } from "../service/HttpService";
+
+export interface GameStore {
+ get(id: string): Promise;
+}
+
+export class GameStoreImpl implements GameStore {
+ httpService: HttpService;
+ constructor(props: { httpService: HttpService }) {
+ this.httpService = props.httpService;
+ }
+
+ async get(id: string): Promise {
+ 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);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/store/KeyStore.ts b/src/store/KeyStore.ts
new file mode 100644
index 0000000..6923ff6
--- /dev/null
+++ b/src/store/KeyStore.ts
@@ -0,0 +1,53 @@
+import { HttpService } from "../service/HttpService"
+
+const user_key = "user_key"
+
+export interface UserKeyStore {
+ getUserKey: () => Promise;
+}
+
+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 {
+ 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 {
+ const response = await this.httpService.get("/register/")
+ return response.text();
+ }
+}
+
+export class UserKeyStoreLocalStorage {
+ public getUserKey(): Promise {
+ const userKey = localStorage.getItem(user_key)
+ return Promise.resolve(userKey === null ? undefined : userKey)
+ }
+
+ public storeUserKey(userKey: string): void {
+ localStorage.setItem(user_key, userKey)
+ }
+}
diff --git a/src/store/key_store.ts b/src/store/key_store.ts
deleted file mode 100644
index a5784e7..0000000
--- a/src/store/key_store.ts
+++ /dev/null
@@ -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)
- }
-}
diff --git a/yarn.lock b/yarn.lock
index 9c9e032..c13b937 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -171,6 +171,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf"
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":
version "7.18.6"
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"
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:
version "2.0.2"
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"
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:
version "1.1.4"
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:
loose-envify "^1.1.0"
-regenerator-runtime@^0.13.7:
+regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
version "0.13.9"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==