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 + '
'+order+' up: '+name+'', '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); });