Version 1.0 #102
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"postman.settings.dotenv-detection-notification-visibility": false
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ class AuthController extends Controller
|
|||||||
'message' => 'Logged out successfully',
|
'message' => 'Logged out successfully',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function register(RegisterRequest $request)
|
public function register(RegisterRequest $request)
|
||||||
{
|
{
|
||||||
if (User::where('email', $request->email)->exists()) {
|
if (User::where('email', $request->email)->exists()) {
|
||||||
@@ -52,11 +53,11 @@ class AuthController extends Controller
|
|||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'password' => bcrypt($request->password),
|
'password' => bcrypt($request->password),
|
||||||
'photo_avatar_filename' => $request->photo,
|
'photo_avatar_filename' => $request->photo,
|
||||||
|
'type' => 'P',
|
||||||
|
'blocked' => false,
|
||||||
|
'coins_balance' => 10,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->coins_balance = 10;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$token = $user->createToken('auth-token')->plainTextToken;
|
$token = $user->createToken('auth-token')->plainTextToken;
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Game;
|
use App\Models\Game;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Http\Requests\StoreGameRequest;
|
use App\Http\Requests\StoreGameRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class GameController extends Controller
|
class GameController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index(Request $request)
|
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'])) {
|
if ($request->has('type') && in_array($request->type, ['3', '9'])) {
|
||||||
$query->where('type', $request->type);
|
$query->where('type', $request->type);
|
||||||
@@ -23,68 +21,114 @@ class GameController extends Controller
|
|||||||
$query->where('status', $request->status);
|
$query->where('status', $request->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
$query->orderBy('began_at', 'desc');
|
||||||
$sortField = $request->input('sort_by', 'began_at');
|
|
||||||
$sortDirection = $request->input('sort_direction', 'desc');
|
|
||||||
|
|
||||||
$allowedSortFields = [
|
return response()->json($query->paginate(15));
|
||||||
'began_at',
|
|
||||||
'ended_at',
|
|
||||||
'total_time',
|
|
||||||
'type',
|
|
||||||
'status'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (in_array($sortField, $allowedSortFields)) {
|
|
||||||
$query->orderBy($sortField, $sortDirection === 'asc' ? 'asc' : 'desc');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination
|
public function store(Request $request)
|
||||||
$perPage = $request->input('per_page', 15);
|
{
|
||||||
$games = $query->paginate($perPage);
|
$validated = $request->validate([
|
||||||
|
'type' => 'required|in:3,9',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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' => $validated['type'],
|
||||||
|
'status' => 'Pending',
|
||||||
|
'player1_user_id' => $user->id,
|
||||||
|
'player2_user_id' => $user->id,
|
||||||
|
'winner_user_id' => $user->id,
|
||||||
|
'loser_user_id' => $user->id,
|
||||||
|
'began_at' => now(),
|
||||||
|
'player1_points' => 0,
|
||||||
|
'player2_points' => 0,
|
||||||
|
'custom' => null
|
||||||
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => $games->items(),
|
'message' => 'Multiplayer game room created',
|
||||||
'meta' => [
|
'game' => $game
|
||||||
'current_page' => $games->currentPage(),
|
], 201);
|
||||||
'last_page' => $games->lastPage(),
|
|
||||||
'per_page' => $games->perPage(),
|
|
||||||
'total' => $games->total()
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function join(Game $game)
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(StoreGameRequest $request)
|
|
||||||
{
|
{
|
||||||
$game = Game::create($request->validated());
|
$user = Auth::user();
|
||||||
return response()->json($game, 201);
|
|
||||||
|
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)
|
public function show(Game $game)
|
||||||
{
|
{
|
||||||
return $game;
|
$game->load(['player1', 'player2', 'winner']);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, Game $game)
|
|
||||||
{
|
|
||||||
$game->update($request->validated());
|
|
||||||
return response()->json($game);
|
return response()->json($game);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function update(Request $request, Game $game)
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Game;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class StoreGameRequest extends FormRequest
|
class StoreGameRequest extends FormRequest
|
||||||
{
|
{
|
||||||
@@ -11,7 +12,8 @@ class StoreGameRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return false;
|
// Só utilizadores autenticados podem criar jogos
|
||||||
|
return Auth::check();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +24,19 @@ class StoreGameRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
//
|
// 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
@@ -4,25 +4,44 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use App\Http\Requests\StoreGameRequest;
|
use App\Http\Requests\StoreGameRequest;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
|
||||||
class Game extends Model
|
class Game extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'player1_id',
|
|
||||||
'player2_id',
|
|
||||||
'winner_id',
|
|
||||||
'type',
|
'type',
|
||||||
'status',
|
'status',
|
||||||
|
'player1_user_id',
|
||||||
|
'player2_user_id',
|
||||||
|
'winner_user_id',
|
||||||
|
'loser_user_id',
|
||||||
'began_at',
|
'began_at',
|
||||||
'ended_at',
|
'ended_at',
|
||||||
'total_time',
|
'total_time',
|
||||||
'player1_moves',
|
'player1_points',
|
||||||
'player2_moves',
|
'player2_points',
|
||||||
|
'custom',
|
||||||
|
'is_draw',
|
||||||
|
'match_id'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function winner()
|
public function winner()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, "winner_user_id");
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
@@ -11,7 +12,7 @@ use Laravel\Sanctum\HasApiTokens;
|
|||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable, HasApiTokens;
|
use HasFactory, Notifiable, HasApiTokens, SoftDeletes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -24,6 +25,10 @@ class User extends Authenticatable
|
|||||||
'password',
|
'password',
|
||||||
'nickname',
|
'nickname',
|
||||||
'photo_avatar_filename',
|
'photo_avatar_filename',
|
||||||
|
'type',
|
||||||
|
'blocked',
|
||||||
|
'coins_balance',
|
||||||
|
'custom',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +51,7 @@ class User extends Authenticatable
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'blocked' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/pint": "^1.26",
|
||||||
"laravel/sail": "^1.48",
|
"laravel/sail": "^1.48",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
|
|||||||
Generated
+15
-14
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6ad8f0aa2267878e2374cd4ba4042946",
|
"content-hash": "353b4e6bf34bd12906db0059bb3a9207",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -6575,16 +6575,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/pint",
|
"name": "laravel/pint",
|
||||||
"version": "v1.25.1",
|
"version": "v1.26.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/pint.git",
|
"url": "https://github.com/laravel/pint.git",
|
||||||
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
|
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||||
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6595,13 +6595,13 @@
|
|||||||
"php": "^8.2.0"
|
"php": "^8.2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"friendsofphp/php-cs-fixer": "^3.87.2",
|
"friendsofphp/php-cs-fixer": "^3.90.0",
|
||||||
"illuminate/view": "^11.46.0",
|
"illuminate/view": "^12.40.1",
|
||||||
"larastan/larastan": "^3.7.1",
|
"larastan/larastan": "^3.8.0",
|
||||||
"laravel-zero/framework": "^11.45.0",
|
"laravel-zero/framework": "^12.0.4",
|
||||||
"mockery/mockery": "^1.6.12",
|
"mockery/mockery": "^1.6.12",
|
||||||
"nunomaduro/termwind": "^2.3.1",
|
"nunomaduro/termwind": "^2.3.3",
|
||||||
"pestphp/pest": "^2.36.0"
|
"pestphp/pest": "^3.8.4"
|
||||||
},
|
},
|
||||||
"bin": [
|
"bin": [
|
||||||
"builds/pint"
|
"builds/pint"
|
||||||
@@ -6627,6 +6627,7 @@
|
|||||||
"description": "An opinionated code formatter for PHP.",
|
"description": "An opinionated code formatter for PHP.",
|
||||||
"homepage": "https://laravel.com",
|
"homepage": "https://laravel.com",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"dev",
|
||||||
"format",
|
"format",
|
||||||
"formatter",
|
"formatter",
|
||||||
"lint",
|
"lint",
|
||||||
@@ -6637,7 +6638,7 @@
|
|||||||
"issues": "https://github.com/laravel/pint/issues",
|
"issues": "https://github.com/laravel/pint/issues",
|
||||||
"source": "https://github.com/laravel/pint"
|
"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",
|
"name": "laravel/sail",
|
||||||
@@ -9380,12 +9381,12 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": {},
|
"stability-flags": [],
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.2"
|
"php": "^8.2"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\AuthController;
|
use App\Http\Controllers\AuthController;
|
||||||
use App\Http\Controllers\GameController;
|
use App\Http\Controllers\GameController;
|
||||||
|
use App\Http\Controllers\UserController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
|
||||||
|
# Authentication Routes
|
||||||
Route::post('/login', [AuthController::class, 'login']);
|
Route::post('/login', [AuthController::class, 'login']);
|
||||||
|
|
||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
@@ -18,4 +19,11 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
|
|
||||||
Route::post('/register', [AuthController::class, 'register']);
|
Route::post('/register', [AuthController::class, 'register']);
|
||||||
|
|
||||||
|
# Game Routes - Protected
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
Route::apiResource('games', GameController::class);
|
Route::apiResource('games', GameController::class);
|
||||||
|
Route::post('games', [GameController::class, 'store']);
|
||||||
|
Route::post('/games/{game}/resign', [GameController::class, 'resign']);
|
||||||
|
Route::get('/users/{user}/matches', [UserController::class, 'getMatches']);
|
||||||
|
Route::post('games/{game}/join', [GameController::class, 'join']);
|
||||||
|
});
|
||||||
@@ -4,252 +4,5 @@ import { toast } from 'vue-sonner'
|
|||||||
import { useApiStore } from './api'
|
import { useApiStore } from './api'
|
||||||
|
|
||||||
export const useGameStore = defineStore('game', () => {
|
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 = ['♥', '♦', '♣', '♠']
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user