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, logout,
onMatchData, onMatchData,
joinMatchmaker, joinMatchmaker,
exitMatchmaker,
} = useNakama(); } = useNakama();
const [username, setUsername] = useState( const [username, setUsername] = useState(
localStorage.getItem("username") ?? "" localStorage.getItem("username") ?? ""
); );
const [selectedGame, setSelectedGame] = useState("tictactoe");
const [selectedMode, setSelectedMode] = useState("classic"); const [selectedMode, setSelectedMode] = useState("classic");
const [isQueueing, setIsQueueing] = useState(false); const [isQueueing, setIsQueueing] = useState(false);
const isRegistered = localStorage.getItem("registered") === "yes"; const isRegistered = localStorage.getItem("registered") === "yes";
@@ -36,14 +38,19 @@ export default function Player({
// ------------------------------------------ // ------------------------------------------
// MATCHMAKING // MATCHMAKING
// ------------------------------------------ // ------------------------------------------
async function startQueue(selectedMode: string) { async function startQueue(
selectedGame: string,
selectedMode: string
) {
setIsQueueing(true); setIsQueueing(true);
const gameMetadata = {
game: selectedGame,
mode: selectedMode,
}
try { try {
const ticket = await joinMatchmaker({ await exitMatchmaker(gameMetadata)
game: 'tictactoe', const ticket = await joinMatchmaker(gameMetadata);
mode: selectedMode,
});
console.log("Queued:", ticket); console.log("Queued:", ticket);
} catch (err) { } catch (err) {
console.error("Matchmaking failed:", err); console.error("Matchmaking failed:", err);
@@ -148,6 +155,24 @@ export default function Player({
Select Game Mode Select Game Mode
</label> </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 <select
value={selectedMode} value={selectedMode}
disabled={isQueueing} disabled={isQueueing}
@@ -169,7 +194,10 @@ export default function Player({
{!isQueueing && ( {!isQueueing && (
<motion.button <motion.button
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
onClick={() => startQueue(selectedMode)} onClick={() => startQueue(
selectedGame,
selectedMode,
)}
style={{ style={{
padding: "10px 20px", padding: "10px 20px",
borderRadius: "12px", borderRadius: "12px",

View File

@@ -39,6 +39,7 @@ export interface NakamaContextType {
loginOrRegister(username: string): Promise<void>; loginOrRegister(username: string): Promise<void>;
logout(): Promise<void>; logout(): Promise<void>;
joinMatchmaker(gameMetadata: GameMetadata): Promise<string>; joinMatchmaker(gameMetadata: GameMetadata): Promise<string>;
exitMatchmaker(gameMetadata: GameMetadata): Promise<void>;
joinMatch(matchId: string): Promise<void>; joinMatch(matchId: string): Promise<void>;
sendMatchData(matchId: string, op: number, data: object): 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 gameMetadataRef = React.useRef<GameMetadata | null>(null);
const [session, setSession] = useState<Session | null>(null); const [session, setSession] = useState<Session | null>(null);
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [matchmakerTicket, setMatchmakerTicket] = useState<string | null>(null);
const [matchId, setMatchId] = useState<string | null>(null); const [matchId, setMatchId] = useState<string | null>(null);
const socketRef = React.useRef<Socket | null>(null); const socketRef = React.useRef<Socket | null>(null);
@@ -212,10 +214,22 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
); );
gameMetadataRef.current = { game, mode }; gameMetadataRef.current = { game, mode };
setMatchmakerTicket(ticket.ticket);
return 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 // EXPLICIT MATCH JOIN
// ---------------------------------------------------- // ----------------------------------------------------
@@ -284,6 +298,7 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
loginOrRegister, loginOrRegister,
logout, logout,
joinMatchmaker, joinMatchmaker,
exitMatchmaker,
joinMatch, joinMatch,
sendMatchData, sendMatchData,
onMatchData, onMatchData,