reworked event communication

This commit is contained in:
Jose Conde 2024-07-12 16:29:35 +02:00
parent d862f94f74
commit 1b058db6c0
25 changed files with 738 additions and 314 deletions

2
.env
View File

@ -1,2 +1,2 @@
VITE_LOG_LEVEL= 'debug' VITE_LOG_LEVEL= 'error'
VITE_API_URL= 'http://localhost:3000/api' VITE_API_URL= 'http://localhost:3000/api'

9
package-lock.json generated
View File

@ -13,6 +13,7 @@
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pino": "^9.2.0", "pino": "^9.2.0",
"pixi-actions": "^1.1.11",
"pixi-filters": "^6.0.4", "pixi-filters": "^6.0.4",
"pixi.js": "^8.2.1", "pixi.js": "^8.2.1",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",
@ -3809,6 +3810,14 @@
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="
}, },
"node_modules/pixi-actions": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/pixi-actions/-/pixi-actions-1.1.11.tgz",
"integrity": "sha512-03gMHGZsMg0KrIuDMUUPHq0mnjBjDBXJsrJkhrQO0o1hzsJ1ejcn9zhHRp99oqHkyicLuVJu5iBmblMg0xEYBA==",
"peerDependencies": {
"pixi.js": ">7.0.0"
}
},
"node_modules/pixi-filters": { "node_modules/pixi-filters": {
"version": "6.0.4", "version": "6.0.4",
"resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.0.4.tgz", "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.0.4.tgz",

View File

@ -19,6 +19,7 @@
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pino": "^9.2.0", "pino": "^9.2.0",
"pixi-actions": "^1.1.11",
"pixi-filters": "^6.0.4", "pixi-filters": "^6.0.4",
"pixi.js": "^8.2.1", "pixi.js": "^8.2.1",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",

View File

@ -1,10 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue' import { inject, onMounted, onUnmounted } from 'vue'
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import type { AuthenticationService } from './services/AuthenticationService' import type { AuthenticationService } from './services/AuthenticationService'
import { useEventBusStore } from './stores/eventBus'
const auth: AuthenticationService = inject<AuthenticationService>('auth') as AuthenticationService const auth: AuthenticationService = inject<AuthenticationService>('auth') as AuthenticationService
auth.fromStorage() auth.fromStorage()
const eventBus = useEventBusStore()
const handleBeforeUnload = (evt: any) => {
// evt.preventDefault()
// const isGame = location.pathname === '/game' ? true : ''
// console.log('isGame :>> ', isGame)
// evt.returnValue = isGame
// eventBus.publish('window-before-unload')
// console.log('location.href :>> ', location.pathname)
}
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
})
onUnmounted(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
})
</script> </script>
<template> <template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

View File

@ -1,7 +1,13 @@
export const defaultContainerOptions = { export const DEFAULT_CONTAINER_OPTIONS = {
width: 100, width: 100,
height: 100, height: 100,
x: 0, x: 0,
y: 0, y: 0,
visible: true visible: true
} }
export const ORIENTATION_ANGLES: { [key: string]: number } = {
north: 0,
east: Math.PI / 2,
south: Math.PI,
west: (3 * Math.PI) / 2
}

View File

@ -1,6 +1,6 @@
import { Graphics, Container } from 'pixi.js' import { Graphics, Container, Text } from 'pixi.js'
import type { ContainerOptions, TileDto } from './interfaces' import type { ContainerOptions, Dimension, TileDto } from './interfaces'
import { defaultContainerOptions } from './constants' import { DEFAULT_CONTAINER_OPTIONS } from './constants'
export function getColorBackground(container: Container, colorName: string, alpha: number = 0.5) { export function getColorBackground(container: Container, colorName: string, alpha: number = 0.5) {
const graphics = new Graphics() const graphics = new Graphics()
@ -14,7 +14,7 @@ export function getColorBackground(container: Container, colorName: string, alph
} }
export function createContainer(options: ContainerOptions) { export function createContainer(options: ContainerOptions) {
const opts = { ...defaultContainerOptions, ...options } const opts = { ...DEFAULT_CONTAINER_OPTIONS, ...options }
const container = new Container() const container = new Container()
const rect = new Graphics().rect(opts.x, opts.y, opts.width, opts.height) const rect = new Graphics().rect(opts.x, opts.y, opts.width, opts.height)
@ -30,6 +30,54 @@ export function createContainer(options: ContainerOptions) {
return container return container
} }
export function createButton(
textStr: string,
dimension: Dimension,
action: Function,
parent?: Container
): Container {
const { x, y, width, height } = dimension
const rectangle = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(0xffff00)
const text = new Text({
text: textStr,
style: {
fontFamily: 'Arial',
fontSize: 12,
fontWeight: 'bold',
fill: 0x121212,
align: 'center'
}
})
text.anchor = 0.5
const container = new Container()
container.addChild(rectangle)
container.addChild(text)
text.y = y + height / 2
text.x = x + width / 2
container.eventMode = 'static'
container.cursor = 'pointer'
rectangle.alpha = 0.7
text.alpha = 0.7
container.on('pointerdown', () => action())
container.on('pointerover', () => {
rectangle.alpha = 1
text.alpha = 1
})
container.on('pointerout', () => {
rectangle.alpha = 0.7
text.alpha = 0.7
})
if (parent !== undefined) {
parent.addChild(container)
}
return container
}
export async function wait(ms: number) { export async function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)) return new Promise((resolve) => setTimeout(resolve, ms))
} }

View File

