feat(matchmaking): add selectedGame support and implement exitMatchmaker to clear active tickets

Added selectedGame state and UI dropdown

Updated startQueue() to pass { game, mode } metadata

Added exitMatchmaker() to remove existing ticket

Stored active matchmaker ticket in context

Prevents duplicate matchmaker ticket errors
This commit is contained in:
2025-12-01 20:55:24 +05:30
parent cc1f45457c
commit 83ae342499
2 changed files with 49 additions and 6 deletions

View File

@@ -14,11 +14,13 @@ export default function Player({
logout,
onMatchData,
joinMatchmaker,
exitMatchmaker,
} = useNakama();
const [username, setUsername] = useState(
localStorage.getItem("username") ?? ""
);
const [selectedGame, setSelectedGame] = useState("tictactoe");
const [selectedMode, setSelectedMode] = useState("classic");
const [isQueueing, setIsQueueing] = useState(false);
const isRegistered = localStorage.getItem("registered") === "yes";
@@ -36,14 +38,19 @@ export default function Player({
// ------------------------------------------
// MATCHMAKING
// ------------------------------------------
async function startQueue(selectedMode: string) {
async function startQueue(
selectedGame: string,
selectedMode: string
) {
setIsQueueing(true);
const gameMetadata = {
game: selectedGame,
mode: selectedMode,
}
try {
const ticket = await joinMatchmaker({
game: 'tictactoe',
mode: selectedMode,
});
await exitMatchmaker(gameMetadata)
const ticket = await joinMatchmaker(gameMetadata);
console.log("Queued:", ticket);
} catch (err) {
console.error("Matchmaking failed:", err);
@@ -148,6 +155,24 @@ export default function Player({
Select Game Mode
</label>
<select
value={selectedGame}
disabled={isQueueing}
onChange={(e) => setSelectedGame(e.target.value)}
style={{
padding: "8px",
margin: "10px 0 16px",
width: "60%",
borderRadius: "10px",
background: "#222",
color: "white",
border: "1px solid #333",
}}
>
<option value="tictactoe">Tic Tac Toe</option>
<option value="battleship">Battleship</option>
</select>
<select
value={selectedMode}
disabled={isQueueing}
@@ -169,7 +194,10 @@ export default function Player({
{!isQueueing && (
<motion.button
whileTap={{ scale: 0.95 }}
onClick={() => startQueue(selectedMode)}
onClick={() => startQueue(
selectedGame,
selectedMode,
)}
style={{
padding: "10px 20px",
borderRadius: "12px",

View File

@@ -39,6 +39,7 @@ export interface NakamaContextType {
loginOrRegister(username: string): Promise<void>;
logout(): Promise<void>;
joinMatchmaker(gameMetadata: GameMetadata): Promise<string>;
exitMatchmaker(gameMetadata: GameMetadata): Promise<void>;
joinMatch(matchId: string): Promise<void>;
sendMatchData(matchId: string, op: number, data: object): void;
@@ -70,6 +71,7 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
const gameMetadataRef = React.useRef<GameMetadata | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [socket, setSocket] = useState<Socket | null>(null);
const [matchmakerTicket, setMatchmakerTicket] = useState<string | null>(null);
const [matchId, setMatchId] = useState<string | null>(null);
const socketRef = React.useRef<Socket | null>(null);
@@ -212,10 +214,22 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
);
gameMetadataRef.current = { game, mode };
setMatchmakerTicket(ticket.ticket);
return ticket.ticket;
}
async function exitMatchmaker(gameMetadata: GameMetadata) {
const socket = socketRef.current;
const game = gameMetadata.game;
const mode = gameMetadata.mode;
if (!socket) throw new Error("socket missing");
console.log(`[Nakama] Exiting Matchmaking... game="${game}" mode="${mode}"`);
if (matchmakerTicket) await socket.removeMatchmaker(matchmakerTicket);
setMatchmakerTicket(null);
}
// ----------------------------------------------------
// EXPLICIT MATCH JOIN
// ----------------------------------------------------
@@ -284,6 +298,7 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
loginOrRegister,
logout,
joinMatchmaker,
exitMatchmaker,
joinMatch,
sendMatchData,
onMatchData,