mancala/src/routes/GamePage.tsx

244 lines
11 KiB
TypeScript
Raw Normal View History

2022-07-31 00:59:18 +03:00
import { CommonMancalaGame, 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 } from '../const/channel_names';
import { Context } from '../context/context';
2022-07-31 00:59:18 +03:00
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 (
2022-07-31 00:59:18 +03:00
<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;