From 7b083fda11f914f696655b34dc33fa53f5e4d59b Mon Sep 17 00:00:00 2001 From: Nathan DECHER Date: Mon, 13 Apr 2020 21:58:53 +0200 Subject: [PATCH] added leaderboard (closes #29) --- .gitignore | 1 + Dockerfile | 2 +- README.md | 4 +- api.js | 103 ++++++++ assets/config.json | 1 + assets/levelList.json | 8 +- assets/metaConfig.json | 5 + index.js | 8 +- init.sql | 8 + install.js | 5 + levels/arcade-survival.json | 3 +- package-lock.json | 486 +++++++++++++++++++++++++++++++++++- package.json | 4 +- public/index.html | 16 +- src/js/leaderboards.js | 109 ++++++++ src/js/levels.js | 27 +- src/js/main.js | 62 +++-- src/js/popup.js | 22 ++ src/less/popup.less | 16 +- src/less/snek.less | 10 + 20 files changed, 852 insertions(+), 48 deletions(-) create mode 100644 api.js create mode 100644 init.sql create mode 100644 install.js create mode 100644 src/js/leaderboards.js diff --git a/.gitignore b/.gitignore index f7e3fbd..4a4e4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ public/css/*.css public/js/*.js public/favicon.ico build/*.png +db.sqlite diff --git a/Dockerfile b/Dockerfile index f39927f..b67f879 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ FROM alpine:3.11.5 -RUN apk add --no-cache nodejs npm git imagemagick make && git clone https://gitdab.com/Codinget/Snek /snek && cd /snek && npm i && make && apk del git make imagemagick && printf '#!/bin/sh\ncd /snek\nnpm start\n' > start.sh && chmod +x start.sh +RUN apk add --no-cache nodejs npm git imagemagick make python3 python2 && npm i -g node-gyp && git clone https://gitdab.com/Codinget/Snek /snek && cd /snek && npm i --unsafe-perm && apk del git make imagemagick python2 python3 && printf '#!/bin/sh\ncd /snek\nnpm start\n' > start.sh && chmod +x start.sh CMD /snek/start.sh diff --git a/README.md b/README.md index 48b3b6a..6fc7db2 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ A "simple" Snake, done as my final JS class project - Make - Node.js and npm, both in the PATH - Node.js 10 and 12 are known to work + - node-gyp and python are required for the database ## Running the game (dev) - `git clone` this repo -- `npm install` the dependencies -- `make` the ressources +- `npm install` the dependencies (this will also build the ressources and initialize the database) - `npm start` the server ## Running the game (prod) diff --git a/api.js b/api.js new file mode 100644 index 0000000..9fe55bd --- /dev/null +++ b/api.js @@ -0,0 +1,103 @@ +const DB=require('better-sqlite3'); +const {Router}=require('express'); + +const db=new DB('db.sqlite'); +const api=new Router(); + +const leaderboardSelect='SELECT username, score, length, time, speed FROM leaderboards'; +const getLeaderboardsBy={ + score: db.prepare(`${leaderboardSelect} WHERE mode=? ORDER BY score DESC LIMIT ? OFFSET ?`), + timeA: db.prepare(`${leaderboardSelect} WHERE mode=? ORDER BY time ASC LIMIT ? OFFSET ?`), + timeD: db.prepare(`${leaderboardSelect} WHERE mode=? ORDER BY time DESC LIMIT ? OFFSET ?`) +}; +const addLeaderboard=db.prepare(` + INSERT + INTO leaderboards(mode, username, score, length, time, speed) + VALUES(@mode, @username, @score, @length, @time, @speed) +`); + +const levelList=require('./assets/levelList.json'); +const validMode=mode => { + try { + const [category, name]=mode.split('/'); + return levelList[category].levels.map(l => ''+l).includes(name); + } catch(e) { + return false; + } +}; + +api.get('/leaderboards/:category/:id', (req, res) => { + const sort=req.query.sort || 'score'; + if(!['score', 'timeA', 'timeD'].includes(sort)) return res.status(400).json({ + ok: false, + err: 'Invalid sort' + }); + const results=+req.query.results || 20; + if((typeof results)!='number' || results<1 || results >100) return res.status(400).json({ + ok: false, + err: 'Invalid result count' + }); + const page=+req.query.page || 1; + if((typeof page)!='number' || page<1) return res.status(400).json({ + ok: false, + err: 'Invalid page' + }); + + const data=getLeaderboardsBy[sort] + .all(req.params.category+'/'+req.params.id, results, (page-1)*results); + return res.status(200).json({ + ok: true, + data + }); +}); + +api.post('/leaderboards/:category/:id', (req, res) => { + const mode=req.params.category+'/'+req.params.id; + if((typeof mode)!='string' || !validMode(mode)) return res.status(400).json({ + ok: false, + err: 'Invalid mode' + }); + const username=req.body.username; + if((typeof username)!='string') return res.status(400).json({ + ok: false, + err: 'Invalid username' + }); + const score=req.body.score; + if((typeof score)!='number' || score%1 || score<0) return res.status(400).json({ + ok: false, + err: 'Invalid score' + }); + const length=req.body.length; + if((typeof length)!='number' || length%1 || length<1) return res.status(400).json({ + ok: false, + err: 'Invalid length' + }); + const time=req.body.time; + if((typeof time)!='number' || time%1 || time<0) return res.status(400).json({ + ok: false, + err: 'Invalid time' + }); + const speed=req.body.speed; + if((typeof speed)!='number' || speed%1 || speed<1) return res.status(400).json({ + ok: false, + err: 'Invalid speed' + }); + + try { + addLeaderboard.run({ + mode, username, + score, length, + time, speed + }); + res.json({ + ok: true + }); + } catch(e) { + res.status(500).json({ + ok: false, + err: 'Failed to add score' + }); + } +}); + +return module.exports=exports=api; diff --git a/assets/config.json b/assets/config.json index 07e886a..09c1aba 100644 --- a/assets/config.json +++ b/assets/config.json @@ -1,5 +1,6 @@ { "player.name": "Player", + "player.leaderboards": false, "input.touchscreen.crosspad.enabled": false, "input.touchscreen.crosspad.overlay": true, diff --git a/assets/levelList.json b/assets/levelList.json index 429c610..1e960a1 100644 --- a/assets/levelList.json +++ b/assets/levelList.json @@ -6,7 +6,9 @@ "speedIncrease": false, "worldWrap": false, "winCondition": "fruit", - "scoreSystem": "speedrun" + "scoreSystem": "speedrun", + "uploadOnDeath": false, + "leaderboardsSort": "timeA" }, "levelFilename": "level.json", "levelDisplay": "Level ", @@ -22,7 +24,9 @@ "speedIncrease": true, "speedMultiplier": 0.9, "speedCap": 50, - "worldWrap": true + "worldWrap": true, + "uploadOnDeath": true, + "leaderboardsSort": "score" }, "levelFilename": "arcade-.json", "levelDisplay": "", diff --git a/assets/metaConfig.json b/assets/metaConfig.json index 9640c94..fded18f 100644 --- a/assets/metaConfig.json +++ b/assets/metaConfig.json @@ -6,6 +6,11 @@ "name": "Player name", "type": "string" }, + "player.leaderboards": { + "name": "Upload scores to leaderboards", + "type": "boolean", + "needsBackend": true + }, "input": { "name": "Input settings" diff --git a/index.js b/index.js index 03d31f6..508f995 100644 --- a/index.js +++ b/index.js @@ -3,8 +3,14 @@ const express=require('express'); const app=express(); const PORT=process.env.PORT || 3000; +app.use(express.json()); app.use(express.static('public')); + +app.use('/api', require('./api')); +app.get('/api/has-nodejs', (req, res) => { + res.json("yes"); +}); + app.listen(PORT, () => { console.log(`Listening on 0.0.0.0:${PORT}`); }); - diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..5fc6e2e --- /dev/null +++ b/init.sql @@ -0,0 +1,8 @@ +CREATE TABLE leaderboards ( + mode TEXT, + username TEXT, + score INTEGER, + length INTEGER, + time INTEGER, + speed INTEGER +); diff --git a/install.js b/install.js new file mode 100644 index 0000000..1b8c370 --- /dev/null +++ b/install.js @@ -0,0 +1,5 @@ +const DB=require('better-sqlite3'); +const fs=require('fs'); + +const db=new DB('db.sqlite'); +db.exec(fs.readFileSync('init.sql', 'utf8')); diff --git a/levels/arcade-survival.json b/levels/arcade-survival.json index efbcd8f..7a31b10 100644 --- a/levels/arcade-survival.json +++ b/levels/arcade-survival.json @@ -44,6 +44,7 @@ "autoSpeadIncreaseTicks": 10, "autoSizeGrow": true, "autoSizeGrowTicks": 100, - "scoreSystem": "survival" + "scoreSystem": "survival", + "leaderboardsSort": "timeD" } } diff --git a/package-lock.json b/package-lock.json index 234e940..da4bcfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,25 @@ "uri-js": "^4.2.2" } }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -69,6 +88,11 @@ "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", "optional": true }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -78,6 +102,52 @@ "tweetnacl": "^0.14.3" } }, + "better-sqlite3": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-6.0.1.tgz", + "integrity": "sha512-4aV1zEknM9g1a6B0mVBx1oIlmYioEJ8gSS3J6EpN1b1bKYEE+N5lmpmXHKNKTi0qjHziSd7XrXwHl1kpqvEcHQ==", + "requires": { + "bindings": "^1.5.0", + "integer": "^3.0.1", + "prebuild-install": "^5.3.3", + "tar": "4.4.10" + } + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -95,6 +165,15 @@ "type-is": "~1.6.17" } }, + "buffer": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", + "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -106,11 +185,21 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "optional": true }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -120,6 +209,11 @@ "delayed-stream": "~1.0.0" } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -146,8 +240,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "optional": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "dashdash": { "version": "1.14.1", @@ -166,12 +259,30 @@ "ms": "2.0.0" } }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "optional": true }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -182,6 +293,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -202,6 +318,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -221,6 +345,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -282,6 +411,11 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "optional": true }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -323,6 +457,34 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -332,6 +494,11 @@ "assert-plus": "^1.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", @@ -354,6 +521,11 @@ "har-schema": "^2.0.0" } }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -385,6 +557,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -396,17 +573,44 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "integer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/integer/-/integer-3.0.1.tgz", + "integrity": "sha512-OqtER6W2GIJTIcnT5o2B/pWGgvurnVOYs4OZCgay40QEIbMTnNq4R0KSaIw1TZyFtPWjm5aNM+pBBMTfc3exmw==", + "requires": { + "bindings": "^1.5.0", + "prebuild-install": "^5.3.3" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "optional": true }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -499,37 +703,101 @@ "mime-db": "1.43.0" } }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "optional": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } }, "mkdirp": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", - "optional": true, "requires": { "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz", + "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "node-abi": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.15.0.tgz", + "integrity": "sha512-FeLpTS0F39U7hHZU1srAK4Vx+5AHNVOTP+hxBNQknR/54laTHSFIJkDWDqiquY1LeLUgTfPN7sLPhMubx0PLAg==", + "requires": { + "semver": "^5.4.1" + } + }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "optional": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -538,6 +806,14 @@ "ee-first": "1.1.1" } }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -554,6 +830,33 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "optional": true }, + "prebuild-install": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", + "integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -584,6 +887,15 @@ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "optional": true }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -611,6 +923,31 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -657,6 +994,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", @@ -695,11 +1037,36 @@ "send": "0.17.1" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -728,6 +1095,86 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", + "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -752,7 +1199,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -786,6 +1232,11 @@ "punycode": "^2.1.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -812,6 +1263,29 @@ "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } } diff --git a/package.json b/package.json index 79b1353..6127cb3 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,8 @@ "description": "A simple Snake", "main": "index.js", "scripts": { - "build": "make", "start": "node index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "prepare": "rm -f db.sqlite && make clean all && node install.js" }, "repository": { "type": "git", @@ -15,6 +14,7 @@ "author": "Codinget", "license": "MIT", "dependencies": { + "better-sqlite3": "^6.0.1", "express": "^4.17.1", "less": "^3.11.1" } diff --git a/public/index.html b/public/index.html index ed37b18..d8c6bbd 100644 --- a/public/index.html +++ b/public/index.html @@ -10,15 +10,16 @@ window.addEventListener('load', () => require('main')); - +

Snek

A "simple" Snake

@@ -34,8 +35,13 @@
diff --git a/src/js/leaderboards.js b/src/js/leaderboards.js new file mode 100644 index 0000000..ab091bc --- /dev/null +++ b/src/js/leaderboards.js @@ -0,0 +1,109 @@ +const Popup=require('popup'); +const levels=require('levels'); +const config=require('config'); + +const upload=async (mode, win, snek) => { + if(!win && !snek.rules.uploadOnDeath) return; + if(window.serverless) return; + + const username=config.getS('player.name'); + const score=snek.score; + const length=snek.length; + const time=snek.endPlayTime; + const speed=snek.speed; + + const rst=await fetch('api/leaderboards/'+mode, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username, + score, length, + time, speed + }) + }); + const dat=await rst.json(); + if(!dat.ok) console.error(rst.err); +}; + +const show=async (mode='speedrun/1', page=1) => { + let popup=new Popup("Leaderboards: "+mode); + + const [category, id]=mode.split('/'); + let modes=[]; + (() => { + Object.keys(window.levelList).forEach(cat => { + window.levelList[cat].levels.forEach(lvl => { + modes.push(cat+'/'+lvl); + }); + }); + })(); + const prevMode=() => { + let idx=modes.indexOf(mode); + return modes[idx-1]||modes[modes.length-1]; + }; + const nextMode=() => { + let idx=modes.indexOf(mode); + return modes[idx+1]||modes[0]; + }; + + const rules=await levels.getRules(category, id); + const sort=rules.leaderboardsSort; + const rst=await fetch('api/leaderboards/'+mode+'?sort='+sort+'&page='+page+'&results=10'); + const {ok, data, err}=await rst.json(); + + popup.buttons.close="Close"; + popup.buttons.modeP="Previous mode"; + popup.buttons.modeN="Next mode"; + popup.large=true; + popup.animation=false; + + if(ok) { + popup.addStrong("Page "+page); + if(data.length==10) popup.buttons.next="Next page"; + if(page>1) popup.buttons.prev="Previous page"; + + if(data.length==0) { + popup.addEm("No data"); + } else { + const rpad=(n, digits=2, pad=' ') => + ((''+n).length>=digits)?(''+n):(rpad(pad+n, digits, pad)); + + popup.addTable(data.map(({username, score, length, speed, time}, i) => { + return { + rank: '#'+(i+(page-1)*10+1), + username, + score: score+'pts', + length, + speed: speed+'tps', + time: rpad(Math.floor(time/60000), 2, '0')+ + ':'+rpad(Math.floor(time/1000)%60, 2, '0')+ + ':'+rpad(time%1000, 3, '0') + }; + }), [ + 'rank', + 'username', + 'score', + 'length', + 'speed', + 'time' + ]); + } + } else { + popup.addStrong("Error loading leaderboards"); + popup.addEm(err); + } + + Popup.dismiss(); + const verb=await popup.display(); + if(verb=='next') return show(mode, page+1); + else if(verb=='prev') return show(mode, page-1); + else if(verb=='modeP') return show(prevMode()); + else if(verb=='modeN') return show(nextMode()); + location.hash=''; +}; + +return module.exports={ + upload, show +}; diff --git a/src/js/levels.js b/src/js/levels.js index a5b1fea..f5fd572 100644 --- a/src/js/levels.js +++ b/src/js/levels.js @@ -7,12 +7,37 @@ const get=async filename => { return cache[filename]=json; }; +const getInfo=(category, id) => { + const cat=levelList[category]; + id=''+id; + + const displayName=cat.levelDisplay + .replace(//g, id) + .replace(//g, id.toLowerCase()); + const fileName=cat.levelFilename + .replace(//g, id) + .replace(//g, id.toLowerCase()); + const levelString=category+'/'+id+'/'+fileName; + + return { + displayName, + fileName, + levelString + }; +}; + +const getRules=async (category, id) => { + const {fileName}=getInfo(category, id); + const json=await get(fileName); + return Object.assign({}, window.levelList[category].rules, json.rules); +}; + const clearCache=() => Object .keys(cache) .forEach(key => delete cache[key]); return module.exports={ - get, + get, getRules, getInfo, clearCache }; diff --git a/src/js/main.js b/src/js/main.js index 5e88584..5cbd3bf 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -8,6 +8,7 @@ const input=require('input'); const levels=require('levels'); const config=require('config'); + const leaderboards=require('leaderboards'); // get a known state await new Promise(ok => assets.onReady(ok)); @@ -20,13 +21,29 @@ const hud=main.querySelector('#hud'); // load data from server - const levelList=assets.get('levelList'); + const levelList=window.levelList=assets.get('levelList'); + + // detect if we're running with a server + const serverless=window.serverless=await (async() => { + const res=await fetch('api/has-nodejs'); + if(!res.ok) return true; + const msg=await res.json(); + return msg!='yes'; + })(); + if(serverless) { + document.body.classList.add('serverless'); + } else { + document.body.classList.add('server'); + } + + // flag the body as loaded + document.body.classList.remove('loading'); // get our global variables let currentGame=null; // forward-declare functions - let resizeCanvas, getLevel, startGame, stopGame, handleWin, handleDeath, menu, help, settings, restart, updateHud; + let resizeCanvas, startGame, stopGame, handleWin, handleDeath, menu, help, settings, showLeaderboards, restart, updateHud; // handle window resize and fullscreen resizeCanvas=() => { @@ -48,26 +65,6 @@ } }); - // get a level for a category and an id - getLevel=(category, id) => { - const cat=levelList[category]; - id=''+id; - - const displayName=cat.levelDisplay - .replace(//g, id) - .replace(//g, id.toLowerCase()); - const fileName=cat.levelFilename - .replace(//g, id) - .replace(//g, id.toLowerCase()); - const levelString=category+'/'+id+'/'+fileName; - - return { - displayName, - fileName, - levelString - }; - }; - // buid menu from level list Object.keys(levelList).forEach(category => { const cat=levelList[category]; @@ -81,7 +78,7 @@ const ul=section.appendChild(document.createElement('ul')); cat.levels.forEach((level, i) => { - const {displayName, fileName, levelString}=getLevel(category, level); + const {displayName, fileName, levelString}=levels.getInfo(category, level); const li=ul.appendChild(document.createElement('li')); const a=li.appendChild(document.createElement('a')); a.href='#'+levelString; @@ -106,6 +103,12 @@ } }; + // display the leaderboards + showLeaderboards=() => { + stopGame(); + leaderboards.show(); + }; + // start a new game startGame=async (category, levelId, filename) => { // stop any running games and clear popups @@ -192,6 +195,9 @@ // fetch userdata from the game const {category, levelId, filename}=snek.userdata; + // upload scores + if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, true, snek); + // create and configure popup let popup=new Popup("Finished!"); popup.addStrong("You won!"); @@ -221,7 +227,7 @@ } else if(result=='next') { const {category, levelId}=snek.userdata; let nextId=(+levelId)+1; - let {levelString}=getLevel(category, nextId) + let {levelString}=levels.getInfo(category, nextId) location.hash=levelString; } }; @@ -231,6 +237,12 @@ // hide the HUD hud.classList.add('hidden'); + // fetch userdata from the game + const {category, levelId, filename}=snek.userdata; + + // upload scores + if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, false, snek); + // create and configure popup let popup=new Popup("Finished!"); popup.addStrong(config.getS('player.name')+' '+snek.death.message); @@ -251,7 +263,6 @@ // act on it if(result=='retry') { - const {category, levelId, filename}=snek.userdata; startGame(category, levelId, filename); } else if(result=='menu') { location.hash='menu'; @@ -304,6 +315,7 @@ if(hash=='' || hash=='menu') return menu(); else if(hash=='help') return help(); else if(hash=='settings') return settings(); + else if(hash=='leaderboards') return showLeaderboards(); const [_, category, levelId, filename]=location.hash.match(/([a-zA-Z0-9_-]+?)\/([a-zA-Z0-9_-]+?)\/(.+)/); startGame(category, levelId, filename); diff --git a/src/js/popup.js b/src/js/popup.js index db8697e..522588e 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -17,6 +17,7 @@ const objToDom=obj => { return ul; } else { let table=document.createElement('table'); + table.classList.add('dual'); Object .keys(obj) .forEach(key => { @@ -34,6 +35,7 @@ class Popup { this.content=content.map(objToDom); this.buttons={...buttons}; this.large=large; + this.animation=true; } addContent(cnt) { @@ -53,6 +55,25 @@ class Popup { hn.innerText=cnt; this.content.push(hn); } + addTable(data, heading=Object.keys(data)) { + let table=document.createElement('table'); + table.classList.add('table'); + let thead=table.appendChild(document.createElement('thead')); + let headingRow=thead.appendChild(document.createElement('tr')); + heading.forEach(key => { + let th=headingRow.appendChild(document.createElement('th')); + th.innerText=key; + }); + let tbody=table.appendChild(document.createElement('tbody')); + data.forEach(row => { + let tr=tbody.appendChild(document.createElement('tr')); + heading.forEach(key => { + let td=tr.appendChild(document.createElement('td')); + td.innerText=row[key]; + }); + }); + this.content.push(table); + } async display(parent=document.body) { let outer=document.createElement('div'); @@ -60,6 +81,7 @@ class Popup { let popup=outer.appendChild(document.createElement('div')); popup.classList.add('content'); if(this.large) popup.classList.add('large'); + if(this.animation) outer.classList.add('animation'); let title=popup.appendChild(document.createElement('h1')); title.innerText=this.title; diff --git a/src/less/popup.less b/src/less/popup.less index e2ada25..b935e51 100644 --- a/src/less/popup.less +++ b/src/less/popup.less @@ -8,7 +8,10 @@ } .popup { - animation: popupAppear 1s linear; + &.animation { + animation: popupAppear 1s linear; + } + background: rgba(0, 0, 0, 90%); position: absolute; @@ -64,7 +67,7 @@ } } - table { + .dual { display: flex; flex-direction: column; width: 100%; @@ -86,6 +89,15 @@ flex: 1; } } + + .table { + width: 100%; + + td, th { + padding: .5rem; + } + } + label { margin-right: 1ex; } diff --git a/src/less/snek.less b/src/less/snek.less index fdbceef..dc14469 100644 --- a/src/less/snek.less +++ b/src/less/snek.less @@ -129,6 +129,16 @@ p { display: none !important; } +body.serverless .server { + display: none !important; +} +body.server .serverless { + display: none !important; +} +body.loading .loaded { + display: none !important; +} + // setup the progress bar @import 'progressBar';