From cf8f93d98400a8ae0960e7c7bed5811c5dbcd1bf Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Sun, 14 Dec 2025 00:26:41 +0000 Subject: [PATCH] Added endpoits to create, join and update the game state, playability should work now --- api/app/Http/Controllers/AuthController.php | 9 +- api/app/Http/Controllers/GameController.php | 201 +++++++++----------- api/app/Http/Controllers/UserController.php | 141 +++++++++++++- api/app/Http/Requests/StoreGameRequest.php | 24 ++- api/app/Models/Game.php | 29 ++- api/app/Models/User.php | 8 +- api/routes/api.php | 3 +- 7 files changed, 283 insertions(+), 132 deletions(-) diff --git a/api/app/Http/Controllers/AuthController.php b/api/app/Http/Controllers/AuthController.php index 9466d28..f60cabd 100644 --- a/api/app/Http/Controllers/AuthController.php +++ b/api/app/Http/Controllers/AuthController.php @@ -40,23 +40,24 @@ class AuthController extends Controller 'message' => 'Logged out successfully', ]); } + public function register(RegisterRequest $request) { if (User::where('email', $request->email)->exists()) { return response()->json(['message' => 'Email already registered'], 409); } - + $user = User::create([ 'email' => $request->email, 'nickname' => $request->nickname, 'name' => $request->name, 'password' => bcrypt($request->password), 'photo_avatar_filename' => $request->photo, + 'type' => 'P', + 'blocked' => false, + 'coins_balance' => 10, ]); - $user->coins_balance = 10; - $user->save(); - $token = $user->createToken('auth-token')->plainTextToken; return response()->json([ diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 77401b4..e731b2a 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -5,15 +5,13 @@ namespace App\Http\Controllers; use App\Models\Game; use Illuminate\Http\Request; use App\Http\Requests\StoreGameRequest; +use Illuminate\Support\Facades\Auth; class GameController extends Controller { - /** - * Display a listing of the resource. - */ public function index(Request $request) { - $query = Game::query()->with(['winner']); + $query = Game::query()->with(['winner', 'player1', 'player2']); if ($request->has('type') && in_array($request->type, ['3', '9'])) { $query->where('type', $request->type); @@ -23,137 +21,114 @@ class GameController extends Controller $query->where('status', $request->status); } - // Sorting - $sortField = $request->input('sort_by', 'began_at'); - $sortDirection = $request->input('sort_direction', 'desc'); + $query->orderBy('began_at', 'desc'); - $allowedSortFields = [ - 'began_at', - 'ended_at', - 'total_time', - 'type', - 'status' - ]; - - if (in_array($sortField, $allowedSortFields)) { - $query->orderBy($sortField, $sortDirection === 'asc' ? 'asc' : 'desc'); - } - - // Pagination - $perPage = $request->input('per_page', 15); - $games = $query->paginate($perPage); - - return response()->json([ - 'data' => $games->items(), - 'meta' => [ - 'current_page' => $games->currentPage(), - 'last_page' => $games->lastPage(), - 'per_page' => $games->perPage(), - 'total' => $games->total() - ] - ]); + return response()->json($query->paginate(15)); } - /** - * Store a newly created resource in storage. - */ - public function store(StoreGameRequest $request) + public function store(Request $request) { - $validated = $request->validated(); - $user = $request->user(); + $validated = $request->validate([ + 'type' => 'required|in:3,9', + ]); - $type = $validated['variant'] === 'bisca-3' ? '3' : '9'; - $initialState = $this->initializeGameState($type); + $user = Auth::user(); + + $activeGame = Game::where(function ($query) use ($user) { + $query->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + }) + ->whereIn('status', ['Pending', 'Playing']) + ->exists(); + + if ($activeGame) { + return response()->json(['message' => 'You already have an active game.'], 400); + } $game = Game::create([ - 'type' => $type, - 'status' => 'in_progress', + 'type' => $validated['type'], + 'status' => 'Pending', 'player1_user_id' => $user->id, - 'player2_user_id' => $validated['mode'] === 'multiplayer' - ? $validated['opponent_id'] - : 1, // REPLACE 1 with your actual Bot User ID + 'player2_user_id' => $user->id, + 'winner_user_id' => $user->id, + 'loser_user_id' => $user->id, 'began_at' => now(), - 'custom' => json_encode($initialState), 'player1_points' => 0, 'player2_points' => 0, + 'custom' => null ]); - + return response()->json([ - 'message' => 'Game created successfully', - 'game_id' => $game->id, - 'game_state' => $initialState, - ], 201); + 'message' => 'Multiplayer game room created', + 'game' => $game + ], 201); } - private function initializeGameState($type) + public function join(Game $game) { - $handSize = $type === 'S' ? 3 : 9; - - $suits = ['H', 'D', 'C', 'S']; - $ranks = ['2', '3', '4', '5', '6', '7', 'J', 'Q', 'K', 'A']; - - $values = [ - '2' => 0, '3' => 0, '4' => 0, '5' => 0, '6' => 0, - '7' => 10, 'J' => 3, 'Q' => 2, 'K' => 4, 'A' => 11 - ]; - - $deck = []; - foreach ($suits as $suit) { - foreach ($ranks as $rank) { - $deck[] = ['suit' => $suit, 'rank' => $rank, 'value' => $values[$rank]]; - } - } - - // Shuffle and deal - shuffle($deck); - $player1Hand = array_splice($deck, 0, $handSize); - $player2Hand = array_splice($deck, 0, $handSize); - - // Trump is the suit of the last card in the remaining stock - $trumpCard = null; - if (count($deck) > 0) { - $trumpCard = $deck[count($deck) - 1]; // Bottom card - } + $user = Auth::user(); - return [ - 'player1_hand' => $player1Hand, - 'player2_hand' => $player2Hand, - 'stock' => $deck, - 'trump' => $trumpCard ? $trumpCard['suit'] : null, - 'current_trick' => [], - 'current_turn' => 'player1', - 'score' => [ - 'player1' => 0, - 'player2' => 0 - ], - 'captured' => [ - 'player1' => [], - 'player2' => [] - ] - ]; + if ($game->player1_user_id == $user->id) { + return response()->json([ + 'message' => 'Waiting for opponent...', + 'game' => $game + ], 200); + } + + if ($game->status !== 'Pending') { + return response()->json(['message' => 'Game is full or already started'], 400); + } + + $activeGame = Game::where(function ($query) use ($user) { + $query->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + }) + ->whereIn('status', ['Pending', 'Playing']) + ->exists(); + + if ($activeGame) { + return response()->json(['message' => 'You are already in another active game.'], 400); + } + + $game->update([ + 'status' => 'Playing', + 'player2_user_id' => $user->id, + ]); + + return response()->json([ + 'message' => 'Joined successfully! Game starts now.', + 'game' => $game + ], 200); } - /** - * Display the specified resource. - */ + public function show(Game $game) { - return $game; - } - - /** - * Update the specified resource in storage. - */ - public function update(Request $request, Game $game) - { - $game->update($request->validated()); + $game->load(['player1', 'player2', 'winner']); return response()->json($game); } - /** - * Remove the specified resource from storage. - */ - public function destroy(Game $game) + public function update(Request $request, Game $game) { - // + if ($game->status == 'Ended') { + return response()->json(['message' => 'Game already ended'], 400); + } + + $data = $request->validate([ + 'status' => 'in:Playing,Ended,Interrupted', + 'winner_user_id' => 'exists:users,id', + 'loser_user_id' => 'exists:users,id', + 'player1_points' => 'integer', + 'player2_points' => 'integer', + 'is_draw' => 'boolean', + 'total_time' => 'numeric' + ]); + + if (isset($data['status']) && $data['status'] === 'Ended') { + $data['ended_at'] = now(); + } + + $game->update($data); + + return response()->json($game); } -} +} \ No newline at end of file diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php index 3359954..d1ee12e 100644 --- a/api/app/Http/Controllers/UserController.php +++ b/api/app/Http/Controllers/UserController.php @@ -2,10 +2,149 @@ namespace App\Http\Controllers; +use App\Models\User; +use App\Models\Game; use Illuminate\Http\Request; -use App\Http\Requests\StoreGameRequest; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rule; class UserController extends Controller { + /** + * GET /api/users + * Lista todos os utilizadores (Apenas para Admins) + */ + public function index(Request $request) + { + if ($request->user()->type !== 'A') { + return response()->json(['message' => 'Unauthorized'], 403); + } + $query = User::query(); + + // Filtros úteis para o Backoffice + if ($request->has('type')) { + $query->where('type', $request->type); + } + if ($request->has('search')) { + $query->where('name', 'like', '%' . $request->search . '%') + ->orWhere('email', 'like', '%' . $request->search . '%') + ->orWhere('nickname', 'like', '%' . $request->search . '%'); + } + + return response()->json($query->paginate(15)); + } + + /** + * GET /api/users/me + * Retorna o perfil do utilizador autenticado atual. + * Usado pelo VueJS para saber quem está logado. + */ + public function me(Request $request) + { + return response()->json($request->user()); + } + + /** + * GET /api/users/{user} + * Mostra um perfil específico. + */ + public function show(User $user) + { + $currentUser = Auth::user(); + if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { + return response()->json([ + 'id' => $user->id, + 'name' => $user->name, + 'nickname' => $user->nickname, + 'photo_avatar_filename' => $user->photo_avatar_filename, + 'type' => $user->type + ]); + } + + return response()->json($user); + } + + /** + * PUT/PATCH /api/users/{user} + * Atualizar perfil (Nome, Nickname, Password, Foto) + */ + public function update(Request $request, User $user) + { + $currentUser = Auth::user(); + + if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { + return response()->json(['message' => 'Unauthorized'], 403); + } + + $validated = $request->validate([ + 'name' => 'sometimes|string|max:255', + 'nickname' => ['sometimes', 'string', 'max:20', Rule::unique('users')->ignore($user->id)], + 'email' => ['sometimes', 'email', Rule::unique('users')->ignore($user->id)], + 'password' => 'sometimes|string|min:3', + 'blocked' => 'sometimes|boolean', + ]); + + if (isset($validated['blocked']) && $currentUser->type !== 'A') { + return response()->json(['message' => 'Only admins can block users'], 403); + } + + // Lógica de Upload de Foto (Exemplo Básico) + // if ($request->hasFile('photo_avatar')) { + // $path = $request->file('photo_avatar')->store('avatars', 'public'); + // $validated['photo_avatar_filename'] = basename($path); + // } + + $user->update($validated); + + return response()->json($user); + } + + /** + * DELETE /api/users/{user} + * Apagar conta (Soft Delete) + */ + public function destroy(User $user) + { + $currentUser = Auth::user(); + + if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { + return response()->json(['message' => 'Unauthorized'], 403); + } + + if ($currentUser->id === $user->id && $currentUser->type === 'A') { + if ($user->type === 'A') { + return response()->json(['message' => 'Admins cannot delete their own account'], 403); + } + } + + $user->delete(); + + return response()->json(['message' => 'User deleted successfully']); + } + + /** + * GET /api/users/{user}/matches + * Histórico de Jogos Multiplayer do Utilizador + */ + public function getMatches(Request $request, User $user) + { + $currentUser = Auth::user(); + + if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { + return response()->json(['message' => 'Unauthorized'], 403); + } + + $matches = Game::query() + ->where(function($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + }) + ->with(['winner', 'player1', 'player2']) + ->orderBy('began_at', 'desc') + ->paginate(10); + + return response()->json($matches); + } } \ No newline at end of file diff --git a/api/app/Http/Requests/StoreGameRequest.php b/api/app/Http/Requests/StoreGameRequest.php index 4aff420..35452e0 100644 --- a/api/app/Http/Requests/StoreGameRequest.php +++ b/api/app/Http/Requests/StoreGameRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Auth; class StoreGameRequest extends FormRequest { @@ -11,7 +12,8 @@ class StoreGameRequest extends FormRequest */ public function authorize(): bool { - return true; + // Só utilizadores autenticados podem criar jogos + return Auth::check(); } /** @@ -22,11 +24,19 @@ class StoreGameRequest extends FormRequest public function rules(): array { return [ - 'variant' => 'required|in:bisca-3,bisca-9', - 'mode' => 'required|in:single-player,multiplayer', - 'opponent_id' => $this->mode === 'multiplayer' - ? 'required|exists:users,id' - : 'nullable', + // Agora validamos diretamente '3' ou '9' para bater certo com a BD + 'type' => 'required|in:3,9', ]; } -} + + /** + * Mensagens personalizadas (Opcional) + */ + public function messages(): array + { + return [ + 'type.required' => 'You must choose a game type (Bisca de 3 or 9).', + 'type.in' => 'Invalid game type. Choose 3 or 9.', + ]; + } +} \ No newline at end of file diff --git a/api/app/Models/Game.php b/api/app/Models/Game.php index 0336e66..dd65b7f 100644 --- a/api/app/Models/Game.php +++ b/api/app/Models/Game.php @@ -4,25 +4,44 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use App\Http\Requests\StoreGameRequest; +use Illuminate\Database\Eloquent\Factories\HasFactory; class Game extends Model { + use HasFactory; + + public $timestamps = false; + protected $fillable = [ - 'player1_id', - 'player2_id', - 'winner_id', 'type', 'status', + 'player1_user_id', + 'player2_user_id', + 'winner_user_id', + 'loser_user_id', 'began_at', 'ended_at', 'total_time', - 'player1_moves', - 'player2_moves', + 'player1_points', + 'player2_points', + 'custom', + 'is_draw', + 'match_id' ]; public function winner() { return $this->belongsTo(User::class, "winner_user_id"); } + + public function player1() + { + return $this->belongsTo(User::class, 'player1_user_id'); + } + + public function player2() + { + return $this->belongsTo(User::class, 'player2_user_id'); + } } diff --git a/api/app/Models/User.php b/api/app/Models/User.php index 10a6d7b..ae8f8d0 100644 --- a/api/app/Models/User.php +++ b/api/app/Models/User.php @@ -4,6 +4,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -11,7 +12,7 @@ use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, HasApiTokens; + use HasFactory, Notifiable, HasApiTokens, SoftDeletes; /** * The attributes that are mass assignable. @@ -24,6 +25,10 @@ class User extends Authenticatable 'password', 'nickname', 'photo_avatar_filename', + 'type', + 'blocked', + 'coins_balance', + 'custom', ]; /** @@ -46,6 +51,7 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'blocked' => 'boolean', ]; } } diff --git a/api/routes/api.php b/api/routes/api.php index fd10844..2b9fdcd 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -22,7 +22,8 @@ Route::post('/register', [AuthController::class, 'register']); # Game Routes - Protected Route::middleware('auth:sanctum')->group(function () { Route::apiResource('games', GameController::class); + Route::post('games', [GameController::class, 'store']); Route::post('/games/{game}/resign', [GameController::class, 'resign']); - Route::post('/games/{game}/play-card', [GameController::class, 'playCard']); Route::get('/users/{user}/matches', [UserController::class, 'getMatches']); + Route::post('games/{game}/join', [GameController::class, 'join']); }); \ No newline at end of file