From d7a6862dcb8fb404050bfb4146ca340800a833e9 Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Thu, 11 Dec 2025 20:42:30 +0000 Subject: [PATCH 01/98] Added necessary stuff for authentication --- api/app/Http/Controllers/GameController.php | 11 +- api/app/Http/Requests/StoreGameRequest.php | 28 +++ api/app/Models/Game.php | 15 ++ api/package-lock.json | 2 +- api/package.json | 2 +- frontend/src/stores/game.js | 255 ++++++++++++++++++++ 6 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 api/app/Http/Requests/StoreGameRequest.php create mode 100644 frontend/src/stores/game.js diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 1a38512..9f1f95c 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Models\Game; use Illuminate\Http\Request; +use App\Http\Requests\StoreGameRequest; class GameController extends Controller { @@ -56,9 +57,10 @@ class GameController extends Controller /** * 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) { - // + return $game; } /** @@ -74,7 +76,8 @@ class GameController extends Controller */ public function update(Request $request, Game $game) { - // + $game->update($request->validated()); + return response()->json($game); } /** diff --git a/api/app/Http/Requests/StoreGameRequest.php b/api/app/Http/Requests/StoreGameRequest.php new file mode 100644 index 0000000..5874a1f --- /dev/null +++ b/api/app/Http/Requests/StoreGameRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/api/app/Models/Game.php b/api/app/Models/Game.php index 44fe639..0336e66 100644 --- a/api/app/Models/Game.php +++ b/api/app/Models/Game.php @@ -3,9 +3,24 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use App\Http\Requests\StoreGameRequest; + 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() { return $this->belongsTo(User::class, "winner_user_id"); diff --git a/api/package-lock.json b/api/package-lock.json index 58c569a..15f979c 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -6,7 +6,7 @@ "": { "devDependencies": { "@tailwindcss/vite": "^4.0.0", - "axios": "^1.11.0", + "axios": "^1.13.2", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", diff --git a/api/package.json b/api/package.json index 7686b29..6ab44ff 100644 --- a/api/package.json +++ b/api/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", - "axios": "^1.11.0", + "axios": "^1.13.2", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", diff --git a/frontend/src/stores/game.js b/frontend/src/stores/game.js new file mode 100644 index 0000000..ae36656 --- /dev/null +++ b/frontend/src/stores/game.js @@ -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, + } +}) -- 2.54.0 From 69705ce61ca6140eaf4a79be8c51993b33b8e1cc Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Thu, 11 Dec 2025 21:02:02 +0000 Subject: [PATCH 02/98] feat: websocket init --- websockets/.env.example | 1 + websockets/events/connection.js | 45 +++++++++++++++++++++++++++++++-- websockets/state/connection.js | 19 ++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 websockets/.env.example diff --git a/websockets/.env.example b/websockets/.env.example new file mode 100644 index 0000000..8f55cbd --- /dev/null +++ b/websockets/.env.example @@ -0,0 +1 @@ +PORT= \ No newline at end of file diff --git a/websockets/events/connection.js b/websockets/events/connection.js index 468d210..db4fe03 100644 --- a/websockets/events/connection.js +++ b/websockets/events/connection.js @@ -1,5 +1,46 @@ +import { addUser, removeUser } from "../state/connection"; + +const HEARTBEAT_INTERVAL = 30000; // 30 seconds +const HEARTBEAT_TIMEOUT = 10000; // 10 seconds + export const handleConnectionEvents = (io, socket) => { - socket.on("echo", (msg) => { - socket.emit("echo", msg); + let heartbeatInterval; + let heartbeatTimeout; + const startHeartbeat = () => { + heartbeatInterval = setInterval(() => { + socket.emit("heartbeat"); + + heartbeatTimeout = setTimeout(() => { + console.log( + `[Heartbeat] No response from ${socket.id}, disconnecting...` + ); + socket.disconnect(true); + }, HEARTBEAT_TIMEOUT); + }, HEARTBEAT_INTERVAL); + }; + + socket.on("heartbeat_ack", () => { + clearTimeout(heartbeatTimeout); + console.log(`[Heartbeat] Received pong from ${socket.id}`); + }); + + socket.on("join", (user) => { + addUser(socket, user); + console.log(`[Connection] User ${user.name} has joined the server`); + console.log(`[Connection] ${getUserCount()} users online`); + }); + + socket.on("leave", () => { + const user = removeUser(socket.id); + if (user) { + console.log(`[Connection] User ${user.name} has left the server`); + console.log(`[Connection] ${getUserCount()} users online`); + } + }); + + socket.on("disconnect", () => { + console.log("Connection Lost:", socket.id); + removeUser(socket.id); + console.log(`[Connection] ${getUserCount()} users online`); }); }; diff --git a/websockets/state/connection.js b/websockets/state/connection.js index e69de29..5f3b49b 100644 --- a/websockets/state/connection.js +++ b/websockets/state/connection.js @@ -0,0 +1,19 @@ +const users = new Map(); + +export const addUser = (socket, user) => { + users.set(socket.id, user); +}; + +export const removeUser = (socketID) => { + const userToDelete = { ...users.get(socketID) }; + users.delete(socketID); + return userToDelete; +}; + +export const getUser = (socketID) => { + return users.get(socketID); +}; + +export const getUserCount = () => { + return users.size; +}; -- 2.54.0 From daa72ce4c66a6337492ff3764e5c3ed462facbf8 Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Thu, 11 Dec 2025 21:13:56 +0000 Subject: [PATCH 03/98] Added the register endpoint --- api/app/Http/Controllers/AuthController.php | 27 +++++++++++++++++ api/app/Http/Requests/RegisterRequest.php | 32 +++++++++++++++++++++ api/app/Models/User.php | 2 ++ api/routes/api.php | 2 ++ 4 files changed, 63 insertions(+) create mode 100644 api/app/Http/Requests/RegisterRequest.php diff --git a/api/app/Http/Controllers/AuthController.php b/api/app/Http/Controllers/AuthController.php index 5064994..9466d28 100644 --- a/api/app/Http/Controllers/AuthController.php +++ b/api/app/Http/Controllers/AuthController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; +use App\Models\User; +use App\Http\Requests\RegisterRequest; class AuthController extends Controller { @@ -38,4 +40,29 @@ class AuthController extends Controller 'message' => 'Logged out successfully', ]); } + public function register(RegisterRequest $request) + { + if (User::where('email', $request->email)->exists()) { + return response()->json(['message' => 'Email already registered'], 409); + } + + $user = User::create([ + 'email' => $request->email, + 'nickname' => $request->nickname, + 'name' => $request->name, + 'password' => bcrypt($request->password), + 'photo_avatar_filename' => $request->photo, + ]); + + $user->coins_balance = 10; + $user->save(); + + $token = $user->createToken('auth-token')->plainTextToken; + + return response()->json([ + 'message' => 'User registered successfully', + 'user' => $user, + 'token' => $token, + ], 201); + } } diff --git a/api/app/Http/Requests/RegisterRequest.php b/api/app/Http/Requests/RegisterRequest.php new file mode 100644 index 0000000..5c0b21f --- /dev/null +++ b/api/app/Http/Requests/RegisterRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => 'required|email|unique:users,email', + 'nickname' => 'required|string|max:255', + 'name' => 'required|string|max:255', + 'password' => 'required|string|min:3', + 'photo' => 'nullable|string', + ]; + } +} diff --git a/api/app/Models/User.php b/api/app/Models/User.php index a6ab88e..10a6d7b 100644 --- a/api/app/Models/User.php +++ b/api/app/Models/User.php @@ -22,6 +22,8 @@ class User extends Authenticatable 'name', 'email', 'password', + 'nickname', + 'photo_avatar_filename', ]; /** diff --git a/api/routes/api.php b/api/routes/api.php index 738544f..a32a56c 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -16,4 +16,6 @@ Route::middleware('auth:sanctum')->group(function () { Route::post('logout', [AuthController::class, 'logout']); }); +Route::post('/register', [AuthController::class, 'register']); + Route::apiResource('games', GameController::class); \ No newline at end of file -- 2.54.0 From 511e645d7ede1874b73d1bc0e1c2089f0567a751 Mon Sep 17 00:00:00 2001 From: Edd Date: Thu, 11 Dec 2025 21:14:25 +0000 Subject: [PATCH 04/98] feature implement login part 1 --- .gitignore | 2 ++ deployment/kubernetes-vue.yml | 2 +- frontend/index.html | 2 +- frontend/src/main.js | 2 +- frontend/src/pages/login/LoginPage.vue | 6 +++--- frontend/src/stores/auth.js | 24 +++++++++++++++++++++++- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 6db0291..6638d86 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,5 @@ Desktop.ini # If you have any local secrets or files not to track, add them here: # /path/to/some/file + +package-lock.json \ No newline at end of file diff --git a/deployment/kubernetes-vue.yml b/deployment/kubernetes-vue.yml index c283891..c67613f 100644 --- a/deployment/kubernetes-vue.yml +++ b/deployment/kubernetes-vue.yml @@ -17,7 +17,7 @@ spec: priorityClassName: low-priority containers: - name: web - image: registry-172.22.21.115.sslip.io/dad-group-x/web:v1.0.0 + image: registry-172.22.21.115.sslip.io/dad-group-46/web:v1.0.1 resources: requests: memory: "64Mi" diff --git a/frontend/index.html b/frontend/index.html index 2b46161..5db9ac1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ - Vite App + Bisca
diff --git a/frontend/src/main.js b/frontend/src/main.js index 1bb9d08..27177c5 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -13,7 +13,7 @@ console.log('[main.js] ws connection', wsConnection) const app = createApp(App) -app.provide('socket', io(wsConnection)) +//app.provide('socket', io(wsConnection)) app.provide('serverBaseURL', `http://${apiDomain}`) app.provide('apiBaseURL', `http://${apiDomain}/api`) diff --git a/frontend/src/pages/login/LoginPage.vue b/frontend/src/pages/login/LoginPage.vue index 7809e98..7ddc2e5 100644 --- a/frontend/src/pages/login/LoginPage.vue +++ b/frontend/src/pages/login/LoginPage.vue @@ -1,5 +1,5 @@ - - diff --git a/frontend/src/stores/api.js b/frontend/src/stores/api.js index 3920e25..8ff3ba8 100644 --- a/frontend/src/stores/api.js +++ b/frontend/src/stores/api.js @@ -1,3 +1,4 @@ +// src/stores/api.js import { defineStore } from 'pinia' import axios from 'axios' import { inject, ref } from 'vue' @@ -21,7 +22,10 @@ export const useAPIStore = defineStore('api', () => { const response = await axios.post(`${API_BASE_URL}/login`, credentials) token.value = response.data.token axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}` + + return response } + const postLogout = async () => { await axios.post(`${API_BASE_URL}/logout`) token.value = undefined @@ -53,11 +57,19 @@ export const useAPIStore = defineStore('api', () => { return axios.get(`${API_BASE_URL}/games?${queryParams}`) } + const initToken = (storedToken) => { + if (storedToken) { + token.value = storedToken + axios.defaults.headers.common['Authorization'] = `Bearer ${storedToken}` + } + } + return { postLogin, postLogout, getAuthUser, getGames, gameQueryParameters, + initToken, } }) diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index c5bef31..1cec1b3 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -1,6 +1,8 @@ +// src/stores/auth.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useAPIStore } from './api' +import axios from 'axios' export const useAuthStore = defineStore('auth', () => { const apiStore = useAPIStore() @@ -20,8 +22,10 @@ export const useAuthStore = defineStore('auth', () => { token.value = jwtToken if (jwtToken) { localStorage.setItem('token', jwtToken) + axios.defaults.headers.common['Authorization'] = `Bearer ${jwtToken}` } else { localStorage.removeItem('token') + delete axios.defaults.headers.common['Authorization'] } } @@ -30,20 +34,57 @@ export const useAuthStore = defineStore('auth', () => { } const login = async (credentials) => { - await apiStore.postLogin(credentials) - const response = await apiStore.getAuthUser() - const jwtToken = response - const user = response.data + try { + // postLogin already sets the token and axios headers, and returns the response + const loginResponse = await apiStore.postLogin(credentials) - setToken(jwtToken) - currentUser.value = response.data - return response.data + // Extract the token from the response + const jwtToken = loginResponse.data.token + + // Store token in auth store (for localStorage) + setToken(jwtToken) + + // Use the user data from login response (it's already there!) + currentUser.value = loginResponse.data.user + + return currentUser.value + } catch (error) { + // Clear everything on login failure + setToken(null) + currentUser.value = undefined + throw error + } } const logout = async () => { - await apiStore.postLogout() - currentUser.value = undefined - setToken(null) + try { + await apiStore.postLogout() + } catch (error) { + console.error('Logout API call failed:', error) + } finally { + currentUser.value = undefined + setToken(null) + } + } + + // Initialize auth from localStorage on app start + const initAuth = async () => { + if (token.value) { + axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}` + apiStore.initToken(token.value) + + // Try to fetch current user if we don't have it + if (!currentUser.value) { + try { + const userResponse = await apiStore.getAuthUser() + currentUser.value = userResponse.data + } catch (error) { + console.error('Failed to restore user session:', error) + // Clear invalid token + setToken(null) + } + } + } } return { @@ -55,5 +96,6 @@ export const useAuthStore = defineStore('auth', () => { getToken, login, logout, + initAuth, } }) diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index 2a678ea..ea53013 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -1,39 +1,58 @@ import { defineStore } from 'pinia' import axios from 'axios' +import { useAuthStore } from './auth' export const useUserStore = defineStore('user', { state: () => ({ user: null, - token: localStorage.getItem('token') || null, + loading: false, + error: null }), + getters: { + currentUser: (state) => state.user + }, + actions: { - async fetchUser() { - if (!this.token) return + async fetchUserProfile() { + const authStore = useAuthStore() + const token = authStore.getToken() + + if (!token) { + this.error = 'No authentication token found' + throw new Error('No authentication token found') + } + + this.loading = true + this.error = null try { const response = await axios.get('/api/users/me', { headers: { - Authorization: `Bearer ${this.token}` + Authorization: `Bearer ${token}`, + Accept: 'application/json' } }) this.user = response.data + return response.data } catch (error) { - console.error("Failed to fetch user") + this.error = error.response?.data?.message || 'Failed to fetch user profile' + + if (error.response?.status === 401) { + authStore.logout() + } + + throw error + } finally { + this.loading = false } }, - setToken(token) { - this.token = token - localStorage.setItem('token', token) - }, - - logout() { - this.token = null + clearUser() { this.user = null - localStorage.removeItem('token') + this.error = null + this.loading = false } } }) - -- 2.54.0 From 2b161e2099fc2151378bcab1e1fa292aa6d9a725 Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Sat, 20 Dec 2025 17:03:18 +0000 Subject: [PATCH 11/98] feat: gameboard ui --- .gitignore | 1 + frontend/public/cards/c1.png | Bin 0 -> 44136 bytes frontend/public/cards/c11.png | Bin 0 -> 260725 bytes frontend/public/cards/c12.png | Bin 0 -> 243233 bytes frontend/public/cards/c13.png | Bin 0 -> 279570 bytes frontend/public/cards/c2.png | Bin 0 -> 20136 bytes frontend/public/cards/c3.png | Bin 0 -> 24757 bytes frontend/public/cards/c4.png | Bin 0 -> 24681 bytes frontend/public/cards/c5.png | Bin 0 -> 29804 bytes frontend/public/cards/c6.png | Bin 0 -> 33457 bytes frontend/public/cards/c7.png | Bin 0 -> 34474 bytes frontend/public/cards/e1.png | Bin 0 -> 42763 bytes frontend/public/cards/e11.png | Bin 0 -> 247507 bytes frontend/public/cards/e12.png | Bin 0 -> 205872 bytes frontend/public/cards/e13.png | Bin 0 -> 201544 bytes frontend/public/cards/e2.png | Bin 0 -> 21863 bytes frontend/public/cards/e3.png | Bin 0 -> 26922 bytes frontend/public/cards/e4.png | Bin 0 -> 26409 bytes frontend/public/cards/e5.png | Bin 0 -> 32219 bytes frontend/public/cards/e6.png | Bin 0 -> 36854 bytes frontend/public/cards/e7.png | Bin 0 -> 37167 bytes frontend/public/cards/o1.png | Bin 0 -> 36810 bytes frontend/public/cards/o11.png | Bin 0 -> 215482 bytes frontend/public/cards/o12.png | Bin 0 -> 195223 bytes frontend/public/cards/o13.png | Bin 0 -> 266252 bytes frontend/public/cards/o2.png | Bin 0 -> 19815 bytes frontend/public/cards/o3.png | Bin 0 -> 24257 bytes frontend/public/cards/o4.png | Bin 0 -> 24212 bytes frontend/public/cards/o5.png | Bin 0 -> 29319 bytes frontend/public/cards/o6.png | Bin 0 -> 33692 bytes frontend/public/cards/o7.png | Bin 0 -> 34168 bytes frontend/public/cards/p1.png | Bin 0 -> 42481 bytes frontend/public/cards/p11.png | Bin 0 -> 208118 bytes frontend/public/cards/p12.png | Bin 0 -> 285495 bytes frontend/public/cards/p13.png | Bin 0 -> 263133 bytes frontend/public/cards/p2.png | Bin 0 -> 23590 bytes frontend/public/cards/p3.png | Bin 0 -> 29161 bytes frontend/public/cards/p4.png | Bin 0 -> 28623 bytes frontend/public/cards/p5.png | Bin 0 -> 34857 bytes frontend/public/cards/p6.png | Bin 0 -> 39972 bytes frontend/public/cards/p7.png | Bin 0 -> 41174 bytes frontend/public/cards/semFace.png | Bin 0 -> 319670 bytes frontend/src/App.vue | 26 +- frontend/src/components/game/DeckArea.vue | 113 +++++ frontend/src/components/game/FlyingCard.vue | 76 +++ frontend/src/components/game/GameBoard.vue | 116 +++++ frontend/src/components/game/GameCard.vue | 39 ++ frontend/src/components/game/GameOver.vue | 295 ++++++++++++ frontend/src/components/game/PlayArea.vue | 100 ++++ frontend/src/components/game/PlayerHand.vue | 98 ++++ frontend/src/components/game/ScoreDisplay.vue | 176 +++++++ frontend/src/pages/TestAllAnimations.vue | 342 ++++++++++++++ frontend/src/pages/TestAnimations.vue | 252 ++++++++++ frontend/src/pages/TestDealing.vue | 275 +++++++++++ frontend/src/pages/TestGameBoard.vue | 444 ++++++++++++++++++ frontend/src/router/index.js | 20 + websockets/.env.example | 3 +- websockets/events/game.js | 19 + websockets/state/game.js | 186 ++++++++ 59 files changed, 2564 insertions(+), 17 deletions(-) create mode 100644 frontend/public/cards/c1.png create mode 100644 frontend/public/cards/c11.png create mode 100644 frontend/public/cards/c12.png create mode 100644 frontend/public/cards/c13.png create mode 100644 frontend/public/cards/c2.png create mode 100644 frontend/public/cards/c3.png create mode 100644 frontend/public/cards/c4.png create mode 100644 frontend/public/cards/c5.png create mode 100644 frontend/public/cards/c6.png create mode 100644 frontend/public/cards/c7.png create mode 100644 frontend/public/cards/e1.png create mode 100644 frontend/public/cards/e11.png create mode 100644 frontend/public/cards/e12.png create mode 100644 frontend/public/cards/e13.png create mode 100644 frontend/public/cards/e2.png create mode 100644 frontend/public/cards/e3.png create mode 100644 frontend/public/cards/e4.png create mode 100644 frontend/public/cards/e5.png create mode 100644 frontend/public/cards/e6.png create mode 100644 frontend/public/cards/e7.png create mode 100644 frontend/public/cards/o1.png create mode 100644 frontend/public/cards/o11.png create mode 100644 frontend/public/cards/o12.png create mode 100644 frontend/public/cards/o13.png create mode 100644 frontend/public/cards/o2.png create mode 100644 frontend/public/cards/o3.png create mode 100644 frontend/public/cards/o4.png create mode 100644 frontend/public/cards/o5.png create mode 100644 frontend/public/cards/o6.png create mode 100644 frontend/public/cards/o7.png create mode 100644 frontend/public/cards/p1.png create mode 100644 frontend/public/cards/p11.png create mode 100644 frontend/public/cards/p12.png create mode 100644 frontend/public/cards/p13.png create mode 100644 frontend/public/cards/p2.png create mode 100644 frontend/public/cards/p3.png create mode 100644 frontend/public/cards/p4.png create mode 100644 frontend/public/cards/p5.png create mode 100644 frontend/public/cards/p6.png create mode 100644 frontend/public/cards/p7.png create mode 100644 frontend/public/cards/semFace.png create mode 100644 frontend/src/components/game/DeckArea.vue create mode 100644 frontend/src/components/game/FlyingCard.vue create mode 100644 frontend/src/components/game/GameBoard.vue create mode 100644 frontend/src/components/game/GameCard.vue create mode 100644 frontend/src/components/game/GameOver.vue create mode 100644 frontend/src/components/game/PlayArea.vue create mode 100644 frontend/src/components/game/PlayerHand.vue create mode 100644 frontend/src/components/game/ScoreDisplay.vue create mode 100644 frontend/src/pages/TestAllAnimations.vue create mode 100644 frontend/src/pages/TestAnimations.vue create mode 100644 frontend/src/pages/TestDealing.vue create mode 100644 frontend/src/pages/TestGameBoard.vue create mode 100644 websockets/events/game.js create mode 100644 websockets/state/game.js diff --git a/.gitignore b/.gitignore index 6db0291..9c38ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ frontend/.env.*.local # Websockets: NodeJs (in websockets) # ------------------------- /websockets/node_modules/ +/websockets/.env # ------------------------- # Other useful ignores # ------------------------- diff --git a/frontend/public/cards/c1.png b/frontend/public/cards/c1.png new file mode 100644 index 0000000000000000000000000000000000000000..b42212405c18f1f72a451f91b354c145089c103f GIT binary patch literal 44136 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