diff --git a/src/routes/GamePage.tsx b/src/routes/GamePage.tsx index 8c02d4b..36df155 100644 --- a/src/routes/GamePage.tsx +++ b/src/routes/GamePage.tsx @@ -1,17 +1,243 @@ +import { CommonMancalaGame, MancalaGame, Pit } from 'mancala.js'; import * as React from 'react'; -import { FunctionComponent } from 'react'; -import { useParams } from 'react-router'; +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; -const GamePage: FunctionComponent<{ context: Context }> = ({ context }) => { - let params = useParams<{gameId : string}>(); return ( -
- Game Route {params.gameId} - -
+ + + + + + + + + + + + +