Merge branch 'develop' into feature/websocket-init
This commit is contained in:
@@ -4,6 +4,7 @@ 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;
|
||||||
|
|
||||||
class GameController extends Controller
|
class GameController extends Controller
|
||||||
{
|
{
|
||||||
@@ -56,9 +57,10 @@ class GameController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Store a newly created resource in storage.
|
* Store a newly created resource in storage.
|
||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(StoreGameRequest $request)
|
||||||
{
|
{
|
||||||
//
|
$game = Game::create($request->validated());
|
||||||
|
return response()->json($game, 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +68,7 @@ class GameController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(Game $game)
|
public function show(Game $game)
|
||||||
{
|
{
|
||||||
//
|
return $game;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +76,8 @@ class GameController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function update(Request $request, Game $game)
|
public function update(Request $request, Game $game)
|
||||||
{
|
{
|
||||||
//
|
$game->update($request->validated());
|
||||||
|
return response()->json($game);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreGameRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,24 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Http\Requests\StoreGameRequest;
|
||||||
|
|
||||||
|
|
||||||
class Game extends Model
|
class Game extends Model
|
||||||
{
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'player1_id',
|
||||||
|
'player2_id',
|
||||||
|
'winner_id',
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'began_at',
|
||||||
|
'ended_at',
|
||||||
|
'total_time',
|
||||||
|
'player1_moves',
|
||||||
|
'player2_moves',
|
||||||
|
];
|
||||||
|
|
||||||
public function winner()
|
public function winner()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, "winner_user_id");
|
return $this->belongsTo(User::class, "winner_user_id");
|
||||||
|
|||||||
Generated
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.13.2",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.13.2",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
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