SyntaxSquad/DADProject#16 fix: Cancle and Return is working and some other fixes involving game duration

This commit is contained in:
AfonsoCMSousa
2026-01-03 02:19:00 +00:00
parent 7e6766c726
commit 3dcd8df4c7
9 changed files with 227 additions and 143 deletions
@@ -244,4 +244,19 @@ class GameController extends Controller
'game' => $game->fresh(), 'game' => $game->fresh(),
]); ]);
} }
public function destroy(Game $game)
{
// Ensure only the host can delete, and ONLY if it's still Pending
if ($game->player1_user_id !== auth()->id()) {
return response()->json(['message' => 'Unauthorized'], 403);
}
if ($game->status !== 'Pending') {
return response()->json(['message' => 'Cannot delete a game in progress'], 400);
}
$game->delete();
return response()->noContent();
}
} }
+73 -19
View File
@@ -291,18 +291,88 @@ const handleBeforeUnload = (event) => {
} }
}; };
onBeforeRouteLeave((to, from, next) => { // --- FIX: SURRENDER LOGIC ---
if (gameOver.value || !shouldShowBoard.value) { const handleSurrender = () => {
if(confirm('Are you sure you want to surrender? You will lose.')) {
wsStore.surrender(gameId.value)
}
}
const handleCloseModal = async () => {
// 1. If game is still in Lobby (Pending) and I am the Host, DELETE it.
if (!shouldShowBoard.value && amIHost.value && !gameOver.value) {
try {
// This calls Route::delete('/{game}', ...) in your api.php
await axios.delete(`${API_BASE_URL}/games/${gameId.value}`);
console.log("Lobby cancelled and deleted.");
} catch (e) {
console.error("Failed to delete pending game:", e);
}
}
// 2. Notify server of voluntary exit (to skip the 10s reconnect timer)
if (wsStore.socket) {
wsStore.socket.emit('notify_disconnect');
}
// 3. Disconnect and go home
wsStore.disconnect();
router.push({ name: 'home' });
}
// --- LIFECYCLE ---
onBeforeRouteLeave(async (to, from, next) => {
// CASE A: Game is Pending (Waiting Room)
if (!shouldShowBoard.value && !gameOver.value) {
if (amIHost.value) {
// Ask for confirmation to close the lobby
const confirmClose = window.confirm("This will cancel the lobby. Are you sure?");
if (confirmClose) {
try {
// Delete the game from DB so it doesn't get stuck as "Pending"
await axios.delete(`${API_BASE_URL}/games/${gameId.value}`);
} catch(e) { console.error(e) }
if(wsStore.socket) wsStore.socket.emit('notify_disconnect');
wsStore.disconnect();
next();
} else {
next(false); // Stay on page
}
return;
} else {
// If I am NOT host (just a guest waiting), just leave
if(wsStore.socket) wsStore.socket.emit('notify_disconnect');
wsStore.disconnect();
next();
return;
}
}
// CASE B: Game is Over (Standard exit)
if (gameOver.value) {
next(); next();
return; return;
} }
// CASE C: Game is Active/Playing (The Surrender Logic we built before)
const answer = window.confirm( const answer = window.confirm(
"If you leave, you will be register has 'losing' or 'surrendered', Are you sure you want to leave?" "If you leave, you will be register has 'losing' or 'surrendered', Are you sure you want to leave?"
); );
if (answer) { if (answer) {
wsStore.surrender(gameId.value); // ... (Your existing surrender logic) ...
try { await wsStore.surrender(gameId.value); } catch (e) {}
if (wsStore.socket) {
wsStore.socket.emit('notify_disconnect', () => {
wsStore.disconnect();
next();
});
setTimeout(() => { if (wsStore.connected) { wsStore.disconnect(); next(); } }, 500);
return;
}
wsStore.disconnect(); wsStore.disconnect();
next(); next();
} else { } else {
@@ -310,22 +380,6 @@ onBeforeRouteLeave((to, from, next) => {
} }
}); });
// --- FIX: SURRENDER LOGIC ---
const handleSurrender = () => {
if(confirm('Are you sure you want to surrender? You will lose.')) {
// Send event to server. Do NOT disconnect locally yet.
// Wait for the server to send "Game Over" event back.
wsStore.surrender(gameId.value)
}
}
const handleCloseModal = () => {
wsStore.disconnect()
router.push({ name: 'home' })
}
// --- LIFECYCLE ---
onMounted(async () => { onMounted(async () => {
await fetchGameMetadata() await fetchGameMetadata()
wsStore.connect() wsStore.connect()
+2
View File
@@ -97,6 +97,8 @@ const getPendingMatches = async (mode = selectedMode.value) => {
mLoading.value = true; mLoading.value = true;
try { try {
// FIX: Incorrect endpoint used previously
// TAG: 404
const response = await axios.get(`${API_BASE_URL}/matches/open`, { const response = await axios.get(`${API_BASE_URL}/matches/open`, {
params: { params: {
type: mode, type: mode,
+7 -1
View File
@@ -621,7 +621,8 @@
</div> </div>
<div class="text-right"> <div class="text-right">
<div class="font-bold text-sm">{{ game.amount }} pts</div>
<div class="font-bold text-sm">{{ game.points }} pts</div>
<div class="text-[10px] text-muted-foreground"> <div class="text-[10px] text-muted-foreground">
{{ new Date(game.began_at).toLocaleDateString() }} {{ new Date(game.began_at).toLocaleDateString() }}
{{ {{
@@ -1185,10 +1186,15 @@ const fetchData = async () => {
return { return {
id: game.id, id: game.id,
player1_id: game.player1.id,
player2_id: game.player2.id,
variant: `Type ${game.type}`, variant: `Type ${game.type}`,
outcome: outcome, outcome: outcome,
opponent: null, opponent: null,
amount: game.total_points || 0, amount: game.total_points || 0,
points: game.player1_user_id === authStore.currentUser.id
? game.player1_points
: game.player2_points,
duration: `${Math.floor(game.total_time / 60)}m ${game.total_time % 60}s`, duration: `${Math.floor(game.total_time / 60)}m ${game.total_time % 60}s`,
began_at: game.began_at, began_at: game.began_at,
details: { details: {
-1
View File
@@ -92,7 +92,6 @@ export const useSocketStore = defineStore('websocket', () => {
socket.value.emit('play-card', { gameId, cardId }) socket.value.emit('play-card', { gameId, cardId })
} }
// --- NEW: Surrender Action ---
const surrender = (gameId) => { const surrender = (gameId) => {
if (!socket.value?.connected) return if (!socket.value?.connected) return
console.log('[Game] Surrendering:', gameId) console.log('[Game] Surrendering:', gameId)
+1
View File
@@ -96,6 +96,7 @@ const performGameEnd = (io, game, gameId, winnerId, loserId, reason) => {
action: "game_ended", action: "game_ended",
winnerId: parseInt(winnerId), winnerId: parseInt(winnerId),
loserId: parseInt(loserId), loserId: parseInt(loserId),
totalTime: game.startTime ? Math.floor((Date.now() - game.startTime) / 1000) : 0,
isDraw: false, isDraw: false,
player1_points: game.players[Object.keys(game.players)[0]]?.points || 0, player1_points: game.players[Object.keys(game.players)[0]]?.points || 0,
player2_points: game.players[Object.keys(game.players)[1]]?.points || 0, player2_points: game.players[Object.keys(game.players)[1]]?.points || 0,
+124 -115
View File
@@ -6,128 +6,137 @@ import gameEvents from "./events/game.js";
import matchEvents from "./events/match.js"; import matchEvents from "./events/match.js";
export const server = { export const server = {
io: null, io: null,
}; };
const API_URL = "http://localhost:8000/api/v1"; const API_URL = "http://localhost:8000/api/v1";
const disconnectTimers = new Map(); const disconnectTimers = new Map();
const DISCONNECT_TIMEOUT = 1 * 60 * 1000; // 1 minutes const RECONNECT_GRACE_PERIOD = 1 * 60 * 1000; // 1 minutes
const userId = (socket) => socket.user && socket.user.id;
export const serverStart = (port) => { export const serverStart = (port) => {
server.io = new Server(port, { server.io = new Server(port, {
cors: { cors: {
origin: "*", origin: "*",
},
});
server.io.use(async (socket, next) => {
let token = socket.handshake.auth.token;
if (!token) {
return next(new Error("Authentication error: No token provided"));
}
if (!token.startsWith("Bearer ")) {
token = "Bearer " + token;
}
try {
const response = await axios.get(`${API_URL}/users/me`, {
headers: {
Authorization: token,
Accept: "application/json"
}, },
});
const user = response.data.data || response.data;
if (!user || !user.id) {
return next(new Error("Authentication error: User data not found"));
}
socket.user = user;
socket.token = token;
socket.handshake.auth.token = token;
if (disconnectTimers.has(user.id)) {
console.log(`[Connection] User ${user.id} reconnected! Cancelling surrender timer.`);
clearTimeout(disconnectTimers.get(user.id));
disconnectTimers.delete(user.id);
}
addUser(socket.id, user);
next();
} catch (error) {
if (error.response) {
console.error(
"❌ Erro Laravel:",
error.response.status,
error.response.data
);
} else {
console.error("❌ Erro Rede:", error.message);
}
next(new Error("Authentication error: Invalid token"));
}
});
server.io.on("connection", (socket) => {
gameEvents(server.io, socket);
console.log(`[Connection] User ${userId} connected. Socket ID: ${socket.id}`);
matchEvents(server.io, socket);
handleConnectionEvents(server.io, socket);
socket.on("disconnect", () => {
// Remove from active socket list
removeUser(socket.id);
if (userId) {
console.log(`[Disconnect] User ${userId} disconnected. Waiting ${RECONNECT_GRACE_PERIOD/1000}s...`);
// -----------------------------------------------------------
// 2. START SURRENDER TIMER
// -----------------------------------------------------------
const timer = setTimeout(async () => {
console.log(`[Timeout] User ${userId} did not return. Forcing surrender.`);
// A. Find the Game ID this user is playing (You need a way to look this up)
// Ideally, your 'socket' object or a global map knows which gameID the user is in.
// For this example, let's assume you stored `socket.activeGameId` when they joined.
const gameId = socket.activeGameId;
if (gameId) {
try {
// B. Call Laravel API to resign on their behalf
// We use the token we saved during the handshake
await axios.post(`${API_URL}/games/${gameId}/resign`, {}, {
headers: {
Authorization: socket.handshake.auth.token,
Accept: "application/json"
}
});
// C. Notify the room (The API likely triggers a Pusher/Socket event,
// but we can also emit locally if needed)
server.io.to(gameId).emit("game_over", {
winner: "opponent",
reason: "disconnect"
});
console.log(`[Timeout] Successfully resigned game ${gameId} for user ${userId}`);
} catch (err) {
console.error(`[Timeout] Failed to resign game for user ${userId}:`, err.message);
}
}
disconnectTimers.delete(userId);
}, RECONNECT_GRACE_PERIOD);
disconnectTimers.set(userId, timer);
}
}); });
});
server.io.use(async (socket, next) => {
let token = socket.handshake.auth.token;
if (!token) {
return next(new Error("Authentication error: No token provided"));
}
if (!token.startsWith("Bearer ")) {
token = "Bearer " + token;
}
try {
const response = await axios.get(`${API_URL}/users/me`, {
headers: {
Authorization: token,
Accept: "application/json"
},
});
const user = response.data.data || response.data;
if (!user || !user.id) {
return next(new Error("Authentication error: User data not found"));
}
socket.user = user;
socket.token = token;
socket.handshake.auth.token = token;
if (disconnectTimers.has(user.id)) {
console.log(`[Connection] User ${user.id} reconnected! Cancelling surrender timer.`);
clearTimeout(disconnectTimers.get(user.id));
disconnectTimers.delete(user.id);
}
addUser(socket.id, user);
next();
} catch (error) {
if (error.response) {
console.error(
"❌ Erro Laravel:",
error.response.status,
error.response.data
);
} else {
console.error("❌ Erro Rede:", error.message);
}
next(new Error("Authentication error: Invalid token"));
}
});
server.io.on("connection", (socket) => {
gameEvents(server.io, socket);
matchEvents(server.io, socket);
handleConnectionEvents(server.io, socket);
socket.on("notify_disconnect", () => {
socket.isVoluntaryDisconnect = true;
});
socket.on("disconnect", () => {
// Remove from active socket list
removeUser(socket.id);
if (socket.isVoluntaryDisconnect) {
console.log(`[Disconnect] User ${userId} disconnected voluntarily. No surrender needed.`);
return;
}
if (userId) {
console.log(`[Disconnect] User ${userId} disconnected. Waiting ${RECONNECT_GRACE_PERIOD / 1000}s...`);
// -----------------------------------------------------------
// 2. START SURRENDER TIMER
// -----------------------------------------------------------
const timer = setTimeout(async () => {
console.log(`[Timeout] User ${userId} did not return. Forcing surrender.`);
// A. Find the Game ID this user is playing (You need a way to look this up)
// Ideally, your 'socket' object or a global map knows which gameID the user is in.
// For this example, let's assume you stored `socket.activeGameId` when they joined.
const gameId = socket.activeGameId;
if (gameId) {
try {
// B. Call Laravel API to resign on their behalf
// We use the token we saved during the handshake
await axios.post(`${API_URL}/games/${gameId}/resign`, {}, {
headers: {
Authorization: socket.handshake.auth.token,
Accept: "application/json"
}
});
// C. Notify the room (The API likely triggers a Pusher/Socket event,
// but we can also emit locally if needed)
server.io.to(gameId).emit("game_over", {
winner: "opponent",
reason: "disconnect"
});
console.log(`[Timeout] Successfully resigned game ${gameId} for user ${userId}`);
} catch (err) {
console.error(`[Timeout] Failed to resign game for user ${userId}:`, err.message);
}
}
disconnectTimers.delete(userId);
}, RECONNECT_GRACE_PERIOD);
disconnectTimers.set(userId, timer);
}
});
});
}; };
-2
View File
@@ -33,5 +33,3 @@ export const removeGame = (id) => {
} }
}; };
export { createGame, getGame, removeGame };