Skip to content

Commit

Permalink
1.0.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
mgrinspan committed Feb 14, 2021
1 parent 816b3a2 commit 69429b0
Show file tree
Hide file tree
Showing 13 changed files with 420 additions and 114 deletions.
100 changes: 82 additions & 18 deletions classes/Game.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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() {
Expand All @@ -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,

Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
};
28 changes: 28 additions & 0 deletions classes/GameList.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
8 changes: 6 additions & 2 deletions create-server.js
Original file line number Diff line number Diff line change
@@ -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;
};
46 changes: 24 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
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;
}

const cleanse = string => string.trim().substr(0, 30);

return {
name: cleanse(input.name),
teams: [
cleanse(input.teams[0]),
cleanse(input.teams[1]),
Expand All @@ -34,44 +25,55 @@ 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(),
teams: teams.map(team => team.getId()),
});
}

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
Loading

0 comments on commit 69429b0

Please sign in to comment.