The lobby offers the bot levels after 10 seconds without an opponent, the home page has a Play with bot menu, and a game against the bot ends with Play again at the same level.
138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
import { CommonMancalaGame } from 'mancala.js';
|
|
import * as React from 'react';
|
|
import { FunctionComponent, useEffect, useState } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import AppHeader from '../components/headerbar/AppHeader';
|
|
import PageContainer from '../components/PageContainer';
|
|
import { BOT_OFFER_DELAY_MS, BotLevel, channel_cancel_new_game, channel_new_bot_game, channel_new_game, channel_on_game_start, channel_on_new_game_rejected, isBotLevel } from '@mancala/core';
|
|
import BotLevelPicker from '../components/BotLevelPicker';
|
|
import { Context } from '../context/context';
|
|
import { Theme } from '@mancala/core';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const LobyPage: FunctionComponent<{
|
|
context: Context,
|
|
userKey?: string,
|
|
theme: Theme
|
|
}> = ({ context }) => {
|
|
|
|
let navigate = useNavigate();
|
|
|
|
const { t } = useTranslation();
|
|
|
|
// /loby?bot=<level> starts a game against the bot at once instead of searching for an opponent.
|
|
const [searchParams] = useSearchParams();
|
|
const botParam = searchParams.get("bot");
|
|
const botLevel: BotLevel | undefined = isBotLevel(botParam) ? botParam : undefined;
|
|
|
|
// Set when the server refuses a new game because the user has one in progress.
|
|
const [activeGameId, setActiveGameId] = useState<string | undefined>(undefined);
|
|
|
|
// Shown when no opponent has been found for a while.
|
|
const [botOffered, setBotOffered] = useState(false);
|
|
|
|
const onNewGameRejected = (message: { gameId: string }) => setActiveGameId(message.gameId);
|
|
|
|
const onGameStart = (message: Object) => {
|
|
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
|
navigate(`/game/${newGame.id}`)
|
|
}
|
|
|
|
const startBotGame = (level: BotLevel) => {
|
|
context.rtmt.sendMessage(channel_new_bot_game, { level });
|
|
}
|
|
|
|
const onCancelNewGameClick = () => {
|
|
if (context.rtmt.connectionState === "connected") {
|
|
context.rtmt.sendMessage(channel_cancel_new_game, "");
|
|
}
|
|
navigate("/")
|
|
}
|
|
useEffect(() => {
|
|
context.rtmt.addMessageListener(channel_on_game_start, onGameStart);
|
|
context.rtmt.addMessageListener(channel_on_new_game_rejected, onNewGameRejected);
|
|
let botOfferTimer: ReturnType<typeof setTimeout> | undefined;
|
|
if (botLevel) {
|
|
startBotGame(botLevel);
|
|
} else {
|
|
context.rtmt.sendMessage(channel_new_game, {});
|
|
botOfferTimer = setTimeout(() => setBotOffered(true), BOT_OFFER_DELAY_MS);
|
|
}
|
|
return () => {
|
|
if (botOfferTimer) clearTimeout(botOfferTimer);
|
|
context.rtmt.removeMessageListener(channel_on_game_start, onGameStart);
|
|
context.rtmt.removeMessageListener(channel_on_new_game_rejected, onNewGameRejected);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<PageContainer>
|
|
<AppHeader context={context} />
|
|
<main className="wrap">
|
|
<section className="search panel" aria-live="polite">
|
|
{!activeGameId && <span className="dot" />}
|
|
<h1>{activeGameId ? t("GameInProgress") : botLevel ? t("PlayWithBot") : t("SearchingOpponent")}</h1>
|
|
{!activeGameId && <p>{`${t("PleaseWait")}...`}</p>}
|
|
{!activeGameId && botOffered && (
|
|
<div className="offer">
|
|
<p>{t("NoOpponentYet")}</p>
|
|
<BotLevelPicker context={context} onChoose={startBotGame} />
|
|
</div>
|
|
)}
|
|
<div className="actions">
|
|
{activeGameId && (
|
|
<button className="btn pri" onClick={() => navigate(`/game/${activeGameId}`)}>{t("Continue")}</button>
|
|
)}
|
|
<button className="btn ghost" onClick={onCancelNewGameClick}>{t("Cancel")}</button>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<style jsx>{`
|
|
.search {
|
|
align-self: center;
|
|
width: 100%;
|
|
max-width: 460px;
|
|
margin-top: 8vh;
|
|
padding: 32px;
|
|
box-sizing: border-box;
|
|
border-radius: 32px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
text-align: center;
|
|
}
|
|
.dot {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--accent);
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 28px;
|
|
letter-spacing: -.02em;
|
|
}
|
|
p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
}
|
|
.offer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-top: 8px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
}
|
|
`}</style>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
export default LobyPage;
|