Version 1.0 #102

Merged
FernandoJVideira merged 163 commits from develop into master 2026-01-03 14:49:00 +00:00
7 changed files with 283 additions and 132 deletions
Showing only changes of commit cf8f93d984 - Show all commits
+5 -4
View File
@@ -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([
+88 -113
View File
@@ -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);
}
}
}
+140 -1
View File
@@ -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);
}
}
+17 -7
View File
@@ -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.',
];
}
}
+24 -5
View File
@@ -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');
}
}
+7 -1
View File
@@ -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',
];
}
}
+2 -1
View File
@@ -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']);
});