469 lines
14 KiB
JavaScript
Executable File
469 lines
14 KiB
JavaScript
Executable File
const fs = require("fs");
|
|
const express = require('express');
|
|
const path = require('path');
|
|
const WebSocket = require('ws'); // new
|
|
|
|
function noop() {}
|
|
function heartbeat() {
|
|
this.isAlive = true;
|
|
}
|
|
|
|
|
|
const app = express();// express code
|
|
const url = require('url');
|
|
const User = require('./user');
|
|
const moment = require('moment');
|
|
const timeFormat = 'h:mm a [PDT]'
|
|
const threshold = 1;
|
|
const playerThreshold = 2;
|
|
var playerTurn = false;
|
|
var turnIndex = false;
|
|
var nextPlayer = false;
|
|
var cardsDir = "cards/";
|
|
var cardsFile = cardsDir + moment().format("YYYY-MM-DD") + ".cards";
|
|
var cards = []; /*
|
|
{
|
|
"title": "Test Card 1",
|
|
"image": "/img/profiles/6.jpg",
|
|
"content": "Angelica approves.",
|
|
"revealed": false,
|
|
"author": "admin"
|
|
},
|
|
{
|
|
"title": "19.jpg",
|
|
"image": "/img/profiles/19.jpg",
|
|
"content": "Taking work calls like",
|
|
"revealed": false,
|
|
"author": "admin"
|
|
},
|
|
{
|
|
"title": "YUGE",
|
|
"image": "/img/profiles/12.jpg",
|
|
"content": ""
|
|
"revealed": false,
|
|
"author": "admin"
|
|
}
|
|
]*/
|
|
var lastCard = false;
|
|
var graveyard = [];
|
|
var incompleteCard = false;
|
|
|
|
fs.readFile(cardsFile, 'utf-8', (err, data) => {
|
|
if (err) console.log(err);
|
|
console.log("Opened %s", cardsFile);
|
|
// cards = JSON.parse(data);
|
|
});
|
|
|
|
|
|
const messages = [
|
|
{'username': 'ADMIN',
|
|
'pId': 0,
|
|
'message': 'Welcome to Blank White Cards!',
|
|
'timestamp': moment().format(timeFormat)
|
|
}
|
|
];
|
|
|
|
var players = [];
|
|
var playerProfilesMap = {
|
|
'panda':{
|
|
'img': '/img/profiles/6.jpg',
|
|
'pId': 1000
|
|
},
|
|
'hodor':{
|
|
'img': '/img/profiles/12.jpg',
|
|
'pId': 2000
|
|
}
|
|
};
|
|
var pId = 0;
|
|
var username = "";
|
|
var profilepic = "";
|
|
|
|
var mode = "draw"; // can also be "play" or "postgame"
|
|
|
|
app.get('/game/admin', (req, res) => {
|
|
console.log(req);
|
|
res.sendFile(path.join(__dirname, 'admin.html'));
|
|
});
|
|
|
|
app.get('/game', (req, res) => {
|
|
const querystring = url.parse(req.url,true).query;
|
|
username = querystring.username;
|
|
res.sendFile(path.join(__dirname, 'game.html'));
|
|
|
|
});
|
|
|
|
const port = process.env.PORT || 8765;
|
|
app.listen(port, () => {
|
|
console.log(`listening http://localhost:${port}`);
|
|
});
|
|
|
|
const wss = new WebSocket.Server({port: 3030});
|
|
|
|
const updateClients = (wss, data) => {
|
|
wss.clients.forEach(function each(client) {
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
data.timestamp = moment(data.timestamp).format(timeFormat);
|
|
client.send(JSON.stringify(data));
|
|
}
|
|
});
|
|
};
|
|
|
|
wss.on('connection', function connection(ws) {
|
|
|
|
ws.isAlive = true;
|
|
ws.on('pong', heartbeat);
|
|
ws.ping(null, null, false);ws._socket.setKeepAlive(true,100);
|
|
const packet = {
|
|
"type": "init",
|
|
"players": players,
|
|
"mode": mode,
|
|
"playerTurn": playerTurn,
|
|
"messages": messages,
|
|
"cards": cards.length,
|
|
"incompleteCard": incompleteCard
|
|
}
|
|
var existingPlayer = false; var status = "entered";
|
|
pId = players.length + 1;
|
|
console.log("Trying to join: " + username + " (Player " + pId + ")");
|
|
/* players.forEach(function each(player) {
|
|
if(player.username == username){
|
|
packet.mode = "error";
|
|
packet.error_msg = "That username is taken. Try another.";
|
|
ws.send(JSON.stringify(packet));
|
|
console.log("Kicked out due to duplicate username: " + username);
|
|
ws.close();
|
|
existingPlayer = true;
|
|
}
|
|
});
|
|
*/
|
|
if(existingPlayer){
|
|
pId = pId - 1;
|
|
return "Fuckoff";
|
|
}
|
|
else{
|
|
if(playerProfilesMap[username]){
|
|
pId = playerProfilesMap[username].pId;
|
|
console.log("Found '" + username + "' in the archives (Player "+pId+").");
|
|
status = "rejoined";
|
|
profilepic = playerProfilesMap[username].img;
|
|
}
|
|
else{
|
|
profilepic = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMjUwcHgiIGhlaWdodD0iMjUwcHgiIHZpZXdCb3g9IjAgMCAyNTAgMjUwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNTAgMjUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxyZWN0IGZpbGw9IiNGNUY1RjUiIHdpZHRoPSIyNTAiIGhlaWdodD0iMjUwIi8+DQo8Zz4NCgk8cGF0aCBmaWxsPSIjRDhEOEQ4IiBkPSJNMjI3LjgxOCwyMDcuMjQ1Yy0xLjA0NS01Ljg0Ny0yLjM2OS0xMS4yNTktMy45NjUtMTYuMjUyYy0xLjU5OC00Ljk5NC0zLjc0Mi05Ljg1OS02LjQ0MS0xNC42MDYNCgkJYy0yLjY5Ny00Ljc0LTUuNzg5LTguNzg1LTkuMjgzLTEyLjEzMWMtMy41MDEtMy4zNDMtNy43NjgtNi4wMTUtMTIuODA5LTguMDEyYy01LjA0NS0xLjk5Ni0xMC42MTEtMi45OTUtMTYuNy0yLjk5NQ0KCQljLTAuODk3LDAtMi45OTQsMS4wNzQtNi4yOTEsMy4yMTljLTMuMjk1LDIuMTUtNy4wMTcsNC41NDYtMTEuMTU4LDcuMTg4Yy00LjE0NiwyLjY0Ni05LjUzNyw1LjA0NC0xNi4xNzYsNy4xODkNCgkJYy02LjY0MiwyLjE0OC0xMy4zMDgsMy4yMjEtMTkuOTk1LDMuMjIxYy02LjY4OSwwLTEzLjM1NC0xLjA3MS0xOS45OTUtMy4yMjFjLTYuNjQzLTIuMTQ2LTEyLjAzNi00LjU0My0xNi4xNzYtNy4xODkNCgkJYy00LjE0OC0yLjY0My03Ljg2My01LjAzNy0xMS4xNTgtNy4xODhjLTMuMjk1LTIuMTQ1LTUuMzkxLTMuMjE5LTYuMjkxLTMuMjE5Yy02LjA5NSwwLTExLjY2MSwwLjk5OS0xNi43MDEsMi45OTUNCgkJYy01LjA0MSwxLjk5Ny05LjMxMyw0LjY2OS0xMi44MDMsOC4wMTJjLTMuNTAxLDMuMzQ2LTYuNTkyLDcuMzkxLTkuMjg3LDEyLjEzMWMtMi42OTYsNC43NDctNC44NDcsOS42MTItNi40NDEsMTQuNjA2DQoJCWMtMS41OTcsNC45OTMtMi45MjIsMTAuNDA1LTMuOTcxLDE2LjI1MmMtMS4wNDYsNS44MzktMS43NDgsMTEuMjgtMi4wOTYsMTYuMzIzYy0wLjM0OSw1LjA0My0wLjUyNCwxMC4yMTMtMC41MjQsMTUuNQ0KCQljMCwzLjkyNSwwLjQzMiw3LjU1LDEuMjExLDEwLjkzMmgyMDguNDY0YzAuNzgxLTMuMzgyLDEuMjEzLTcuMDA3LDEuMjEzLTEwLjkzMmMwLTUuMjg3LTAuMTc2LTEwLjQ1Ny0wLjUyNi0xNS41DQoJCUMyMjkuNTY2LDIxOC41MjUsMjI4Ljg2OSwyMTMuMDg0LDIyNy44MTgsMjA3LjI0NXoiLz4NCgk8cGF0aCBmaWxsPSIjRDhEOEQ4IiBkPSJNMTI1LDE2Mi44MzRjMTUuODc1LDAsMjkuNDMtNS42MTcsNDAuNjY2LTE2Ljg1YzExLjIzMi0xMS4yMzUsMTYuODUtMjQuNzg5LDE2Ljg1LTQwLjY2Nw0KCQljMC0xNS44NzctNS42MTctMjkuNDI5LTE2Ljg1LTQwLjY2M0MxNTQuNDMsNTMuNDIyLDE0MC44NzUsNDcuODA0LDEyNSw0Ny44MDRzLTI5LjQzNCw1LjYxOS00MC42NjQsMTYuODUyDQoJCUM3My4xLDc1Ljg5LDY3LjQ4NCw4OS40NDEsNjcuNDg0LDEwNS4zMThjMCwxNS44NzgsNS42MTUsMjkuNDMxLDE2Ljg1Miw0MC42NjdDOTUuNTY2LDE1Ny4yMTcsMTA5LjEyNSwxNjIuODM0LDEyNSwxNjIuODM0eiIvPg0KPC9nPg0KPC9zdmc+DQo=";
|
|
}
|
|
|
|
newPlayer = {
|
|
"pId": pId,
|
|
"username": username,
|
|
"profilepic": profilepic
|
|
}
|
|
players.push(newPlayer);
|
|
newData = {
|
|
'pId': newPlayer.pId,
|
|
'img': newPlayer.profilepic
|
|
}
|
|
playerProfilesMap[newPlayer.username] = newData;
|
|
|
|
console.log("'" + username + "' has "+status+" the game.");
|
|
|
|
if(newPlayer.username == playerTurn.username){console.log(">> It's "+newPlayer.username+"'s turn.");}
|
|
|
|
packet.id = pId;
|
|
ws.pId = pId;
|
|
ws.send(JSON.stringify(packet));
|
|
|
|
const newPlayerInfo = {
|
|
'type': 'newPlayer',
|
|
'player': newPlayer
|
|
}
|
|
updateClients(wss, newPlayerInfo);
|
|
}
|
|
ws.on('message', function incoming(data) {
|
|
data = JSON.parse(data);
|
|
response = {
|
|
"type": "error",
|
|
"content": "Unknown server error."
|
|
}
|
|
switch(data.type){
|
|
case "startPlayMode":
|
|
if((mode != "play") && (cards.length + 1 > threshold) && (players.length + 1 > playerThreshold)){
|
|
mode = "play";
|
|
console.log("'" + data.player + "' started the game. (%s cards)", cards.length);
|
|
console.log(">> GAME MODE IS NOW: " + mode);
|
|
type = "changeMode";
|
|
nextPlayerMove(type);
|
|
}
|
|
else{
|
|
if(cards.length < threshold){
|
|
console.log("'" + data.player + "' tried to start the game. (Error: Not enough cards)");
|
|
response = {
|
|
"type": "error",
|
|
"content": "The game won't start until we have at least "+threshold+" cards."
|
|
}
|
|
ws.send(JSON.stringify(response));
|
|
}
|
|
if(players.length < playerThreshold){
|
|
console.log("'" + data.player + "' tried to start the game. (Error: Not enough players)");
|
|
response = {
|
|
"type": "error",
|
|
"content": "The game won't start until we have at least "+playerThreshold+" players."
|
|
}
|
|
ws.send(JSON.stringify(response));
|
|
}
|
|
}
|
|
break;
|
|
case "message":
|
|
console.log("Received message from '%s': '%s'", data.username, data.message);
|
|
messages.push(data);
|
|
updateClients(wss,data);
|
|
|
|
break;
|
|
case "vote":
|
|
console.log("Caught a vote for Player "+data.pId);
|
|
updateClients(wss,data);
|
|
break;
|
|
case "changeProfilePic":
|
|
console.log("'" + data.player.username + "' has changed their profile picture.");
|
|
players.map(function(p){
|
|
if(p.pId == data.player.pId){
|
|
p.profilepic = data.href;
|
|
}
|
|
});
|
|
newData = {
|
|
'pId': data.player.pId,
|
|
'img': data.href
|
|
}
|
|
|
|
playerProfilesMap[data.player.username] = newData;
|
|
updateClients(wss,data);
|
|
break;
|
|
case "changeUsername":
|
|
response.type = "notification";
|
|
if(username == data.username){
|
|
response.type = "error";
|
|
response.content = "Already your name.";
|
|
}
|
|
else{
|
|
var someoneElse = false;
|
|
players.forEach(function(p){
|
|
if(p.username == data.username){
|
|
someoneElse = true;
|
|
}
|
|
});
|
|
if(someoneElse){ // if there's a current player with this name
|
|
response.type = "error";
|
|
response.content = "Already someone's name.";
|
|
}
|
|
else{ // Not already your name, and not someone else's name
|
|
if(playerProfilesMap[data.username]){
|
|
// We've seen this username before
|
|
if(playerProfilesMap[data.username].pId == pId){
|
|
// Same pId, good to go
|
|
}
|
|
else{
|
|
// Someone else's profile, don't touch it
|
|
response.content = "This looks like someone else's name.";
|
|
response.type = "error";
|
|
}
|
|
}
|
|
else {
|
|
// Never seen this before
|
|
username = data.username;
|
|
players.forEach(function(p){
|
|
if(p.pId == pId){
|
|
// This is us within the players array; update it
|
|
p.username = username;
|
|
}
|
|
});
|
|
|
|
playerProfilesMap[username] = {
|
|
'pId': pId,
|
|
'img': profilepic
|
|
}
|
|
}
|
|
}
|
|
response.content = "Changed your name to '"+username+"'.";
|
|
|
|
console.log("Changed player %s to '%s'.", pId, username);
|
|
};
|
|
updateClients(wss,data);
|
|
ws.send(JSON.stringify(response));
|
|
break;
|
|
|
|
case "pullCard":
|
|
|
|
if(incompleteCard){
|
|
// There's a card in memory that hasn't been played yet
|
|
pulledCard = incompleteCard;
|
|
|
|
}else{
|
|
mycard = true;
|
|
pulledCardIndex = Math.floor(Math.random() * cards.length);
|
|
/* while(mycard){
|
|
|
|
if(cards[pulledCardIndex].author != username){
|
|
mycard = false
|
|
}
|
|
} */
|
|
pulledCard = cards.splice(pulledCardIndex,1)[0];
|
|
graveyard.push(pulledCard); // Keep track of it for later viewing/saving/reloading
|
|
incompleteCard = pulledCard; // Make sure this card remains active until the player does something with it
|
|
|
|
console.log("Pulled card ("+cards.length+" remaining).");
|
|
console.log(graveyard);
|
|
|
|
|
|
if(cards.length == 0){
|
|
console.log("[[ LAST CARD! ]]");
|
|
lastCard = true;
|
|
}
|
|
}
|
|
|
|
response = {
|
|
'type': 'pulledCard',
|
|
'card': pulledCard,
|
|
'last': lastCard
|
|
}
|
|
|
|
ws.send(JSON.stringify(response));
|
|
|
|
break;
|
|
case "revealCard":
|
|
console.log(">> '"+playerTurn.username+"' has revealed their card.");
|
|
data.card.revealed = true;
|
|
incompleteCard.revealed = true;
|
|
updateClients(wss,data);
|
|
break;
|
|
case "endTurn":
|
|
console.log(">> '"+playerTurn.username+"' has ended their turn.");
|
|
incompleteCard = false;
|
|
if(lastCard){
|
|
// Game over
|
|
gameOver();
|
|
|
|
}else{
|
|
nextPlayerMove("nextTurn");
|
|
}
|
|
break;
|
|
case "card":
|
|
var dupe = false;
|
|
const card = data.card;
|
|
const author = data.card.author;
|
|
console.log("'%s' submitted a card.", author);
|
|
|
|
cards.forEach(function each(c){
|
|
if(c.content == card.content){
|
|
console.log("Duplicate card");
|
|
dupe = true;
|
|
}
|
|
});
|
|
|
|
if(dupe){
|
|
response = {
|
|
'type':'error',
|
|
'content': 'This looks like a duplicate of a card we already have.'
|
|
}
|
|
}
|
|
else{
|
|
cards.push(card);
|
|
response = {
|
|
'type':'updateCardsList',
|
|
'author': author
|
|
}
|
|
}
|
|
|
|
updateClients(wss, response);
|
|
break;
|
|
default:
|
|
console.log("Unknown client request:\n" + data);
|
|
}
|
|
});
|
|
ws.on('close', function close() {
|
|
console.log('Client disconnected');
|
|
players = players.filter(function(p){
|
|
return p.pId != ws.pId;
|
|
});
|
|
pId = pId - 1;
|
|
});
|
|
});
|
|
|
|
const gameOver = () => {
|
|
console.log("Game is over");
|
|
notice = {
|
|
'type': "gameOver",
|
|
"content": "Thanks for playing - the game's over!"
|
|
}
|
|
updateClients(wss, notice);
|
|
updateClients(wss, {
|
|
'type': 'message',
|
|
'pId': 0,
|
|
'username': "ADMIN",
|
|
'message': "Thanks for playing - the game's over!"
|
|
});
|
|
|
|
var saveFile = cardsDir + moment().format("YYYY-MM-DD-hh-mm") + ".cards";
|
|
fs.writeFile(saveFile, JSON.stringify(graveyard), (err) => {
|
|
if (err) console.log(err);
|
|
});
|
|
|
|
|
|
}
|
|
|
|
const nextPlayerMove = (type) => {
|
|
if(type == "changeMode"){
|
|
// First move of the game
|
|
console.log("First move; choosing random player");
|
|
turnIndex = Math.floor(Math.random() * players.length); // For now, just random
|
|
}
|
|
else{
|
|
// Mid-game
|
|
console.log(players[turnIndex].username + " just finished. Incrementing..");
|
|
turnIndex = turnIndex + 1;
|
|
if(!players[turnIndex]){turnIndex = 0}
|
|
}
|
|
console.log(players[turnIndex].username + " is up now.");
|
|
turnIndex = 1;
|
|
|
|
playerTurn = players[turnIndex];
|
|
|
|
response = {
|
|
"type": type,
|
|
"mode": "play",
|
|
"playerTurn": playerTurn,
|
|
"message": null
|
|
}
|
|
updateClients(wss,response);
|
|
|
|
|
|
/*
|
|
var order = "Next";
|
|
var firstLine = playerTurn.username + ' has ended their turn.';
|
|
var name = nextPlayer.username;
|
|
|
|
if(type == "changeMode"){
|
|
name = playerTurn.username;
|
|
order = "First";
|
|
firstLine = 'The game has started.';
|
|
}
|
|
message = {
|
|
'username': 'ADMIN',
|
|
'pId': 0,
|
|
'message': firstLine + '<br/>'+order+' up: <b>'+name+'</b>',
|
|
'timestamp': moment().format(timeFormat)
|
|
}
|
|
*/
|
|
}
|
|
|
|
const interval = setInterval(function ping() {
|
|
wss.clients.forEach(function each(ws) {
|
|
if (ws.isAlive === false) return ws.terminate();
|
|
|
|
ws.isAlive = false;
|
|
ws.ping(noop);
|
|
});
|
|
}, 30000);
|
|
|
|
|
|
wss.on('close', function close() {
|
|
clearInterval(interval);
|
|
});
|