This commit is contained in:
Jose Conde 2024-07-07 23:27:14 +02:00
parent 9a6f430e4d
commit d999bb3479
15 changed files with 213 additions and 76 deletions

View File

@ -1,5 +1,10 @@
<script setup lang="ts">
import { inject } from 'vue'
import { RouterView } from 'vue-router'
import type { AuthenticationService } from './services/AuthenticationService'
const auth: AuthenticationService = inject<AuthenticationService>('auth') as AuthenticationService
auth.fromStorage()
</script>
<template>

View File

@ -32,6 +32,10 @@ export interface MatchSessionState {
maxPlayers: number
numPlayers: number
waitingSeconds: number
scoreboard: Map<string, number>
matchWinner: PlayerDto | null
matchInProgress: boolean
playersReady: number
}
export interface GameState {
@ -48,9 +52,6 @@ export interface GameState {
tileSelectionPhase: boolean
boardFreeEnds: number[]
lastMove: Movement
scoreboard: Map<string, number>
matchWinner: PlayerDto | null
matchInProgress: boolean
}
export interface Movement {
@ -79,3 +80,8 @@ export interface Dimension {
x: number
y: number
}
export interface SocketEvent {
event: string
data: any
}

View File

@ -21,7 +21,7 @@ const game = new Game(
{
width: 1200,
height: 650,
boardScale: 0.8,
boardScale: 0.7,
handScale: 1
},
emit,
@ -52,7 +52,6 @@ onMounted(async () => {
() => gameStore.gameState,
(value: GameState | undefined) => {
if (value === undefined) return
logger.debug('gameState-------------------------------------- :>> ', value)
game.board?.setState(value, playerState.value?.id ?? '')
}
)
@ -61,6 +60,7 @@ onMounted(async () => {
() => gameStore.sessionState,
(value: MatchSessionState | undefined) => {
if (value === undefined) return
// logger.debug('gameSessionState-------------------------------------- :>> ', value)
}
)

View File

@ -17,7 +17,7 @@ export class Game {
boardScale: 1,
handScale: 1,
width: 1200,
height: 650
height: 800
},
private emit: any,
private socketService: SocketIoClientService,
@ -27,7 +27,7 @@ export class Game {
async setup(): Promise<HTMLCanvasElement> {
const width = 1200
const height = 650
const height = 800
await this.app.init({ width, height })
return this.app.canvas
@ -77,7 +77,7 @@ export class Game {
this.hand.on('nextClick', async () => {
await this.socketService.sendMessageWithAck('playerReady', {
user: this.playerId,
userId: this.playerId,
sessionId: this.sessionId
})
})

View File

@ -48,12 +48,16 @@ export class Hand extends EventEmitter {
gameFinished() {
this.logger.debug('gameFinished')
this.tiles = []
this.container.removeChildren()
this.initialized = false
this.buttonNext = this.createButton(
'NEXT',
{ x: this.width / 2 - 25, y: this.height / 2, width: 50, height: 20 },
'nextClick'
() => {
this.container.removeChildren()
this.container.removeChild(this.buttonNext)
this.emit('nextClick')
}
)
}

View File

@ -11,6 +11,7 @@ import router from './router'
import { SocketIoClientService } from '@/services/SocketIoClientService'
import { LoggingService } from '@/services/LoggingService'
import { AuthenticationService } from './services/AuthenticationService'
import { GameService } from './services/GameService'
const app = createApp(App)
@ -20,5 +21,6 @@ app.use(router)
app.provide('socket', new SocketIoClientService('http://localhost:3000'))
app.provide('logger', new LoggingService())
app.provide('auth', new AuthenticationService())
app.provide('game', new GameService())
app.mount('#app')

View File

@ -1,6 +1,6 @@
import { useGameStore } from '@/stores/game'
import { wait } from '@/common/helpers'
import type { MatchSessionState } from '@/common/interfaces'
import type { MatchSessionState, SocketEvent } from '@/common/interfaces'
import { storeToRefs } from 'pinia'
import { useEventBusStore } from '@/stores/eventBus'
@ -8,7 +8,45 @@ export class SocketIoEventManager {
gameStore: any = useGameStore()
eventBus = useEventBusStore()
handleSessionStateEvent(data: MatchSessionState) {
handleGameEvent(gameEvent: SocketEvent) {
const { event, data } = gameEvent
switch (event) {
case 'session-created':
this.updateSessionState(data)
break
case 'game-finished':
default:
this.eventBus.publish(event, data)
break
}
}
handleGameEventAck(gameEvent: SocketEvent) {
const { event, data } = gameEvent
try {
switch (event) {
case 'update-match-session-state':
this.updateSessionState(data)
break
case 'update-game-state':
this.updateGameState(data)
break
case 'update-player-state':
this.updatePlayerState(data)
break
case 'ask-client-for-move':
return this.handleCanMakeMoveEvent(data)
default:
this.eventBus.publish(event, data)
break
}
return { status: 'ok' }
} catch (error) {
return { status: 'error', error }
}
}
private updateSessionState(data: MatchSessionState) {
const { updateSessionState } = this.gameStore
updateSessionState(data)
return {
@ -16,7 +54,7 @@ export class SocketIoEventManager {
}
}
handleGameStateEvent(data: any) {
private updateGameState(data: any) {
const { updateGameState } = this.gameStore
updateGameState(data)
return {
@ -24,7 +62,7 @@ export class SocketIoEventManager {
}
}
handlePlayerStateEvent(data: any) {
private updatePlayerState(data: any) {
const { updatePlayerState } = this.gameStore
updatePlayerState(data)
return {
@ -32,7 +70,7 @@ export class SocketIoEventManager {
}
}
async handleCanMakeMoveEvent(data: any) {
private async handleCanMakeMoveEvent(data: any) {
const { canMakeMove, moveToMake } = storeToRefs(this.gameStore)
const { updateCanMakeMove, setIncomingFreeEnds } = this.gameStore
setIncomingFreeEnds(data.freeHands)
@ -46,7 +84,7 @@ export class SocketIoEventManager {
}
}
async handleCanSelectTileEvent() {
private async handleCanSelectTileEvent() {
const { canSelectTile } = storeToRefs(this.gameStore)
const { updateCanSelectTile } = this.gameStore
updateCanSelectTile(true)
@ -57,8 +95,4 @@ export class SocketIoEventManager {
status: 'ok'
}
}
handleGameFinishedEvent() {
this.eventBus.publish('game-finished')
}
}

View File

@ -2,9 +2,9 @@ import { ServiceBase } from '@/services/ServiceBase'
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
import { NetworkService } from '@/services/NetworkService'
import dayjs from 'dayjs'
export class AuthenticationService extends ServiceBase {
private apiUrl = import.meta.env.VITE_API_URL
private networkService = new NetworkService()
isAuthenticated() {
@ -28,6 +28,10 @@ export class AuthenticationService extends ServiceBase {
}
async logout() {
this.removePersistence()
}
private removePersistence() {
const auth = useAuthStore()
const { setJwt, setUser } = auth
setJwt(undefined)
@ -53,6 +57,27 @@ export class AuthenticationService extends ServiceBase {
return JSON.parse(window.atob(base64))
}
fromStorage() {
const token = sessionStorage.getItem('token')
if (token) {
try {
const parsed = this.parseJwt(token)
const isAfter = dayjs().isAfter(parsed.exp * 1000)
if (isAfter) {
this.removePersistence()
return
}
this.persist(token)
this.logger.debug('Token loaded from storage', parsed)
} catch (error) {
this.logger.error(error, 'Error parsing token')
this.removePersistence()
}
} else {
this.removePersistence()
}
}
hasRoles(rolesToCheck: string[]) {
const auth = useAuthStore()
const { roles } = storeToRefs(auth)

View File

@ -0,0 +1,16 @@
import { NetworkService } from './NetworkService'
import { ServiceBase } from './ServiceBase'
export class GameService extends ServiceBase {
private networkService = new NetworkService()
async createMatch(sessionName: string, seed: string) {
const response = await this.networkService.post({
uri: '/game/match',
body: { sessionName, seed },
auth: true
})
const { sessionId } = response
return sessionId
}
}

View File

@ -27,7 +27,15 @@ export class LoggingService {
} else {
messages.unshift(firstMessage)
}
console.log(`${logStr.join(' ')}:`, ...messages)
if (messages.length > 0) {
console.log(
`${logStr.join(' ')}:`,
...messages.filter((m) => m !== undefined && m !== null)
)
} else {
console.log(logStr.join(' '))
}
}
}
}

View File

@ -82,7 +82,7 @@ export class NetworkService {
'Content-Type': 'application/json'
}
if (auth) {
headers.Authorization = jwt
headers.Authorization = jwt.value
}
return headers
}

View File

@ -1,29 +1,45 @@
import type { MatchSessionState, GameState, PlayerDto } from '@/common/interfaces'
import { io, Socket } from 'socket.io-client'
import { SocketIoEventManager } from '@/managers/SocketIoEventManager'
import { LoggingService } from './LoggingService'
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
import { ServiceBase } from './ServiceBase'
export class SocketIoClientService {
public socket: Socket
export class SocketIoClientService extends ServiceBase {
private socket!: Socket
private isConnected = false
private gameEventManager = new SocketIoEventManager()
private logger: LoggingService = new LoggingService()
constructor(url: string) {
this.socket = io(url)
this.addEvents()
constructor(private url: string) {
super()
}
addEvents(): void {
async connect(): Promise<void> {
const auth = useAuthStore()
const { jwt, isLoggedIn } = storeToRefs(auth)
return new Promise((resolve, reject) => {
if (!isLoggedIn) {
reject('Not logged in')
}
this.socket = io(this.url, {
auth: {
token: jwt.value
}
})
this.socket.on('connect', () => {
this.isConnected = true
if (this.socket && this.socket.recovered) {
console.log('socket recovered succesfully')
} else {
console.log('socket connected')
}
this.isConnected = true
this.addEvents()
resolve()
})
})
}
addEvents(): void {
this.socket.on('disconnect', () => {
this.isConnected = false
console.log('Disconnected from server')
@ -39,36 +55,28 @@ export class SocketIoClientService {
console.log('Failed to reconnect to server')
})
this.socket.on('matchState', (data: MatchSessionState, callback: any) => {
callback(this.gameEventManager.handleSessionStateEvent(data))
})
this.socket.on('gameState', (data: GameState, callback: any) => {
callback(this.gameEventManager.handleGameStateEvent(data))
})
this.socket.on('playerState', (data: PlayerDto, callback: any) => {
this.logger.debug('playerState', data)
callback(this.gameEventManager.handlePlayerStateEvent(data))
})
this.socket.on('makeMove', async (data: any, callback: any) => {
callback(await this.gameEventManager.handleCanMakeMoveEvent(data))
})
this.socket.on('chooseTile', async (data: any, callback: any) => {
callback(await this.gameEventManager.handleCanSelectTileEvent())
})
this.socket.on('game-finished', () => {
this.logger.debug('game-finished event received')
this.gameEventManager.handleGameFinishedEvent()
})
this.socket.on('ping', () => {
console.log('Ping received from server')
this.socket.emit('pong') // Send pong response
})
// Custom events
// this.socket.on('makeMove', async (data: any, callback: any) => {
// callback(await this.gameEventManager.handleCanMakeMoveEvent(data))
// })
// this.socket.on('chooseTile', async (data: any, callback: any) => {
// callback(await this.gameEventManager.handleCanSelectTileEvent())
// })
this.socket.on('game-event', (data: any) => {
this.gameEventManager.handleGameEvent(data)
})
this.socket.on('game-event-ack', async (data: any, callback: any) => {
callback(await this.gameEventManager.handleGameEventAck(data))
})
}
sendMessage(event: string, data: any): void {

View File

@ -15,6 +15,12 @@ export const useGameStore = defineStore('game', () => {
const showReadyButton = ref(false)
const isSessionStarted = computed(() => sessionState.value !== undefined)
const amIHost = computed(
() =>
sessionState.value !== undefined &&
playerState.value !== undefined &&
playerState.value.id === sessionState.value.creator
)
function updateSessionState(newState: MatchSessionState) {
sessionState.value = newState
@ -77,6 +83,7 @@ export const useGameStore = defineStore('game', () => {
setShowReadyButton,
setReadyForStart,
updateGameFinished,
isSessionStarted
isSessionStarted,
amIHost
}
})

View File

@ -60,7 +60,7 @@ function copySeed() {
FreeEnds: {{ gameState?.boardFreeEnds }} - Current Player:{{
gameState?.currentPlayer?.name
}}
- Score: {{ gameState?.scoreboard }}
- Score: {{ sessionState?.scoreboard }}
</p>
<p v-if="sessionState?.id">
SessionID: {{ sessionState.id }} PlayerID: {{ playerState?.id }}

View File

@ -1,21 +1,24 @@
<script setup lang="ts">
import { inject, ref } from 'vue'
import { useRouter } from 'vue-router'
import useClipboard from 'vue-clipboard3'
import { useGameStore } from '@/stores/game'
import { storeToRefs } from 'pinia'
import { LoggingService } from '@/services/LoggingService'
import type { GameService } from '@/services/GameService'
let seed = ref('')
let sessionName = ref('Test Value')
let sessionId = ref('')
const router = useRouter()
const gameStore = useGameStore()
const { toClipboard } = useClipboard()
const socketService: any = inject('socket')
const gameService: GameService = inject<GameService>('game') as GameService
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
const { readyForStart, sessionState, isSessionStarted, playerState } = storeToRefs(gameStore)
const { readyForStart, sessionState, isSessionStarted, playerState, amIHost } =
storeToRefs(gameStore)
async function setPlayerReady() {
logger.debug('Starting game')
@ -23,8 +26,12 @@ async function setPlayerReady() {
logger.error('No session found')
return
}
if (!playerState.value) {
logger.error('No player found')
return
}
await socketService.sendMessageWithAck('playerReady', {
user: 'arhuako',
userId: playerState.value.id,
sessionId: sessionState.value.id
})
readyForStart.value = true
@ -32,7 +39,9 @@ async function setPlayerReady() {
async function createMatch() {
logger.debug('Creating match')
socketService.sendMessageWithAck('createSession', { user: 'arhuako' })
await socketService.connect()
sessionId.value = await gameService.createMatch(sessionName.value, seed.value)
logger.debug('Match reated successfully')
}
async function joinMatch() {
@ -62,12 +71,25 @@ async function startMatch() {
<div class="block">
<p>This is a protected route.</p>
<p>{{ sessionState || 'No session' }}</p>
<p>{{ playerState?.ready || 'No player state' }}</p>
<p>{{ playerState || 'No player state' }}</p>
<p>Session started: {{ isSessionStarted }}</p>
<p>Host: {{ amIHost }}</p>
</div>
<div class="block">
<div class="block" v-if="!isSessionStarted">
<div class="grid">
<div class="cell">
<input
class="input"
style="margin-bottom: 0"
v-model="sessionName"
placeholder="Session Name"
/>
</div>
<div class="cell">
<input class="input" style="margin-bottom: 0" v-model="seed" placeholder="Seed" />
</div>
</div>
</div>
<button class="button" @click="createMatch" v-if="!isSessionStarted">
Create Match Session
</button>