feature/game-endpoints #62

Merged
Edd merged 3 commits from feature/game-endpoints into develop 2025-12-14 00:56:05 +00:00
11 changed files with 327 additions and 328 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"postman.settings.dotenv-detection-notification-visibility": false
}
+5 -4
View File
@@ -40,23 +40,24 @@ 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()) {
return response()->json(['message' => 'Email already registered'], 409); return response()->json(['message' => 'Email already registered'], 409);
} }
$user = User::create([ $user = User::create([
'email' => $request->email, 'email' => $request->email,
'nickname' => $request->nickname, 'nickname' => $request->nickname,
'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([
+95 -51
View File
@@ -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)) { public function store(Request $request)
$query->orderBy($sortField, $sortDirection === 'asc' ? 'asc' : 'desc'); {
$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);
} }
// Pagination $game = Game::create([
$perPage = $request->input('per_page', 15); 'type' => $validated['type'],
$games = $query->paginate($perPage); '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);
} }
} }
+150
View File
@@ -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);
}
}
+17 -3
View File
@@ -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
View File
@@ -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');
}
} }
+7 -1
View File
@@ -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
View File
@@ -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",
+15 -14
View File
@@ -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"
} }
+10 -2
View File
@@ -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']);
Route::apiResource('games', GameController::class); # 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']);
});
-247
View File
@@ -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,
}
}) })