Merge pull request #31 from jhalitaksoy/feature/spectator
Feature/spectator
This commit is contained in:
commit
f5b1a6f9b5
@ -15,8 +15,10 @@
|
||||
"dependencies": {
|
||||
"@szhsin/react-menu": "^3.0.2",
|
||||
"@types/": "szhsin/react-menu",
|
||||
"@types/eventemitter2": "^4.1.0",
|
||||
"@types/styled-jsx": "^3.4.4",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eventemitter2": "^6.4.7",
|
||||
"mancala.js": "^0.0.2-beta.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@ -29,35 +29,32 @@ const MancalaApp: FunctionComponent = () => {
|
||||
setConnetionState("connected");
|
||||
};
|
||||
const onConnectionLost = () => {
|
||||
connectToServer("reconnecting");
|
||||
setConnetionState("reconnecting");
|
||||
connectToServer();
|
||||
};
|
||||
const onConnectionError = (event: Event) => {
|
||||
setConnetionState("error");
|
||||
connectToServer();
|
||||
};
|
||||
const connectToServer = async (connectionState: ConnectionState) => {
|
||||
setConnetionState(connectionState);
|
||||
const onThemeChange = (theme: Theme) => {
|
||||
setTheme(theme)
|
||||
}
|
||||
const connectToServer = async () => {
|
||||
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");
|
||||
}
|
||||
(context.rtmt as RTMTWS).initWebSocket(userKey);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
connectToServer("connecting");
|
||||
context.themeManager.onThemeChange = (theme: Theme) => {
|
||||
setTheme(theme);
|
||||
}
|
||||
context.rtmt.on("open", onConnectionDone);
|
||||
context.rtmt.on("close", onConnectionLost);
|
||||
context.rtmt.on("error", onConnectionError);
|
||||
context.themeManager.on("themechange", onThemeChange);
|
||||
setConnetionState("connecting");
|
||||
connectToServer();
|
||||
return () => {
|
||||
// todo: dispose rtmt.dispose
|
||||
//context.rtmt.dispose();
|
||||
context.rtmt.dispose();
|
||||
context.themeManager.off("themechange", onThemeChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -77,6 +74,7 @@ const MancalaApp: FunctionComponent = () => {
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
if (!userKey) return <></>;
|
||||
return (
|
||||
<>
|
||||
<BrowserRouter>
|
||||
|
||||
@ -2,70 +2,83 @@ import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../context/context";
|
||||
import { Game } from "../models/Game";
|
||||
import { User } from "../models/User";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import CircularPanel from "./CircularPanel";
|
||||
|
||||
function getInfoPanelTextByGameState(params: {
|
||||
context: Context;
|
||||
game?: Game;
|
||||
crashMessage?: string;
|
||||
userKey?: string;
|
||||
userKeyWhoLeave?: string;
|
||||
currentUser: User;
|
||||
whitePlayer: User;
|
||||
blackPlayer: User;
|
||||
leftPlayer?: User;
|
||||
isSpectator?: boolean;
|
||||
}): string | undefined {
|
||||
const {
|
||||
context,
|
||||
game,
|
||||
crashMessage,
|
||||
userKey,
|
||||
userKeyWhoLeave,
|
||||
currentUser,
|
||||
whitePlayer,
|
||||
blackPlayer,
|
||||
leftPlayer,
|
||||
isSpectator
|
||||
} = params;
|
||||
if (crashMessage) {
|
||||
return context.texts.GameCrashed + " " + crashMessage;
|
||||
} else if (userKeyWhoLeave) {
|
||||
let message = context.texts.OpponentLeavesTheGame;
|
||||
if (userKeyWhoLeave == userKey) {
|
||||
message = context.texts.YouLeftTheGame;
|
||||
|
||||
if (leftPlayer) {
|
||||
return isSpectator ? `${leftPlayer.name} ${context.texts.UserLeftTheGame}` :
|
||||
leftPlayer.id == currentUser.id ? context.texts.YouLeftTheGame : context.texts.OpponentLeftTheGame;
|
||||
}
|
||||
return message;
|
||||
} else if (game?.mancalaGame.state == "ended") {
|
||||
const wonPlayer = game.mancalaGame.getWonPlayerId();
|
||||
let whoWon =
|
||||
game.mancalaGame.getWonPlayerId() === userKey
|
||||
|
||||
const isGameEnded = game?.mancalaGame.state == "ended";
|
||||
if (isGameEnded) {
|
||||
const wonPlayerID = game.mancalaGame.getWonPlayerId();
|
||||
const wonPlayer = wonPlayerID == whitePlayer.id ? whitePlayer : blackPlayer;
|
||||
let whoWon;
|
||||
if (wonPlayer) {
|
||||
whoWon = isSpectator ? `${wonPlayer.name} ${context.texts.Won}` :
|
||||
game.mancalaGame.getWonPlayerId() === currentUser.id
|
||||
? context.texts.YouWon
|
||||
: context.texts.YouLost;
|
||||
if (!wonPlayer) {
|
||||
} else {
|
||||
whoWon = context.texts.GameDraw;
|
||||
}
|
||||
return context.texts.GameEnded + " " + whoWon;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (game) {
|
||||
return userKey ? game.mancalaGame.checkIsPlayerTurn(userKey)
|
||||
const playingPlayer = game.mancalaGame.checkIsPlayerTurn(whitePlayer.id) ? whitePlayer : blackPlayer;
|
||||
return isSpectator ? `${playingPlayer.name} ${context.texts.Playing}` : game.mancalaGame.checkIsPlayerTurn(currentUser.id)
|
||||
? context.texts.YourTurn
|
||||
: context.texts.OpponentTurn : undefined;
|
||||
}
|
||||
: context.texts.OpponentTurn;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const InfoPanel: FunctionComponent<{
|
||||
context: Context;
|
||||
game?: Game;
|
||||
crashMessage?: string;
|
||||
userKey?: string;
|
||||
userKeyWhoLeave?: string;
|
||||
currentUser: User;
|
||||
whitePlayer: User;
|
||||
blackPlayer: User;
|
||||
leftPlayer?: User;
|
||||
style?: React.CSSProperties;
|
||||
visible?: boolean;
|
||||
isSpectator?: boolean;
|
||||
}> = ({
|
||||
context,
|
||||
game,
|
||||
crashMessage,
|
||||
userKey,
|
||||
userKeyWhoLeave,
|
||||
currentUser,
|
||||
whitePlayer,
|
||||
blackPlayer,
|
||||
leftPlayer,
|
||||
style,
|
||||
visible
|
||||
visible,
|
||||
isSpectator
|
||||
}) => {
|
||||
if (visible === false) return <></>;
|
||||
const isUserTurn = userKey ? game?.mancalaGame.checkIsPlayerTurn(userKey) : false;
|
||||
const isUserTurn = currentUser.id ? game?.mancalaGame.checkIsPlayerTurn(currentUser.id) : false;
|
||||
const containerColor = isUserTurn
|
||||
? context.themeManager.theme.playerTurnColor
|
||||
: context.themeManager.theme.boardColor;
|
||||
@ -77,9 +90,11 @@ const InfoPanel: FunctionComponent<{
|
||||
const text = getInfoPanelTextByGameState({
|
||||
context,
|
||||
game,
|
||||
crashMessage,
|
||||
userKey,
|
||||
userKeyWhoLeave
|
||||
currentUser,
|
||||
whitePlayer,
|
||||
blackPlayer,
|
||||
leftPlayer,
|
||||
isSpectator
|
||||
});
|
||||
if (text) {
|
||||
return (
|
||||
|
||||
@ -13,49 +13,39 @@ const BoardView: FunctionComponent<{
|
||||
context: Context;
|
||||
boardId: string;
|
||||
boardViewModel: BoardViewModel;
|
||||
userKey: string;
|
||||
revert: boolean;
|
||||
onPitSelect: (index: number, pit: Pit) => void;
|
||||
}> = ({ game, context, boardId, boardViewModel, userKey, onPitSelect: onPitSelect }) => {
|
||||
}> = ({ game, context, boardId, boardViewModel, revert, onPitSelect: onPitSelect }) => {
|
||||
const mancalaGame = game?.mancalaGame;
|
||||
const createPitView = (key: any, pitViewModel: PitViewModel, onClick: () => void) => {
|
||||
return <PitView key={key} pitViewModel={pitViewModel} onClick={onClick} />;
|
||||
};
|
||||
const player1Pits = mancalaGame?.board.player1Pits.map((pit, index) => {
|
||||
const pitViewModel = boardViewModel.pits[pit.index];
|
||||
return createPitView(index, pitViewModel, () => {
|
||||
if (mancalaGame?.turnPlayerId === mancalaGame?.player1Id && userKey === mancalaGame?.player1Id)
|
||||
onPitSelect(mancalaGame?.board.player1Pits.indexOf(pit), pit);
|
||||
});
|
||||
});
|
||||
const player2Pits = mancalaGame?.board.player2Pits.map((pit, index) => {
|
||||
const pitViewModel = boardViewModel.pits[pit.index];
|
||||
return createPitView(index, pitViewModel, () => {
|
||||
if (mancalaGame?.turnPlayerId === mancalaGame?.player2Id && userKey === mancalaGame?.player2Id)
|
||||
onPitSelect(mancalaGame?.board.player2Pits.indexOf(pit), pit);
|
||||
});
|
||||
});
|
||||
const theme = context.themeManager.theme;
|
||||
const player1BankViewModel =
|
||||
boardViewModel.pits[mancalaGame?.board.player1BankIndex()];
|
||||
const player2BankViewModel =
|
||||
boardViewModel.pits[mancalaGame?.board.player2BankIndex()];
|
||||
const isPlayer2 = userKey === mancalaGame?.player2Id;
|
||||
|
||||
const createPitView = (key: any, pit: Pit, pitViewModel: PitViewModel) => {
|
||||
return <PitView key={key} pitViewModel={pitViewModel} onClick={() => onPitSelect(pit.index, pit)} />;
|
||||
};
|
||||
const createPitViewList = (pits: Pit[]) => pits.map((pit, index) => createPitView(index, pit, boardViewModel.pits[pit.index]));
|
||||
|
||||
const player1Pits = createPitViewList(mancalaGame?.board.player1Pits);
|
||||
const player2Pits = createPitViewList(mancalaGame?.board.player2Pits);
|
||||
const player1BankIndex = mancalaGame?.board.player1BankIndex();
|
||||
const player2BankIndex = mancalaGame?.board.player2BankIndex();
|
||||
const player1BankViewModel = boardViewModel.pits[player1BankIndex];
|
||||
const player2BankViewModel = boardViewModel.pits[player2BankIndex];
|
||||
return (
|
||||
<div className="board" style={{ background: theme.boardColor }}>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={isPlayer2 ? player2BankViewModel : player1BankViewModel}
|
||||
pitViewModel={revert ? player2BankViewModel : player1BankViewModel}
|
||||
gridColumn="8 / 9"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={isPlayer2 ? player1BankViewModel : player2BankViewModel}
|
||||
pitViewModel={revert ? player1BankViewModel : player2BankViewModel}
|
||||
gridColumn="1 / 2"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
{isPlayer2 ? player1Pits?.reverse() : player2Pits?.reverse()}
|
||||
{isPlayer2 ? player2Pits : player1Pits}
|
||||
{revert ? player1Pits?.reverse() : player2Pits?.reverse()}
|
||||
{revert ? player2Pits : player1Pits}
|
||||
<style jsx>{`
|
||||
.board {
|
||||
padding: 2vw;
|
||||
|
||||
@ -9,3 +9,5 @@ export const channel_on_game_user_leave = "on_game_user_leave"
|
||||
export const channel_ping = "ping"
|
||||
export const channel_pong = "pong"
|
||||
export const channel_on_user_connection_change = "channel_on_user_connection_change"
|
||||
export const channel_listen_game_events = "channel_listen_game_events"
|
||||
export const channel_unlisten_game_events = "channel_unlisten_game_events"
|
||||
@ -8,6 +8,7 @@ export type Texts = {
|
||||
GameEnded: string,
|
||||
GameCrashed: string,
|
||||
YouWon: string,
|
||||
Won: string,
|
||||
YouLost: string,
|
||||
Connecting: string,
|
||||
Connected: string,
|
||||
@ -16,14 +17,16 @@ export type Texts = {
|
||||
ConnectingAgain: string,
|
||||
ServerError: string,
|
||||
SearchingOpponet: string,
|
||||
OpponentLeavesTheGame: string,
|
||||
OpponentLeftTheGame: string,
|
||||
YouLeftTheGame: string,
|
||||
UserLeftTheGame: string,
|
||||
SearchingOpponent: string,
|
||||
PleaseWait : string,
|
||||
GameDraw : string,
|
||||
Anonymous: string,
|
||||
GameNotFound: string,
|
||||
Loading: string,
|
||||
Playing: string,
|
||||
}
|
||||
|
||||
export const EnUs: Texts = {
|
||||
@ -35,6 +38,7 @@ export const EnUs: Texts = {
|
||||
GameEnded: "Game Ended",
|
||||
GameCrashed: "Game Crashed",
|
||||
YouWon: "You Won",
|
||||
Won: "Won",
|
||||
YouLost: "You Lost",
|
||||
Connecting: "Connecting",
|
||||
Connected: "Connected",
|
||||
@ -43,14 +47,16 @@ export const EnUs: Texts = {
|
||||
ConnectingAgain: "Connecting Again",
|
||||
ServerError: "Server Error",
|
||||
SearchingOpponet: "Searching Opponet",
|
||||
OpponentLeavesTheGame: "Opponent Leaves The Game",
|
||||
OpponentLeftTheGame: "Opponent Leaves The Game",
|
||||
YouLeftTheGame: "You Left The Game",
|
||||
UserLeftTheGame: "Left The Game",
|
||||
SearchingOpponent: "Searching Opponent",
|
||||
PleaseWait : "Please Wait",
|
||||
GameDraw : "Draw",
|
||||
Anonymous: "Anonymous",
|
||||
GameNotFound: "Game Not Found",
|
||||
Loading: "Loading",
|
||||
Playing: "Playing",
|
||||
}
|
||||
|
||||
export const TrTr: Texts = {
|
||||
@ -62,6 +68,7 @@ export const TrTr: Texts = {
|
||||
GameEnded: "Oyun Bitti",
|
||||
GameCrashed: "Oyunda Hata Oluştu",
|
||||
YouWon: "Kazandın",
|
||||
Won: "Kazandı",
|
||||
YouLost: "Kaybettin",
|
||||
Connecting: "Bağlanılıyor",
|
||||
Connected: "Bağlandı",
|
||||
@ -70,12 +77,14 @@ export const TrTr: Texts = {
|
||||
ConnectingAgain: "Tekrar Bağlanılıyor",
|
||||
ServerError: "Sunucu Hatası",
|
||||
SearchingOpponet: "Rakip Aranıyor",
|
||||
OpponentLeavesTheGame: "Rakip Oyundan Ayrıldı",
|
||||
OpponentLeftTheGame: "Rakip Oyundan Ayrıldı",
|
||||
YouLeftTheGame: "Sen Oyundan Ayrıldın",
|
||||
UserLeftTheGame: "Oyundan Ayrıldı",
|
||||
SearchingOpponent: "Rakip Aranıyor",
|
||||
PleaseWait: "Lütfen Bekleyin",
|
||||
GameDraw : "Berabere",
|
||||
Anonymous: "Anonim",
|
||||
GameNotFound: "Oyun Bulunamadı",
|
||||
Loading: "Yükleniyor",
|
||||
Playing: "Oynuyor",
|
||||
}
|
||||
@ -17,7 +17,7 @@ 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 { channel_on_game_update, channel_on_game_crashed, channel_on_game_user_leave, channel_on_user_connection_change, channel_leave_game, channel_game_move, channel_listen_game_events, channel_unlisten_game_events } from '../const/channel_names';
|
||||
import { Context } from '../context/context';
|
||||
import useWindowDimensions from '../hooks/useWindowDimensions';
|
||||
import { ConnectionState } from '../models/ConnectionState';
|
||||
@ -39,8 +39,6 @@ const GamePage: FunctionComponent<{
|
||||
|
||||
const [game, setGame] = useState<Game | undefined>(undefined);
|
||||
|
||||
const [crashMessage, setCrashMessage] = useState<string | undefined>(undefined);
|
||||
|
||||
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string | undefined>(undefined);
|
||||
|
||||
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel | undefined>(undefined);
|
||||
@ -61,7 +59,12 @@ const GamePage: FunctionComponent<{
|
||||
|
||||
const [gameLoadingState, setLoadingStateGame] = useState<LoadingState<Game>>(LoadingState.Unset());
|
||||
|
||||
|
||||
const checkIsSpectator = (game: Game) => userKey !== game.mancalaGame.player1Id && userKey !== game.mancalaGame.player2Id;
|
||||
|
||||
const mancalaGame: MancalaGame | undefined = game?.mancalaGame;
|
||||
const isSpectator = game ? checkIsSpectator(game) : undefined;
|
||||
const isPlayer2 = !isSpectator && userKey === mancalaGame?.player2Id;
|
||||
|
||||
const onGameUpdate = (pitAnimator: PitAnimator, newGame: Game) => {
|
||||
setGame(newGame);
|
||||
@ -88,7 +91,6 @@ const GamePage: FunctionComponent<{
|
||||
const newCrashMessage = message as string;
|
||||
console.error("on_game_crash");
|
||||
console.error(newCrashMessage);
|
||||
setCrashMessage(newCrashMessage);
|
||||
}
|
||||
const onGameUserLeave = (message: any) => {
|
||||
const userKeyWhoLeave = message;
|
||||
@ -100,17 +102,19 @@ const GamePage: FunctionComponent<{
|
||||
setGameUsersConnectionInfo(gameUsersConnectionInfo);
|
||||
};
|
||||
|
||||
const listenMessages = (pitAnimator: PitAnimator): () => void => {
|
||||
const listenMessages = (game: Game, pitAnimator: PitAnimator): () => void => {
|
||||
const _onGameUpdate = (message: object) => onGameUpdateEvent(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)
|
||||
context.rtmt.addMessageListener(channel_on_game_update, _onGameUpdate);
|
||||
context.rtmt.addMessageListener(channel_on_game_crashed, onGameCrashed);
|
||||
context.rtmt.addMessageListener(channel_on_game_user_leave, onGameUserLeave);
|
||||
context.rtmt.addMessageListener(channel_on_user_connection_change, onUserConnectionChange);
|
||||
checkIsSpectator(game) && userKey && context.rtmt.sendMessage(channel_listen_game_events, game.id);
|
||||
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);
|
||||
checkIsSpectator(game) && userKey && context.rtmt.sendMessage(channel_unlisten_game_events, game.id);
|
||||
context.rtmt.removeMessageListener(channel_on_game_update, _onGameUpdate);
|
||||
context.rtmt.removeMessageListener(channel_on_game_crashed, onGameCrashed);
|
||||
context.rtmt.removeMessageListener(channel_on_game_user_leave, onGameUserLeave);
|
||||
context.rtmt.removeMessageListener(channel_on_user_connection_change, onUserConnectionChange);
|
||||
}
|
||||
};
|
||||
|
||||
@ -140,20 +144,28 @@ const GamePage: FunctionComponent<{
|
||||
};
|
||||
|
||||
const onPitSelect = (index: number, pit: Pit) => {
|
||||
if (!game || isSpectator || !userKey) {
|
||||
return;
|
||||
}
|
||||
const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2);
|
||||
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey ||
|
||||
!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
|
||||
return;
|
||||
}
|
||||
if (checkHasAnOngoingAction()) {
|
||||
return;
|
||||
}
|
||||
setHasOngoingAction(true);
|
||||
if (!boardViewModel) return;
|
||||
//TODO : stoneCount comes from view model!
|
||||
//TODO: this check should be in mancala.js
|
||||
if (pit.stoneCount === 0) {
|
||||
//TODO : warn user
|
||||
return;
|
||||
}
|
||||
boardViewModel.pits[getBoardIndex(index)].pitColor =
|
||||
boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor =
|
||||
context.themeManager.theme.pitSelectedColor;
|
||||
updateBoardViewModel(boardViewModel);
|
||||
const gameMove: GameMove = { index: index };
|
||||
const gameMove: GameMove = { index: pitIndexForUser };
|
||||
context.rtmt.sendMessage(channel_game_move, gameMove);
|
||||
};
|
||||
|
||||
@ -166,7 +178,7 @@ const GamePage: FunctionComponent<{
|
||||
pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
||||
setPitAnimator(pitAnimator);
|
||||
onGameUpdate(pitAnimator, game);
|
||||
unlistenMessages = listenMessages(pitAnimator);
|
||||
unlistenMessages = listenMessages(game, pitAnimator);
|
||||
setLoadingStateGame(LoadingState.Loaded({ value: game }))
|
||||
} else {
|
||||
setLoadingStateGame(LoadingState.Error({ errorMessage: context.texts.GameNotFound }))
|
||||
@ -183,17 +195,53 @@ const GamePage: FunctionComponent<{
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.mancalaGame.state == "ended");
|
||||
const showBoardView = game && boardViewModel && userKey && true;
|
||||
const opponentId = getOpponentId();
|
||||
const opponentUser = { id: getOpponentId() || "0", name: "Anonymous", isOnline: opponentId ? isUserOnline(opponentId) : false, isAnonymous: true };
|
||||
const user = { id: userKey || "1", name: "Anonymous", isOnline: connectionState === "connected", isAnonymous: true };
|
||||
|
||||
const isMobile = width < 600;
|
||||
|
||||
const renderNewGameBtn = isSpectator || (userKeyWhoLeave || !game || (game && game.mancalaGame.state == "ended"));
|
||||
const showBoardView = game && boardViewModel && userKey && true;
|
||||
const topLocatedUserId = (isSpectator ? mancalaGame?.player2Id : getOpponentId()) || "0";
|
||||
const bottomLocatedUserId = (isSpectator ? mancalaGame?.player1Id : userKey) || "1";
|
||||
const topLocatedUser = {
|
||||
id: topLocatedUserId,
|
||||
name: "Anonymous",
|
||||
isOnline: isUserOnline(topLocatedUserId),
|
||||
isAnonymous: true
|
||||
};
|
||||
const bottomLocatedUser = {
|
||||
id: bottomLocatedUserId,
|
||||
name: "Anonymous",
|
||||
isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : connectionState === "connected",
|
||||
isAnonymous: true
|
||||
};
|
||||
const currentUser = isSpectator ? {
|
||||
id: "2",
|
||||
name: "Anonymous",
|
||||
isOnline: connectionState === "connected",
|
||||
isAnonymous: true
|
||||
} : bottomLocatedUser;
|
||||
const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined;
|
||||
return (
|
||||
<PageContainer theme={theme!}>
|
||||
<HeaderBar color={theme?.appBarBgColor}>
|
||||
{renderHeaderBar()}
|
||||
{renderMobileBoardToolbar()}
|
||||
{buildBoardTopToolbar()}
|
||||
{showBoardView && (
|
||||
<BoardView
|
||||
game={game}
|
||||
boardId={boardId}
|
||||
boardViewModel={boardViewModel}
|
||||
context={context}
|
||||
onPitSelect={onPitSelect}
|
||||
revert={isPlayer2} />
|
||||
)}
|
||||
<Center>
|
||||
<LoadingComponent context={context} loadingState={gameLoadingState}></LoadingComponent>
|
||||
</Center>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
function renderHeaderBar() {
|
||||
return <HeaderBar color={theme?.appBarBgColor}>
|
||||
<Row>
|
||||
<Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||
<HeaderbarIcon />
|
||||
@ -210,50 +258,44 @@ const GamePage: FunctionComponent<{
|
||||
text={renderNewGameBtn ? context.texts.NewGame : context.texts.Leave}
|
||||
onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
|
||||
</Row>
|
||||
</HeaderBar>
|
||||
<BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
|
||||
</HeaderBar>;
|
||||
}
|
||||
|
||||
function renderMobileBoardToolbar() {
|
||||
return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
|
||||
{buildInfoPanel()}
|
||||
</BoardToolbar>;
|
||||
}
|
||||
|
||||
function buildBoardTopToolbar() {
|
||||
return <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={topLocatedUser} visible={showBoardView || false} />
|
||||
{buildInfoPanel()}
|
||||
<UserStatus style={{
|
||||
marginBottom: "0.5rem", marginRight: "6%", maxWidth: isMobile ? "40vw" : "30vw",
|
||||
width: isMobile ? "40vw" : "30vw"
|
||||
}} context={context} layoutMode="right" user={bottomLocatedUser} visible={showBoardView || false} />
|
||||
</BoardToolbar>;
|
||||
}
|
||||
|
||||
function buildInfoPanel() {
|
||||
return (
|
||||
<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>
|
||||
currentUser={currentUser}
|
||||
whitePlayer={topLocatedUser}
|
||||
blackPlayer={bottomLocatedUser}
|
||||
leftPlayer={leftPlayer}
|
||||
visible={!isMobile}
|
||||
isSpectator={isSpectator} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GamePage;
|
||||
|
||||
|
||||
@ -23,18 +23,19 @@ const LobyPage: FunctionComponent<{
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
listenMessages();
|
||||
context.rtmt.sendMessage("new_game", {});
|
||||
}, []);
|
||||
|
||||
const listenMessages = () => {
|
||||
context.rtmt.listenMessage(channel_on_game_start, (message: Object) => {
|
||||
const onGameStart = (message: Object) => {
|
||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||
navigate(`/game/${newGame.id}`)
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
context.rtmt.addMessageListener(channel_on_game_start, onGameStart);
|
||||
context.rtmt.sendMessage("new_game", {});
|
||||
return () => {
|
||||
context.rtmt.removeMessageListener(channel_on_game_start, onGameStart);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const textColorOnAppBar = getColorByBrightness(
|
||||
context.themeManager.theme.appBarBgColor,
|
||||
context.themeManager.theme.textColor,
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
import EventEmitter2, { Listener } from "eventemitter2"
|
||||
|
||||
export type Bytes = Uint8Array
|
||||
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
|
||||
|
||||
export type RtmtEventTypes = "open" | "close" | "connected" | "error" | "disconnected" | "message";
|
||||
|
||||
export interface RTMT extends EventEmitter2 {
|
||||
sendMessage: (channel: string, message: Object) => void;
|
||||
addMessageListener(channel: string, callback: (message: any) => void);
|
||||
removeMessageListener(channel: string, callback: (message: any) => void);
|
||||
on(event: RtmtEventTypes, callback: (...value: any[]) => void): Listener | this;
|
||||
off(event: RtmtEventTypes, callback: (...value: any[]) => void): this;
|
||||
dispose();
|
||||
}
|
||||
@ -1,101 +1,93 @@
|
||||
import { decode, encode } from "./encode_decode_message";
|
||||
import { Bytes, OnMessage, RTMT } from "./rtmt";
|
||||
import { channel_ping, channel_pong } from "../const/channel_names";
|
||||
import { server } from "../const/config";
|
||||
import EventEmitter2, { Listener } from "eventemitter2";
|
||||
import { Bytes, RTMT, RtmtEventTypes } from "./rtmt";
|
||||
|
||||
const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000;
|
||||
const MESSAGE_CHANNEL_PREFIX = "message_channel";
|
||||
|
||||
export class RTMTWS implements RTMT {
|
||||
private messageChannels: Map<String, OnMessage | undefined>;
|
||||
private ws: WebSocket;
|
||||
|
||||
export class RTMTWS extends EventEmitter2 implements RTMT {
|
||||
private webSocket: WebSocket;
|
||||
private pingTimeout?: number = undefined;
|
||||
|
||||
constructor() {
|
||||
this.messageChannels = new Map<String, OnMessage>();
|
||||
super();
|
||||
}
|
||||
|
||||
initWebSocket(
|
||||
userKey: string,
|
||||
onopen: () => any,
|
||||
onClose: () => any,
|
||||
onError: (event: Event) => any
|
||||
) {
|
||||
public initWebSocket(userKey: string) {
|
||||
const url = server.wsServerAdress + "?userKey=" + userKey;
|
||||
const ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer"; //for firefox
|
||||
ws.onopen = () => {
|
||||
console.info("(RTMT) ws has opened");
|
||||
this.ws = ws;
|
||||
const webSocket = new WebSocket(url);
|
||||
webSocket.onopen = () => {
|
||||
console.info("(RTMT) WebSocket has opened");
|
||||
this.webSocket = webSocket;
|
||||
this.heartbeat();
|
||||
onopen();
|
||||
this.emit("open");
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.info("(RTMT) ws has closed");
|
||||
//this.ws = undefined
|
||||
webSocket.onclose = () => {
|
||||
console.info("(RTMT) WebSocket has closed");
|
||||
//this.WebSocket = undefined
|
||||
clearTimeout(this.pingTimeout);
|
||||
onClose();
|
||||
this.emit("close");
|
||||
};
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
this.onWebSocketMessage(this, event);
|
||||
webSocket.onmessage = (event: MessageEvent) => {
|
||||
const { channel, message } = decode(event.data);
|
||||
this.onMessage(channel, message);
|
||||
};
|
||||
|
||||
ws.addEventListener("error", (ev) => {
|
||||
console.error({ ws_error: ev });
|
||||
onError(ev);
|
||||
});
|
||||
webSocket.onerror = (error) => {
|
||||
console.error(error);
|
||||
this.emit("error", error);
|
||||
}
|
||||
}
|
||||
|
||||
heartbeat() {
|
||||
private heartbeat() {
|
||||
clearTimeout(this.pingTimeout);
|
||||
|
||||
// Use `WebSocket#terminate()`, which immediately destroys the connection,
|
||||
// instead of `WebSocket#close()`, which waits for the close timer.
|
||||
// Delay should be equal to the interval at which your server
|
||||
// sends out pings plus a conservative assumption of the latency.
|
||||
this.pingTimeout = setTimeout(() => {
|
||||
this.ws.close();
|
||||
this.webSocket.close();
|
||||
}, PING_INTERVAL + PING_INTERVAL_BUFFER_TIME);
|
||||
}
|
||||
|
||||
|
||||
sendMessage(channel: string, message: Object) {
|
||||
if (this.ws === undefined) {
|
||||
console.error("(RTMT) ws is undefined");
|
||||
public sendMessage(channel: string, message: Object) {
|
||||
if (this.webSocket === undefined) {
|
||||
console.error("(RTMT) WebSocket is undefined");
|
||||
return;
|
||||
}
|
||||
const data = encode(channel, message);
|
||||
this.ws.send(data);
|
||||
this.webSocket.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);
|
||||
}
|
||||
|
||||
onMessage(channel: string, message: Bytes) {
|
||||
private onMessage(channel: string, message: Bytes) {
|
||||
if (channel === channel_ping) {
|
||||
this.heartbeat();
|
||||
this.sendMessage(channel_pong, {});
|
||||
return;
|
||||
}
|
||||
const callback = this.messageChannels.get(channel);
|
||||
// TODO: Maybe we should warn if there is not any listener for channel
|
||||
this.emit(MESSAGE_CHANNEL_PREFIX + channel, message);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(message);
|
||||
} else {
|
||||
console.warn("(RTMT) Channel callback not found!" + channel);
|
||||
}
|
||||
public on(event: RtmtEventTypes, callback: (...value: any[]) => void): Listener | this {
|
||||
return super.on(event, callback);
|
||||
}
|
||||
|
||||
public off(event: RtmtEventTypes, callback: (...value: any[]) => void): this {
|
||||
return super.off(event, callback);
|
||||
}
|
||||
|
||||
public addMessageListener(channel: string, callback: (message: any) => void) {
|
||||
super.on(MESSAGE_CHANNEL_PREFIX + channel, callback);
|
||||
}
|
||||
|
||||
public removeMessageListener(channel: string, callback: (message: any) => void) {
|
||||
super.off(MESSAGE_CHANNEL_PREFIX + channel, callback);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.webSocket.close();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,18 @@ import lightTheme from "./LightTheme";
|
||||
import greyTheme from "./GreyTheme";
|
||||
import { Theme } from "./Theme";
|
||||
import darkTheme from "./DarkTheme";
|
||||
import EventEmitter2, { Listener } from "eventemitter2";
|
||||
|
||||
export const themes = [lightTheme, darkTheme, greyTheme];
|
||||
|
||||
const THEME_ID = "theme_id";
|
||||
|
||||
export default class ThemeManager {
|
||||
_theme: Theme;
|
||||
onThemeChange: (theme: Theme) => void;
|
||||
export type ThemeManagerEvents = "themechange";
|
||||
|
||||
export default class ThemeManager extends EventEmitter2 {
|
||||
private _theme: Theme;
|
||||
constructor() {
|
||||
super();
|
||||
this._theme = this.readFromLocalStorage() || lightTheme;
|
||||
}
|
||||
|
||||
@ -20,7 +23,7 @@ export default class ThemeManager {
|
||||
|
||||
public set theme(value: Theme) {
|
||||
this._theme = value;
|
||||
this.onThemeChange?.(value);
|
||||
this.emit("themechange", value);
|
||||
this.writetToLocalStorage(value);
|
||||
}
|
||||
|
||||
@ -28,7 +31,7 @@ export default class ThemeManager {
|
||||
localStorage.setItem(THEME_ID, value.id);
|
||||
}
|
||||
|
||||
private readFromLocalStorage(): Theme {
|
||||
private readFromLocalStorage(): Theme | undefined {
|
||||
const themeID = localStorage.getItem(THEME_ID);
|
||||
const theme = themes.find((eachTheme: Theme) => themeID === eachTheme.id);
|
||||
return theme;
|
||||
@ -37,4 +40,11 @@ export default class ThemeManager {
|
||||
public get themes(): Theme[] {
|
||||
return themes;
|
||||
}
|
||||
|
||||
public on(event: ThemeManagerEvents, callback: (...value: any[]) => void): Listener | this {
|
||||
return super.on(event, callback);
|
||||
}
|
||||
public off(event: ThemeManagerEvents, callback: (...value: any[]) => void): this {
|
||||
return super.off(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@ -1087,6 +1087,13 @@
|
||||
prop-types "^15.7.2"
|
||||
react-transition-state "^1.1.4"
|
||||
|
||||
"@types/eventemitter2@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/eventemitter2/-/eventemitter2-4.1.0.tgz#090b4a67f25aa0fc087c8f2403e46f5ccc3fdfbc"
|
||||
integrity sha512-IyrCYFL+FakW3gVd/x2b0QIpcVrdgcNCkj985xoBVinc0rNwoV87IbBx7KlS5aP+bx7uIZxVypLCiSwmI4jZrg==
|
||||
dependencies:
|
||||
eventemitter2 "*"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
@ -1421,6 +1428,11 @@ escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
eventemitter2@*, eventemitter2@^6.4.7:
|
||||
version "6.4.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
|
||||
integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
|
||||
|
||||
gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
|
||||
@ -1735,7 +1747,7 @@ prop-types@^15.7.2:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
"react-dom@^16.0.0 || ^17.0.0 || ^18.0.0":
|
||||
react-dom@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
@ -1778,7 +1790,7 @@ react-transition-state@^1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/react-transition-state/-/react-transition-state-1.1.4.tgz#113224eaa27e0ff81661305e44d5e0348cdf61ac"
|
||||
integrity sha512-6nQLWWx95gYazCm6OdtD1zGbRiirvVXPrDtHAGsYb4xs9spMM7bA8Vx77KCpjL8PJ8qz1lXFGz2PTboCSvt7iw==
|
||||
|
||||
"react@^16.0.0 || ^17.0.0 || ^18.0.0":
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user