From 7686aff5467afbc9940f495b6ba79f402902edba Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Fri, 12 Dec 2025 00:05:37 +0000 Subject: [PATCH 1/3] Added initial game logic --- api/app/Http/Controllers/GameController.php | 73 ++++++++++++++++++++- api/app/Http/Controllers/UserController.php | 11 ++++ api/app/Http/Requests/StoreGameRequest.php | 8 ++- api/composer.json | 2 +- api/composer.lock | 29 ++++---- api/routes/api.php | 11 +++- frontend/src/stores/game.js | 2 +- 7 files changed, 114 insertions(+), 22 deletions(-) create mode 100644 api/app/Http/Controllers/UserController.php diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 9f1f95c..77401b4 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -59,10 +59,79 @@ class GameController extends Controller */ public function store(StoreGameRequest $request) { - $game = Game::create($request->validated()); - return response()->json($game, 201); + $validated = $request->validated(); + $user = $request->user(); + + $type = $validated['variant'] === 'bisca-3' ? '3' : '9'; + $initialState = $this->initializeGameState($type); + + $game = Game::create([ + 'type' => $type, + 'status' => 'in_progress', + 'player1_user_id' => $user->id, + 'player2_user_id' => $validated['mode'] === 'multiplayer' + ? $validated['opponent_id'] + : 1, // REPLACE 1 with your actual Bot User ID + 'began_at' => now(), + 'custom' => json_encode($initialState), + 'player1_points' => 0, + 'player2_points' => 0, + ]); + + return response()->json([ + 'message' => 'Game created successfully', + 'game_id' => $game->id, + 'game_state' => $initialState, + ], 201); } + private function initializeGameState($type) + { + $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 + } + + 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' => [] + ] + ]; + } /** * Display the specified resource. */ diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..3359954 --- /dev/null +++ b/api/app/Http/Controllers/UserController.php @@ -0,0 +1,11 @@ + 'required|in:bisca-3,bisca-9', + 'mode' => 'required|in:single-player,multiplayer', + 'opponent_id' => $this->mode === 'multiplayer' + ? 'required|exists:users,id' + : 'nullable', ]; } } diff --git a/api/composer.json b/api/composer.json index 5b5a9c0..bd2f68f 100644 --- a/api/composer.json +++ b/api/composer.json @@ -17,7 +17,7 @@ "require-dev": { "fakerphp/faker": "^1.23", "laravel/pail": "^1.2.2", - "laravel/pint": "^1.24", + "laravel/pint": "^1.26", "laravel/sail": "^1.48", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", diff --git a/api/composer.lock b/api/composer.lock index f127d15..3780ad4 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6ad8f0aa2267878e2374cd4ba4042946", + "content-hash": "353b4e6bf34bd12906db0059bb3a9207", "packages": [ { "name": "brick/math", @@ -6575,16 +6575,16 @@ }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "shasum": "" }, "require": { @@ -6595,13 +6595,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.90.0", + "illuminate/view": "^12.40.1", + "larastan/larastan": "^3.8.0", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" @@ -6627,6 +6627,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -6637,7 +6638,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2025-11-25T21:15:52+00:00" }, { "name": "laravel/sail", @@ -9380,12 +9381,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/api/routes/api.php b/api/routes/api.php index a32a56c..fd10844 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -2,11 +2,12 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\GameController; +use App\Http\Controllers\UserController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; - +# Authentication Routes Route::post('/login', [AuthController::class, 'login']); Route::middleware('auth:sanctum')->group(function () { @@ -18,4 +19,10 @@ Route::middleware('auth:sanctum')->group(function () { Route::post('/register', [AuthController::class, 'register']); -Route::apiResource('games', GameController::class); \ No newline at end of file +# Game Routes - Protected +Route::middleware('auth:sanctum')->group(function () { + Route::apiResource('games', GameController::class); + 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']); +}); \ No newline at end of file diff --git a/frontend/src/stores/game.js b/frontend/src/stores/game.js index ae36656..61a2429 100644 --- a/frontend/src/stores/game.js +++ b/frontend/src/stores/game.js @@ -38,7 +38,7 @@ export const useGameStore = defineStore('game', () => { // Initialize deck const initializeDeck = () => { - const suits = ['♥', '♦', '♣', '♠'] + const suits = ['H', 'D', 'C', 'S'] const ranks = ['2', '3', '4', '5', '6', '7', 'J', 'Q', 'K', 'A'] const values = { '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, 'J': 2, 'Q': 3, 'K': 4, 'A': 11 } 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 2/3] 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 From 5473a4cc6962c6b7ec9117ee5ff9f4859b048f78 Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Sun, 14 Dec 2025 00:28:58 +0000 Subject: [PATCH 3/3] removed crap i did, sorry frontends --- .vscode/settings.json | 3 + frontend/src/stores/game.js | 247 ------------------------------------ 2 files changed, 3 insertions(+), 247 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6b0e5ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "postman.settings.dotenv-detection-notification-visibility": false +} \ No newline at end of file diff --git a/frontend/src/stores/game.js b/frontend/src/stores/game.js index 61a2429..f979538 100644 --- a/frontend/src/stores/game.js +++ b/frontend/src/stores/game.js @@ -4,252 +4,5 @@ import { toast } from 'vue-sonner' import { useApiStore } from './api' export const useGameStore = defineStore('game', () => { - const apiStore = useApiStore() - // Game state - const game = ref({ - players: { - human: { hand: [], score: 0, wonCards: [] }, - bot: { hand: [], score: 0, wonCards: [] }, - }, - deck: [], - trump: null, - currentTrick: [], - lastWinner: null, - gameOver: false, - winner: null, - }) - - const gameStarted = ref(false) - const isHumanTurn = ref(true) - const selectedCard = ref(null) - const beganAt = ref(null) - const endedAt = ref(null) - const moves = ref([]) - - // Computed - const humanScore = computed(() => { - return game.value.players.human.wonCards.reduce((sum, card) => sum + card.value, 0) - }) - - const botScore = computed(() => { - return game.value.players.bot.wonCards.reduce((sum, card) => sum + card.value, 0) - }) - - // Initialize deck - const initializeDeck = () => { - const suits = ['H', 'D', 'C', 'S'] - const ranks = ['2', '3', '4', '5', '6', '7', 'J', 'Q', 'K', 'A'] - const values = { '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, 'J': 2, 'Q': 3, 'K': 4, 'A': 11 } - - const deck = [] - for (const suit of suits) { - for (const rank of ranks) { - deck.push({ rank, suit, value: values[rank] }) - } - } - - return deck.sort(() => Math.random() - 0.5) - } - - // Start new game - const startGame = () => { - game.value.deck = initializeDeck() - game.value.trump = game.value.deck[game.value.deck.length - 1] - game.value.players.human.hand = game.value.deck.splice(0, 3) - game.value.players.bot.hand = game.value.deck.splice(0, 3) - game.value.currentTrick = [] - game.value.gameOver = false - game.value.winner = null - game.value.players.human.score = 0 - game.value.players.bot.score = 0 - game.value.players.human.wonCards = [] - game.value.players.bot.wonCards = [] - gameStarted.value = true - isHumanTurn.value = true - beganAt.value = new Date() - endedAt.value = null - moves.value = [] - } - - // Play card - const playCard = async (cardIndex) => { - if (!isHumanTurn.value || game.value.gameOver) return - - const card = game.value.players.human.hand[cardIndex] - game.value.currentTrick.push({ player: 'human', card }) - game.value.players.human.hand.splice(cardIndex, 1) - selectedCard.value = null - - // Track move - moves.value.push({ - player: 'human', - card, - timestamp: new Date(), - }) - - isHumanTurn.value = false - - // Bot plays - setTimeout(() => { - botPlay() - }, 1000) - } - - // Bot play logic - const botPlay = () => { - const botHand = game.value.players.bot.hand - let cardToPlay - - if (game.value.currentTrick.length === 0) { - cardToPlay = botHand[0] - } else { - const leadCard = game.value.currentTrick[0].card - const canWin = botHand.some((c) => canBeat(c, leadCard)) - - if (canWin) { - cardToPlay = botHand.filter((c) => canBeat(c, leadCard)).sort((a, b) => a.value - b.value)[0] - } else { - cardToPlay = botHand.sort((a, b) => a.value - b.value)[0] - } - } - - game.value.currentTrick.push({ player: 'bot', card: cardToPlay }) - game.value.players.bot.hand.splice(game.value.players.bot.hand.indexOf(cardToPlay), 1) - - // Track move - moves.value.push({ - player: 'bot', - card: cardToPlay, - timestamp: new Date(), - }) - - // Resolve trick - setTimeout(() => { - resolveTrick() - }, 500) - } - - // Check if card can beat - const canBeat = (card, leadCard) => { - if (card.suit === leadCard.suit) { - return rankValue(card.rank) > rankValue(leadCard.rank) - } - return card.suit === game.value.trump.suit - } - - // Get rank value for comparison - const rankValue = (rank) => { - const values = { '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, 'J': 11, 'Q': 12, 'K': 13, 'A': 14 } - return values[rank] || 0 - } - - // Resolve trick - const resolveTrick = () => { - const trick = game.value.currentTrick - const card1 = trick[0].card - const card2 = trick[1].card - - let winner - if (card2.suit === card1.suit) { - winner = rankValue(card2.rank) > rankValue(card1.rank) ? 'bot' : 'human' - } else if (card2.suit === game.value.trump.suit) { - winner = 'bot' - } else { - winner = 'human' - } - - game.value.lastWinner = winner - game.value.players[winner].wonCards.push(card1, card2) - - // Refill hands - refillHands(winner) - - game.value.currentTrick = [] - - // Check if game is over - if (game.value.players.human.hand.length === 0 && game.value.deck.length === 0) { - endGame() - } else { - isHumanTurn.value = winner === 'human' - if (winner === 'bot') { - setTimeout(() => botPlay(), 1000) - } - } - } - - // Refill hands - const refillHands = (winner) => { - const order = winner === 'human' ? ['human', 'bot'] : ['bot', 'human'] - - for (const player of order) { - while (game.value.players[player].hand.length < 3 && game.value.deck.length > 0) { - game.value.players[player].hand.push(game.value.deck.pop()) - } - } - } - - // End game - const endGame = () => { - game.value.gameOver = true - game.value.winner = humanScore.value > botScore.value ? 'human' : 'bot' - endedAt.value = new Date() - } - - // Save game to API - const saveGame = async () => { - const gameData = { - type: 'S', // Standalone game - status: 'E', // Ended - player1_moves: moves.value, - began_at: beganAt.value, - ended_at: endedAt.value, - total_time: Math.ceil((endedAt.value - beganAt.value) / 1000), - } - - return toast.promise(apiStore.postGame(gameData), { - loading: 'Sending data to API...', - success: () => { - return '[API] Game saved successfully' - }, - error: (data) => `[API] Error saving game - ${data?.response?.data?.message}`, - }) - } - - // Reset game - const reset = () => { - gameStarted.value = false - selectedCard.value = null - beganAt.value = null - endedAt.value = null - moves.value = [] - game.value = { - players: { - human: { hand: [], score: 0, wonCards: [] }, - bot: { hand: [], score: 0, wonCards: [] }, - }, - deck: [], - trump: null, - currentTrick: [], - lastWinner: null, - gameOver: false, - winner: null, - } - } - - return { - game, - gameStarted, - isHumanTurn, - selectedCard, - beganAt, - endedAt, - moves, - humanScore, - botScore, - startGame, - playCard, - saveGame, - reset, - } })