game flow

This commit is contained in:
Jose Conde 2024-07-06 20:28:48 +02:00
parent c40dcd74db
commit 9a6f430e4d
22 changed files with 937 additions and 260 deletions

2
.env Normal file
View File

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

260
package-lock.json generated
View File

@ -9,7 +9,10 @@
"version": "0.0.0",
"dependencies": {
"bulma": "^1.0.1",
"colorette": "^2.0.20",
"dayjs": "^1.11.11",
"pinia": "^2.1.7",
"pino": "^9.2.0",
"pixi-filters": "^6.0.4",
"pixi.js": "^8.2.1",
"socket.io-client": "^4.7.5",
@ -1492,6 +1495,17 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
@ -1620,12 +1634,39 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -1665,6 +1706,29 @@
"node": ">=8"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/bulma": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.1.tgz",
@ -1798,6 +1862,11 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1909,6 +1978,11 @@
"node": ">=18"
}
},
"node_modules/dayjs": {
"version": "1.11.11",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz",
"integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg=="
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@ -2370,11 +2444,27 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@ -2450,6 +2540,14 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"engines": {
"node": ">=6"
}
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
@ -2751,6 +2849,25 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@ -3391,6 +3508,14 @@
"integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==",
"dev": true
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -3649,6 +3774,41 @@
}
}
},
"node_modules/pino": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz",
"integrity": "sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^1.2.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^3.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz",
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==",
"dependencies": {
"readable-stream": "^4.0.0",
"split2": "^4.0.0"
}
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="
},
"node_modules/pixi-filters": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.0.4.tgz",
@ -3789,6 +3949,19 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz",
"integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@ -3836,6 +4009,11 @@
}
]
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -3855,6 +4033,21 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/readable-stream": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -3867,6 +4060,14 @@
"node": ">=8.10.0"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@ -4015,6 +4216,33 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safe-stable-stringify": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -4150,6 +4378,14 @@
"node": ">=10.0.0"
}
},
"node_modules/sonic-boom": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz",
"integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
@ -4158,6 +4394,14 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@ -4170,6 +4414,14 @@
"integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
"dev": true
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -4336,6 +4588,14 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",

View File

