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 (