@ -17,8 +17,10 @@ export interface TileDto {
y?: number y?: number
width?: number width?: number
height?: number height?: number
playerId?: string
} }
export interface MatchSessionState { export interface MatchSessionDto {
_id: string
id: string id: string
name: string name: string
creator: string creator: string
@ -38,7 +40,7 @@ export interface MatchSessionState {
playersReady: number playersReady: number
} }
export interface GameState { export interface GameDto {
id: string id: string
players: PlayerDto[] players: PlayerDto[]
tilesInBoneyard: TileDto[] tilesInBoneyard: TileDto[]
@ -85,3 +87,13 @@ export interface SocketEvent {
event: string event: string
data: any data: any
} }
export interface AnimationOptions {
x?: number
y?: number
rotation?: number
scale?: number
duration?: number
width?: number
height?: number
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MatchSessionState, GameState, PlayerDto } from '@/common/interfaces' import type { MatchSessionDto, GameDto, PlayerDto } from '@/common/interfaces'
import { onMounted, onUnmounted, ref, watch, inject } from 'vue' import { onMounted, onUnmounted, ref, watch, inject } from 'vue'
import { Game } from '@/game/Game' import { Game } from '@/game/Game'
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
@ -13,7 +13,8 @@ const socketService: any = inject('socket')
const gameStore = useGameStore() const gameStore = useGameStore()
const eventBus = useEventBusStore() const eventBus = useEventBusStore()
const { playerState, sessionState } = storeToRefs(gameStore) const { playerState, sessionState, canMakeMove } = storeToRefs(gameStore)
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
let appEl = ref<HTMLElement | null>(null) let appEl = ref<HTMLElement | null>(null)
@ -30,6 +31,31 @@ const game = new Game(
sessionState.value?.id || '' sessionState.value?.id || ''
) )
// watch(
// () => gameStore.gameState,
// async (value: GameDto | undefined) => {
// if (value === undefined) return
// await game.board?.setState(value)
// }
// )
// watch(
// () => gameStore.sessionState,
// (value: MatchSessionDto | undefined) => {
// if (value === undefined) return
// // logger.debug('gameSessionState-------------------------------------- :>> ', value)
// }
// )
// watch(
// () => gameStore.playerState,
// (value: PlayerDto | undefined) => {
// if (value === undefined) return
// game.hand.update(value as PlayerDto)
// }
// )
onMounted(async () => { onMounted(async () => {
if (appEl.value === null) return if (appEl.value === null) return
const canvas = await game.setup() const canvas = await game.setup()
@ -37,41 +63,41 @@ onMounted(async () => {
await game.preload() await game.preload()
await game.start() await game.start()
eventBus.subscribe('game-finished', () => { eventBus.subscribe('server:game-finished', (data) => {
game.gameFinished() console.log('server:game-finished :>> ', data)
game.gameFinished(data)
}) })
watch( eventBus.subscribe('server:server-player-move', (data) => {
() => gameStore.canMakeMove, console.log('server:player-move :>> ', data)
(value: boolean) => { game.serverPlayerMove(data, playerState.value?.id ?? '')
game.setCanMakeMove(value) })
}
)
watch( eventBus.subscribe('game:player-turn-started', (data: any) => {
() => gameStore.gameState, console.log('game:player-turn-started :>> ', data)
(value: GameState | undefined) => { // game.setCanMakeMove(true)
if (value === undefined) return })
game.board?.setState(value, playerState.value?.id ?? '')
}
)
watch( eventBus.subscribe('server:hand-dealt', (playerState: PlayerDto) => {
() => gameStore.sessionState, console.log('server:hand-dealt :>> ', playerState)
(value: MatchSessionState | undefined) => { game.hand.update(playerState)
if (value === undefined) return })
// logger.debug('gameSessionState-------------------------------------- :>> ', value) eventBus.subscribe('server:next-turn', (gameState: GameDto) => {
} console.log('server:next-turn :>> ', gameState)
) updateGameState(gameState)
game.setNextPlayer(gameState)
})
watch( eventBus.subscribe('server:match-finished', (data) => {
() => gameStore.playerState, console.log('server:match-finished :>> ', data)
(value: PlayerDto | undefined) => { game.matchFinished(data)
if (value === undefined) return })
game.hand.update(value as PlayerDto)
} // eventBus.subscribe('server:game-state', (data) => {
) // console.log('server:game-state :>> ', data)
// game.serverGameState(data)
// })
// mockMove(game, [6, 6], 'left') // mockMove(game, [6, 6], 'left')
// mockMove(game, [6, 4], 'left') // mockMove(game, [6, 4], 'left')

View File

@ -9,13 +9,14 @@ import {
Ticker Ticker
} from 'pixi.js' } from 'pixi.js'
import { Scale, type ScaleFunction } from '@/game/utilities/scale' import { Scale, type ScaleFunction } from '@/game/utilities/scale'
import type { GameState, Movement, TileDto } from '@/common/interfaces' import type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces'
import { Tile } from '@/game/Tile' import { Tile } from '@/game/Tile'
import { DIRECTIONS, createContainer, isTilePair, isTileVertical } from '@/common/helpers' import { DIRECTIONS, createContainer, isTilePair } from '@/common/helpers'
import { createText } from '@/game/utilities/fonts' import { createText } from '@/game/utilities/fonts'
import { LoggingService } from '@/services/LoggingService' import { LoggingService } from '@/services/LoggingService'
import { inject } from 'vue' import { inject } from 'vue'
import { GlowFilter } from 'pixi-filters' import { GlowFilter } from 'pixi-filters'
import { ORIENTATION_ANGLES } from '@/common/constants'
export class Board extends EventEmitter { export class Board extends EventEmitter {
private _scale: number = 1 private _scale: number = 1
@ -28,7 +29,6 @@ export class Board extends EventEmitter {
grain: number = 25 grain: number = 25
scaleY: ScaleFunction scaleY: ScaleFunction
scaleX: ScaleFunction scaleX: ScaleFunction
state?: GameState
container!: Container container!: Container
initialContainer!: Container initialContainer!: Container
tilesContainer!: Container tilesContainer!: Container
@ -49,6 +49,7 @@ export class Board extends EventEmitter {
rightDirection: string = 'east' rightDirection: string = 'east'
playerHand: Tile[] = [] playerHand: Tile[] = []
firstTile?: Tile firstTile?: Tile
currentPlayer!: PlayerDto
constructor(app: Application) { constructor(app: Application) {
super() super()
@ -65,7 +66,7 @@ export class Board extends EventEmitter {
parent: app.stage parent: app.stage
}) })
const background = new Sprite(Assets.get('bg-1')) const background = new Sprite(Assets.get('bg-green'))
// background.width = this.width // background.width = this.width
// background.height = this.height // background.height = this.height
this.container.addChild(background) this.container.addChild(background)
@ -160,10 +161,12 @@ export class Board extends EventEmitter {
} }
} }
setState(state: GameState, playerId: string) { async setServerPlayerTurn(currentPlayer: PlayerDto) {
this.state = state this.showText(`${currentPlayer.name}'s turn!\n Please wait...`)
const { lastMove } = state }
async playerMove(move: any, playerId: string) {
const { move: lastMove } = move
if (lastMove === null) { if (lastMove === null) {
return return
} }
@ -177,12 +180,13 @@ export class Board extends EventEmitter {
this.nextTile = tile this.nextTile = tile
lastMove.tile = tile.toPlain() lastMove.tile = tile.toPlain()
this.movements.push(lastMove) this.movements.push(lastMove)
this.addTile(tile, lastMove) await this.addTile(tile, lastMove)
this.setFreeEnd(lastMove) this.setFreeEnd(lastMove)
} }
} }
addTile(tile: Tile, move: Movement) { async addTile(tile: Tile, move: Movement) {
console.log('adding tile', tile.pips)
let orientation = '' let orientation = ''
let x: number = let x: number =
move.type === 'left' move.type === 'left'
@ -244,14 +248,25 @@ export class Board extends EventEmitter {
availablePosition && ([x, y] = availablePosition) availablePosition && ([x, y] = availablePosition)
} }
} }
const endTile = isLeft ? this.leftTile : this.rightTile tile.addTo(this.tilesContainer)
const isEndVertical = endTile?.isVertical() ?? false
const isNextVertical = orientation === 'north' || orientation === 'south'
tile.setPosition(this.scaleX(x), this.scaleY(y))
tile.setOrientation(orientation)
tile.reScale(this.scale) tile.reScale(this.scale)
this.tiles.push(tile) this.tiles.push(tile)
tile.addTo(this.tilesContainer)
const animation: AnimationOptions = {
x: this.scaleX(x),
y: this.scaleY(y),
rotation: ORIENTATION_ANGLES[orientation],
duration: 20
}
tile.setPosition(this.scaleX(x), this.scaleY(y))
tile.setOrientation(orientation)
// tile.setPosition(this.scaleX(0), this.height + tile.height / 2)
// console.log('going to animate', tile.pips)
// await tile.animateTo(animation)
// console.log('animated', tile.pips)
this.emit('game:tile-animation-ended', tile.toPlain())
} }
getPlayedTile(id: string): Tile | undefined { getPlayedTile(id: string): Tile | undefined {
@ -300,7 +315,7 @@ export class Board extends EventEmitter {
} }
} }
updateBoard(move: Movement) { async updateBoard(move: Movement) {
try { try {
const { tile: tileDto } = move const { tile: tileDto } = move
const tile = this.getTileInHand(tileDto?.id ?? '') const tile = this.getTileInHand(tileDto?.id ?? '')
@ -310,7 +325,7 @@ export class Board extends EventEmitter {
} }
this.movements.push(move) this.movements.push(move)
this.addTile(tile, move) await this.addTile(tile, move)
this.setFreeEnd(move) this.setFreeEnd(move)
} catch (error) { } catch (error) {
this.logger.error(error, 'Error updating board') this.logger.error(error, 'Error updating board')
@ -437,7 +452,7 @@ export class Board extends EventEmitter {
dot.alpha = 0.1 dot.alpha = 0.1
dot.interactive = true dot.interactive = true
dot.on('pointerdown', () => { dot.on('pointerdown', () => {
this.emit(`${side}Click`, direction && { direction, x, y }) this.emit(`game:board-${side}-action-click`, direction && { direction, x, y })
this.cleanInteractions() this.cleanInteractions()
}) })
dot.on('pointerover', () => { dot.on('pointerover', () => {
@ -566,7 +581,8 @@ export class Board extends EventEmitter {
return [canPlayNorth, canPlayEast, canPlaySouth, canPlayWest] return [canPlayNorth, canPlayEast, canPlaySouth, canPlayWest]
} }
gameFinished() { gameFinished(data: any) {
const { lastGame, gameState } = data
this.tiles = [] this.tiles = []
this.boneyard = [] this.boneyard = []
this.movements = [] this.movements = []
@ -580,6 +596,10 @@ export class Board extends EventEmitter {
this.firstTile = undefined this.firstTile = undefined
this.tilesContainer.removeChildren() this.tilesContainer.removeChildren()
this.interactionContainer.removeChildren() this.interactionContainer.removeChildren()
this.showText('Game finished') this.showText(`Game finished \n Winner ${lastGame?.winner?.name ?? 'No winner'}`)
}
matchFinished(data: any) {
// this.showText(`Game finished \n Winner ${lastGame?.winner?.name ?? 'No winner'}`)
} }
} }

View File

@ -3,14 +3,19 @@ import { Board } from '@/game/Board'
import { assets } from '@/game/utilities/assets' import { assets } from '@/game/utilities/assets'
import { Tile } from '@/game/Tile' import { Tile } from '@/game/Tile'
import { Hand } from '@/game/Hand' import { Hand } from '@/game/Hand'
import type { Movement, TileDto } from '@/common/interfaces' import type { GameDto, Movement, PlayerDto, TileDto } from '@/common/interfaces'
import type { SocketIoClientService } from '@/services/SocketIoClientService' import type { SocketIoClientService } from '@/services/SocketIoClientService'
import { useEventBusStore } from '@/stores/eventBus'
import { wait } from '@/common/helpers'
import { Actions } from 'pixi-actions'
export class Game { export class Game {
public board!: Board public board!: Board
public hand!: Hand public hand!: Hand
private app: Application = new Application() private app: Application = new Application()
private selectedTile: TileDto | undefined private selectedTile: TileDto | undefined
private eventBus: any = useEventBusStore()
private currentMove: Movement | undefined
constructor( constructor(
private options: { boardScale: number; handScale: number; width: number; height: number } = { private options: { boardScale: number; handScale: number; width: number; height: number } = {
@ -30,16 +35,23 @@ export class Game {
const height = 800 const height = 800
await this.app.init({ width, height }) await this.app.init({ width, height })
this.app.ticker.add((tick) => Actions.tick(tick.deltaTime / 60))
return this.app.canvas return this.app.canvas
} }
start() { async start() {
this.board = new Board(this.app) this.board = new Board(this.app)
this.hand = new Hand(this.app) this.hand = new Hand(this.app)
this.hand.scale = this.options.handScale this.hand.scale = this.options.handScale
this.board.scale = this.options.boardScale this.board.scale = this.options.boardScale
this.setBoardEvents() this.setBoardEvents()
this.setHandEvents() this.setHandEvents()
this.initEventBus()
wait(3000)
this.socketService.sendMessage('client:set-client-ready', {
sessionId: this.sessionId,
userId: this.playerId
})
} }
async preload() { async preload() {
@ -52,35 +64,44 @@ export class Game {
this.app.destroy() this.app.destroy()
} }
private initEventBus() {}
private setHandEvents() { private setHandEvents() {
this.hand.on('handUpdated', (tiles: Tile[]) => { this.hand.on('hand-updated', (tiles: Tile[]) => {
this.board.setPlayerHand(tiles) this.board.setPlayerHand(tiles)
}) })
this.hand.on('tileClick', (tile: TileDto) => { this.hand.on('game:tile-click', (tile: TileDto) => this.highlightMoves(tile))
this.hand.on('game:button-pass-click', async () => {
const move: Movement = {
id: '',
type: 'pass',
playerId: this.playerId
}
this.socketService.sendMessage('client:player-move', {
sessionId: this.sessionId,
move: move
})
await this.board.updateBoard(move)
})
this.hand.on('nextClick', () => {
this.socketService.sendMessage('client:set-player-ready', {
userId: this.playerId,
sessionId: this.sessionId
})
})
this.hand.on('hand-initialized', () => {})
}
highlightMoves(tile: TileDto) {
this.selectedTile = tile this.selectedTile = tile
if (tile !== undefined) { if (tile !== undefined) {
this.board.setMovesForTile(this.getMoves(tile), tile) this.board.setMovesForTile(this.getMoves(tile), tile)
} else { } else {
this.board.cleanInteractions() this.board.cleanInteractions()
} }
})
this.hand.on('passClick', () => {
const move: Movement = {
id: '',
type: 'pass',
playerId: this.playerId
}
this.emit('move', move)
this.board.updateBoard(move)
})
this.hand.on('nextClick', async () => {
await this.socketService.sendMessageWithAck('playerReady', {
userId: this.playerId,
sessionId: this.sessionId
})
})
} }
getMoves(tile: any): [boolean, boolean] { getMoves(tile: any): [boolean, boolean] {
@ -100,12 +121,22 @@ export class Game {
} }
public setCanMakeMove(value: boolean) { public setCanMakeMove(value: boolean) {
this.hand.canMove = value this.hand.setCanMove(value, this.board.count === 0, this.board.freeEnds)
this.board.canMove = value this.board.canMove = value
} }
async setNextPlayer(state: GameDto) {
const currentPlayer = state?.currentPlayer!
if (currentPlayer.id !== this.playerId) {
this.setCanMakeMove(false)
this.board.setServerPlayerTurn(currentPlayer)
} else {
this.setCanMakeMove(true)
}
}
private setBoardEvents() { private setBoardEvents() {
this.board.on('leftClick', (data) => { this.board.on('game:board-left-action-click', async (data) => {
console.log('left data :>> ', data) console.log('left data :>> ', data)
if (this.selectedTile === undefined) return if (this.selectedTile === undefined) return
const move: Movement = { const move: Movement = {
@ -114,12 +145,12 @@ export class Game {
playerId: this.playerId, playerId: this.playerId,
...data ...data
} }
this.emit('move', move) this.currentMove = move
this.hand.tileMoved(this.selectedTile) this.hand.tileMoved(this.selectedTile)
this.board.updateBoard({ ...move, tile: this.selectedTile }) await this.board.updateBoard({ ...move, tile: this.selectedTile })
}) })
this.board.on('rightClick', (data) => { this.board.on('game:board-right-action-click', async (data) => {
console.log('right data :>> ', data) console.log('right data :>> ', data)
if (this.selectedTile === undefined) return if (this.selectedTile === undefined) return
const move: Movement = { const move: Movement = {
@ -128,24 +159,53 @@ export class Game {
playerId: this.playerId, playerId: this.playerId,
...data ...data
} }
this.emit('move', move) this.currentMove = move
this.hand.tileMoved(this.selectedTile) this.hand.tileMoved(this.selectedTile)
this.board.updateBoard({ ...move, tile: this.selectedTile }) await this.board.updateBoard({ ...move, tile: this.selectedTile })
})
this.board.on('game:tile-animation-ended', async (tile) => {
console.log('animation ended', tile)
if (tile.playerId === this.playerId) {
this.socketService.sendMessage('client:player-move', {
sessionId: this.sessionId,
move: this.currentMove
})
}
}) })
} }
gameFinished() { // sendMoveEvent(move: Movement) {
// this.board.on('game:tile-animation-ended', async (tile) => {
// this.eventBus.publish('game:tile-animation-ended', tile)
// // this.socketService.sendMessageWithAck('client:tile-animation-ended', {
// // sessionId: this.sessionId,
// // tile
// // })
// })
// }
gameFinished(data: any) {
this.hand.gameFinished() this.hand.gameFinished()
this.board.gameFinished() this.board.gameFinished(data)
}
matchFinished(data: any) {
// this.hand.matchFinished()
this.board.matchFinished(data)
}
serverPlayerMove(data: any, playerId: string) {
this.board.playerMove(data, playerId)
} }
private removeBoardEvents() { private removeBoardEvents() {
this.board.off('leftClick') this.board.off('game:board-left-action-click')
this.board.off('rightClick') this.board.off('game:board-right-action-click')
} }
private removeHandEvents() { private removeHandEvents() {
this.hand.off('tileClick') this.hand.off('game:tile-click')
this.hand.off('passClick') this.hand.off('game:button-pass-click')
} }
} }

View File

@ -1,18 +1,11 @@
import { import { Application, Container, EventEmitter, Sprite, Texture, Ticker } from 'pixi.js'
Application,
Container,
EventEmitter,
Graphics,
Sprite,
Text,
Texture,
Ticker
} from 'pixi.js'
import { Tile } from '@/game/Tile' import { Tile } from '@/game/Tile'
import type { Dimension, PlayerDto, TileDto } from '@/common/interfaces' import type { PlayerDto, TileDto } from '@/common/interfaces'
import { GlowFilter } from 'pixi-filters' import { GlowFilter } from 'pixi-filters'
import { Scale, type ScaleFunction } from './utilities/scale' import { Scale, type ScaleFunction } from './utilities/scale'
import { LoggingService } from '@/services/LoggingService' import { LoggingService } from '@/services/LoggingService'
import { createButton, createContainer } from '@/common/helpers'
import { Action, Actions } from 'pixi-actions'
export class Hand extends EventEmitter { export class Hand extends EventEmitter {
tiles: Tile[] = [] tiles: Tile[] = []
@ -31,18 +24,40 @@ export class Hand extends EventEmitter {
scaleX!: ScaleFunction scaleX!: ScaleFunction
grain: number = 25 grain: number = 25
logger: LoggingService = new LoggingService() logger: LoggingService = new LoggingService()
availableTiles: Tile[] = []
tilesLayer!: Container
interactionsLayer!: Container
constructor(app: Application) { constructor(app: Application) {
super() super()
app.stage.addChild(this.container) app.stage.addChild(this.container)
this.ticker = app.ticker this.ticker = app.ticker
this.height = 130 this.height = 130 * this.scale
this.width = app.canvas.width this.width = app.canvas.width
this.container.y = app.canvas.height - this.height this.container.y = app.canvas.height - this.height
this.container.width = this.width this.container.width = this.width
this.container.height = this.height this.container.height = this.height
this.calculateScale() this.calculateScale()
this.initLayers()
}
initLayers() {
this.container.removeChildren()
this.addBg() this.addBg()
this.tilesLayer = createContainer({
width: this.width,
height: this.height,
x: 0,
y: 0,
parent: this.container
})
this.interactionsLayer = createContainer({
width: this.width,
height: this.height,
x: 0,
y: 0,
parent: this.container
})
} }
gameFinished() { gameFinished() {
@ -50,14 +65,15 @@ export class Hand extends EventEmitter {
this.tiles = [] this.tiles = []
this.initialized = false this.initialized = false
this.buttonNext = this.createButton( this.buttonNext = 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 },
() => { () => {
this.container.removeChildren() this.tilesLayer.removeChildren()
this.container.removeChild(this.buttonNext) this.interactionsLayer.removeChild(this.buttonNext)
this.emit('nextClick') this.emit('nextClick')
} },
this.interactionsLayer
) )
} }
@ -72,24 +88,47 @@ export class Hand extends EventEmitter {
this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height]) this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height])
} }
set canMove(value: boolean) { setCanMove(value: boolean, isFirstMove: boolean, freeEnds?: [number, number]) {
this._canMove = value console.log('this.tiles :>> ', this.tiles.length)
if (value) { this.availableTiles =
!value || isFirstMove
? this.tiles
: this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds))
console.log('this.availableTiles :>> ', this.availableTiles.length)
if (value && this.availableTiles.length === 0) {
this.createPassButton() this.createPassButton()
} else { } else {
this.container.removeChild(this.buttonPass) this.interactionsLayer.removeChild(this.buttonPass)
}
this.availableTiles.forEach((tile) => {
if (value) {
tile.animateTo({
x: tile.x,
y: tile.y - 10
})
// const action: Action = Actions.moveTo(tile.getSprite(), tile.x, tile.y - 10, 1,).play()
} }
this.tiles.forEach((tile) => {
tile.interactive = value tile.interactive = value
}) })
this._canMove = value
}
hasMoves(tile: TileDto, freeEnds?: [number, number]): boolean {
if (tile === undefined || freeEnds === undefined) return false
let hasMoves: boolean = false
if (tile.pips != undefined) {
hasMoves = tile.pips.includes(freeEnds[0]) || tile.pips.includes(freeEnds[1])
}
return hasMoves
} }
initialize(playerState: PlayerDto) { initialize(playerState: PlayerDto) {
this.tiles = this.createTiles(playerState) this.tiles = this.createTiles(playerState)
this.emit('handUpdated', this.tiles)
this.initialized = this.tiles.length > 0 this.initialized = this.tiles.length > 0
this.renderTiles() this.renderTiles()
this.emit('hand-updated', this.tiles)
} }
private addBg() { private addBg() {
@ -109,8 +148,9 @@ export class Hand extends EventEmitter {
const selected = this.tiles.find((t) => t.selected) const selected = this.tiles.find((t) => t.selected)
if (selected) { if (selected) {
this.deselectTile(selected) this.deselectTile(selected)
if (selected.id === tile.id) { if (selected.id === tile.id) {
this.emit('tileClick') this.emit('game:tile-click')
return return
} }
} }
@ -118,12 +158,10 @@ export class Hand extends EventEmitter {
tile.selected = true tile.selected = true
tile.alpha = 1 tile.alpha = 1
tile.animateTo(tile.x, tile.y - 10) this.emit('game:tile-click', tile.toPlain())
this.emit('tileClick', tile.toPlain())
} }
private deselectTile(selected: Tile) { private deselectTile(selected: Tile) {
selected.animateTo(selected.x, selected.y + 10)
selected.selected = false selected.selected = false
selected.alpha = 0.7 selected.alpha = 0.7
} }
@ -133,6 +171,17 @@ export class Hand extends EventEmitter {
if (!tile) return if (!tile) return
this.availableTiles
.filter((t) => t.id !== tileDto.id)
.forEach((t) => {
t.animateTo({
x: t.x,
y: t.y + 10
})
})
this.tiles = this.tiles.filter((t) => t.id !== tileDto.id)
tile.interactive = false tile.interactive = false
tile.clearFilters() tile.clearFilters()
tile.off('pointerdown') tile.off('pointerdown')
@ -140,59 +189,14 @@ export class Hand extends EventEmitter {
tile.off('pointerout') tile.off('pointerout')
} }
private createButton(
textStr: string,
dimension: Dimension,
action: string | Function
): Container {
const { x, y, width, height } = dimension
const rectangle = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(0xffff00)
const text = new Text({
text: textStr,
style: {
fontFamily: 'Arial',
fontSize: 12,
fontWeight: 'bold',
fill: 0x121212,
align: 'center'
}
})
text.anchor = 0.5
const container = new Container()
container.addChild(rectangle)
container.addChild(text)
text.y = y + height / 2
text.x = x + width / 2
container.eventMode = 'static'
container.cursor = 'pointer'
rectangle.alpha = 0.7
text.alpha = 0.7
container.on('pointerdown', () => {
action instanceof Function ? action() : this.emit(action)
})
container.on('pointerover', () => {
rectangle.alpha = 1
text.alpha = 1
})
container.on('pointerout', () => {
rectangle.alpha = 0.7
text.alpha = 0.7
})
this.container.addChild(container)
return container
}
private createPassButton() { private createPassButton() {
const lastTile = this.tiles[this.tiles.length - 1] const lastTile = this.tiles[this.tiles.length - 1]
const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0) const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0)
this.buttonPass = this.createButton( this.buttonPass = createButton(
'PASS', 'PASS',
{ x, y: this.height / 2, width: 50, height: 20 }, { x, y: this.height / 2, width: 50, height: 20 },
'passClick' () => this.emit('game:button-pass-click'),
this.interactionsLayer
) )
} }
@ -205,21 +209,22 @@ export class Hand extends EventEmitter {
(tile: Tile) => !playerState.hand.find((t) => t.id === tile.id) (tile: Tile) => !playerState.hand.find((t) => t.id === tile.id)
) )
if (missing) { if (missing) {
this.container.removeChild(missing.getSprite()) this.tilesLayer.removeChild(missing.getSprite())
this.tiles = this.tiles.filter((tile) => tile.id !== missing.id) this.tiles = this.tiles.filter((tile) => tile.id !== missing.id)
this.emit('handUpdated', this.tiles) this.emit('hand-updated', this.tiles)
} }
this.renderTiles() this.renderTiles()
} }
private createTiles(playerState: PlayerDto) { private createTiles(playerState: PlayerDto) {
return playerState.hand.map((tile) => { return playerState.hand.map((tile: TileDto) => {
const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale) const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale, tile.playerId)
newTile.alpha = 0.7 newTile.alpha = 0.7
newTile.anchor = 0.5 newTile.anchor = 0.5
newTile.addTo(this.container) newTile.addTo(this.tilesLayer)
newTile.on('pointerdown', () => this.onTileClick(newTile)) newTile.on('pointerdown', () => this.onTileClick(newTile))
newTile.on('pointerover', () => { newTile.on('pointerover', () => {
this.emit('tileHover', newTile.toPlain())
newTile.alpha = 1 newTile.alpha = 1
newTile.setFilters([ newTile.setFilters([
new GlowFilter({ new GlowFilter({
@ -233,6 +238,7 @@ export class Hand extends EventEmitter {
}) })
newTile.on('pointerout', () => { newTile.on('pointerout', () => {
if (!newTile.selected) { if (!newTile.selected) {
this.emit('tileHover')
newTile.alpha = 0.7 newTile.alpha = 0.7
newTile.getSprite().filters = [] newTile.getSprite().filters = []
} }

View File

@ -1,4 +1,6 @@
import type { AnimationOptions } from '@/common/interfaces'
import { Sprite, Texture, Ticker } from 'pixi.js' import { Sprite, Texture, Ticker } from 'pixi.js'
import { Tile } from './Tile'
export abstract class SpriteBase { export abstract class SpriteBase {
private _interactive: boolean = false private _interactive: boolean = false
@ -81,28 +83,56 @@ export abstract class SpriteBase {
this.sprite.filters = [] this.sprite.filters = []
} }
animateTo(x: number, y: number) { animateTo(options: AnimationOptions): Promise<void> {
return new Promise((resolve) => {
const {
x: targetX,
y: targetY,
rotation: targetRotation,
duration = 10,
width: targetWidth,
height: targetHeight
} = options
const initialX = this.sprite.x const initialX = this.sprite.x
const initialY = this.sprite.y const initialY = this.sprite.y
const initialRotation = this.sprite.rotation
const initialWidth = this.sprite.width
const initialHeight = this.sprite.height
const deltaX = targetX ? targetX - this.sprite.x : null
const deltaY = targetY ? targetY - this.sprite.y : null
const deltaRotation = targetRotation ? targetRotation - this.sprite.rotation : null
const deltaWidth = targetWidth ? targetWidth - this.sprite.width : null
const deltaHeight = targetHeight ? targetHeight - this.sprite.height : null
const deltaX = x - this.sprite.x
const deltaY = y - this.sprite.y
let elapsed: number = 0 let elapsed: number = 0
const duration: number = 10
const tick: any = (delta: any) => { const tick: any = (delta: any) => {
elapsed += delta.deltaTime elapsed += delta.deltaTime
const progress = Math.min(elapsed / duration, 1) const progress = Math.min(elapsed / duration, 1)
this.sprite.x = initialX + deltaX * progress // Linear interpolation
this.sprite.y = initialY + deltaY * progress if (deltaX !== null) this.sprite.x = initialX + deltaX * progress
if (deltaY !== null) this.sprite.y = initialY + deltaY * progress
// Rotation interpolation
if (deltaRotation !== null)
this.sprite.rotation = initialRotation + deltaRotation * progress
// Scale interpolation
// this.sprite.width = initialWidth + deltaWidth * progress
// this.sprite.height = initialHeight + deltaHeight * progress
//
if (progress === 1) { if (progress === 1) {
this.ticker?.remove(tick) this.ticker?.remove(tick)
resolve()
} }
} }
this.ticker?.add(tick) this.ticker?.add(tick)
})
} }
addTo(container: any) { addTo(container: any) {

View File

@ -1,5 +1,6 @@
import { Texture, Ticker } from 'pixi.js' import { Texture, Ticker } from 'pixi.js'
import { SpriteBase } from '@/game/SpriteBase' import { SpriteBase } from '@/game/SpriteBase'
import { ORIENTATION_ANGLES } from '@/common/constants'
export class Tile extends SpriteBase { export class Tile extends SpriteBase {
selected: boolean = false selected: boolean = false
@ -11,7 +12,8 @@ export class Tile extends SpriteBase {
public id: string, public id: string,
ticker?: Ticker, ticker?: Ticker,
public pips?: [number, number], public pips?: [number, number],
scale: number = 1 scale: number = 1,
public playerId?: string
) { ) {
super(ticker, scale) super(ticker, scale)
this.id = id this.id = id
@ -26,6 +28,7 @@ export class Tile extends SpriteBase {
toPlain() { toPlain() {
return { return {
playerId: this.playerId,
id: this.id, id: this.id,
pips: this.pips, pips: this.pips,
orientation: this.orientation, orientation: this.orientation,
@ -69,16 +72,16 @@ export class Tile extends SpriteBase {
setOrientation(value: string) { setOrientation(value: string) {
switch (value) { switch (value) {
case 'north': case 'north':
this.sprite.rotation = 0 this.sprite.rotation = ORIENTATION_ANGLES.north
break break
case 'east': case 'east':
this.sprite.rotation = Math.PI / 2 this.sprite.rotation = ORIENTATION_ANGLES.east
break break
case 'south': case 'south':
this.sprite.rotation = Math.PI this.sprite.rotation = ORIENTATION_ANGLES.south
break break
case 'west': case 'west':
this.sprite.rotation = (3 * Math.PI) / 2 this.sprite.rotation = ORIENTATION_ANGLES.west
break break
} }
this.orientation = value this.orientation = value

View File

@ -30,6 +30,7 @@ import tile6_6 from '@/assets/images/tiles/6-6.png'
import dot from '@/assets/images/circle.png' import dot from '@/assets/images/circle.png'
import bgWood_1 from '@/assets/images/backgrounds/wood-1.jpg' import bgWood_1 from '@/assets/images/backgrounds/wood-1.jpg'
import bg_1 from '@/assets/images/backgrounds/bg-1.png' import bg_1 from '@/assets/images/backgrounds/bg-1.png'
import bg_green from '@/assets/images/backgrounds/bg-green.png'
export const assets = [ export const assets = [
{ alias: 'tile-back', src: tileBack }, { alias: 'tile-back', src: tileBack },
@ -63,5 +64,6 @@ export const assets = [
{ alias: 'tile-6_6', src: tile6_6 }, { alias: 'tile-6_6', src: tile6_6 },
{ alias: 'dot', src: dot }, { alias: 'dot', src: dot },
{ alias: 'bg-wood-1', src: bgWood_1 }, { alias: 'bg-wood-1', src: bgWood_1 },
{ alias: 'bg-1', src: bg_1 } { alias: 'bg-1', src: bg_1 },
{ alias: 'bg-green', src: bg_green }
] ]

View File

@ -1,5 +1,5 @@
import { Tile } from '../Tile' import { Tile } from '../Tile'
import type { GameState, Movement } from '../../common/interfaces' import type { GameDto, Movement } from '../../common/interfaces'
import type { Game } from '../Game' import type { Game } from '../Game'
import { wait } from '../../common/helpers' import { wait } from '../../common/helpers'
@ -46,7 +46,7 @@ export const playerState = {
] ]
} }
export const gameState_0: GameState = { export const gameState_0: GameDto = {
id: 'f043051e-6850-444f-857c-b889220fc187', id: 'f043051e-6850-444f-857c-b889220fc187',
lastMove: { lastMove: {
tile: { tile: {

View File

@ -1,29 +1,19 @@
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
import { wait } from '@/common/helpers' import { wait } from '@/common/helpers'
import type { MatchSessionState, SocketEvent } from '@/common/interfaces' import type { MatchSessionDto, Movement, SocketEvent } from '@/common/interfaces'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useEventBusStore } from '@/stores/eventBus' import { useEventBusStore } from '@/stores/eventBus'
import type { SocketIoClientService } from '@/services/SocketIoClientService'
export class SocketIoEventManager { export class SocketIoEventManager {
gameStore: any = useGameStore() gameStore: any = useGameStore()
eventBus = useEventBusStore() eventBus = useEventBusStore()
callbacksMap: Map<string, any> = new Map()
constructor(private socketService: SocketIoClientService) {}
handleGameEvent(gameEvent: SocketEvent) { handleGameEvent(gameEvent: SocketEvent) {
const { event, data } = gameEvent 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) { switch (event) {
case 'update-match-session-state': case 'update-match-session-state':
this.updateSessionState(data) this.updateSessionState(data)
@ -34,54 +24,45 @@ export class SocketIoEventManager {
case 'update-player-state': case 'update-player-state':
this.updatePlayerState(data) this.updatePlayerState(data)
break break
case 'ask-client-for-move': case 'server:player-turn':
return this.handleCanMakeMoveEvent(data) this.onCanMakeMoveEvent(data)
break
default: default:
this.eventBus.publish(event, data) this.eventBus.publish(event, data)
break break
} }
return { status: 'ok' } }
} catch (error) {
return { status: 'error', error } handleGameEventAck(gameEvent: SocketEvent, callback: any) {
const { event, data } = gameEvent
switch (event) {
// case 'ask-client-for-move':
// return this.handleCanMakeMoveEvent(data, callback)
default:
this.eventBus.publish(event, data)
break
} }
} }
private updateSessionState(data: MatchSessionState) { private updateSessionState(data: MatchSessionDto) {
const { updateSessionState } = this.gameStore const { updateSessionState } = this.gameStore
updateSessionState(data) updateSessionState(data)
return {
status: 'ok'
}
} }
private updateGameState(data: any) { private updateGameState(data: any) {
const { updateGameState } = this.gameStore const { updateGameState } = this.gameStore
updateGameState(data) updateGameState(data)
return {
status: 'ok'
}
} }
private updatePlayerState(data: any) { private updatePlayerState(data: any) {
const { updatePlayerState } = this.gameStore const { updatePlayerState } = this.gameStore
updatePlayerState(data) updatePlayerState(data)
return {
status: 'ok'
}
} }
private async handleCanMakeMoveEvent(data: any) { private onCanMakeMoveEvent(data: any) {
const { canMakeMove, moveToMake } = storeToRefs(this.gameStore) const { setIncomingFreeEnds } = this.gameStore
const { updateCanMakeMove, setIncomingFreeEnds } = this.gameStore setIncomingFreeEnds(data.firstMove ? undefined : data.freeHands)
setIncomingFreeEnds(data.freeHands) this.eventBus.publish('game:player-turn-started', data)
updateCanMakeMove(true)
while (canMakeMove.value) {
await wait(500)
}
return {
status: 'ok',
...moveToMake.value
}
} }
private async handleCanSelectTileEvent() { private async handleCanSelectTileEvent() {

View File

@ -36,6 +36,20 @@ const router = createRouter({
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
// component: () => import('../views/AboutView.vue') // component: () => import('../views/AboutView.vue')
}, },
{
path: '/match/:id',
component: AuthenticatedLayout,
children: [
{
path: '',
name: 'match',
component: () => import('@/views/MatchView.vue'),
meta: {
requiresAuth: true
}
}
]
},
{ {
path: '/game', path: '/game',
component: AuthenticatedLayout, component: AuthenticatedLayout,

View File

@ -4,7 +4,7 @@ import { ServiceBase } from './ServiceBase'
export class GameService extends ServiceBase { export class GameService extends ServiceBase {
private networkService = new NetworkService() private networkService = new NetworkService()
async createMatch(sessionName: string, seed: string) { async createMatchSession(sessionName: string, seed: string) {
const response = await this.networkService.post({ const response = await this.networkService.post({
uri: '/game/match', uri: '/game/match',
body: { sessionName, seed }, body: { sessionName, seed },
@ -13,4 +13,36 @@ export class GameService extends ServiceBase {
const { sessionId } = response const { sessionId } = response
return sessionId return sessionId
} }
async cancelMatchSession(sessionId: string) {
const response = await this.networkService.delete({
uri: `/game/match/${sessionId}`,
auth: true
})
return response
}
async joinMatchSession(sessionId: string) {
const response = await this.networkService.put({
uri: `/game/match/${sessionId}`,
auth: true
})
return response
}
async listMatchSessions() {
const response = await this.networkService.get({
uri: '/game/match',
auth: true
})
return response
}
async getMatchSession(sessionId: string) {
const response = await this.networkService.get({
uri: `/game/match/${sessionId}`,
auth: true
})
return response
}
} }

View File

@ -23,6 +23,11 @@ export class NetworkService {
return await this.request(options) return await this.request(options)
} }
async put(options: RequestOptions) {
options.method = 'PUT'
return await this.request(options)
}
async patch(options: RequestOptions) { async patch(options: RequestOptions) {
options.method = 'PATCH' options.method = 'PATCH'
return await this.request(options) return await this.request(options)

View File

@ -1,4 +1,3 @@
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 { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
@ -8,10 +7,11 @@ import { ServiceBase } from './ServiceBase'
export class SocketIoClientService extends ServiceBase { export class SocketIoClientService extends ServiceBase {
private socket!: Socket private socket!: Socket
private isConnected = false private isConnected = false
private gameEventManager = new SocketIoEventManager() private gameEventManager: SocketIoEventManager
constructor(private url: string) { constructor(private url: string) {
super() super()
this.gameEventManager = new SocketIoEventManager(this)
} }
async connect(): Promise<void> { async connect(): Promise<void> {
@ -70,18 +70,19 @@ export class SocketIoClientService extends ServiceBase {
// callback(await this.gameEventManager.handleCanSelectTileEvent()) // callback(await this.gameEventManager.handleCanSelectTileEvent())
// }) // })
this.socket.on('game-event', (data: any) => { this.socket.on('server:game-event', (data: any) => {
this.gameEventManager.handleGameEvent(data) this.gameEventManager.handleGameEvent(data)
}) })
this.socket.on('game-event-ack', async (data: any, callback: any) => { this.socket.on('server:game-event-ack', async (data: any, callback: any) => {
callback(await this.gameEventManager.handleGameEventAck(data)) await this.gameEventManager.handleGameEventAck(data, callback)
}) })
} }
sendMessage(event: string, data: any): void { sendMessage(event: string, data: any): void {
if (this.isConnected) { if (this.isConnected) {
this.socket?.emit(event, data) this.socket?.emit('client:event', { event, data })
console.log('sendMessage :>> ', event, data)
} else { } else {
console.log('Not connected to server') console.log('Not connected to server')
} }
@ -89,7 +90,8 @@ export class SocketIoClientService extends ServiceBase {
async sendMessageWithAck(event: string, data: any): Promise<any> { async sendMessageWithAck(event: string, data: any): Promise<any> {
if (this.isConnected) { if (this.isConnected) {
return await this.socket?.emitWithAck(event, data) console.log('sendMessageWithAck :>> ', event, data)
return await this.socket?.emitWithAck('client:event-with-ack', { event, data })
} else { } else {
console.log('Not connected to server') console.log('Not connected to server')
} }

View File

@ -1,15 +1,14 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { MatchSessionState, GameState, Movement, PlayerDto } from '@/common/interfaces' import type { MatchSessionDto, GameDto, Movement, PlayerDto } from '@/common/interfaces'
export const useGameStore = defineStore('game', () => { export const useGameStore = defineStore('game', () => {
const sessionState = ref<MatchSessionState | undefined>(undefined) const sessionState = ref<MatchSessionDto | undefined>(undefined)
const gameState = ref<GameState | undefined>(undefined) const gameState = ref<GameDto | undefined>(undefined)
const playerState = ref<PlayerDto | undefined>(undefined) const playerState = ref<PlayerDto | undefined>(undefined)
const canMakeMove = ref(false) const canMakeMove = ref(false)
const canSelectTile = ref(false) const canSelectTile = ref(false)
const gameFinished = ref(false) const gameFinished = ref(false)
const readyForStart = ref(false)
const moveToMake = ref<Movement | undefined>(undefined) const moveToMake = ref<Movement | undefined>(undefined)
const incomingFreeEnds = ref<[number, number] | undefined>(undefined) const incomingFreeEnds = ref<[number, number] | undefined>(undefined)
const showReadyButton = ref(false) const showReadyButton = ref(false)
@ -21,16 +20,17 @@ export const useGameStore = defineStore('game', () => {
playerState.value !== undefined && playerState.value !== undefined &&
playerState.value.id === sessionState.value.creator playerState.value.id === sessionState.value.creator
) )
const readyForStart = computed(() => playerState.value !== undefined && playerState.value.ready)
function updateSessionState(newState: MatchSessionState) { function updateSessionState(newState: MatchSessionDto | undefined) {
sessionState.value = newState sessionState.value = newState
} }
function updateGameState(newState: GameState) { function updateGameState(newState: GameDto | undefined) {
gameState.value = newState gameState.value = newState
} }
function updatePlayerState(newState: PlayerDto) { function updatePlayerState(newState: PlayerDto | undefined) {
playerState.value = newState playerState.value = newState
} }
@ -54,10 +54,6 @@ export const useGameStore = defineStore('game', () => {
showReadyButton.value = value showReadyButton.value = value
} }
function setReadyForStart(value: boolean) {
readyForStart.value = value
}
function updateGameFinished(value: boolean) { function updateGameFinished(value: boolean) {
gameFinished.value = value gameFinished.value = value
} }
@ -81,7 +77,6 @@ export const useGameStore = defineStore('game', () => {
setIncomingFreeEnds, setIncomingFreeEnds,
updateCanSelectTile, updateCanSelectTile,
setShowReadyButton, setShowReadyButton,
setReadyForStart,
updateGameFinished, updateGameFinished,
isSessionStarted, isSessionStarted,
amIHost amIHost

View File

@ -2,19 +2,17 @@
import GameComponent from '@/components/GameComponent.vue' import GameComponent from '@/components/GameComponent.vue'
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { inject, onBeforeUnmount, ref } from 'vue' import { onBeforeUnmount } from 'vue'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import useClipboard from 'vue-clipboard3' import useClipboard from 'vue-clipboard3'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const socketService: any = inject('socket')
const { toClipboard } = useClipboard() const { toClipboard } = useClipboard()
const gameStore = useGameStore() const gameStore = useGameStore()
const { moveToMake, canMakeMove, sessionState, gameState, playerState } = storeToRefs(gameStore) const { moveToMake, canMakeMove, sessionState, gameState, playerState } = storeToRefs(gameStore)
onMounted(async () => { onMounted(async () => {
startMatch() // startMatch()
}) })
if (!playerState?.value) { if (!playerState?.value) {
@ -25,25 +23,13 @@ if (!playerState?.value) {
function makeMove(move: any) { function makeMove(move: any) {
moveToMake.value = move moveToMake.value = move
canMakeMove.value = false canMakeMove.value = false
console.log('makemove :>> ', move)
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
// socketService.disconnect() // socketService.disconnect()
}) })
async function startMatch() {
const sessionId = sessionState?.value?.id
const seed = sessionState?.value?.seed
const playerId = playerState?.value?.id
if (sessionId) {
await socketService.sendMessageWithAck('startSession', {
sessionId: sessionId,
playerId: playerId,
seed: seed?.trim()
})
}
}
function copySeed() { function copySeed() {
if (sessionState?.value?.seed) toClipboard(sessionState.value.seed) if (sessionState?.value?.seed) toClipboard(sessionState.value.seed)
} }
@ -63,7 +49,8 @@ function copySeed() {
- Score: {{ sessionState?.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 }} - canMakeMove
{{ canMakeMove }}
</p> </p>
</section> </section>
<section class="block"> <section class="block">

View File

@ -1,17 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject, ref } from 'vue' import { inject, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/game' import { useGameStore } from '@/stores/game'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { LoggingService } from '@/services/LoggingService' import type { LoggingService } from '@/services/LoggingService'
import type { GameService } from '@/services/GameService' import type { GameService } from '@/services/GameService'
import type { MatchSessionDto } from '@/common/interfaces'
import { useEventBusStore } from '@/stores/eventBus'
import { useAuthStore } from '@/stores/auth'
let seed = ref('') let seed = ref('')
let sessionName = ref('Test Value') let sessionName = ref('Test Value')
let sessionId = ref('') let sessionId = ref('')
let matchSessions = ref<MatchSessionDto[]>([])
let dataInterval: any
const router = useRouter() const router = useRouter()
const gameStore = useGameStore() const gameStore = useGameStore()
const auth = useAuthStore()
const socketService: any = inject('socket') const socketService: any = inject('socket')
const gameService: GameService = inject<GameService>('game') as GameService const gameService: GameService = inject<GameService>('game') as GameService
@ -19,41 +25,58 @@ const logger: LoggingService = inject<LoggingService>('logger') as LoggingServic
const { readyForStart, sessionState, isSessionStarted, playerState, amIHost } = const { readyForStart, sessionState, isSessionStarted, playerState, amIHost } =
storeToRefs(gameStore) storeToRefs(gameStore)
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
const { user } = storeToRefs(auth)
async function setPlayerReady() { // function setPlayerReady() {
logger.debug('Starting game') // logger.debug('Starting game')
if (!sessionState.value) { // if (!sessionState.value) {
logger.error('No session found') // logger.error('No session found')
return // return
} // }
if (!playerState.value) { // if (!playerState.value) {
logger.error('No player found') // logger.error('No player found')
return // return
} // }
await socketService.sendMessageWithAck('playerReady', { // socketService.sendMessage('client:set-player-ready', {
userId: playerState.value.id, // userId: playerState.value.id,
sessionId: sessionState.value.id // sessionId: sessionState.value.id
// })
// }
const eventBus = useEventBusStore()
eventBus.subscribe('window-before-unload', () => {
logger.debug('Window before unload')
}) })
readyForStart.value = true
}
async function createMatch() { async function createMatch() {
logger.debug('Creating match') logger.debug('Creating match')
await socketService.connect() await socketService.connect()
sessionId.value = await gameService.createMatch(sessionName.value, seed.value) const id = await gameService.createMatchSession(sessionName.value, seed.value)
logger.debug('Match reated successfully') logger.debug('Match created successfully')
router.push({ name: 'match', params: { id } })
} }
async function joinMatch() { async function cancelMatch() {
const sessionId = sessionState?.value?.id logger.debug('Cancelling match')
const playerId = playerState?.value?.id await gameService.cancelMatchSession(sessionId.value)
if (sessionId && playerId) { await socketService.disconnect()
await socketService.sendMessageWithAck('joinSession', { sessionId.value = ''
user: 'pepe', seed.value = ''
sessionId: sessionId sessionName.value = ''
}) updateSessionState(undefined)
// sessionId.value = response.sessionId updatePlayerState(undefined)
// playerId.value = response.playerId updateGameState(undefined)
logger.debug('Match cancelled successfully')
loadData()
}
async function joinMatch(id: string) {
if (id) {
await socketService.connect()
await gameService.joinMatchSession(id)
router.push({ name: 'match', params: { id } })
} }
} }
@ -62,12 +85,28 @@ async function startMatch() {
router.push({ name: 'game' }) router.push({ name: 'game' })
} }
} }
async function loadData() {
matchSessions.value = await gameService.listMatchSessions()
sessionName.value = `Test #${matchSessions.value.length + 1}`
}
onMounted(() => {
logger.debug('Home view mounted')
loadData()
dataInterval = setInterval(loadData, 5000)
})
onUnmounted(() => {
logger.debug('Home view unmounted')
clearInterval(dataInterval)
})
</script> </script>
<template> <template>
<div class="block home"> <div class="block home">
<section class="section"> <section class="section">
<h1 class="title is-2">Welcome to the Player's Home Page</h1> <h1 class="title is-2">Welcome to the {{ user.username }}'s Home Page</h1>
<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>
@ -93,17 +132,30 @@ async function startMatch() {
<button class="button" @click="createMatch" v-if="!isSessionStarted"> <button class="button" @click="createMatch" v-if="!isSessionStarted">
Create Match Session Create Match Session
</button> </button>
<button class="button" @click="setPlayerReady" v-if="isSessionStarted"> <!-- <button class="button" @click="setPlayerReady" v-if="isSessionStarted">
<span v-if="!readyForStart">Ready</span><span v-else>Unready</span> <span v-if="!readyForStart">Ready</span><span v-else>Unready</span>
</button> </button> -->
<button class="button" @click="startMatch" v-if="readyForStart"> <!-- <button class="button" @click="startMatch" v-if="readyForStart">
<span>Start</span> <span>Start</span>
</button> </button>
<button class="button" @click="cancelMatch" v-if="isSessionStarted">
<span>Cancel</span>
</button> -->
</section> </section>
<section class="section available-sessions"> <section class="section available-sessions" v-if="!isSessionStarted">
<h2 class="title is-4">Available Sessions</h2> <h2 class="title is-4">Available Sessions</h2>
<div class="bloc"> <div class="block">
<p>There are no available sessions at the moment.</p> <div v-if="matchSessions.length === 0">
<p>No sessions available</p>
</div>
<div v-else v-for="session in matchSessions" :key="session.id">
<p>{{ session.name }}</p>
<p>{{ session }}</p>
<button class="button" @click="() => joinMatch(session._id)">
Join ({{ session._id }})
</button>
</div>
</div> </div>
</section> </section>
</div> </div>

114
src/views/MatchView.vue Normal file
View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import type { MatchSessionDto } from '@/common/interfaces'
import type { GameService } from '@/services/GameService'
import type { LoggingService } from '@/services/LoggingService'
import { useEventBusStore } from '@/stores/eventBus'
import { useGameStore } from '@/stores/game'
import { storeToRefs } from 'pinia'
import { inject, onBeforeMount, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const gameStore = useGameStore()
const eventBus = useEventBusStore()
const socketService: any = inject('socket')
const gameService: GameService = inject<GameService>('game') as GameService
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
let sessionId: string
let matchSession = ref<MatchSessionDto | undefined>(undefined)
const { readyForStart, sessionState, isSessionStarted, playerState, amIHost } =
storeToRefs(gameStore)
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
async function setPlayerReady() {
logger.debug('Starting game')
if (!sessionState.value) {
logger.error('No session found')
return
}
if (!playerState.value) {
logger.error('No player found')
return
}
await socketService.sendMessage('client:set-player-ready', {
userId: playerState.value.id,
sessionId: sessionState.value.id
})
}
async function startMatch() {
const sessionId = sessionState?.value?.id
const playerId = playerState?.value?.id
if (sessionId) {
await socketService.sendMessageWithAck('client:start-session', {
sessionId: sessionId,
playerId: playerId
})
}
}
async function cancelMatch() {
logger.debug('Cancelling match')
await gameService.cancelMatchSession(sessionId)
updateSessionState(undefined)
updatePlayerState(undefined)
updateGameState(undefined)
logger.debug('Match cancelled successfully')
router.push({ name: 'home' })
}
async function loadData() {
await gameService.getMatchSession(sessionId)
}
eventBus.subscribe('window-before-unload', async () => {
logger.debug('Window before unload')
await cancelMatch()
})
eventBus.subscribe('server:match-starting', () => {
logger.debug('Match starting')
router.push({ name: 'game' })
})
onBeforeMount(() => {
sessionId = route.params.id as string
if (sessionId) {
setInterval(loadData, 5000)
} else {
router.push({ name: 'home' })
}
})
</script>
<template>
<div>
<h1 class="title is-2">Match Page</h1>
<div class="block" v-if="matchSession">
<p>Session ID: {{ matchSession._id }}</p>
<p>Session Name: {{ matchSession.name }}</p>
<p>Session started: {{ isSessionStarted }}</p>
<p>Host: {{ amIHost }}</p>
<p>{{ sessionState || 'No session' }}</p>
</div>
<div class="block">
<p v-if="!amIHost && !readyForStart">Waiting for host to start session</p>
<button class="button" @click="setPlayerReady" v-if="isSessionStarted">
<span v-if="!readyForStart">Ready</span><span v-else>Unready</span>
</button>
<button class="button" @click="startMatch" v-if="amIHost && readyForStart">
<span>Start</span>
</button>
<button class="button" @click="cancelMatch" v-if="isSessionStarted">
<span>Cancel</span>
</button>
</div>
</div>
</template>
<style scoped lang="scss"></style>