feat(app): show leaderboard & enable sending result on game done

This commit is contained in:
ayo 2026-04-03 14:57:51 +02:00
parent 8ba28261f8
commit c09181f380
7 changed files with 1090 additions and 19 deletions

View file

@ -16,8 +16,6 @@
<div id="body-wrapper">
<div id="app">
Please use Chrome or Firefox.
<br />
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
</div>
</div>
<script type="module" src="./main.js"></script>

View file

@ -2,22 +2,41 @@ import mnswpr from '@ayo-run/mnswpr/mnswpr.js'
import '@ayo-run/mnswpr/mnswpr.css'
import * as pkg from '@ayo-run/mnswpr/package.json'
import { LoadingService } from '../utils/'
import { LeaderBoardService } from './modules/leader-board/leader-board.js'
const leaderBoardService = new LeaderBoardService()
const version = import.meta.env.MODE === 'development'
? 'dev'
: pkg.version
const initializeGameBoard = async (level) => {
const prevousLeaderBoard = document.getElementById('leaderboard')
const loadingService = new LoadingService()
const loadingWrapper = document.createElement('div')
loadingWrapper.id = 'loading-wrapper'
loadingService.addLoading(loadingWrapper)
const appElement = document.getElementById('app')
if (prevousLeaderBoard){
const parent = prevousLeaderBoard.parentNode
parent.replaceChild(loadingWrapper, prevousLeaderBoard)
}else{
appElement.append(loadingWrapper)
}
const leaderBoardWrapper = await leaderBoardService.update(level.id, `Best Times (${level.name})`)
leaderBoardWrapper.id = 'leaderboard'
appElement.replaceChild(leaderBoardWrapper, loadingWrapper)
}
const sendGameResult = (game) => {
leaderBoardService.send(game, 'time')
}
const game = new mnswpr('app', version, {
levelChanged: () => console.log('[hook]: level reset'),
gameDone: (game) => console.log('[hook]: game done', game)
levelChanged: (level) => initializeGameBoard(level),
gameDone: (game) => sendGameResult(game)
})
game.initialize()
const loadingService = new LoadingService()
const loadingWrapper = document.createElement('div')
loadingWrapper.id = 'loading-wrapper'
loadingService.addLoading(loadingWrapper)
const appElement = document.getElementById('app')
appElement.append(loadingWrapper)

View file

