reworked event communication
This commit is contained in:
		
							
								
								
									
										21
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								src/App.vue
									
									
									
									
									
								
							@@ -1,10 +1,29 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { inject } from 'vue'
 | 
			
		||||
import { inject, onMounted, onUnmounted } from 'vue'
 | 
			
		||||
import { RouterView } from 'vue-router'
 | 
			
		||||
import type { AuthenticationService } from './services/AuthenticationService'
 | 
			
		||||
import { useEventBusStore } from './stores/eventBus'
 | 
			
		||||
 | 
			
		||||
const auth: AuthenticationService = inject<AuthenticationService>('auth') as AuthenticationService
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/backgrounds/bg-green.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/images/backgrounds/bg-green.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.9 MiB  | 
@@ -1,7 +1,13 @@
 | 
			
		||||
export const defaultContainerOptions = {
 | 
			
		||||
export const DEFAULT_CONTAINER_OPTIONS = {
 | 
			
		||||
  width: 100,
 | 
			
		||||
  height: 100,
 | 
			
		||||
  x: 0,
 | 
			
		||||
  y: 0,
 | 
			
		||||
  visible: true
 | 
			
		||||
}
 | 
			
		||||
export const ORIENTATION_ANGLES: { [key: string]: number } = {
 | 
			
		||||
  north: 0,
 | 
			
		||||
  east: Math.PI / 2,
 | 
			
		||||
  south: Math.PI,
 | 
			
		||||
  west: (3 * Math.PI) / 2
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Graphics, Container } from 'pixi.js'
 | 
			
		||||
import type { ContainerOptions, TileDto } from './interfaces'
 | 
			
		||||
import { defaultContainerOptions } from './constants'
 | 
			
		||||
import { Graphics, Container, Text } from 'pixi.js'
 | 
			
		||||
import type { ContainerOptions, Dimension, TileDto } from './interfaces'
 | 
			
		||||
import { DEFAULT_CONTAINER_OPTIONS } from './constants'
 | 
			
		||||
 | 
			
		||||
export function getColorBackground(container: Container, colorName: string, alpha: number = 0.5) {
 | 
			
		||||
  const graphics = new Graphics()
 | 
			
		||||
@@ -14,7 +14,7 @@ export function getColorBackground(container: Container, colorName: string, alph
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createContainer(options: ContainerOptions) {
 | 
			
		||||
  const opts = { ...defaultContainerOptions, ...options }
 | 
			
		||||
  const opts = { ...DEFAULT_CONTAINER_OPTIONS, ...options }
 | 
			
		||||
  const container = new Container()
 | 
			
		||||
 | 
			
		||||
  const rect = new Graphics().rect(opts.x, opts.y, opts.width, opts.height)
 | 
			
		||||
@@ -30,6 +30,54 @@ export function createContainer(options: ContainerOptions) {
 | 
			
		||||
  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) {
 | 
			
		||||
  return new Promise((resolve) => setTimeout(resolve, ms))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,10 @@ export interface TileDto {
 | 
			
		||||
  y?: number
 | 
			
		||||
  width?: number
 | 
			
		||||
  height?: number
 | 
			
		||||
  playerId?: string
 | 
			
		||||
}
 | 
			
		||||
export interface MatchSessionState {
 | 
			
		||||
export interface MatchSessionDto {
 | 
			
		||||
  _id: string
 | 
			
		||||
  id: string
 | 
			
		||||
  name: string
 | 
			
		||||
  creator: string
 | 
			
		||||
@@ -38,7 +40,7 @@ export interface MatchSessionState {
 | 
			
		||||
  playersReady: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GameState {
 | 
			
		||||
export interface GameDto {
 | 
			
		||||
  id: string
 | 
			
		||||
  players: PlayerDto[]
 | 
			
		||||
  tilesInBoneyard: TileDto[]
 | 
			
		||||
@@ -85,3 +87,13 @@ export interface SocketEvent {
 | 
			
		||||
  event: string
 | 
			
		||||
  data: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AnimationOptions {
 | 
			
		||||
  x?: number
 | 
			
		||||
  y?: number
 | 
			
		||||
  rotation?: number
 | 
			
		||||
  scale?: number
 | 
			
		||||
  duration?: number
 | 
			
		||||
  width?: number
 | 
			
		||||
  height?: number
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<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 { Game } from '@/game/Game'
 | 
			
		||||
import { useGameStore } from '@/stores/game'
 | 
			
		||||
@@ -13,7 +13,8 @@ const socketService: any = inject('socket')
 | 
			
		||||
 | 
			
		||||
const gameStore = useGameStore()
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
@@ -30,6 +31,31 @@ const game = new Game(
 | 
			
		||||
  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 () => {
 | 
			
		||||
  if (appEl.value === null) return
 | 
			
		||||
  const canvas = await game.setup()
 | 
			
		||||
@@ -37,41 +63,41 @@ onMounted(async () => {
 | 
			
		||||
  await game.preload()
 | 
			
		||||
  await game.start()
 | 
			
		||||
 | 
			
		||||
  eventBus.subscribe('game-finished', () => {
 | 
			
		||||
    game.gameFinished()
 | 
			
		||||
  eventBus.subscribe('server:game-finished', (data) => {
 | 
			
		||||
    console.log('server:game-finished :>> ', data)
 | 
			
		||||
    game.gameFinished(data)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => gameStore.canMakeMove,
 | 
			
		||||
    (value: boolean) => {
 | 
			
		||||
      game.setCanMakeMove(value)
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  eventBus.subscribe('server:server-player-move', (data) => {
 | 
			
		||||
    console.log('server:player-move :>> ', data)
 | 
			
		||||
    game.serverPlayerMove(data, playerState.value?.id ?? '')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => gameStore.gameState,
 | 
			
		||||
    (value: GameState | undefined) => {
 | 
			
		||||
      if (value === undefined) return
 | 
			
		||||
      game.board?.setState(value, playerState.value?.id ?? '')
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  eventBus.subscribe('game:player-turn-started', (data: any) => {
 | 
			
		||||
    console.log('game:player-turn-started :>> ', data)
 | 
			
		||||
    // game.setCanMakeMove(true)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => gameStore.sessionState,
 | 
			
		||||
    (value: MatchSessionState | undefined) => {
 | 
			
		||||
      if (value === undefined) return
 | 
			
		||||
  eventBus.subscribe('server:hand-dealt', (playerState: PlayerDto) => {
 | 
			
		||||
    console.log('server:hand-dealt :>> ', playerState)
 | 
			
		||||
    game.hand.update(playerState)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
      // logger.debug('gameSessionState-------------------------------------- :>> ', value)
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  eventBus.subscribe('server:next-turn', (gameState: GameDto) => {
 | 
			
		||||
    console.log('server:next-turn :>> ', gameState)
 | 
			
		||||
    updateGameState(gameState)
 | 
			
		||||
    game.setNextPlayer(gameState)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => gameStore.playerState,
 | 
			
		||||
    (value: PlayerDto | undefined) => {
 | 
			
		||||
      if (value === undefined) return
 | 
			
		||||
      game.hand.update(value as PlayerDto)
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  eventBus.subscribe('server:match-finished', (data) => {
 | 
			
		||||
    console.log('server:match-finished :>> ', data)
 | 
			
		||||
    game.matchFinished(data)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // eventBus.subscribe('server:game-state', (data) => {
 | 
			
		||||
  //   console.log('server:game-state :>> ', data)
 | 
			
		||||
  //   game.serverGameState(data)
 | 
			
		||||
  // })
 | 
			
		||||
 | 
			
		||||
  // mockMove(game, [6, 6], 'left')
 | 
			
		||||
  // mockMove(game, [6, 4], 'left')
 | 
			
		||||
 
 | 
			
		||||
@@ -9,13 +9,14 @@ import {
 | 
			
		||||
  Ticker
 | 
			
		||||
} from 'pixi.js'
 | 
			
		||||
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 { DIRECTIONS, createContainer, isTilePair, isTileVertical } from '@/common/helpers'
 | 
			
		||||
import { DIRECTIONS, createContainer, isTilePair } from '@/common/helpers'
 | 
			
		||||
import { createText } from '@/game/utilities/fonts'
 | 
			
		||||
import { LoggingService } from '@/services/LoggingService'
 | 
			
		||||
import { inject } from 'vue'
 | 
			
		||||
import { GlowFilter } from 'pixi-filters'
 | 
			
		||||
import { ORIENTATION_ANGLES } from '@/common/constants'
 | 
			
		||||
 | 
			
		||||
export class Board extends EventEmitter {
 | 
			
		||||
  private _scale: number = 1
 | 
			
		||||
@@ -28,7 +29,6 @@ export class Board extends EventEmitter {
 | 
			
		||||
  grain: number = 25
 | 
			
		||||
  scaleY: ScaleFunction
 | 
			
		||||
  scaleX: ScaleFunction
 | 
			
		||||
  state?: GameState
 | 
			
		||||
  container!: Container
 | 
			
		||||
  initialContainer!: Container
 | 
			
		||||
  tilesContainer!: Container
 | 
			
		||||
@@ -49,6 +49,7 @@ export class Board extends EventEmitter {
 | 
			
		||||
  rightDirection: string = 'east'
 | 
			
		||||
  playerHand: Tile[] = []
 | 
			
		||||
  firstTile?: Tile
 | 
			
		||||
  currentPlayer!: PlayerDto
 | 
			
		||||
 | 
			
		||||
  constructor(app: Application) {
 | 
			
		||||
    super()
 | 
			
		||||
@@ -65,7 +66,7 @@ export class Board extends EventEmitter {
 | 
			
		||||
      parent: app.stage
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const background = new Sprite(Assets.get('bg-1'))
 | 
			
		||||
    const background = new Sprite(Assets.get('bg-green'))
 | 
			
		||||
    // background.width = this.width
 | 
			
		||||
    // background.height = this.height
 | 
			
		||||
    this.container.addChild(background)
 | 
			
		||||
@@ -160,10 +161,12 @@ export class Board extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setState(state: GameState, playerId: string) {
 | 
			
		||||
    this.state = state
 | 
			
		||||
    const { lastMove } = state
 | 
			
		||||
  async setServerPlayerTurn(currentPlayer: PlayerDto) {
 | 
			
		||||
    this.showText(`${currentPlayer.name}'s turn!\n Please wait...`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async playerMove(move: any, playerId: string) {
 | 
			
		||||
    const { move: lastMove } = move
 | 
			
		||||
    if (lastMove === null) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
@@ -177,12 +180,13 @@ export class Board extends EventEmitter {
 | 
			
		||||
      this.nextTile = tile
 | 
			
		||||
      lastMove.tile = tile.toPlain()
 | 
			
		||||
      this.movements.push(lastMove)
 | 
			
		||||
      this.addTile(tile, lastMove)
 | 
			
		||||
      await this.addTile(tile, lastMove)
 | 
			
		||||
      this.setFreeEnd(lastMove)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addTile(tile: Tile, move: Movement) {
 | 
			
		||||
  async addTile(tile: Tile, move: Movement) {
 | 
			
		||||
    console.log('adding tile', tile.pips)
 | 
			
		||||
    let orientation = ''
 | 
			
		||||
    let x: number =
 | 
			
		||||
      move.type === 'left'
 | 
			
		||||
@@ -244,14 +248,25 @@ export class Board extends EventEmitter {
 | 
			
		||||
        availablePosition && ([x, y] = availablePosition)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const endTile = isLeft ? this.leftTile : this.rightTile
 | 
			
		||||
    const isEndVertical = endTile?.isVertical() ?? false
 | 
			
		||||
    const isNextVertical = orientation === 'north' || orientation === 'south'
 | 
			
		||||
    tile.setPosition(this.scaleX(x), this.scaleY(y))
 | 
			
		||||
    tile.setOrientation(orientation)
 | 
			
		||||
    tile.addTo(this.tilesContainer)
 | 
			
		||||
    tile.reScale(this.scale)
 | 
			
		||||
    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 {
 | 
			
		||||
@@ -300,7 +315,7 @@ export class Board extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateBoard(move: Movement) {
 | 
			
		||||
  async updateBoard(move: Movement) {
 | 
			
		||||
    try {
 | 
			
		||||
      const { tile: tileDto } = move
 | 
			
		||||
      const tile = this.getTileInHand(tileDto?.id ?? '')
 | 
			
		||||
@@ -310,7 +325,7 @@ export class Board extends EventEmitter {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.movements.push(move)
 | 
			
		||||
      this.addTile(tile, move)
 | 
			
		||||
      await this.addTile(tile, move)
 | 
			
		||||
      this.setFreeEnd(move)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.logger.error(error, 'Error updating board')
 | 
			
		||||
@@ -437,7 +452,7 @@ export class Board extends EventEmitter {
 | 
			
		||||
    dot.alpha = 0.1
 | 
			
		||||
    dot.interactive = true
 | 
			
		||||
    dot.on('pointerdown', () => {
 | 
			
		||||
      this.emit(`${side}Click`, direction && { direction, x, y })
 | 
			
		||||
      this.emit(`game:board-${side}-action-click`, direction && { direction, x, y })
 | 
			
		||||
      this.cleanInteractions()
 | 
			
		||||
    })
 | 
			
		||||
    dot.on('pointerover', () => {
 | 
			
		||||
@@ -566,7 +581,8 @@ export class Board extends EventEmitter {
 | 
			
		||||
    return [canPlayNorth, canPlayEast, canPlaySouth, canPlayWest]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  gameFinished() {
 | 
			
		||||
  gameFinished(data: any) {
 | 
			
		||||
    const { lastGame, gameState } = data
 | 
			
		||||
    this.tiles = []
 | 
			
		||||
    this.boneyard = []
 | 
			
		||||
    this.movements = []
 | 
			
		||||
@@ -580,6 +596,10 @@ export class Board extends EventEmitter {
 | 
			
		||||
    this.firstTile = undefined
 | 
			
		||||
    this.tilesContainer.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'}`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										118
									
								
								src/game/Game.ts
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								src/game/Game.ts
									
									
									
									
									
								
							@@ -3,14 +3,19 @@ import { Board } from '@/game/Board'
 | 
			
		||||
import { assets } from '@/game/utilities/assets'
 | 
			
		||||
import { Tile } from '@/game/Tile'
 | 
			
		||||
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 { useEventBusStore } from '@/stores/eventBus'
 | 
			
		||||
import { wait } from '@/common/helpers'
 | 
			
		||||
import { Actions } from 'pixi-actions'
 | 
			
		||||
 | 
			
		||||
export class Game {
 | 
			
		||||
  public board!: Board
 | 
			
		||||
  public hand!: Hand
 | 
			
		||||
  private app: Application = new Application()
 | 
			
		||||
  private selectedTile: TileDto | undefined
 | 
			
		||||
  private eventBus: any = useEventBusStore()
 | 
			
		||||
  private currentMove: Movement | undefined
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private options: { boardScale: number; handScale: number; width: number; height: number } = {
 | 
			
		||||
@@ -30,16 +35,23 @@ export class Game {
 | 
			
		||||
    const height = 800
 | 
			
		||||
 | 
			
		||||
    await this.app.init({ width, height })
 | 
			
		||||
    this.app.ticker.add((tick) => Actions.tick(tick.deltaTime / 60))
 | 
			
		||||
    return this.app.canvas
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  start() {
 | 
			
		||||
  async start() {
 | 
			
		||||
    this.board = new Board(this.app)
 | 
			
		||||
    this.hand = new Hand(this.app)
 | 
			
		||||
    this.hand.scale = this.options.handScale
 | 
			
		||||
    this.board.scale = this.options.boardScale
 | 
			
		||||
    this.setBoardEvents()
 | 
			
		||||
    this.setHandEvents()
 | 
			
		||||
    this.initEventBus()
 | 
			
		||||
    wait(3000)
 | 
			
		||||
    this.socketService.sendMessage('client:set-client-ready', {
 | 
			
		||||
      sessionId: this.sessionId,
 | 
			
		||||
      userId: this.playerId
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async preload() {
 | 
			
		||||
@@ -52,35 +64,44 @@ export class Game {
 | 
			
		||||
    this.app.destroy()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private initEventBus() {}
 | 
			
		||||
 | 
			
		||||
  private setHandEvents() {
 | 
			
		||||
    this.hand.on('handUpdated', (tiles: Tile[]) => {
 | 
			
		||||
    this.hand.on('hand-updated', (tiles: Tile[]) => {
 | 
			
		||||
      this.board.setPlayerHand(tiles)
 | 
			
		||||
    })
 | 
			
		||||
    this.hand.on('tileClick', (tile: TileDto) => {
 | 
			
		||||
      this.selectedTile = tile
 | 
			
		||||
      if (tile !== undefined) {
 | 
			
		||||
        this.board.setMovesForTile(this.getMoves(tile), tile)
 | 
			
		||||
      } else {
 | 
			
		||||
        this.board.cleanInteractions()
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    this.hand.on('game:tile-click', (tile: TileDto) => this.highlightMoves(tile))
 | 
			
		||||
 | 
			
		||||
    this.hand.on('passClick', () => {
 | 
			
		||||
    this.hand.on('game:button-pass-click', async () => {
 | 
			
		||||
      const move: Movement = {
 | 
			
		||||
        id: '',
 | 
			
		||||
        type: 'pass',
 | 
			
		||||
        playerId: this.playerId
 | 
			
		||||
      }
 | 
			
		||||
      this.emit('move', move)
 | 
			
		||||
      this.board.updateBoard(move)
 | 
			
		||||
      this.socketService.sendMessage('client:player-move', {
 | 
			
		||||
        sessionId: this.sessionId,
 | 
			
		||||
        move: move
 | 
			
		||||
      })
 | 
			
		||||
      await this.board.updateBoard(move)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.hand.on('nextClick', async () => {
 | 
			
		||||
      await this.socketService.sendMessageWithAck('playerReady', {
 | 
			
		||||
    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
 | 
			
		||||
    if (tile !== undefined) {
 | 
			
		||||
      this.board.setMovesForTile(this.getMoves(tile), tile)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.board.cleanInteractions()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMoves(tile: any): [boolean, boolean] {
 | 
			
		||||
@@ -100,12 +121,22 @@ export class Game {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setCanMakeMove(value: boolean) {
 | 
			
		||||
    this.hand.canMove = value
 | 
			
		||||
    this.hand.setCanMove(value, this.board.count === 0, this.board.freeEnds)
 | 
			
		||||
    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() {
 | 
			
		||||
    this.board.on('leftClick', (data) => {
 | 
			
		||||
    this.board.on('game:board-left-action-click', async (data) => {
 | 
			
		||||
      console.log('left data :>> ', data)
 | 
			
		||||
      if (this.selectedTile === undefined) return
 | 
			
		||||
      const move: Movement = {
 | 
			
		||||
@@ -114,12 +145,12 @@ export class Game {
 | 
			
		||||
        playerId: this.playerId,
 | 
			
		||||
        ...data
 | 
			
		||||
      }
 | 
			
		||||
      this.emit('move', move)
 | 
			
		||||
      this.currentMove = move
 | 
			
		||||
      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)
 | 
			
		||||
      if (this.selectedTile === undefined) return
 | 
			
		||||
      const move: Movement = {
 | 
			
		||||
@@ -128,24 +159,53 @@ export class Game {
 | 
			
		||||
        playerId: this.playerId,
 | 
			
		||||
        ...data
 | 
			
		||||
      }
 | 
			
		||||
      this.emit('move', move)
 | 
			
		||||
      this.currentMove = move
 | 
			
		||||
      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.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() {
 | 
			
		||||
    this.board.off('leftClick')
 | 
			
		||||
    this.board.off('rightClick')
 | 
			
		||||
    this.board.off('game:board-left-action-click')
 | 
			
		||||
    this.board.off('game:board-right-action-click')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private removeHandEvents() {
 | 
			
		||||
    this.hand.off('tileClick')
 | 
			
		||||
    this.hand.off('passClick')
 | 
			
		||||
    this.hand.off('game:tile-click')
 | 
			
		||||
    this.hand.off('game:button-pass-click')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										166
									
								
								src/game/Hand.ts
									
									
									
									
									
								
							
							
						
						
									
										166
									
								
								src/game/Hand.ts
									
									
									
									
									
								
							@@ -1,18 +1,11 @@
 | 
			
		||||
import {
 | 
			
		||||
  Application,
 | 
			
		||||
  Container,
 | 
			
		||||
  EventEmitter,
 | 
			
		||||
  Graphics,
 | 
			
		||||
  Sprite,
 | 
			
		||||
  Text,
 | 
			
		||||
  Texture,
 | 
			
		||||
  Ticker
 | 
			
		||||
} from 'pixi.js'
 | 
			
		||||
import { Application, Container, EventEmitter, Sprite, Texture, Ticker } from 'pixi.js'
 | 
			
		||||
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 { Scale, type ScaleFunction } from './utilities/scale'
 | 
			
		||||
import { LoggingService } from '@/services/LoggingService'
 | 
			
		||||
import { createButton, createContainer } from '@/common/helpers'
 | 
			
		||||
import { Action, Actions } from 'pixi-actions'
 | 
			
		||||
 | 
			
		||||
export class Hand extends EventEmitter {
 | 
			
		||||
  tiles: Tile[] = []
 | 
			
		||||
@@ -31,18 +24,40 @@ export class Hand extends EventEmitter {
 | 
			
		||||
  scaleX!: ScaleFunction
 | 
			
		||||
  grain: number = 25
 | 
			
		||||
  logger: LoggingService = new LoggingService()
 | 
			
		||||
  availableTiles: Tile[] = []
 | 
			
		||||
  tilesLayer!: Container
 | 
			
		||||
  interactionsLayer!: Container
 | 
			
		||||
 | 
			
		||||
  constructor(app: Application) {
 | 
			
		||||
    super()
 | 
			
		||||
    app.stage.addChild(this.container)
 | 
			
		||||
    this.ticker = app.ticker
 | 
			
		||||
    this.height = 130
 | 
			
		||||
    this.height = 130 * this.scale
 | 
			
		||||
    this.width = app.canvas.width
 | 
			
		||||
    this.container.y = app.canvas.height - this.height
 | 
			
		||||
    this.container.width = this.width
 | 
			
		||||
    this.container.height = this.height
 | 
			
		||||
    this.calculateScale()
 | 
			
		||||
    this.initLayers()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initLayers() {
 | 
			
		||||
    this.container.removeChildren()
 | 
			
		||||
    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() {
 | 
			
		||||
@@ -50,14 +65,15 @@ export class Hand extends EventEmitter {
 | 
			
		||||
    this.tiles = []
 | 
			
		||||
 | 
			
		||||
    this.initialized = false
 | 
			
		||||
    this.buttonNext = this.createButton(
 | 
			
		||||
    this.buttonNext = createButton(
 | 
			
		||||
      'NEXT',
 | 
			
		||||
      { x: this.width / 2 - 25, y: this.height / 2, width: 50, height: 20 },
 | 
			
		||||
      () => {
 | 
			
		||||
        this.container.removeChildren()
 | 
			
		||||
        this.container.removeChild(this.buttonNext)
 | 
			
		||||
        this.tilesLayer.removeChildren()
 | 
			
		||||
        this.interactionsLayer.removeChild(this.buttonNext)
 | 
			
		||||
        this.emit('nextClick')
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
      this.interactionsLayer
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -72,24 +88,47 @@ export class Hand extends EventEmitter {
 | 
			
		||||
    this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set canMove(value: boolean) {
 | 
			
		||||
    this._canMove = value
 | 
			
		||||
    if (value) {
 | 
			
		||||
  setCanMove(value: boolean, isFirstMove: boolean, freeEnds?: [number, number]) {
 | 
			
		||||
    console.log('this.tiles :>> ', this.tiles.length)
 | 
			
		||||
    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()
 | 
			
		||||
    } else {
 | 
			
		||||
      this.container.removeChild(this.buttonPass)
 | 
			
		||||
      this.interactionsLayer.removeChild(this.buttonPass)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.tiles.forEach((tile) => {
 | 
			
		||||
    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()
 | 
			
		||||
      }
 | 
			
		||||
      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) {
 | 
			
		||||
    this.tiles = this.createTiles(playerState)
 | 
			
		||||
    this.emit('handUpdated', this.tiles)
 | 
			
		||||
    this.initialized = this.tiles.length > 0
 | 
			
		||||
    this.renderTiles()
 | 
			
		||||
    this.emit('hand-updated', this.tiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private addBg() {
 | 
			
		||||
@@ -109,8 +148,9 @@ export class Hand extends EventEmitter {
 | 
			
		||||
    const selected = this.tiles.find((t) => t.selected)
 | 
			
		||||
    if (selected) {
 | 
			
		||||
      this.deselectTile(selected)
 | 
			
		||||
 | 
			
		||||
      if (selected.id === tile.id) {
 | 
			
		||||
        this.emit('tileClick')
 | 
			
		||||
        this.emit('game:tile-click')
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -118,12 +158,10 @@ export class Hand extends EventEmitter {
 | 
			
		||||
    tile.selected = true
 | 
			
		||||
    tile.alpha = 1
 | 
			
		||||
 | 
			
		||||
    tile.animateTo(tile.x, tile.y - 10)
 | 
			
		||||
    this.emit('tileClick', tile.toPlain())
 | 
			
		||||
    this.emit('game:tile-click', tile.toPlain())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private deselectTile(selected: Tile) {
 | 
			
		||||
    selected.animateTo(selected.x, selected.y + 10)
 | 
			
		||||
    selected.selected = false
 | 
			
		||||
    selected.alpha = 0.7
 | 
			
		||||
  }
 | 
			
		||||
@@ -133,6 +171,17 @@ export class Hand extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
    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.clearFilters()
 | 
			
		||||
    tile.off('pointerdown')
 | 
			
		||||
@@ -140,59 +189,14 @@ export class Hand extends EventEmitter {
 | 
			
		||||
    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() {
 | 
			
		||||
    const lastTile = this.tiles[this.tiles.length - 1]
 | 
			
		||||
    const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0)
 | 
			
		||||
    this.buttonPass = this.createButton(
 | 
			
		||||
    this.buttonPass = createButton(
 | 
			
		||||
      'PASS',
 | 
			
		||||
      { 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)
 | 
			
		||||
    )
 | 
			
		||||
    if (missing) {
 | 
			
		||||
      this.container.removeChild(missing.getSprite())
 | 
			
		||||
      this.tilesLayer.removeChild(missing.getSprite())
 | 
			
		||||
      this.tiles = this.tiles.filter((tile) => tile.id !== missing.id)
 | 
			
		||||
      this.emit('handUpdated', this.tiles)
 | 
			
		||||
      this.emit('hand-updated', this.tiles)
 | 
			
		||||
    }
 | 
			
		||||
    this.renderTiles()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private createTiles(playerState: PlayerDto) {
 | 
			
		||||
    return playerState.hand.map((tile) => {
 | 
			
		||||
      const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale)
 | 
			
		||||
    return playerState.hand.map((tile: TileDto) => {
 | 
			
		||||
      const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale, tile.playerId)
 | 
			
		||||
      newTile.alpha = 0.7
 | 
			
		||||
      newTile.anchor = 0.5
 | 
			
		||||
      newTile.addTo(this.container)
 | 
			
		||||
      newTile.addTo(this.tilesLayer)
 | 
			
		||||
      newTile.on('pointerdown', () => this.onTileClick(newTile))
 | 
			
		||||
      newTile.on('pointerover', () => {
 | 
			
		||||
        this.emit('tileHover', newTile.toPlain())
 | 
			
		||||
        newTile.alpha = 1
 | 
			
		||||
        newTile.setFilters([
 | 
			
		||||
          new GlowFilter({
 | 
			
		||||
@@ -233,6 +238,7 @@ export class Hand extends EventEmitter {
 | 
			
		||||
      })
 | 
			
		||||
      newTile.on('pointerout', () => {
 | 
			
		||||
        if (!newTile.selected) {
 | 
			
		||||
          this.emit('tileHover')
 | 
			
		||||
          newTile.alpha = 0.7
 | 
			
		||||
          newTile.getSprite().filters = []
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
import type { AnimationOptions } from '@/common/interfaces'
 | 
			
		||||
import { Sprite, Texture, Ticker } from 'pixi.js'
 | 
			
		||||
import { Tile } from './Tile'
 | 
			
		||||
 | 
			
		||||
export abstract class SpriteBase {
 | 
			
		||||
  private _interactive: boolean = false
 | 
			
		||||
@@ -81,28 +83,56 @@ export abstract class SpriteBase {
 | 
			
		||||
    this.sprite.filters = []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  animateTo(x: number, y: number) {
 | 
			
		||||
    const initialX = this.sprite.x
 | 
			
		||||
    const initialY = this.sprite.y
 | 
			
		||||
  animateTo(options: AnimationOptions): Promise<void> {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
      const {
 | 
			
		||||
        x: targetX,
 | 
			
		||||
        y: targetY,
 | 
			
		||||
        rotation: targetRotation,
 | 
			
		||||
        duration = 10,
 | 
			
		||||
        width: targetWidth,
 | 
			
		||||
        height: targetHeight
 | 
			
		||||
      } = options
 | 
			
		||||
 | 
			
		||||
    const deltaX = x - this.sprite.x
 | 
			
		||||
    const deltaY = y - this.sprite.y
 | 
			
		||||
    let elapsed: number = 0
 | 
			
		||||
    const duration: number = 10
 | 
			
		||||
      const initialX = this.sprite.x
 | 
			
		||||
      const initialY = this.sprite.y
 | 
			
		||||
      const initialRotation = this.sprite.rotation
 | 
			
		||||
      const initialWidth = this.sprite.width
 | 
			
		||||
      const initialHeight = this.sprite.height
 | 
			
		||||
 | 
			
		||||
    const tick: any = (delta: any) => {
 | 
			
		||||
      elapsed += delta.deltaTime
 | 
			
		||||
      const progress = Math.min(elapsed / duration, 1)
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
      this.sprite.x = initialX + deltaX * progress
 | 
			
		||||
      this.sprite.y = initialY + deltaY * progress
 | 
			
		||||
      let elapsed: number = 0
 | 
			
		||||
 | 
			
		||||
      if (progress === 1) {
 | 
			
		||||
        this.ticker?.remove(tick)
 | 
			
		||||
      const tick: any = (delta: any) => {
 | 
			
		||||
        elapsed += delta.deltaTime
 | 
			
		||||
        const progress = Math.min(elapsed / duration, 1)
 | 
			
		||||
 | 
			
		||||
        // Linear interpolation
 | 
			
		||||
        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) {
 | 
			
		||||
          this.ticker?.remove(tick)
 | 
			
		||||
          resolve()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.ticker?.add(tick)
 | 
			
		||||
      this.ticker?.add(tick)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addTo(container: any) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { Texture, Ticker } from 'pixi.js'
 | 
			
		||||
import { SpriteBase } from '@/game/SpriteBase'
 | 
			
		||||
import { ORIENTATION_ANGLES } from '@/common/constants'
 | 
			
		||||
 | 
			
		||||
export class Tile extends SpriteBase {
 | 
			
		||||
  selected: boolean = false
 | 
			
		||||
@@ -11,7 +12,8 @@ export class Tile extends SpriteBase {
 | 
			
		||||
    public id: string,
 | 
			
		||||
    ticker?: Ticker,
 | 
			
		||||
    public pips?: [number, number],
 | 
			
		||||
    scale: number = 1
 | 
			
		||||
    scale: number = 1,
 | 
			
		||||
    public playerId?: string
 | 
			
		||||
  ) {
 | 
			
		||||
    super(ticker, scale)
 | 
			
		||||
    this.id = id
 | 
			
		||||
@@ -26,6 +28,7 @@ export class Tile extends SpriteBase {
 | 
			
		||||
 | 
			
		||||
  toPlain() {
 | 
			
		||||
    return {
 | 
			
		||||
      playerId: this.playerId,
 | 
			
		||||
      id: this.id,
 | 
			
		||||
      pips: this.pips,
 | 
			
		||||
      orientation: this.orientation,
 | 
			
		||||
@@ -69,16 +72,16 @@ export class Tile extends SpriteBase {
 | 
			
		||||
  setOrientation(value: string) {
 | 
			
		||||
    switch (value) {
 | 
			
		||||
      case 'north':
 | 
			
		||||
        this.sprite.rotation = 0
 | 
			
		||||
        this.sprite.rotation = ORIENTATION_ANGLES.north
 | 
			
		||||
        break
 | 
			
		||||
      case 'east':
 | 
			
		||||
        this.sprite.rotation = Math.PI / 2
 | 
			
		||||
        this.sprite.rotation = ORIENTATION_ANGLES.east
 | 
			
		||||
        break
 | 
			
		||||
      case 'south':
 | 
			
		||||
        this.sprite.rotation = Math.PI
 | 
			
		||||
        this.sprite.rotation = ORIENTATION_ANGLES.south
 | 
			
		||||
        break
 | 
			
		||||
      case 'west':
 | 
			
		||||
        this.sprite.rotation = (3 * Math.PI) / 2
 | 
			
		||||
        this.sprite.rotation = ORIENTATION_ANGLES.west
 | 
			
		||||
        break
 | 
			
		||||
    }
 | 
			
		||||
    this.orientation = value
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ import tile6_6 from '@/assets/images/tiles/6-6.png'
 | 
			
		||||
import dot from '@/assets/images/circle.png'
 | 
			
		||||
import bgWood_1 from '@/assets/images/backgrounds/wood-1.jpg'
 | 
			
		||||
import bg_1 from '@/assets/images/backgrounds/bg-1.png'
 | 
			
		||||
import bg_green from '@/assets/images/backgrounds/bg-green.png'
 | 
			
		||||
 | 
			
		||||
export const assets = [
 | 
			
		||||
  { alias: 'tile-back', src: tileBack },
 | 
			
		||||
@@ -63,5 +64,6 @@ export const assets = [
 | 
			
		||||
  { alias: 'tile-6_6', src: tile6_6 },
 | 
			
		||||
  { alias: 'dot', src: dot },
 | 
			
		||||
  { 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 }
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { Tile } from '../Tile'
 | 
			
		||||
import type { GameState, Movement } from '../../common/interfaces'
 | 
			
		||||
import type { GameDto, Movement } from '../../common/interfaces'
 | 
			
		||||
import type { Game } from '../Game'
 | 
			
		||||
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',
 | 
			
		||||
  lastMove: {
 | 
			
		||||
    tile: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,87 +1,68 @@
 | 
			
		||||
import { useGameStore } from '@/stores/game'
 | 
			
		||||
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 { useEventBusStore } from '@/stores/eventBus'
 | 
			
		||||
import type { SocketIoClientService } from '@/services/SocketIoClientService'
 | 
			
		||||
 | 
			
		||||
export class SocketIoEventManager {
 | 
			
		||||
  gameStore: any = useGameStore()
 | 
			
		||||
  eventBus = useEventBusStore()
 | 
			
		||||
  callbacksMap: Map<string, any> = new Map()
 | 
			
		||||
 | 
			
		||||
  constructor(private socketService: SocketIoClientService) {}
 | 
			
		||||
 | 
			
		||||
  handleGameEvent(gameEvent: SocketEvent) {
 | 
			
		||||
    const { event, data } = gameEvent
 | 
			
		||||
    switch (event) {
 | 
			
		||||
      case 'session-created':
 | 
			
		||||
      case 'update-match-session-state':
 | 
			
		||||
        this.updateSessionState(data)
 | 
			
		||||
        break
 | 
			
		||||
      case 'game-finished':
 | 
			
		||||
      case 'update-game-state':
 | 
			
		||||
        this.updateGameState(data)
 | 
			
		||||
        break
 | 
			
		||||
      case 'update-player-state':
 | 
			
		||||
        this.updatePlayerState(data)
 | 
			
		||||
        break
 | 
			
		||||
      case 'server:player-turn':
 | 
			
		||||
        this.onCanMakeMoveEvent(data)
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        this.eventBus.publish(event, data)
 | 
			
		||||
        break
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleGameEventAck(gameEvent: SocketEvent) {
 | 
			
		||||
  handleGameEventAck(gameEvent: SocketEvent, callback: any) {
 | 
			
		||||
    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 }
 | 
			
		||||
    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
 | 
			
		||||
    updateSessionState(data)
 | 
			
		||||
    return {
 | 
			
		||||
      status: 'ok'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateGameState(data: any) {
 | 
			
		||||
    const { updateGameState } = this.gameStore
 | 
			
		||||
    updateGameState(data)
 | 
			
		||||
    return {
 | 
			
		||||
      status: 'ok'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updatePlayerState(data: any) {
 | 
			
		||||
    const { updatePlayerState } = this.gameStore
 | 
			
		||||
    updatePlayerState(data)
 | 
			
		||||
    return {
 | 
			
		||||
      status: 'ok'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleCanMakeMoveEvent(data: any) {
 | 
			
		||||
    const { canMakeMove, moveToMake } = storeToRefs(this.gameStore)
 | 
			
		||||
    const { updateCanMakeMove, setIncomingFreeEnds } = this.gameStore
 | 
			
		||||
    setIncomingFreeEnds(data.freeHands)
 | 
			
		||||
    updateCanMakeMove(true)
 | 
			
		||||
    while (canMakeMove.value) {
 | 
			
		||||
      await wait(500)
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      status: 'ok',
 | 
			
		||||
      ...moveToMake.value
 | 
			
		||||
    }
 | 
			
		||||
  private onCanMakeMoveEvent(data: any) {
 | 
			
		||||
    const { setIncomingFreeEnds } = this.gameStore
 | 
			
		||||
    setIncomingFreeEnds(data.firstMove ? undefined : data.freeHands)
 | 
			
		||||
    this.eventBus.publish('game:player-turn-started', data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async handleCanSelectTileEvent() {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,20 @@ const router = createRouter({
 | 
			
		||||
      // which is lazy-loaded when the route is visited.
 | 
			
		||||
      // component: () => import('../views/AboutView.vue')
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/match/:id',
 | 
			
		||||
      component: AuthenticatedLayout,
 | 
			
		||||
      children: [
 | 
			
		||||
        {
 | 
			
		||||
          path: '',
 | 
			
		||||
          name: 'match',
 | 
			
		||||
          component: () => import('@/views/MatchView.vue'),
 | 
			
		||||
          meta: {
 | 
			
		||||
            requiresAuth: true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/game',
 | 
			
		||||
      component: AuthenticatedLayout,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { ServiceBase } from './ServiceBase'
 | 
			
		||||
export class GameService extends ServiceBase {
 | 
			
		||||
  private networkService = new NetworkService()
 | 
			
		||||
 | 
			
		||||
  async createMatch(sessionName: string, seed: string) {
 | 
			
		||||
  async createMatchSession(sessionName: string, seed: string) {
 | 
			
		||||
    const response = await this.networkService.post({
 | 
			
		||||
      uri: '/game/match',
 | 
			
		||||
      body: { sessionName, seed },
 | 
			
		||||
@@ -13,4 +13,36 @@ export class GameService extends ServiceBase {
 | 
			
		||||
    const { sessionId } = response
 | 
			
		||||
    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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,11 @@ export class NetworkService {
 | 
			
		||||
    return await this.request(options)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async put(options: RequestOptions) {
 | 
			
		||||
    options.method = 'PUT'
 | 
			
		||||
    return await this.request(options)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async patch(options: RequestOptions) {
 | 
			
		||||
    options.method = 'PATCH'
 | 
			
		||||
    return await this.request(options)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import type { MatchSessionState, GameState, PlayerDto } from '@/common/interfaces'
 | 
			
		||||
import { io, Socket } from 'socket.io-client'
 | 
			
		||||
import { SocketIoEventManager } from '@/managers/SocketIoEventManager'
 | 
			
		||||
import { useAuthStore } from '@/stores/auth'
 | 
			
		||||
@@ -8,10 +7,11 @@ import { ServiceBase } from './ServiceBase'
 | 
			
		||||
export class SocketIoClientService extends ServiceBase {
 | 
			
		||||
  private socket!: Socket
 | 
			
		||||
  private isConnected = false
 | 
			
		||||
  private gameEventManager = new SocketIoEventManager()
 | 
			
		||||
  private gameEventManager: SocketIoEventManager
 | 
			
		||||
 | 
			
		||||
  constructor(private url: string) {
 | 
			
		||||
    super()
 | 
			
		||||
    this.gameEventManager = new SocketIoEventManager(this)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async connect(): Promise<void> {
 | 
			
		||||
@@ -70,18 +70,19 @@ export class SocketIoClientService extends ServiceBase {
 | 
			
		||||
    //   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.socket.on('game-event-ack', async (data: any, callback: any) => {
 | 
			
		||||
      callback(await this.gameEventManager.handleGameEventAck(data))
 | 
			
		||||
    this.socket.on('server:game-event-ack', async (data: any, callback: any) => {
 | 
			
		||||
      await this.gameEventManager.handleGameEventAck(data, callback)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sendMessage(event: string, data: any): void {
 | 
			
		||||
    if (this.isConnected) {
 | 
			
		||||
      this.socket?.emit(event, data)
 | 
			
		||||
      this.socket?.emit('client:event', { event, data })
 | 
			
		||||
      console.log('sendMessage :>> ', event, data)
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log('Not connected to server')
 | 
			
		||||
    }
 | 
			
		||||
@@ -89,7 +90,8 @@ export class SocketIoClientService extends ServiceBase {
 | 
			
		||||
 | 
			
		||||
  async sendMessageWithAck(event: string, data: any): Promise<any> {
 | 
			
		||||
    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 {
 | 
			
		||||
      console.log('Not connected to server')
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,14 @@
 | 
			
		||||
import { computed, ref } from 'vue'
 | 
			
		||||
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', () => {
 | 
			
		||||
  const sessionState = ref<MatchSessionState | undefined>(undefined)
 | 
			
		||||
  const gameState = ref<GameState | undefined>(undefined)
 | 
			
		||||
  const sessionState = ref<MatchSessionDto | undefined>(undefined)
 | 
			
		||||
  const gameState = ref<GameDto | undefined>(undefined)
 | 
			
		||||
  const playerState = ref<PlayerDto | undefined>(undefined)
 | 
			
		||||
  const canMakeMove = ref(false)
 | 
			
		||||
  const canSelectTile = ref(false)
 | 
			
		||||
  const gameFinished = ref(false)
 | 
			
		||||
  const readyForStart = ref(false)
 | 
			
		||||
  const moveToMake = ref<Movement | undefined>(undefined)
 | 
			
		||||
  const incomingFreeEnds = ref<[number, number] | undefined>(undefined)
 | 
			
		||||
  const showReadyButton = ref(false)
 | 
			
		||||
@@ -21,16 +20,17 @@ export const useGameStore = defineStore('game', () => {
 | 
			
		||||
      playerState.value !== undefined &&
 | 
			
		||||
      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
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updateGameState(newState: GameState) {
 | 
			
		||||
  function updateGameState(newState: GameDto | undefined) {
 | 
			
		||||
    gameState.value = newState
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updatePlayerState(newState: PlayerDto) {
 | 
			
		||||
  function updatePlayerState(newState: PlayerDto | undefined) {
 | 
			
		||||
    playerState.value = newState
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -54,10 +54,6 @@ export const useGameStore = defineStore('game', () => {
 | 
			
		||||
    showReadyButton.value = value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function setReadyForStart(value: boolean) {
 | 
			
		||||
    readyForStart.value = value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updateGameFinished(value: boolean) {
 | 
			
		||||
    gameFinished.value = value
 | 
			
		||||
  }
 | 
			
		||||
@@ -81,7 +77,6 @@ export const useGameStore = defineStore('game', () => {
 | 
			
		||||
    setIncomingFreeEnds,
 | 
			
		||||
    updateCanSelectTile,
 | 
			
		||||
    setShowReadyButton,
 | 
			
		||||
    setReadyForStart,
 | 
			
		||||
    updateGameFinished,
 | 
			
		||||
    isSessionStarted,
 | 
			
		||||
    amIHost
 | 
			
		||||
 
 | 
			
		||||
@@ -2,19 +2,17 @@
 | 
			
		||||
import GameComponent from '@/components/GameComponent.vue'
 | 
			
		||||
import { useGameStore } from '@/stores/game'
 | 
			
		||||
import { storeToRefs } from 'pinia'
 | 
			
		||||
import { inject, onBeforeUnmount, ref } from 'vue'
 | 
			
		||||
import { onBeforeUnmount } from 'vue'
 | 
			
		||||
import { onMounted } from 'vue'
 | 
			
		||||
import useClipboard from 'vue-clipboard3'
 | 
			
		||||
import { useRouter } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const socketService: any = inject('socket')
 | 
			
		||||
 | 
			
		||||
const { toClipboard } = useClipboard()
 | 
			
		||||
const gameStore = useGameStore()
 | 
			
		||||
const { moveToMake, canMakeMove, sessionState, gameState, playerState } = storeToRefs(gameStore)
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  startMatch()
 | 
			
		||||
  // startMatch()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
if (!playerState?.value) {
 | 
			
		||||
@@ -25,25 +23,13 @@ if (!playerState?.value) {
 | 
			
		||||
function makeMove(move: any) {
 | 
			
		||||
  moveToMake.value = move
 | 
			
		||||
  canMakeMove.value = false
 | 
			
		||||
  console.log('makemove :>> ', move)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
  // 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() {
 | 
			
		||||
  if (sessionState?.value?.seed) toClipboard(sessionState.value.seed)
 | 
			
		||||
}
 | 
			
		||||
@@ -63,7 +49,8 @@ function copySeed() {
 | 
			
		||||
        - Score: {{ sessionState?.scoreboard }}
 | 
			
		||||
      </p>
 | 
			
		||||
      <p v-if="sessionState?.id">
 | 
			
		||||
        SessionID: {{ sessionState.id }} PlayerID: {{ playerState?.id }}
 | 
			
		||||
        SessionID: {{ sessionState.id }} PlayerID: {{ playerState?.id }} - canMakeMove
 | 
			
		||||
        {{ canMakeMove }}
 | 
			
		||||
      </p>
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="block">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,23 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { inject, ref } from 'vue'
 | 
			
		||||
import { inject, onMounted, onUnmounted, ref } from 'vue'
 | 
			
		||||
import { useRouter } from 'vue-router'
 | 
			
		||||
import { useGameStore } from '@/stores/game'
 | 
			
		||||
import { storeToRefs } from 'pinia'
 | 
			
		||||
import { LoggingService } from '@/services/LoggingService'
 | 
			
		||||
import type { LoggingService } from '@/services/LoggingService'
 | 
			
		||||
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 sessionName = ref('Test Value')
 | 
			
		||||
let sessionId = ref('')
 | 
			
		||||
let matchSessions = ref<MatchSessionDto[]>([])
 | 
			
		||||
let dataInterval: any
 | 
			
		||||
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const gameStore = useGameStore()
 | 
			
		||||
const auth = useAuthStore()
 | 
			
		||||
 | 
			
		||||
const socketService: any = inject('socket')
 | 
			
		||||
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 } =
 | 
			
		||||
  storeToRefs(gameStore)
 | 
			
		||||
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
 | 
			
		||||
const { user } = storeToRefs(auth)
 | 
			
		||||
 | 
			
		||||
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.sendMessageWithAck('playerReady', {
 | 
			
		||||
    userId: playerState.value.id,
 | 
			
		||||
    sessionId: sessionState.value.id
 | 
			
		||||
  })
 | 
			
		||||
  readyForStart.value = true
 | 
			
		||||
}
 | 
			
		||||
// function setPlayerReady() {
 | 
			
		||||
//   logger.debug('Starting game')
 | 
			
		||||
//   if (!sessionState.value) {
 | 
			
		||||
//     logger.error('No session found')
 | 
			
		||||
//     return
 | 
			
		||||
//   }
 | 
			
		||||
//   if (!playerState.value) {
 | 
			
		||||
//     logger.error('No player found')
 | 
			
		||||
//     return
 | 
			
		||||
//   }
 | 
			
		||||
//   socketService.sendMessage('client:set-player-ready', {
 | 
			
		||||
//     userId: playerState.value.id,
 | 
			
		||||
//     sessionId: sessionState.value.id
 | 
			
		||||
//   })
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
const eventBus = useEventBusStore()
 | 
			
		||||
eventBus.subscribe('window-before-unload', () => {
 | 
			
		||||
  logger.debug('Window before unload')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function createMatch() {
 | 
			
		||||
  logger.debug('Creating match')
 | 
			
		||||
  await socketService.connect()
 | 
			
		||||
  sessionId.value = await gameService.createMatch(sessionName.value, seed.value)
 | 
			
		||||
  logger.debug('Match reated successfully')
 | 
			
		||||
  const id = await gameService.createMatchSession(sessionName.value, seed.value)
 | 
			
		||||
  logger.debug('Match created successfully')
 | 
			
		||||
  router.push({ name: 'match', params: { id } })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function joinMatch() {
 | 
			
		||||
  const sessionId = sessionState?.value?.id
 | 
			
		||||
  const playerId = playerState?.value?.id
 | 
			
		||||
  if (sessionId && playerId) {
 | 
			
		||||
    await socketService.sendMessageWithAck('joinSession', {
 | 
			
		||||
      user: 'pepe',
 | 
			
		||||
      sessionId: sessionId
 | 
			
		||||
    })
 | 
			
		||||
    // sessionId.value = response.sessionId
 | 
			
		||||
    // playerId.value = response.playerId
 | 
			
		||||
async function cancelMatch() {
 | 
			
		||||
  logger.debug('Cancelling match')
 | 
			
		||||
  await gameService.cancelMatchSession(sessionId.value)
 | 
			
		||||
  await socketService.disconnect()
 | 
			
		||||
  sessionId.value = ''
 | 
			
		||||
  seed.value = ''
 | 
			
		||||
  sessionName.value = ''
 | 
			
		||||
  updateSessionState(undefined)
 | 
			
		||||
  updatePlayerState(undefined)
 | 
			
		||||
  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' })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="block home">
 | 
			
		||||
    <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">
 | 
			
		||||
        <p>This is a protected route.</p>
 | 
			
		||||
        <p>{{ sessionState || 'No session' }}</p>
 | 
			
		||||
@@ -93,17 +132,30 @@ async function startMatch() {
 | 
			
		||||
      <button class="button" @click="createMatch" v-if="!isSessionStarted">
 | 
			
		||||
        Create Match Session
 | 
			
		||||
      </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>
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="button" @click="startMatch" v-if="readyForStart">
 | 
			
		||||
      </button> -->
 | 
			
		||||
      <!-- <button class="button" @click="startMatch" v-if="readyForStart">
 | 
			
		||||
        <span>Start</span>
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <button class="button" @click="cancelMatch" v-if="isSessionStarted">
 | 
			
		||||
        <span>Cancel</span>
 | 
			
		||||
      </button> -->
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="section available-sessions">
 | 
			
		||||
    <section class="section available-sessions" v-if="!isSessionStarted">
 | 
			
		||||
      <h2 class="title is-4">Available Sessions</h2>
 | 
			
		||||
      <div class="bloc">
 | 
			
		||||
        <p>There are no available sessions at the moment.</p>
 | 
			
		||||
      <div class="block">
 | 
			
		||||
        <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>
 | 
			
		||||
    </section>
 | 
			
		||||
  </div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										114
									
								
								src/views/MatchView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/views/MatchView.vue
									
									
									
									
									
										Normal 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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user