diff --git a/classes/Game.js b/classes/Game.js index afe8171..11a8b3b 100644 --- a/classes/Game.js +++ b/classes/Game.js @@ -1,15 +1,22 @@ +const dayjs = require('dayjs'); const crypto = require('crypto'); const Team = require('./Team'); module.exports = class Game { + onStateChange = null; + id = crypto.randomUUID(); - name = null; + champions = null; teams = null; + expiration = dayjs().add(24, 'hour'); + + roundExpiration = null; bans = [[], []]; picks = [[], []]; + hover = [null, null]; ready = [false, false]; order = [ @@ -37,12 +44,18 @@ module.exports = class Game { ]; current = 0; - constructor(name, teamNames) { - this.name = name; + constructor(teamNames, champions, onStateChange) { + this.champions = champions; this.teams = [ new Team(teamNames[0]), new Team(teamNames[1]), ]; + + this.onStateChange = onStateChange; + } + + getExpiration() { + return this.expiration; } getId() { @@ -52,12 +65,14 @@ module.exports = class Game { getState() { return { id: this.id, - name: this.name, ready: this.ready, + hover: this.hover, bans: this.bans, picks: this.picks, + roundExpiration: Number(this.roundExpiration), + order: this.order, current: this.current, @@ -85,7 +100,25 @@ module.exports = class Game { return index === null ? null : this.teams[index]; } - canAct(id, action) { + endRound() { + this.roundExpiration = null; + clearTimeout(this.roundTimeout); + } + + startRound() { + this.endRound(); + + const ROUND_LENGTH_SECONDS = 33; + + this.roundExpiration = dayjs().add(ROUND_LENGTH_SECONDS, 'second'); + this.roundTimeout = setTimeout(() => this.autoAct(this.order[this.current][1]), ROUND_LENGTH_SECONDS * 1000); + } + + canAct(id, action, value) { + if(value === null) { + return false; + } + const teamIndex = this.getTeamIndexById(id); if(teamIndex === null) { @@ -98,28 +131,59 @@ module.exports = class Game { const currentRound = this.order[this.current]; - return currentRound[0] === teamIndex && currentRound[1] === action; + if([this.picks, this.bans].flat(Infinity).includes(value)) { + return false; + } + + return currentRound[0] === teamIndex && ['hover', currentRound[1]].includes(action); } - act(id, action, championId) { - if(!this.canAct(id, action)) { - return false; + act(id, action, value, bypass = false) { + if(!bypass && !this.canAct(id, action, value)) { + return; } const teamIndex = this.getTeamIndexById(id); - if(action === 'ban') { - this.bans[teamIndex].push(championId); + switch(action) { + case 'ban': + case 'pick': + this[action + 's'][teamIndex].push(value); + this.hover[teamIndex] = null; + ++this.current; - ++this.current; - } else if(action === 'pick') { - this.picks[teamIndex].push(championId); + if(this.order[this.current][1] === 'done') { + this.endRound(); + } else { + this.startRound(); + } + + break; + + case 'hover': + case 'ready': + this[action][teamIndex] = value; - ++this.current; - } else if(action === 'ready') { - this.ready[teamIndex] = true; + if(action === 'ready' && !this.ready.includes(false)) { + this.startRound(); + } + + break; + } + + this.onStateChange(this); + } + + autoAct(action) { + const teamIndex = this.order[this.current][0]; + + let value = null; + if(this.hover[teamIndex] !== null) { + value = this.hover[teamIndex]; + } else if(action === 'pick') { + value = this.champions.filter(champion => ![this.picks, this.bans].flat().includes(champion.id)).sort(() => Math.random() < 0.5 ? 1 : -1)[0].id; } - return true; + this.act(this.teams[teamIndex].getId(), action, value, true); } }; diff --git a/classes/GameList.js b/classes/GameList.js new file mode 100644 index 0000000..2ba216c --- /dev/null +++ b/classes/GameList.js @@ -0,0 +1,28 @@ +const dayjs = require('dayjs'); + +module.exports = class GameList { + static games = {}; + + static add(id, game) { + this.games[id] = game; + } + + static get(id) { + return this.games[id]; + } + + static flushExpired() { + const now = dayjs(); + const expiredIds = []; + + for(let id in this.games) { + if(this.games.hasOwnProperty(id) && now.isAfter(this.games[id].getExpiration())) { + delete this.games[id]; + + expiredIds.push(id); + } + } + + return expiredIds; + } +}; diff --git a/create-server.js b/create-server.js index e153aca..fa70d89 100644 --- a/create-server.js +++ b/create-server.js @@ -1,24 +1,28 @@ module.exports = function createServer(createGame, onJoinGame, onGameAction) { const express = require('express'); const app = express(); + const compression = require('compression'); const bodyParser = require('body-parser'); const http = require('http').Server(app); const io = require('socket.io')(http); const HTTP_PORT = 80; const SOCKET_PORT = 3000; + app.use(compression()); app.use(express.static('public')); app.use(bodyParser.json()); app.post('/game', createGame); io.on('connection', function (socket) { - onJoinGame(io, socket); + onJoinGame(socket); socket.on('game-action', function (data) { - onGameAction(io, socket, data); + onGameAction(socket, data); }); }); http.listen(HTTP_PORT); io.listen(SOCKET_PORT); + + return io; }; diff --git a/index.js b/index.js index e46a857..58f0668 100644 --- a/index.js +++ b/index.js @@ -1,17 +1,9 @@ const ChampionList = require('./classes/ChampionList'); +const GameList = require('./classes/GameList'); const Game = require('./classes/Game'); const createServer = require('./create-server'); -global.games = {}; - -setInterval(ChampionList.update, 3600); // Hourly -ChampionList.update(); - function validateGameCreation(input) { - if(typeof input.name !== 'string') { - return false; - } - if(!Array.isArray(input.teams) || input.teams.length !== 2 || typeof input.teams[0] !== 'string' || typeof input.teams[1] !== 'string') { return false; } @@ -19,7 +11,6 @@ function validateGameCreation(input) { const cleanse = string => string.trim().substr(0, 30); return { - name: cleanse(input.name), teams: [ cleanse(input.teams[0]), cleanse(input.teams[1]), @@ -34,10 +25,10 @@ function createGame(req, res) { res.end(); } - const game = new Game(input.name, input.teams); + const game = new Game(input.teams, ChampionList.get(), game => io.to(`game.${game.getId()}`).emit('game-state', game.getState())); const teams = game.getTeams(); - global.games[game.getId()] = game; + GameList.add(game.getId(), game); res.send({ game: game.getId(), @@ -45,33 +36,44 @@ function createGame(req, res) { }); } -function onJoinGame(io, socket) { - const game = global.games[socket.handshake.query.game]; +function onJoinGame(socket) { + const game = GameList.get(socket.handshake.query.game); if(game === undefined) { + socket.emit('game-expired'); socket.disconnect(true); return; } - socket.emit('game-state', game.getState()); socket.emit('champions', ChampionList.get()); + socket.emit('assign-team', game.getTeamIndexById(socket.handshake.query.team)); + socket.emit('game-state', game.getState()); socket.join(`game.${game.getId()}`); } -function onGameAction(io, socket, data) { - const game = global.games[socket.handshake.query.game]; +function onGameAction(socket, data) { + const game = GameList.get(socket.handshake.query.game); if(game === undefined) { + socket.emit('game-expired'); socket.disconnect(true); return; } - const success = game.act(socket.handshake.query.team, data.action, data.champion); - const recipient = success ? io.to(`game.${game.getId()}`) : socket; - - recipient.emit('game-state', game.getState()); + game.act(socket.handshake.query.team, data.action, data.value); } -createServer(createGame, onJoinGame, onGameAction); +const io = createServer(createGame, onJoinGame, onGameAction); + +setInterval(() => ChampionList.update(), 3600000); // 1 hour +ChampionList.update(); + +setInterval(() => { + const expired = GameList.flushExpired(); + + for(let id of expired) { + io.to(`game.${id}`).emit('game-expired'); + } +}, 3600000); // 1 hour diff --git a/package-lock.json b/package-lock.json index 12e1210..ca820e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "version": "0.0.1", "license": "proprietary", "dependencies": { + "compression": "^1.7.4", + "dayjs": "^1.10.4", "express": "^4.17.1", "node-fetch": "^2.6.1", "socket.io": "^3.1.1" @@ -3804,7 +3806,6 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -3816,7 +3817,6 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", @@ -3834,7 +3834,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true, "engines": { "node": ">= 0.8" } @@ -4870,6 +4869,11 @@ "type": "^1.0.1" } }, + "node_modules/dayjs": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz", + "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==" + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -8510,7 +8514,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -18786,7 +18789,6 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, "requires": { "mime-db": ">= 1.43.0 < 2" } @@ -18795,7 +18797,6 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, "requires": { "accepts": "~1.3.5", "bytes": "3.0.0", @@ -18809,8 +18810,7 @@ "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" } } }, @@ -19644,6 +19644,11 @@ "type": "^1.0.1" } }, + "dayjs": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz", + "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==" + }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -22470,8 +22475,7 @@ "on-headers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" }, "once": { "version": "1.4.0", diff --git a/package.json b/package.json index 3f1ade5..5db7382 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,15 @@ { "name": "prodraft", - "version": "0.0.1", + "version": "1.0.0", "description": "A remake of http://prodraft.leagueoflegends.com", "main": "index.js", "dependencies": { + "compression": "^1.7.4", + "dayjs": "^1.10.4", "express": "^4.17.1", "node-fetch": "^2.6.1", "socket.io": "^3.1.1" }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, "keywords": [], "author": "CSV IT Consulting", "license": "proprietary", diff --git a/src/components/ChampionBanList.vue b/src/components/ChampionBanList.vue index ece9ceb..b1b2d9e 100644 --- a/src/components/ChampionBanList.vue +++ b/src/components/ChampionBanList.vue @@ -3,8 +3,10 @@
@@ -17,12 +19,40 @@ import ChampionIcon from './ChampionIcon'; export default { components: { ChampionIcon }, + computed: { + bannedChampions() { + const champions = this.bans.map(id => ({ + data: id === null ? null : this.champions.find(champion => champion.id === id), + hovered: false, + })); + + if(this.hovered !== null) { + champions.push({ + data: this.champions.find(champion => champion.id === this.hovered), + hovered: true, + }); + } + + return champions; + }, + }, + props: { bans: { type: Array, required: true, }, + champions: { + type: Array, + required: true, + }, + + hovered: { + type: String, + default: null, + }, + team: { type: Number, required: true, @@ -30,3 +60,32 @@ export default { }, }; + diff --git a/src/components/ChampionIcon.vue b/src/components/ChampionIcon.vue index 86c523c..0fc74fe 100644 --- a/src/components/ChampionIcon.vue +++ b/src/components/ChampionIcon.vue @@ -1,6 +1,6 @@ + \ No newline at end of file diff --git a/src/components/ChampionPicker.vue b/src/components/ChampionPicker.vue index 74715be..0fc92b3 100644 --- a/src/components/ChampionPicker.vue +++ b/src/components/ChampionPicker.vue @@ -1,20 +1,20 @@