feat: initial npm package @ayo-run/mnswpr

This commit is contained in:
ayo 2026-04-03 02:02:50 +02:00
parent aea4ac2518
commit 304557163b
10 changed files with 342 additions and 1182 deletions

View file

@ -19,6 +19,10 @@
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
</div>
</div>
<script type="module" src="./src/main.js"></script>
<script type="module">
import MineSweeper from './src/main.js'
const mnswpr = new MineSweeper('app', 'dev')
mnswpr.initialize()
</script>
</body>
</html>

View file

@ -1,8 +1,9 @@
{
"name": "@ayco/mnswpr",
"version": "0.4.14",
"name": "@ayo-run/mnswpr",
"version": "0.0.1",
"private": true,
"description": "Classic Minesweeper browser game",
"author": "Ayo Ayco",
"author": "Ayo",
"type": "module",
"repository": {
"type": "git",
@ -14,22 +15,22 @@
"dev": "vite",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"build:preview": "npm run build && npm run preview",
"prepare": "husky",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "bumpp && node scripts/release.js"
},
"files": [
"dist",
"README.md"
],
"license": "BSD-2-Clause",
"dependencies": {
"firebase": "^12.11.0"
},
"devDependencies": {
"@eslint/css": "^1.1.0",
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.10.0",
"bumpp": "^11.0.1",
"esbuild": "^0.28.0",
"eslint": "^10.1.0",
"globals": "^17.4.0",
"husky": "^9.1.7",

File diff suppressed because it is too large Load diff

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
packages:
- "package"

View file

@ -2,5 +2,4 @@ import './modules/loading/loading.css'
import './mnswpr.css'
import Minesweeper from './mnswpr.js'
const myMinesweeper = new Minesweeper('app')
myMinesweeper.initialize()
export default Minesweeper

View file

@ -1,32 +1,26 @@
// @ts-check
import {
LeaderBoardService,
LoggerService,
LoadingService,
StorageService,
TimerService
} from './modules/index.js'
import { levels } from './levels.js'
import * as pkg from '../package.json'
const TEST_MODE = false // set to true if you want to test the game with visual hints and separate leaderboard
const VERSION = import.meta.env.MODE === 'development' ? 'dev' : `v${pkg.version}`
const TEST_MODE = false // set to true if you want to test the game with visual hints
const MOBILE_BUSY_DELAY = 250
const PC_BUSY_DELAY = 500
/**
* Create Minesweeper game board
* @param {String} appId
* @param {String} version
*/
export const Minesweeper = function(appId) {
export const Minesweeper = function(appId, version) {
const _this = this
const storageService = new StorageService()
const timerService = new TimerService()
const loggerService = new LoggerService()
const leaderBoardService = new LeaderBoardService()
const loadingService = new LoadingService()
let grid = document.createElement('table')
grid.setAttribute('id', 'grid')
@ -78,8 +72,8 @@ export const Minesweeper = function(appId) {
const headingElement = document.createElement('h1')
const gameBoard = document.createElement('div')
headingElement.innerHTML = `<span>Minesweeper</span><sup>${VERSION}</sup>`
document.title = `mnswpr [${VERSION}]`
headingElement.innerHTML = `<span>Minesweeper</span><sup>${version}</sup>`
document.title = `mnswpr [${version}]`
gameBoard.setAttribute('id', 'game-board')
gameBoard.append(initializeToolbar(), grid, initializeFootbar())
if(appElement) {
@ -87,7 +81,7 @@ export const Minesweeper = function(appId) {
appElement.append(headingElement, gameBoard)
appElement.append(initializeSourceLink())
}
generateGrid(true)
generateGrid()
}
function initializeSourceLink() {
@ -100,22 +94,6 @@ export const Minesweeper = function(appId) {
return sourceLink
}
async function initializeLeaderBoard() {
const title = `Best Times (${setting.name})`
const previousLeaderBoard = document.getElementById('leaderboard')
let loading = document.createElement('div')
loadingService.addLoading(loading)
if (previousLeaderBoard)
appElement?.replaceChild(loading, previousLeaderBoard)
else
appElement?.append(loading)
const leaderBoard = await leaderBoardService.update(setting.id ?? setting.name, title)
leaderBoard.id = 'leaderboard'
appElement?.replaceChild(leaderBoard, loading)
}
function initializeFootbar() {
const footBar = document.createElement('div')
@ -188,11 +166,11 @@ export const Minesweeper = function(appId) {
function updateSetting(key) {
setting = levels[key]
storageService.saveToLocal('setting', setting)
generateGrid(true)
generateGrid()
}
function generateGrid(initial = false) {
function generateGrid() {
firstClick = true
grid.innerHTML = ''
grid.oncontextmenu = () => false
@ -225,8 +203,12 @@ export const Minesweeper = function(appId) {
appElement.style.margin = '0 auto'
}
if (initial)
initializeLeaderBoard()
/**
* TODO: add hook afterGridGenerated
* - for initializing the leaderboard
*/
console.log('[hook]: after grid generated')
timerService.initialize(timerDisplay)
updateFlagsCountDisplay()
@ -488,9 +470,11 @@ export const Minesweeper = function(appId) {
isMobile
}
if (!TEST_MODE) {
leaderBoardService.send(game, 'time')
}
/**
* TODO: add hook after gameSession send back `game`
* - for sending the game score to the db
*/
console.log('[hook]: after game session', game)
}

View file

@ -1,7 +1,5 @@
export * from './leader-board/leader-board.js'
export * from './loading/loading.js'
export * from './logger/logger.js'
export * from './storage/storage.js'
export * from './timer/timer.js'
export * from './user/user.js'

View file

@ -1,164 +0,0 @@
import { TimerService } from '../timer/timer'
import { UserService } from '../user/user'
import { LoggerService } from '../logger/logger'
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
}
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 })
if (this.configuration && game.status === this.configuration.passingStatus && game[key] < this.lastPlace) {
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.')
}
}
}

View file

@ -1,21 +0,0 @@
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
}
}

12
vite.config.js Normal file
View file

@ -0,0 +1,12 @@
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: resolve(import.meta.dirname, 'src/main.js'),
name: 'mnswpr',
fileName: 'mnswpr'
}
}
})