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"> <script setup lang="ts">
import { inject } from 'vue'
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import type { AuthenticationService } from './services/AuthenticationService'
const auth: AuthenticationService = inject<AuthenticationService>('auth') as AuthenticationService
auth.fromStorage()
</script> </script>
<template> <template>

View File

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

View File

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

View File

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

View File

@ -48,12 +48,16 @@ export class Hand extends EventEmitter {
gameFinished() { gameFinished() {
this.logger.debug('gameFinished') this.logger.debug('gameFinished')
this.tiles = [] this.tiles = []
this.container.removeChildren()
this.initialized = false this.initialized = false
this.buttonNext = this.createButton( this.buttonNext = this.createButton(
'NEXT', 'NEXT',
{ x: this.width / 2 - 25, y: this.height / 2, width: 50, height: 20 }, { 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 { SocketIoClientService } from '@/services/SocketIoClientService'
import { LoggingService } from '@/services/LoggingService' import { LoggingService } from '@/services/LoggingService'
import { AuthenticationService } from './services/AuthenticationService' import { AuthenticationService } from './services/AuthenticationService'
import { GameService } from './services/GameService'
const app = createApp(App) const app = createApp(App)
@ -20,5 +21,6 @@ app.use(router)
app.provide('socket', new SocketIoClientService('http://localhost:3000')) app.provide('socket', new SocketIoClientService('http://localhost:3000'))
app.provide('logger', new LoggingService()) app.provide('logger', new LoggingService())
app.provide('auth', new AuthenticationService()) app.provide('auth', new AuthenticationService())
app.provide('game', new GameService())
app.mount('#app') app.mount('#app')

View File

@ -1,6 +1,6 @@
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
import { wait } from '@/common/helpers' import { wait } from '@/common/helpers'
import type { MatchSessionState } from '@/common/interfaces' import type { MatchSessionState, SocketEvent } from '@/common/interfaces'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useEventBusStore } from '@/stores/eventBus' import { useEventBusStore } from '@/stores/eventBus'
@ -8,7 +8,45 @@ export class SocketIoEventManager {
gameStore: any = useGameStore() gameStore: any = useGameStore()
eventBus = useEventBusStore() 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 const { updateSessionState } = this.gameStore
updateSessionState(data) updateSessionState(data)
return { return {
@ -16,7 +54,7 @@ export class SocketIoEventManager {
} }
} }
handleGameStateEvent(data: any) { private updateGameState(data: any) {
const { updateGameState } = this.gameStore const { updateGameState } = this.gameStore
updateGameState(data) updateGameState(data)
return { return {
@ -24,7 +62,7 @@ export class SocketIoEventManager {
} }
} }
handlePlayerStateEvent(data: any) { private updatePlayerState(data: any) {
const { updatePlayerState } = this.gameStore const { updatePlayerState } = this.gameStore
updatePlayerState(data) updatePlayerState(data)
return { return {
@ -32,7 +70,7 @@ export class SocketIoEventManager {
} }
} }
async handleCanMakeMoveEvent(data: any) { private async handleCanMakeMoveEvent(data: any) {
const { canMakeMove, moveToMake } = storeToRefs(this.gameStore) const { canMakeMove, moveToMake } = storeToRefs(this.gameStore)
const { updateCanMakeMove, setIncomingFreeEnds } = this.gameStore const { updateCanMakeMove, setIncomingFreeEnds } = this.gameStore
setIncomingFreeEnds(data.freeHands) setIncomingFreeEnds(data.freeHands)
@ -46,7 +84,7 @@ export class SocketIoEventManager {
} }
} }
async handleCanSelectTileEvent() { private async handleCanSelectTileEvent() {
const { canSelectTile } = storeToRefs(this.gameStore) const { canSelectTile } = storeToRefs(this.gameStore)
const { updateCanSelectTile } = this.gameStore const { updateCanSelectTile } = this.gameStore
updateCanSelectTile(true) updateCanSelectTile(true)
@ -57,8 +95,4 @@ export class SocketIoEventManager {
status: 'ok' 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 { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { NetworkService } from '@/services/NetworkService' import { NetworkService } from '@/services/NetworkService'
import dayjs from 'dayjs'
export class AuthenticationService extends ServiceBase { export class AuthenticationService extends ServiceBase {
private apiUrl = import.meta.env.VITE_API_URL
private networkService = new NetworkService() private networkService = new NetworkService()
isAuthenticated() { isAuthenticated() {
@ -28,6 +28,10 @@ export class AuthenticationService extends ServiceBase {
} }
async logout() { async logout() {
this.removePersistence()
}
private removePersistence() {
const auth = useAuthStore() const auth = useAuthStore()
const { setJwt, setUser } = auth const { setJwt, setUser } = auth
setJwt(undefined) setJwt(undefined)
@ -53,6 +57,27 @@ export class AuthenticationService extends ServiceBase {
return JSON.parse(window.atob(base64)) 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[]) { hasRoles(rolesToCheck: string[]) {
const auth = useAuthStore() const auth = useAuthStore()
const { roles } = storeToRefs(auth) 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 { } else {
messages.unshift(firstMessage) 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' 'Content-Type': 'application/json'
} }
if (auth) { if (auth) {
headers.Authorization = jwt headers.Authorization = jwt.value
} }
return headers return headers
} }

View File

@ -1,29 +1,45 @@
import type { MatchSessionState, GameState, PlayerDto } from '@/common/interfaces' import type { MatchSessionState, GameState, PlayerDto } from '@/common/interfaces'
import { io, Socket } from 'socket.io-client' import { io, Socket } from 'socket.io-client'
import { SocketIoEventManager } from '@/managers/SocketIoEventManager' 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 { export class SocketIoClientService extends ServiceBase {
public socket: Socket private socket!: Socket
private isConnected = false private isConnected = false
private gameEventManager = new SocketIoEventManager() private gameEventManager = new SocketIoEventManager()
private logger: LoggingService = new LoggingService()
constructor(url: string) { constructor(private url: string) {
this.socket = io(url) super()
this.addEvents()
} }
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.socket.on('connect', () => {
this.isConnected = true
if (this.socket && this.socket.recovered) { if (this.socket && this.socket.recovered) {
console.log('socket recovered succesfully') console.log('socket recovered succesfully')
} else { } else {
console.log('socket connected') console.log('socket connected')
} }
this.isConnected = true
this.addEvents()
resolve()
}) })
})
}
addEvents(): void {
this.socket.on('disconnect', () => { this.socket.on('disconnect', () => {
this.isConnected = false this.isConnected = false
console.log('Disconnected from server') console.log('Disconnected from server')
@ -39,36 +55,28 @@ export class SocketIoClientService {
console.log('Failed to reconnect to server') 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', () => { this.socket.on('ping', () => {
console.log('Ping received from server') console.log('Ping received from server')
this.socket.emit('pong') // Send pong response 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 { sendMessage(event: string, data: any): void {

View File

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

View File

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

View File

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