@ -0,0 +1,176 @@
import { TimerService } from '../../../utils/timer/timer'
import { LoggerService } from '../../../utils/logger/logger'
import { UserService } from '../user/user'
import { initializeApp } from 'firebase/app'
import {
getFirestore, doc, getDocs, getDoc, setDoc, collection, query, orderBy, limit
} from 'firebase/firestore/lite'
export class LeaderBoardService {
timerService = new TimerService()
loggerService = new LoggerService()
user = new UserService()
/**
*
* Create the Leader Board service
* @param {String} leaders
* @param {String} all
* @param {String} configuration
*/
constructor() {
// necessary keys to interact with firebase
// not a secret
// https://stackoverflow.com/questions/37482366/is-it-safe-to-expose-firebase-apikey-to-the-public/37484053#37484053
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const config = {
apiKey: 'AIzaSyCTi_5Sm5dHFNf0d_Gn0MNWmlGheFBf6MQ',
authDomain: 'moment-188701.firebaseapp.com',
databaseURL: 'https://moment-188701.firebaseio.com',
projectId: 'secure-moment-188701',
storageBucket: 'secure-moment-188701.firebasestorage.app',
messagingSenderId: '113827947104',
appId: '1:113827947104:web:b176f746d8358302c51905',
measurementId: 'G-LZRDY0TG46'
}
const app = initializeApp(config)
this._store = getFirestore(app)
const configRef = doc(this.store, 'mw-config', 'configuration')
getDoc(configRef)
.then(res => {
this.configuration = res.data()
})
}
get store() {
return this._store
}
/**
* Update the leader board
* @param {String} level - the id of the game level
* @param {String} title - the displayed name of the game level
* @returns {HTMLDivElement} - element with the rendered leader board
*/
async update(level, title) {
const displayElement = document.createElement('div')
this.lastPlace = Number.MAX_SAFE_INTEGER
const q = query(
collection(this.store, 'mw-leaders', level, 'games'),
orderBy('time'),
limit(10)
)
this.topListSnapshot = await getDocs(q)
this.renderList(displayElement, title, this.topListSnapshot.docs)
return displayElement
}
renderList(displayElement, title, docs) {
if (!displayElement) return
displayElement.innerHTML = ''
const leaderHeading = document.createElement('h3')
leaderHeading.innerText = title
leaderHeading.style.borderBottom = '1px solid #c0c0c0'
leaderHeading.style.paddingBottom = '10px'
displayElement.style.maxWidth = '270px'
displayElement.style.margin = '0 auto'
const leaderList = document.createElement('div')
leaderList.innerHTML = ''
leaderList.style.listStyle = 'none'
leaderList.style.textAlign = 'left'
leaderList.style.marginTop = '-15px'
if (docs && docs.length) {
let i = 1
docs.forEach(game => {
if (game) {
const prettyTime = this.timerService.pretty(game.data().time)
const name = game.data().name || 'Anonymous'
const item = document.createElement('div')
item.style.display = 'flex'
const nameElement =document.createElement('div')
nameElement.innerHTML = name
nameElement.setAttribute('title', name)
nameElement.style.textOverflow = 'ellipsis'
nameElement.style.whiteSpace = 'nowrap'
nameElement.style.overflow = 'hidden'
nameElement.style.padding = '0 5px'
nameElement.style.cursor = 'pointer'
nameElement.style.fontWeight = 'bold'
nameElement.style.fontStyle = 'italic'
// nameElement.onmousedown = () => console.log(game.data());
const indexElement = document.createElement('div')
indexElement.innerText = `#${i++}`
const timeElement = document.createElement('div')
timeElement.innerText = prettyTime
item.append(indexElement, nameElement, timeElement)
leaderList.append(item)
}
})
if (docs.length >= 10) {
this.lastPlace = docs[9].data().time
}
displayElement.append(leaderHeading, leaderList)
} else {
const message = document.createElement('em')
message.innerText = 'Be the first to the top!'
displayElement.append(leaderHeading, message)
}
}
async send(game, key) {
const sessionId = new Date().toDateString().replace(/\s/g, '_')
const gameId = new Date().toTimeString().replace(/\s/g, '_')
const data = { }
data[gameId] = game
const sessionRef = doc(this.store, 'mw-all', this.user.browserId, 'games', sessionId)
await setDoc(sessionRef, data, { merge: true })
const winningCondigion = (
this.configuration
&& game.status === this.configuration.passingStatus
&& game[key] < this.lastPlace
)
if (winningCondigion) {
let name = window.prompt(this.configuration.message)
if (!name) {
name = 'Anonymous'
}
const newGame = {
name,
browserId: this.user.browserId,
...game
}
const gameScoreRef = doc(collection(this.store, 'mw-leaders', game.level, 'games'))
await setDoc(gameScoreRef, newGame)
}
}
configurationPromt() {
if (!this.configuration) {
this.loggerService.debug('Failed to fetch server configuration. Please contact your developer.')
}
}
}

21
app/modules/user/user.js Normal file
View file

@ -0,0 +1,21 @@
export class UserService {
constructor() {
if (!this.id) {
this.browserId = this.generateId()
}
}
generateId() {
var nav = window.navigator
var screen = window.screen
var guid = nav.mimeTypes.length
guid += nav.userAgent.replace(/\D+/g, '')
guid += nav.plugins.length
guid += screen.height || ''
guid += screen.width || ''
guid += screen.pixelDepth || ''
return guid
}
}

View file

@ -5,7 +5,8 @@
"private": true,
"main": "main.js",
"devDependencies": {
"@ayo-run/mnswpr": "workspace:*"
"@ayo-run/mnswpr": "workspace:*",
"firebase": "^12.11.0"
},
"author": "Ayo Ayco"
}

View file

@ -21,7 +21,7 @@ const PC_BUSY_DELAY = 500
* @param {String} appId
* @param {String} version
* @param {{
* levelChanged: () => void,
* levelChanged: (setting: any) => void,
* gameDone: (game: any) => void
* } | undefined } hooks
*/
@ -224,7 +224,7 @@ const Minesweeper = function(appId, version, hooks = undefined) {
* - for initializing the leaderboard
*/
if (initial)
hooks.levelChanged()
hooks.levelChanged(setting)
timerService.initialize(timerDisplay)
updateFlagsCountDisplay()

File diff suppressed because it is too large Load diff