Merge pull request 'feature/game-endpoints' (#62) from feature/game-endpoints into develop
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/62 Reviewed-by: FernandoJVideira <[email protected]> Reviewed-by: Edd <[email protected]>
This commit was merged in pull request #62.
This commit is contained in:
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',
|
||||
]);
|
||||
}
|
||||
|
||||
public function register(RegisterRequest $request)
|
||||
{
|
||||
if (User::where('email', $request->email)->exists()) {
|
||||
@@ -52,11 +53,11 @@ class AuthController extends Controller
|
||||
'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([
|
||||
|
||||
@@ -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,68 +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');
|
||||
return response()->json($query->paginate(15));
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$games = $query->paginate($perPage);
|
||||
public function store(Request $request)
|
||||
{
|
||||
$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([
|
||||
'data' => $games->items(),
|
||||
'meta' => [
|
||||
'current_page' => $games->currentPage(),
|
||||
'last_page' => $games->lastPage(),
|
||||
'per_page' => $games->perPage(),
|
||||
'total' => $games->total()
|
||||
]
|
||||
]);
|
||||
'message' => 'Multiplayer game room created',
|
||||
'game' => $game
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StoreGameRequest $request)
|
||||
public function join(Game $game)
|
||||
{
|
||||
$game = Game::create($request->validated());
|
||||
return response()->json($game, 201);
|
||||
$user = Auth::user();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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 false;
|
||||
// Só utilizadores autenticados podem criar jogos
|
||||
return Auth::check();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +24,19 @@ class StoreGameRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
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 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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
Generated
+15
-14
@@ -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"
|
||||
}
|
||||
|
||||
+9
-1
@@ -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,11 @@ Route::middleware('auth:sanctum')->group(function () {
|
||||
|
||||
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::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'
|
||||
|
||||
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