@ -15,7 +15,10 @@
},
"dependencies": {
"bulma": "^1.0.1",
"colorette": "^2.0.20",
"dayjs": "^1.11.11",
"pinia": "^2.1.7",
"pino": "^9.2.0",
"pixi-filters": "^6.0.4",
"pixi.js": "^8.2.1",
"socket.io-client": "^4.7.5",

View File

@ -3,8 +3,10 @@ import type { Container } from 'pixi.js'
export interface PlayerDto {
id: string
name: string
score?: number
hand?: string[]
score: number
hand: TileDto[]
teamedWith: PlayerDto | null
ready: boolean
}
export interface TileDto {
@ -16,7 +18,7 @@ export interface TileDto {
width?: number
height?: number
}
export interface GameSessionState {
export interface MatchSessionState {
id: string
name: string
creator: string
@ -46,14 +48,9 @@ export interface GameState {
tileSelectionPhase: boolean
boardFreeEnds: number[]
lastMove: Movement
}
export interface PlayerState {
id: string
name: string
score: number
hand: TileDto[]
teamedWith: string | undefined
scoreboard: Map<string, number>
matchWinner: PlayerDto | null
matchInProgress: boolean
}
export interface Movement {
@ -75,3 +72,10 @@ export interface ContainerOptions {
visible?: boolean
parent?: Container
}
export interface Dimension {
width: number
height: number
x: number
y: number
}

View File

@ -1,17 +1,22 @@
<script setup lang="ts">
import type { GameSessionState, GameState, PlayerState } from '@/common/interfaces'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import type { MatchSessionState, GameState, PlayerDto } from '@/common/interfaces'
import { onMounted, onUnmounted, ref, watch, inject } from 'vue'
import { Game } from '@/game/Game'
import { useGameStore } from '@/stores/game'
import { useEventBusStore } from '@/stores/eventBus'
import type { LoggingService } from '@/services/LoggingService'
import { storeToRefs } from 'pinia'
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
const emit = defineEmits(['move'])
const props = defineProps({
playerId: String
})
const socketService: any = inject('socket')
const gameStore = useGameStore()
const eventBus = useEventBusStore()
const { playerState, sessionState } = storeToRefs(gameStore)
let appEl = ref<HTMLElement | null>(null)
const game = new Game(
{
width: 1200,
@ -20,38 +25,9 @@ const game = new Game(
handScale: 1
},
emit,
props
)
watch(
() => gameStore.canMakeMove,
(value: boolean) => {
game.setCanMakeMove(value)
}
)
watch(
() => gameStore.gameState,
(value: GameState | undefined) => {
if (value === undefined) return
game.board.setState(value, props.playerId ?? '')
// console.log('gameState-------------------------------------- :>> ', value)
}
)
watch(
() => gameStore.sessionState,
(value: GameSessionState | undefined) => {
if (value === undefined) return
// console.log('gameSessionState-------------------------------------- :>> ', value)
}
)
watch(
() => gameStore.playerState,
(value: PlayerState | undefined) => {
if (value === undefined) return
game.hand.update(value as PlayerState)
}
socketService,
playerState.value?.id || '',
sessionState.value?.id || ''
)
onMounted(async () => {
@ -61,6 +37,42 @@ onMounted(async () => {
await game.preload()
await game.start()
eventBus.subscribe('game-finished', () => {
game.gameFinished()
})
watch(
() => gameStore.canMakeMove,
(value: boolean) => {
game.setCanMakeMove(value)
}
)
watch(
() => gameStore.gameState,
(value: GameState | undefined) => {
if (value === undefined) return
logger.debug('gameState-------------------------------------- :>> ', value)
game.board?.setState(value, playerState.value?.id ?? '')
}
)
watch(
() => gameStore.sessionState,
(value: MatchSessionState | 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)
}
)
// mockMove(game, [6, 6], 'left')
// mockMove(game, [6, 4], 'left')
// mockMove(game, [4, 4], 'left')

View File

@ -14,10 +14,13 @@ import { Tile } from '@/game/Tile'
import { DIRECTIONS, createContainer, isTilePair } from '@/common/helpers'
import { createText } from '@/game/utilities/fonts'
import { Dot } from '@/game/Dot'
import { LoggingService } from '@/services/LoggingService'
import { inject } from 'vue'
export class Board extends EventEmitter {
private _scale: number = 1
private _canMove: boolean = false
private logger = inject<LoggingService>('logger')!
ticker: Ticker
height: number
@ -25,7 +28,7 @@ export class Board extends EventEmitter {
grain: number = 25
scaleY: ScaleFunction
scaleX: ScaleFunction
state: GameState | undefined
state?: GameState
container!: Container
initialContainer!: Container
tilesContainer!: Container
@ -45,7 +48,7 @@ export class Board extends EventEmitter {
leftDirection: string = 'west'
rightDirection: string = 'east'
playerHand: Tile[] = []
firstTile: Tile | undefined
firstTile?: Tile
constructor(app: Application) {
super()
@ -63,8 +66,8 @@ export class Board extends EventEmitter {
})
const background = new Sprite(Assets.get('bg-1'))
background.width = this.width
background.height = this.height
// background.width = this.width
// background.height = this.height
this.container.addChild(background)
this.initialContainer = createContainer({
@ -102,7 +105,13 @@ export class Board extends EventEmitter {
this.tilesContainer.addChild(verticalLine)
this.tilesContainer.addChild(horizontalLine)
this.createTexts()
this.textContainer = createContainer({
width: this.width,
height: this.height,
parent: this.container
})
this.showText('Starting game...')
}
private calculateScale() {
@ -138,20 +147,17 @@ export class Board extends EventEmitter {
this.playerHand = tiles
}
createTexts() {
this.textContainer = new Container()
this.textWaitForPlayers = createText('Waiting for players', this.scaleX(0), 100)
this.textYourTurn = createText('Your turn!', this.scaleX(0), 100)
this.container.addChild(this.textContainer)
this.textContainer.addChild(this.textWaitForPlayers)
this.textContainer.addChild(this.textYourTurn)
this.textYourTurn.visible = false
showText(text: string) {
this.textContainer.removeChildren()
this.textContainer.addChild(createText(text, this.scaleX(0), 100))
}
private updateCanMoveText() {
this.textWaitForPlayers.visible = !this.canMove
this.textYourTurn.visible = this.canMove
if (this.canMove) {
this.showText('Your turn!')
} else {
this.showText('Waiting for players')
}
}
setState(state: GameState, playerId: string) {
@ -161,7 +167,6 @@ export class Board extends EventEmitter {
if (lastMove === null) {
return
}
console.log('lastMove :>> ', lastMove)
if (
lastMove !== null &&
lastMove.tile !== undefined &&
@ -319,7 +324,6 @@ export class Board extends EventEmitter {
y += isEndVertical && !isNextVertical ? 0 : 1
}
}
console.log('position::>>', tile.pips, x, y)
tile.setPosition(this.scaleX(x), this.scaleY(y))
tile.setOrientation(orientation)
tile.reScale(this.scale)
@ -386,7 +390,7 @@ export class Board extends EventEmitter {
this.addTile(tile, move)
this.setFreeEnd(move)
} catch (error) {
console.log('error :>> ', error)
this.logger.error(error, 'Error updating board')
}
}
@ -395,7 +399,6 @@ export class Board extends EventEmitter {
}
setValidEnds(values: boolean[], tile: TileDto) {
console.log('validEnds')
if (this.count === 0) {
this.createInteractionsII('right', [[0, 0], undefined, undefined, undefined])
return
@ -405,15 +408,12 @@ export class Board extends EventEmitter {
const side = 'left'
const validInteractions = this.nextTileValidMoves(tile, side)
const validPoints = this.nextTileValidPoints(tile, side, validInteractions)
console.log('validInteractions :>> ', validInteractions)
// this.createInteractions(side, tile)
this.createInteractionsII(side, validPoints)
}
if (values[1]) {
const side = 'right'
const validInteractions = this.nextTileValidMoves(tile, side)
const validPoints = this.nextTileValidPoints(tile, side, validInteractions)
console.log('validInteractions :>> ', validInteractions)
this.createInteractionsII(side, validPoints)
}
}
@ -501,7 +501,6 @@ export class Board extends EventEmitter {
dot.alpha = 0.5
dot.interactive = true
dot.on('pointerdown', () => {
console.log('direction :>> ', direction)
this.emit(`${side}Click`, direction && { direction, x, y })
this.cleanInteractions()
})
@ -567,4 +566,21 @@ export class Board extends EventEmitter {
}
return [canPlayNorth, canPlayEast, canPlaySouth, canPlayWest]
}
gameFinished() {
this.tiles = []
this.boneyard = []
this.movements = []
this.playerHand = []
this.freeEnds = undefined
this.leftTile = undefined
this.rightTile = undefined
this.nextTile = undefined
this.leftDirection = 'west'
this.rightDirection = 'east'
this.firstTile = undefined
this.tilesContainer.removeChildren()
this.interactionContainer.removeChildren()
this.showText('Game finished')
}
}

View File

@ -4,6 +4,7 @@ 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 { SocketIoClientService } from '@/services/SocketIoClientService'
export class Game {
public board!: Board
@ -19,7 +20,9 @@ export class Game {
height: 650
},
private emit: any,
private props: any
private socketService: SocketIoClientService,
private playerId: string,
private sessionId: string
) {}
async setup(): Promise<HTMLCanvasElement> {
@ -66,11 +69,18 @@ export class Game {
const move: Movement = {
id: '',
type: 'pass',
playerId: this.props.playerId ?? ''
playerId: this.playerId
}
this.emit('move', move)
this.board.updateBoard(move)
})
this.hand.on('nextClick', async () => {
await this.socketService.sendMessageWithAck('playerReady', {
user: this.playerId,
sessionId: this.sessionId
})
})
}
getMoves(tile: any): [boolean, boolean] {
@ -101,7 +111,7 @@ export class Game {
const move: Movement = {
tile: this.selectedTile,
type: 'left',
playerId: this.props.playerId ?? '',
playerId: this.playerId,
...data
}
this.emit('move', move)
@ -115,7 +125,7 @@ export class Game {
const move: Movement = {
tile: this.selectedTile,
type: 'right',
playerId: this.props.playerId ?? '',
playerId: this.playerId,
...data
}
this.emit('move', move)
@ -124,6 +134,11 @@ export class Game {
})
}
gameFinished() {
this.hand.gameFinished()
this.board.gameFinished()
}
private removeBoardEvents() {
this.board.off('leftClick')
this.board.off('rightClick')

View File

@ -9,13 +9,16 @@ import {
Ticker
} from 'pixi.js'
import { Tile } from '@/game/Tile'
import type { PlayerState, TileDto } from '@/common/interfaces'
import type { Dimension, PlayerDto, TileDto } from '@/common/interfaces'
import { GlowFilter } from 'pixi-filters'
import { Scale, type ScaleFunction } from './utilities/scale'
import { LoggingService } from '@/services/LoggingService'
export class Hand extends EventEmitter {
tiles: Tile[] = []
container: Container = new Container()
buttonPassContainer: Container = new Container()
buttonPass: Container = new Container()
buttonNext: Container = new Container()
height: number
width: number
ticker: Ticker
@ -24,6 +27,10 @@ export class Hand extends EventEmitter {
initialized: boolean = false
_canMove: boolean = false
scale: number = 1
scaleY!: ScaleFunction
scaleX!: ScaleFunction
grain: number = 25
logger: LoggingService = new LoggingService()
constructor(app: Application) {
super()
@ -34,24 +41,47 @@ export class Hand extends EventEmitter {
this.container.y = app.canvas.height - this.height
this.container.width = this.width
this.container.height = this.height
this.calculateScale()
this.addBg()
this.createPassButton()
}
gameFinished() {
this.logger.debug('gameFinished')
this.tiles = []
this.container.removeChildren()
this.initialized = false
this.buttonNext = this.createButton(
'NEXT',
{ x: this.width / 2 - 25, y: this.height / 2, width: 50, height: 20 },
'nextClick'
)
}
get canMove() {
return this._canMove
}
private calculateScale() {
const scaleXSteps = Math.floor(this.width / (this.grain * this.scale)) / 2
const scaleYSteps = Math.floor(this.height / (this.grain * this.scale)) / 2
this.scaleX = Scale([-scaleXSteps, scaleXSteps], [0, this.width])
this.scaleY = Scale([-scaleYSteps, scaleYSteps], [0, this.height])
}
set canMove(value: boolean) {
this._canMove = value
this.buttonPassContainer.eventMode = value ? 'static' : 'none'
this.buttonPassContainer.cursor = value ? 'pointer' : 'default'
if (value) {
this.createPassButton()
} else {
this.container.removeChild(this.buttonPass)
}
this.tiles.forEach((tile) => {
tile.interactive = value
})
}
initialize(playerState: PlayerState) {
initialize(playerState: PlayerDto) {
this.tiles = this.createTiles(playerState)
this.emit('handUpdated', this.tiles)
this.initialized = this.tiles.length > 0
@ -106,11 +136,15 @@ export class Hand extends EventEmitter {
tile.off('pointerout')
}
private createPassButton() {
const rectangle = new Graphics().roundRect(0, 0, 80, 30, 10).fill(0xffff00)
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: 'PASS',
text: textStr,
style: {
fontFamily: 'Arial',
fontSize: 12,
@ -120,38 +154,45 @@ export class Hand extends EventEmitter {
}
})
text.anchor = 0.5
const container = new Container()
container.addChild(rectangle)
container.addChild(text)
this.buttonPassContainer = new Container()
text.y = y + height / 2
text.x = x + width / 2
this.buttonPassContainer.addChild(rectangle)
this.buttonPassContainer.addChild(text)
text.y = this.buttonPassContainer.height / 2 - 4
text.x = this.buttonPassContainer.width / 2 - 8
this.buttonPassContainer.eventMode = 'none'
this.buttonPassContainer.cursor = 'default'
this.buttonPassContainer.x = 20
this.buttonPassContainer.y = this.height / 2 - 10
container.eventMode = 'static'
container.cursor = 'pointer'
rectangle.alpha = 0.7
text.alpha = 0.7
this.buttonPassContainer.on('pointerdown', () => {
this.emit('passClick')
container.on('pointerdown', () => {
action instanceof Function ? action() : this.emit(action)
})
this.buttonPassContainer.on('pointerover', () => {
container.on('pointerover', () => {
rectangle.alpha = 1
text.alpha = 1
})
this.buttonPassContainer.on('pointerout', () => {
container.on('pointerout', () => {
rectangle.alpha = 0.7
text.alpha = 0.7
})
this.container.addChild(this.buttonPassContainer)
this.container.addChild(container)
return container
}
update(playerState: PlayerState) {
private createPassButton() {
const lastTile = this.tiles[this.tiles.length - 1]
const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0)
this.buttonPass = this.createButton(
'PASS',
{ x, y: this.height / 2, width: 50, height: 20 },
'passClick'
)
}
update(playerState: PlayerDto) {
if (!this.initialized) {
this.initialize(playerState)
return
@ -167,7 +208,7 @@ export class Hand extends EventEmitter {
this.renderTiles()
}
private createTiles(playerState: PlayerState) {
private createTiles(playerState: PlayerDto) {
return playerState.hand.map((tile) => {
const newTile: Tile = new Tile(tile.id, this.ticker, tile.pips, this.scale)
newTile.alpha = 0.7

View File

@ -9,6 +9,8 @@ import App from './App.vue'
import router from './router'
import { SocketIoClientService } from '@/services/SocketIoClientService'
import { LoggingService } from '@/services/LoggingService'
import { AuthenticationService } from './services/AuthenticationService'
const app = createApp(App)
@ -16,5 +18,7 @@ app.use(createPinia())
app.use(router)
app.provide('socket', new SocketIoClientService('http://localhost:3000'))
app.provide('logger', new LoggingService())
app.provide('auth', new AuthenticationService())
app.mount('#app')

View File

@ -1,12 +1,14 @@
import { useGameStore } from '@/stores/game'
import { wait } from '@/common/helpers'
import type { GameSessionState } from '@/common/interfaces'
import type { MatchSessionState } from '@/common/interfaces'
import { storeToRefs } from 'pinia'
import { useEventBusStore } from '@/stores/eventBus'
export class SocketIoEventManager {
gameStore: any = useGameStore()
eventBus = useEventBusStore()
handleSessionStateEvent(data: GameSessionState) {
handleSessionStateEvent(data: MatchSessionState) {
const { updateSessionState } = this.gameStore
updateSessionState(data)
return {
@ -55,4 +57,8 @@ export class SocketIoEventManager {
status: 'ok'
}
}
handleGameFinishedEvent() {
this.eventBus.publish('game-finished')
}
}

View File

@ -54,7 +54,7 @@ const router = createRouter({
})
router.beforeEach((to, from, next) => {
const isLoggedIn = !!localStorage.getItem('token')
const isLoggedIn = !!sessionStorage.getItem('token')
if (to.matched.some((record) => record.meta.requiresAuth) && !isLoggedIn) {
next({ name: 'landing' })
} else {

View File

@ -0,0 +1,61 @@
import { ServiceBase } from '@/services/ServiceBase'
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
import { NetworkService } from '@/services/NetworkService'
export class AuthenticationService extends ServiceBase {
private apiUrl = import.meta.env.VITE_API_URL
private networkService = new NetworkService()
isAuthenticated() {
const auth = useAuthStore()
const { isLoggedIn } = storeToRefs(auth)
return isLoggedIn
}
async login(username: string, password: string) {
try {
const res = await this.networkService.post({
uri: '/login',
body: { username, password }
})
const { token } = res
this.persist(token)
return token
} catch (error) {
console.error(error)
}
}
async logout() {
const auth = useAuthStore()
const { setJwt, setUser } = auth
setJwt(undefined)
setUser(undefined)
sessionStorage.removeItem('token')
}
private persist(jwt: string) {
const auth = useAuthStore()
const { setJwt, setUser } = auth
const loggedUser = this.parseJwt(jwt)
setJwt(jwt)
setUser(loggedUser)
sessionStorage.setItem('token', jwt)
}
private parseJwt(token: string) {
if (!token) {
return
}
const base64Url = token.split('.')[1] ?? ''
const base64 = base64Url.replace('-', '+').replace('_', '/')
return JSON.parse(window.atob(base64))
}
hasRoles(rolesToCheck: string[]) {
const auth = useAuthStore()
const { roles } = storeToRefs(auth)
return roles.value.some((role) => rolesToCheck.includes(role))
}
}

View File

@ -0,0 +1,86 @@
import { createColors } from 'colorette'
import dayjs from 'dayjs'
import pino, { type BaseLogger } from 'pino'
import { isProxy, toRaw } from 'vue'
const { blue, cyan, green, red } = createColors({ useColor: true })
export class LoggingService {
private _logger: BaseLogger
constructor() {
this._logger = pino({
browser: {
asObject: true,
transmit: {
level: import.meta.env.VITE_LOG_LEVEL || 'error',
send: (level, logEvent) => {
const { ts, messages } = logEvent
const logStr: string[] = [dayjs(ts).format('HH:mm:ss.SSS')]
logStr.push(this.colors[level](level.toUpperCase()))
const firstMessage = messages.shift()
if (firstMessage.type === 'Error') {
logStr.push(red(firstMessage.message))
logStr.push(red(firstMessage.stack || ''))
} else if (typeof firstMessage === 'string') {
logStr.push(cyan(firstMessage))
} else {
messages.unshift(firstMessage)
}
console.log(`${logStr.join(' ')}:`, ...messages)
}
}
}
})
}
private get colors(): any {
return {
info: green,
debug: blue,
error: red
}
}
debug(message: string, data?: any) {
this._logger.debug(message, data)
}
info(message: string, data?: any) {
this._logger.info(this._getMessageWidthObject(message, data))
}
warn(message: string, data?: any) {
this._logger.warn(this._getMessageWidthObject(message, data))
}
error(error: any, message?: string) {
this._logger.error(error, message)
}
fatal(message: string, data?: any) {
this._logger.fatal(this._getMessageWidthObject(message, data))
}
trace(message: string, data?: any) {
this._logger.trace(this._getMessageWidthObject(message, data))
}
object(message: any) {
this._logger.info(this._getStringObject(message))
}
_getMessageWidthObject(message: string, data?: any) {
if (!data) {
return message
}
return `${message}\n${this._getStringObject(data)}`
}
_getStringObject(data: any): any {
if (isProxy(data)) {
return this._getStringObject(toRaw(data))
}
return JSON.stringify(data, null, 2)
}
}

View File

@ -0,0 +1,89 @@
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
interface RequestOptions {
uri: string
params?: Record<string, string>
body?: any
auth?: boolean
method?: string
}
export class NetworkService {
private API_URL = import.meta.env.VITE_API_URL
private auth = useAuthStore()
async post(options: RequestOptions) {
options.method = 'POST'
return await this.request(options)
}
async get(options: RequestOptions) {
options.method = 'GET'
return await this.request(options)
}
async patch(options: RequestOptions) {
options.method = 'PATCH'
return await this.request(options)
}
async delete(options: RequestOptions) {
options.method = 'DELETE'
return await this.request(options)
}
async request(options: RequestOptions) {
const { uri, params } = options
if (!uri) {
throw new Error('URL is required')
}
const fetchOptions = this.getFetchOptions(options)
const urlParams = this.getURLParams(params)
const res = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
if (!res.ok) {
throw new Error('Network response was not ok')
}
const text = await res.text()
if (text === '') {
return
} else {
return JSON.parse(text)
}
}
getURLParams(params: any) {
if (!params) {
return ''
}
const urlParams = new URLSearchParams()
Object.keys(params).forEach((key) => urlParams.append(key, params[key]))
return `?${urlParams.toString()}`
}
getFetchOptions(opts: RequestOptions): any {
const { body, auth, method = 'GET' } = opts
const options: any = {
method,
headers: this.getHeaders({ auth })
}
if (!['GET', 'HEAD'].includes(method) && body) {
options.body = typeof body === 'string' ? body : JSON.stringify(body)
}
return options
}
getHeaders({ auth = true }): any {
const { jwt } = storeToRefs(this.auth)
const headers: any = {
'Content-Type': 'application/json'
}
if (auth) {
headers.Authorization = jwt
}
return headers
}
}

View File

@ -0,0 +1,5 @@
import { LoggingService } from './LoggingService'
export class ServiceBase {
protected logger: LoggingService = new LoggingService()
}

View File

@ -1,11 +1,13 @@
import type { GameSessionState, GameState, PlayerState } from '@/common/interfaces'
import type { MatchSessionState, GameState, PlayerDto } from '@/common/interfaces'
import { io, Socket } from 'socket.io-client'
import { SocketIoEventManager } from '@/managers/SocketIoEventManager'
import { LoggingService } from './LoggingService'
export class SocketIoClientService {
public socket: Socket
private isConnected = false
private gameEventManager = new SocketIoEventManager()
private logger: LoggingService = new LoggingService()
constructor(url: string) {
this.socket = io(url)
@ -37,7 +39,7 @@ export class SocketIoClientService {
console.log('Failed to reconnect to server')
})
this.socket.on('sessionState', (data: GameSessionState, callback: any) => {
this.socket.on('matchState', (data: MatchSessionState, callback: any) => {
callback(this.gameEventManager.handleSessionStateEvent(data))
})
@ -45,7 +47,8 @@ export class SocketIoClientService {
callback(this.gameEventManager.handleGameStateEvent(data))
})
this.socket.on('playerState', (data: PlayerState, callback: any) => {
this.socket.on('playerState', (data: PlayerDto, callback: any) => {
this.logger.debug('playerState', data)
callback(this.gameEventManager.handlePlayerStateEvent(data))
})
@ -57,6 +60,11 @@ export class SocketIoClientService {
callback(await this.gameEventManager.handleCanSelectTileEvent())
})
this.socket.on('game-finished', () => {
this.logger.debug('game-finished event received')
this.gameEventManager.handleGameFinishedEvent()
})
this.socket.on('ping', () => {
console.log('Ping received from server')
this.socket.emit('pong') // Send pong response

33
src/stores/auth.ts Normal file
View File

@ -0,0 +1,33 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const jwt = ref<string | undefined>(undefined)
const user = ref<any | undefined>(undefined)
const roles = ref<string[]>([])
const isLoggedIn = computed(() => jwt.value !== undefined)
function setJwt(token: string | undefined) {
jwt.value = token
}
function setUser(userIn: any | undefined) {
user.value = userIn
setRoles(userIn?.roles ?? [])
}
function setRoles(rolesIn: string[]) {
roles.value = rolesIn
}
return {
jwt,
user,
roles,
isLoggedIn,
setJwt,
setUser,
setRoles
}
})

43
src/stores/eventBus.ts Normal file
View File

@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useEventBusStore = defineStore('eventBus', () => {
const events = ref<{ [key: string]: { callback: (payload?: any) => void; once: boolean }[] }>({})
const subscribe = (event: string, callback: (payload?: any) => void) => {
if (!events.value[event]) {
events.value[event] = []
}
events.value[event].push({ callback, once: false })
}
const subscribeOnce = (event: string, callback: (payload?: any) => void) => {
if (!events.value[event]) {
events.value[event] = []
}
events.value[event].push({ callback, once: true })
}
const unsubscribe = (event: string, callback: (payload?: any) => void) => {
if (events.value[event]) {
events.value[event] = events.value[event].filter((e) => e.callback !== callback)
}
}
const publish = (event: string, payload?: any) => {
if (events.value[event]) {
events.value[event] = events.value[event].filter((e) => {
e.callback(payload)
return !e.once // Retain if it's not a one-time event
})
}
}
return {
events,
subscribe,
subscribeOnce,
unsubscribe,
publish
}
})

View File

@ -1,17 +1,22 @@
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import type { GameSessionState, GameState, Movement, PlayerState } from '@/common/interfaces'
import type { MatchSessionState, GameState, Movement, PlayerDto } from '@/common/interfaces'
export const useGameStore = defineStore('game', () => {
const sessionState = ref<GameSessionState | undefined>(undefined)
const sessionState = ref<MatchSessionState | undefined>(undefined)
const gameState = ref<GameState | undefined>(undefined)
const playerState = ref<PlayerState | 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)
function updateSessionState(newState: GameSessionState) {
const isSessionStarted = computed(() => sessionState.value !== undefined)
function updateSessionState(newState: MatchSessionState) {
sessionState.value = newState
}
@ -19,7 +24,7 @@ export const useGameStore = defineStore('game', () => {
gameState.value = newState
}
function updatePlayerState(newState: PlayerState) {
function updatePlayerState(newState: PlayerDto) {
playerState.value = newState
}
@ -39,6 +44,18 @@ export const useGameStore = defineStore('game', () => {
incomingFreeEnds.value = freeEnds
}
function setShowReadyButton(value: boolean) {
showReadyButton.value = value
}
function setReadyForStart(value: boolean) {
readyForStart.value = value
}
function updateGameFinished(value: boolean) {
gameFinished.value = value
}
return {
sessionState,
gameState,
@ -47,12 +64,19 @@ export const useGameStore = defineStore('game', () => {
moveToMake,
incomingFreeEnds,
canSelectTile,
showReadyButton,
readyForStart,
gameFinished,
updateSessionState,
updateGameState,
updatePlayerState,
updateCanMakeMove,
setMoveToMake,
setIncomingFreeEnds,
updateCanSelectTile
updateCanSelectTile,
setShowReadyButton,
setReadyForStart,
updateGameFinished,
isSessionStarted
}
})

View File

@ -5,28 +5,22 @@ import { storeToRefs } from 'pinia'
import { inject, onBeforeUnmount, ref } from 'vue'
import { onMounted } from 'vue'
import useClipboard from 'vue-clipboard3'
import { useRouter } from 'vue-router'
const socketService: any = inject('socket')
let data = ref('')
let responseField = ref('')
let statusField = ref('')
let sessionId = ref('')
let seed = ref('')
let playerId = ref('')
let selectdAction: any = undefined
const { toClipboard } = useClipboard()
const gameStore = useGameStore()
const { moveToMake, canMakeMove, sessionState, gameState } = storeToRefs(gameStore)
const options = [
{ value: 'createSession', default: '{"user": "arhuako"}' },
{ value: 'startSession', default: (id: string) => `{"sessionId": "${id}"}` }
// { value: 'joinSession', default: '{"user": "pepe", "sessionId": "arhuako"}' },
// { value: 'leaveSession', default: '{"user": "pepe", "sessionId": "arhuako"}' },
// { value: 'chat message', default: 'chat message' }
]
const { moveToMake, canMakeMove, sessionState, gameState, playerState } = storeToRefs(gameStore)
onMounted(async () => {})
onMounted(async () => {
startMatch()
})
if (!playerState?.value) {
const router = useRouter()
router.push({ name: 'home' })
}
function makeMove(move: any) {
moveToMake.value = move
@ -37,73 +31,19 @@ onBeforeUnmount(() => {
// socketService.disconnect()
})
function actionSelected() {
if (selectdAction.value === 'createSession') {
responseField.value = ''
} else if (selectdAction.value === 'startSession') {
data.value = selectdAction.default(sessionId.value)
return
}
data.value = selectdAction.default
}
const getMessage = (msg: string) => {
if (msg.startsWith('{') && msg.endsWith('}')) return JSON.parse(msg)
return msg
}
async function createSession() {
const response = await socketService.sendMessageWithAck('createSession', { user: 'arhuako' })
sessionId.value = response.sessionId
playerId.value = response.playerId
}
async function startSession() {
if (sessionId.value) {
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.value,
seed: seed.value.trim()
sessionId: sessionId,
playerId: playerId,
seed: seed?.trim()
})
}
}
async function joinSession() {
if (sessionId.value) {
const response = await socketService.sendMessageWithAck('joinSession', {
user: 'pepe',
sessionId: sessionId.value
})
// sessionId.value = response.sessionId
playerId.value = response.playerId
}
}
async function sendMessage() {
if (selectdAction && data.value.trim() !== '') {
const response = await socketService.sendMessageWithAck(
selectdAction.value,
getMessage(data.value.trim())
)
handleResponse(response)
}
// socketService.emit('message', data.value)
}
function handleResponse(response: any) {
if (selectdAction.value === 'createSession') {
sessionId.value = response.sessionId
playerId.value = response.playerId
}
data.value = ''
const responseStr = JSON.stringify(response, null, 2)
responseField.value = !responseField.value
? responseStr
: responseField.value + '\n---\n ' + responseStr
selectdAction = undefined
}
function copySeed() {
if (sessionState?.value?.seed) toClipboard(sessionState.value.seed)
}
@ -116,18 +56,25 @@ function copySeed() {
Running: {{ sessionState?.sessionInProgress }} Seed: {{ sessionState?.seed }}
<button @click="copySeed">Copy!</button>
</p>
<p>FreeEnds: {{ gameState?.boardFreeEnds }} - {{ gameState?.currentPlayer?.name }}</p>
<p v-if="sessionId">SessionID: {{ sessionId }} PlayerID: {{ playerId }}</p>
<p>
FreeEnds: {{ gameState?.boardFreeEnds }} - Current Player:{{
gameState?.currentPlayer?.name
}}
- Score: {{ gameState?.scoreboard }}
</p>
<p v-if="sessionState?.id">
SessionID: {{ sessionState.id }} PlayerID: {{ playerState?.id }}
</p>
</section>
<section class="block">
<div class="game-container">
<GameComponent :playerId="playerId" :canMakeMove="canMakeMove" @move="makeMove" />
<GameComponent :playerId="playerState?.id" :canMakeMove="canMakeMove" @move="makeMove" />
</div>
</section>
<section class="block">
<!-- <section class="block">
<div class="fixed-grid has-8-cols">
<div class="grid" v-if="!sessionId">
<div class="grid" v-if="!sessionState?.id">
<div class="cell">
<button style="width: 200px" class="button" @click="createSession">
Create Session
@ -156,60 +103,8 @@ function copySeed() {
/>
</div>
</div>
<div class="mt-1 action-select"></div>
</div>
<div class="grid" style="margin-top: 16px; display: none">
<div>
<!-- <ul id="messages"></ul> -->
<form id="form" action="">
<div class="action-select select">
<select
v-model="selectdAction"
id="event"
autocomplete="off"
@change="actionSelected"
>
<option value="">Select event</option>
<option :key="option.value" v-for="option in options" :value="option">
{{ option.value }}
</option>
</select>
<button @click.prevent.stop="sendMessage">Send</button>
</div>
<!-- <p><input id="room" autocomplete="off" /></p> -->
<p>
<textarea
v-model="data"
id="message"
autocomplete="off"
placeholder="Data"
></textarea>
</p>
</form>
</div>
<div>
<div class="grid">
<div>
<textarea
:value="responseField"
id="response"
autocomplete="off"
placeholder="Response"
></textarea>
</div>
<div>
<textarea
:value="statusField"
id="status"
autocomplete="off"
placeholder="Game status"
></textarea>
</div>
</div>
</div>
</div>
</section>
</section> -->
</div>
</template>

View File

@ -1,10 +1,57 @@
<script setup lang="ts">
import { inject, ref } from 'vue'
import { useRouter } from 'vue-router'
import useClipboard from 'vue-clipboard3'
import { useGameStore } from '@/stores/game'
import { storeToRefs } from 'pinia'
import { LoggingService } from '@/services/LoggingService'
let seed = ref('')
const router = useRouter()
const gameStore = useGameStore()
const { toClipboard } = useClipboard()
const socketService: any = inject('socket')
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
function startGame() {
router.push({ name: 'game' })
const { readyForStart, sessionState, isSessionStarted, playerState } = storeToRefs(gameStore)
async function setPlayerReady() {
logger.debug('Starting game')
if (!sessionState.value) {
logger.error('No session found')
return
}
await socketService.sendMessageWithAck('playerReady', {
user: 'arhuako',
sessionId: sessionState.value.id
})
readyForStart.value = true
}
async function createMatch() {
logger.debug('Creating match')
socketService.sendMessageWithAck('createSession', { user: 'arhuako' })
}
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 startMatch() {
if (sessionState.value && sessionState.value.id) {
router.push({ name: 'game' })
}
}
</script>
@ -14,8 +61,22 @@ function startGame() {
<h1 class="title is-2">Welcome to the Player's Home Page</h1>
<div class="block">
<p>This is a protected route.</p>
<p>{{ sessionState || 'No session' }}</p>
<p>{{ playerState?.ready || 'No player state' }}</p>
<p>Session started: {{ isSessionStarted }}</p>
</div>
<button class="button" @click="startGame">Start Game</button>
<div class="block">
<input class="input" style="margin-bottom: 0" v-model="seed" placeholder="Seed" />
</div>
<button class="button" @click="createMatch" v-if="!isSessionStarted">
Create Match Session
</button>
<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">
<span>Start</span>
</button>
</section>
<section class="section available-sessions">
<h2 class="title is-4">Available Sessions</h2>

View File

@ -1,15 +1,24 @@
<script setup lang="ts">
import { ref } from 'vue'
import { AuthenticationService } from '@/services/AuthenticationService'
import { inject, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const username = ref('')
const password = ref('')
function login() {
const authService = inject<AuthenticationService>('auth')
async function login() {
try {
await authService?.login(username.value, password.value)
router.push({ name: 'home' })
} catch (error) {
alert('Invalid username or password')
}
// if (username.value === 'admin' && password.value === 'password') {
localStorage.setItem('token', 'true')
router.push({ name: 'home' })
// localStorage.setItem('token', 'true')
// router.push({ name: 'home' })
// } else {
// alert('Invalid username or password')
// }