From e84dd993afd91d3cb5132397c64d9b2b71486190 Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Fri, 2 Jan 2026 00:39:39 +0000 Subject: [PATCH 1/2] SyntaxSquad/DADProject#15 feat: Initial Idea --- api/app/Models/Game.php | 11 + api/routes/api.php | 38 ++- frontend/src/App.vue | 3 +- frontend/src/components/game/GameBoard.vue | 4 +- frontend/src/components/game/PlayerHand.vue | 8 +- .../src/pages/game/MultiplayerGamePage.vue | 304 ++++++++++++++++++ frontend/src/pages/home/HomePage.vue | 53 ++- frontend/src/router/index.js | 7 + frontend/src/stores/socket.js | 144 ++++++++- websockets/events/connection.js | 2 +- websockets/state/connection.js | 2 +- 11 files changed, 539 insertions(+), 37 deletions(-) create mode 100644 frontend/src/pages/game/MultiplayerGamePage.vue 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 8e60833..7ece2e8 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -46,24 +46,36 @@ 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 + 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); + }); + + 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 562dc55..8450f33 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -44,7 +44,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..2807944 100644 --- a/frontend/src/components/game/GameBoard.vue +++ b/frontend/src/components/game/GameBoard.vue @@ -67,11 +67,13 @@ const props = defineProps({ 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/pages/game/MultiplayerGamePage.vue b/frontend/src/pages/game/MultiplayerGamePage.vue new file mode 100644 index 0000000..6cb66ed --- /dev/null +++ b/frontend/src/pages/game/MultiplayerGamePage.vue @@ -0,0 +1,304 @@ + + + diff --git a/frontend/src/pages/home/HomePage.vue b/frontend/src/pages/home/HomePage.vue index 1ab5a49..6eda4c8 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() @@ -159,6 +160,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)) + } +} +