diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 22f068a..057306e 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -13,10 +13,10 @@ class GameController extends Controller public function index(Request $request) { $this->authorize('viewAny', Game::class); - + $user = Auth::user(); $query = Game::query()->with(["winner", "player1", "player2"]); - + if ($user->type !== 'A') { $query->where(function ($q) use ($user) { $q->where('player1_user_id', $user->id) @@ -52,7 +52,7 @@ class GameController extends Controller public function store(Request $request) { $this->authorize('create', Game::class); - + $validated = $request->validate([ "type" => "required|in:3,9", ]); @@ -74,7 +74,7 @@ class GameController extends Controller "type" => $validated["type"], "status" => "Pending", "player1_user_id" => $user->id, - "player2_user_id" => self::GHOST_ID, + "player2_user_id" => self::GHOST_ID, "winner_user_id" => self::GHOST_ID, "loser_user_id" => self::GHOST_ID, "began_at" => now(), @@ -89,38 +89,30 @@ class GameController extends Controller ], 201); } - public function join(Game $game) - { - $this->authorize('join', $game); - - $user = Auth::user(); - - if ($game->player1_user_id == $user->id) { - return response()->json([ - "message" => "You are the host. Waiting for opponent...", - "game" => $game - ], 200); - } - - if ($game->player2_user_id !== self::GHOST_ID) { - return response()->json(["message" => "Game is full"], 400); - } - - $game->update([ - "status" => "Playing", - "player2_user_id" => $user->id, - ]); - - return response()->json([ - "message" => "Joined successfully!", - "game" => $game, - ], 200); +public function join(Request $request, Game $game) +{ + // Prevent joining if already full or started + if ($game->status !== 'Pending' || $game->player2_user_id !== 1) { + return response()->json(['message' => 'Game is not available'], 400); } + // Prevent joining your own game + if ($game->player1_user_id === $request->user()->id) { + return response()->json(['message' => 'Cannot join your own game'], 400); + } + + $game->update([ + 'player2_user_id' => $request->user()->id, + 'status' => 'Playing', + ]); + + return response()->json($game); +} + public function show(Game $game) { $this->authorize('view', $game); - + $game->load(["player1", "player2", "winner"]); return response()->json($game); } @@ -128,7 +120,7 @@ class GameController extends Controller public function update(Request $request, Game $game) { $this->authorize('update', $game); - + if ($game->status == "Ended") { return response()->json(["message" => "Game already ended"], 400); } @@ -155,13 +147,13 @@ class GameController extends Controller public function resign(Game $game) { $this->authorize('resign', $game); - + $user = Auth::user(); - + $game->update([ 'status' => 'Ended', - 'winner_user_id' => $game->player1_user_id === $user->id - ? $game->player2_user_id + 'winner_user_id' => $game->player1_user_id === $user->id + ? $game->player2_user_id : $game->player1_user_id, 'loser_user_id' => $user->id, 'ended_at' => now(), diff --git a/api/app/Models/Game.php b/api/app/Models/Game.php index 13f2865..d37524f 100644 --- a/api/app/Models/Game.php +++ b/api/app/Models/Game.php @@ -29,11 +29,22 @@ class Game extends Model 'match_id' ]; + protected $casts = [ + 'began_at' => 'datetime', + 'ended_at' => 'datetime', + 'is_draw' => 'boolean', + ]; + public function winner() { return $this->belongsTo(User::class, "winner_user_id"); } + public function loser() + { + return $this->belongsTo(User::class, "loser_user_id"); + } + public function player1() { return $this->belongsTo(User::class, 'player1_user_id'); diff --git a/api/routes/api.php b/api/routes/api.php index 8ff3405..1809c01 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -47,24 +47,50 @@ Route::prefix('v1')->group(function () { Route::get('/{user}/transactions', [UserController::class, 'getTransactions']); }); - // Game Resources Route::prefix('games')->group(function () { - Route::apiResource('/', GameController::class)->parameters([ - '' => 'game', - ]); - Route::post('/{game}/join', [GameController::class, 'join']); + //Host a new multiplayer game - MUST come before resource routes + Route::post('host', function (\Illuminate\Http\Request $request) { + $request->validate([ + 'type' => 'required|in:3,9' + ]); + + $game = \App\Models\Game::create([ + 'type' => $request->type, + 'status' => 'Pending', + 'player1_user_id' => $request->user()->id, + 'player2_user_id' => 1, // Ghost player + 'began_at' => now(), + ]); + + return response()->json($game); + }); + + // List open games (available to join) + Route::get('open', function (\Illuminate\Http\Request $request) { + $type = $request->query('type', '9'); + + $games = \App\Models\Game::where('status', 'Pending') + ->where('player2_user_id', 1) // Only games waiting for a second player + ->where('type', $type) + ->with('player1') // Load player1 relationship + ->orderBy('began_at', 'asc') + ->paginate(10); + + return response()->json($games); + }); + + Route::get('/', [GameController::class, 'index']); + Route::get('/{game}', [GameController::class, 'show']); + Route::post('/', [GameController::class, 'store']); + Route::put('/{game}', [GameController::class, 'update']); + Route::delete('/{game}', [GameController::class, 'destroy']); Route::post('/{game}/resign', [GameController::class, 'resign']); + Route::post('/{game}/join', [GameController::class, 'join']); }); - // Match Resources Route::prefix('matches')->group(function () { - Route::apiResource('/', MatchController::class)->parameters([ - '' => 'match', - ]); - Route::post('/{match}/join', [ - MatchController::class, - 'join', - ]); + Route::apiResource('/', MatchController::class)->parameters(['' => 'match']); + Route::post('/{match}/join', [MatchController::class, 'join']); }); // Admin Routes diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 7e26030..da16d8c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -42,7 +42,8 @@ const logout = () => { onMounted(async () => { await authStore.restoreSession() - socketStore.handleConnection() + // REMOVED - The WebSocket for multiplayer is handled separately in the WebSocketStore + // socketStore.handleConnection() }) diff --git a/frontend/src/components/game/GameBoard.vue b/frontend/src/components/game/GameBoard.vue index 125c065..caf35a6 100644 --- a/frontend/src/components/game/GameBoard.vue +++ b/frontend/src/components/game/GameBoard.vue @@ -7,6 +7,8 @@ :player-score="playerScore" :opponent-score="opponentScore" :current-turn="currentTurn" + :player-name="playerName" + :opponent-name="opponentName" :round-number="1" /> @@ -62,16 +64,20 @@ const props = defineProps({ // REMOVIDO: playableCardIndices (já não precisamos dele) playerScore: { type: Number, default: 0 }, opponentScore: { type: Number, default: 0 }, + playerName: { type: String, default: 'You' }, + opponentName: { type: String, default: 'Bot' }, currentTurn: { type: String, default: 'player', validator: (value) => ['player', 'opponent'].includes(value), }, + isMyTurn: { type: Boolean, default: false }, }) const emit = defineEmits(['play-card']) const handleCardClick = (payload) => { + if (!props.isMyTurn) return emit('play-card', payload.card) } - \ No newline at end of file + diff --git a/frontend/src/components/game/PlayerHand.vue b/frontend/src/components/game/PlayerHand.vue index 40ea923..9451190 100644 --- a/frontend/src/components/game/PlayerHand.vue +++ b/frontend/src/components/game/PlayerHand.vue @@ -51,6 +51,10 @@ const props = defineProps({ default: 3, validator: (value) => [3, 9].includes(value), }, + isDisabled: { + type: Boolean, + default: false, + }, }) const emit = defineEmits(['card-clicked']) @@ -81,7 +85,7 @@ const overlapPercentage = computed(() => { return props.maxCards === 3 ? 0.7 : 0.85 }) -const cardWidth = 128 +const cardWidth = 128 const getCardStyle = (index) => { const totalCards = props.cards.length const overlapOffset = cardWidth * (1 - overlapPercentage.value) @@ -105,4 +109,4 @@ const getCardStyle = (index) => { zIndex: hoveredIndex.value === index ? 50 : index + 1, } } - \ No newline at end of file + diff --git a/frontend/src/components/game/ScoreDisplay.vue b/frontend/src/components/game/ScoreDisplay.vue index ecc14b7..6571a10 100644 --- a/frontend/src/components/game/ScoreDisplay.vue +++ b/frontend/src/components/game/ScoreDisplay.vue @@ -33,7 +33,8 @@ turnChanged && 'animate-pulse', ]" > - {{ currentTurn === 'player' ? 'Your Turn' : "Bot's Turn" }} + + {{ currentTurn === 'player' ? playerName + "'s Turn" : opponentName + "'s Turn" }} Round {{ roundNumber }} diff --git a/frontend/src/pages/game/MultiplayerGamePage.vue b/frontend/src/pages/game/MultiplayerGamePage.vue new file mode 100644 index 0000000..fe0d7cf --- /dev/null +++ b/frontend/src/pages/game/MultiplayerGamePage.vue @@ -0,0 +1,323 @@ + + + diff --git a/frontend/src/pages/home/HomePage.vue b/frontend/src/pages/home/HomePage.vue index 69655ab..554ef38 100644 --- a/frontend/src/pages/home/HomePage.vue +++ b/frontend/src/pages/home/HomePage.vue @@ -12,6 +12,7 @@ import { import { useAPIStore } from '@/stores/api' import { useRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' +import axios from 'axios' const API_BASE_URL = inject('apiBaseURL') const apiStore = useAPIStore() @@ -56,14 +57,19 @@ const getPendingGames = async (mode = selectedMode.value) => { if (gLoading.value) return; gLoading.value = true; - apiStore.gameQueryParameters.page = gPage.value++; - apiStore.gameQueryParameters.filters.sort_direction = "asc" - apiStore.gameQueryParameters.filters.type = mode - apiStore.gameQueryParameters.filters.status = 'Pending' try { - const response = await apiStore.getGames(); + const response = await axios.get(`${API_BASE_URL}/games/open`, { + params: { + type: mode, + page: gPage.value + } + }); + const newGames = response.data.data; openGames.value = [...openGames.value, ...newGames]; + gPage.value++; + } catch (error) { + console.error('Failed to fetch games:', error); } finally { gLoading.value = false; } @@ -170,6 +176,52 @@ onMounted(async () => { await nextTick(); setupGamesObserver(); }) + +const hostGame = async () => { + showHostModal.value = true + + try { + const response = await axios.post(`${API_BASE_URL}/games/host`, { + type: selectedMode.value + }) + + const gameId = response.data.id + + console.log('Game hosted:', gameId) + + router.push({ + name: 'multiplayer-game', + params: { id: gameId }, + query: { type: selectedMode.value } + }) + } catch (error) { + console.error('Failed to host game:', error) + console.error('Error details:', error.response?.data) + showHostModal.value = false + alert('Failed to host game: ' + (error.response?.data?.message || error.message)) + } +} + +const joinGame = async (game) => { + selectedGame.value = game + + try { + await axios.post(`${API_BASE_URL}/games/${game.id}/join`) + + console.log('Joined game:', game.id) + + router.push({ + name: 'multiplayer-game', + params: { id: game.id }, + query: { type: selectedMode.value } + }) + } catch (error) { + console.error('Failed to join game:', error) + console.error('Error details:', error.response?.data) + alert('Failed to join game: ' + (error.response?.data?.message || error.message)) + } +} +