mancala/src/routes/GamePage.tsx

322 lines
14 KiB
TypeScript
Raw Normal View History

import { MancalaGame, Pit } from 'mancala.js';
import * as React from 'react';
2022-07-31 00:59:18 +03:00
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, channel_listen_game_events, channel_unlisten_game_events } from '../const/channel_names';
import { Context } from '../context/context';
2022-07-31 00:59:18 +03:00
import useWindowDimensions from '../hooks/useWindowDimensions';
import { GameMove } from '../models/GameMove';
import { LoadingState } from '../models/LoadingState';
import { Theme } from '../theme/Theme';
import { getColorByBrightness } from '../util/ColorUtil';
import BoardViewModel from '../viewmodel/BoardViewModel';
import Center from '../components/Center';
import { Game, GameUsersConnectionInfo } from '../models/Game';
2022-09-04 00:04:32 +03:00
import notyf from '../util/Notyf';
2022-09-04 00:16:14 +03:00
import swal from 'sweetalert';
2022-09-04 01:06:26 +03:00
import Util from '../util/Util';
2022-07-31 00:59:18 +03:00
const GamePage: FunctionComponent<{
context: Context,
userKey?: string,
theme: Theme,
2022-09-04 01:06:26 +03:00
}> = ({ context, userKey, theme }) => {
2022-07-31 00:59:18 +03:00
let params = useParams<{ gameId: string }>();
const [game, setGame] = useState<Game | undefined>(undefined);
2022-07-31 00:59:18 +03:00
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 [gameUsersConnectionInfo, setGameUsersConnectionInfo] = useState<GameUsersConnectionInfo | undefined>();
2022-07-31 00:59:18 +03:00
const { height, width } = useWindowDimensions();
const navigate = useNavigate();
const [gameLoadingState, setLoadingStateGame] = useState<LoadingState<Game>>(LoadingState.Unset());
2022-07-31 00:59:18 +03:00
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);
pitAnimator.setUpdatedGame(newGame);
2022-07-31 00:59:18 +03:00
setHasOngoingAction(false);
setGameUsersConnectionInfo(newGame.gameUsersConnectionInfo);
}
const isUserOnline = (userId: string) => {
if (!gameUsersConnectionInfo) return false;
const user1ConnectionInfo = gameUsersConnectionInfo.user1ConnectionInfo;
const user2ConnectionInfo = gameUsersConnectionInfo.user2ConnectionInfo;
if (user1ConnectionInfo.userId === userId) return user1ConnectionInfo.isOnline;
if (user2ConnectionInfo.userId === userId) return user2ConnectionInfo.isOnline;
return false;
}
const onGameUpdateEvent = (pitAnimator: PitAnimator, message: Object) => {
const newGame: Game = message as Game;
newGame.mancalaGame = MancalaGame.createFromMancalaGame(newGame.mancalaGame);
onGameUpdate(pitAnimator, newGame);
2022-07-31 00:59:18 +03:00
}
const onGameCrashed = (message: any) => {
const newCrashMessage = message as string;
2022-09-04 00:04:32 +03:00
notyf.error(context.texts.InternalErrorOccurred);
2022-07-31 00:59:18 +03:00
console.error("on_game_crash");
console.error(newCrashMessage);
}
const onGameUserLeave = (message: any) => {
const userKeyWhoLeave = message;
setUserKeyWhoLeave(userKeyWhoLeave);
setHasOngoingAction(false);
};
const onUserConnectionChange = (message: any) => {
const gameUsersConnectionInfo = message as GameUsersConnectionInfo;
setGameUsersConnectionInfo(gameUsersConnectionInfo);
2022-07-31 00:59:18 +03:00
};
const listenMessages = (game: Game, pitAnimator: PitAnimator): () => void => {
const _onGameUpdate = (message: object) => onGameUpdateEvent(pitAnimator, message);
2022-09-02 00:04:21 +03:00
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);
2022-07-31 00:59:18 +03:00
return () => {
checkIsSpectator(game) && userKey && context.rtmt.sendMessage(channel_unlisten_game_events, game.id);
2022-09-02 00:04:21 +03:00
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);
2022-07-31 00:59:18 +03:00
}
};
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
boardViewModel.id = v4();
setBoardId(boardViewModel.id);
setBoardViewModel(boardViewModel);
};
const getBoardIndex = (index: number) => {
if (!game || !mancalaGame) return -1;
const pitsLenght = mancalaGame.board.pits.length;
if (userKey === mancalaGame.player2Id) return index + pitsLenght / 2;
2022-07-31 00:59:18 +03:00
return index;
};
const getOpponentId = () => mancalaGame?.player1Id === userKey ? mancalaGame?.player2Id : mancalaGame?.player1Id;
2022-07-31 00:59:18 +03:00
const checkHasAnOngoingAction = () => hasOngoingAction;
const onLeaveGameClick = () => {
2022-09-04 19:31:33 +03:00
if (Util.checkConnectionAndMaybeAlert(context)) return;
2022-09-04 00:16:14 +03:00
swal({
title: context.texts.AreYouSureToLeaveGame,
icon: "warning",
buttons: [context.texts.Yes, context.texts.Cancel],
dangerMode: true,
})
.then((cancel) => {
if (!cancel) {
context.rtmt.sendMessage(channel_leave_game, {});
}
});
2022-07-31 00:59:18 +03:00
};
const onNewGameClick = () => {
2022-09-04 19:31:33 +03:00
if (Util.checkConnectionAndMaybeAlert(context)) return;
2022-07-31 00:59:18 +03:00
navigate("/loby")
};
const onPitSelect = (index: number, pit: Pit) => {
if (!game || isSpectator || !userKey) {
return;
}
2022-09-04 19:31:33 +03:00
if (Util.checkConnectionAndMaybeAlert(context)) return;
2022-09-04 00:16:14 +03:00
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey) {
2022-09-04 00:04:32 +03:00
notyf.error(context.texts.UCanOnlyPlayYourOwnPits);
return;
}
const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2);
2022-09-04 00:04:32 +03:00
if (!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
notyf.error(context.texts.OpponentTurn);
return;
}
2022-07-31 00:59:18 +03:00
if (checkHasAnOngoingAction()) {
2022-09-04 00:04:32 +03:00
notyf.error(context.texts.UMustWaitUntilCurrentMoveComplete);
2022-07-31 00:59:18 +03:00
return;
}
if (!boardViewModel) return;
//TODO: this check should be in mancala.js
2022-07-31 00:59:18 +03:00
if (pit.stoneCount === 0) {
2022-09-04 00:04:32 +03:00
notyf.error(context.texts.UCanNotPlayEmptyPit);
2022-07-31 00:59:18 +03:00
return;
}
2022-09-04 00:04:32 +03:00
setHasOngoingAction(true);
boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor =
2022-07-31 00:59:18 +03:00
context.themeManager.theme.pitSelectedColor;
updateBoardViewModel(boardViewModel);
const gameMove: GameMove = { index: pitIndexForUser };
2022-07-31 00:59:18 +03:00
context.rtmt.sendMessage(channel_game_move, gameMove);
};
React.useEffect(() => {
let pitAnimator: PitAnimator | undefined;
let unlistenMessages: () => void;
2022-07-31 00:59:18 +03:00
setLoadingStateGame(LoadingState.Loading())
context.gameStore.get(params.gameId!!).then((game) => {
if (game) {
pitAnimator = new PitAnimator(context, updateBoardViewModel);
setPitAnimator(pitAnimator);
onGameUpdate(pitAnimator, game);
unlistenMessages = listenMessages(game, pitAnimator);
2022-07-31 00:59:18 +03:00
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 isMobile = width < 600;
2022-09-02 00:04:21 +03:00
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",
2022-09-04 01:06:26 +03:00
isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : context.rtmt.connectionState === "connected",
2022-09-02 00:04:21 +03:00
isAnonymous: true
};
const currentUser = isSpectator ? {
id: "2",
name: "Anonymous",
2022-09-04 01:06:26 +03:00
isOnline: context.rtmt.connectionState === "connected",
2022-09-02 00:04:21 +03:00
isAnonymous: true
} : bottomLocatedUser;
const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined;
return (
2022-07-31 00:59:18 +03:00
<PageContainer theme={theme!}>
2022-09-02 00:04:21 +03:00
{renderHeaderBar()}
{renderMobileBoardToolbar()}
{buildBoardTopToolbar()}
2022-07-31 00:59:18 +03:00
{showBoardView && (
<BoardView
game={game}
boardId={boardId}
boardViewModel={boardViewModel}
context={context}
onPitSelect={onPitSelect}
revert={isPlayer2} />
2022-07-31 00:59:18 +03:00
)}
<Center>
<LoadingComponent context={context} loadingState={gameLoadingState}></LoadingComponent>
</Center>
</PageContainer>
);
2022-09-02 00:04:21 +03:00
function renderHeaderBar() {
return <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>;
}
function renderMobileBoardToolbar() {
return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
2022-09-03 21:06:52 +03:00
{buildInfoPanel({ visible: isMobile })}
2022-09-02 00:04:21 +03:00
</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} />
2022-09-03 21:06:52 +03:00
{buildInfoPanel({ visible: !isMobile })}
2022-09-02 00:04:21 +03:00
<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>;
}
2022-09-03 21:06:52 +03:00
function buildInfoPanel(params: { visible: boolean }) {
2022-09-02 00:04:21 +03:00
return (
<InfoPanel
style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}
context={context}
game={game}
currentUser={currentUser}
whitePlayer={topLocatedUser}
blackPlayer={bottomLocatedUser}
leftPlayer={leftPlayer}
2022-09-03 21:06:52 +03:00
visible={params.visible}
2022-09-02 00:04:21 +03:00
isSpectator={isSpectator} />
);
}
}
2022-09-02 00:04:21 +03:00
export default GamePage;