/ server.js
server.js
  1  'use strict';
  2  import express from 'express';
  3  let app = express();
  4  let server = app.listen(process.env.catan_port || 8888, () => {
  5      console.log(`Server started at ${process.env.catan_port || 8888}`);
  6  });
  7  app.use('/health', (req, res) => {
  8    res.send('Ok');
  9  });
 10  app.use('', express.static('public_html'));
 11  
 12  import { Server } from 'socket.io';
 13  const io = new Server(server);
 14  
 15  import game from './game.js';
 16  let data = {};
 17  
 18  import {CONST} from './public_html/script/src/const.js';
 19  import {adjacent} from './public_html/script/src/adjacent.js';
 20  import {countVPs} from './public_html/script/src/arrange.js';
 21  
 22  io.on('connection', (socket) => {
 23      let playerName, gameName;
 24      socket.on('error', (err) => {
 25          console.error(err);
 26      });
 27      //Main game events
 28      socket.on('game:join', ([g, p], res) => {
 29          gameName = encodeURIComponent(g);
 30          playerName = p;
 31          console.log(`${playerName} is attempting to join ${gameName}`);
 32          socket.join(gameName);
 33          if(data[gameName] === undefined) {
 34              data[gameName] = {};
 35              if(game.exists(gameName)) {
 36                  data[gameName] = game.load(gameName);
 37                  for(let player in data[gameName].players) {
 38                      data[gameName].players[player].connected = false;
 39                  }
 40              } else {
 41                  data[gameName] = game.new(gameName);
 42              }
 43          }
 44          if(data[gameName].players[playerName] === undefined) {
 45              if(Object.keys(data[gameName].players).length < 4 && data[gameName].gameState === CONST.OPEN) {
 46                  data[gameName].players[playerName] = {
 47                      turn: Object.keys(data[gameName].players).length,
 48                      color: '',
 49                      hand: [
 50                          [0,0,0,0,0],
 51                          [[0,0,0,0,0],[0,0,0,0,0]]
 52                      ],
 53                      connected: true,
 54                      response: {
 55                          robber: null,
 56                          trade: null
 57                      },
 58                      longestRoadCount: 0,
 59                      knights: 0,
 60                      largestArmy: false,
 61                      longestRoad: false
 62                  };
 63              } else {
 64                  socket.leave(gameName);
 65                  res(1, `${gameName} is full. Please choose a different game`);
 66                  return;
 67              }
 68          } else {
 69              if(!data[gameName].players[playerName].connected) {
 70                  data[gameName].players[playerName].connected = true;
 71              } else {
 72                  socket.leave(gameName);
 73                  res(1, `${playerName} is already connected. Please do not join twice`);
 74                  return;
 75              }
 76          }
 77          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
 78          res(null, data[gameName]);
 79      });
 80      socket.on('game:color', (color, res) => {
 81          let colors = [];
 82          Object.keys(data[gameName].players).forEach((name) => {
 83              colors.push(data[gameName].players[name].color);
 84          });
 85          if(colors.indexOf(color) == -1) {
 86              data[gameName].players[playerName].color = color;
 87          }
 88          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
 89          res(null, data[gameName]);
 90      });
 91      socket.on('game:start', () => {
 92          if(data[gameName].gameState == CONST.OPEN) {
 93              data[gameName].gameState = CONST.SETUP;
 94          }
 95          let turns = Object.keys(data[gameName].players).length == 4 ? [0, 1, 2, 3] : [0, 1, 2];
 96          Object.keys(data[gameName].players).forEach((player) => {
 97              data[gameName].players[player].turn = turns.splice((Math.random() * 10) % turns.length, 1)[0];
 98          });
 99          data[gameName].turn = 0;
100          io.to(gameName).emit('game:data', data[gameName]);
101      });
102      socket.on('game:turn', (x, res) => {
103          if(data[gameName].gameState == CONST.SETUP) {
104              if(data[gameName].turnCount < Object.keys(data[gameName].players).length - 1) {
105                  data[gameName].turn++;
106              } else if(data[gameName].turnCount >= Object.keys(data[gameName].players).length) {
107                  data[gameName].turn--;
108              }
109              if(data[gameName].turn < 0) {
110                  data[gameName].gameState = CONST.PLAY;
111                  data[gameName].turn = 0;
112                  data[gameName].turnCount = 0;
113              }
114          } else {
115              data[gameName].turn = (data[gameName].turn + 1) % Object.keys(data[gameName].players).length;
116          }
117          data[gameName].turnCount++;
118          data[gameName].rolled = false;
119          for(let player in data[gameName].players) {
120              data[gameName].players[player].response = {
121                  robber: null,
122                  trade: null
123              };
124              for(let i = 0; i < data[gameName].players[player].hand[CONST.DEVELOPMENT][CONST.READY].length; i++) {
125                  //Transfer bought devcards to ready devcards
126                  data[gameName].players[player].hand[CONST.DEVELOPMENT][CONST.READY][i] += data[gameName].players[player].hand[CONST.DEVELOPMENT][CONST.BOUGHT][i];
127                  data[gameName].players[player].hand[CONST.DEVELOPMENT][CONST.BOUGHT][i] = 0;
128              }
129          }
130          game.save(gameName, data[gameName]);
131          if(countVPs(data[gameName], playerName, playerName) >= 10) {
132              io.to(gameName).emit('game:win', [data[gameName], playerName]);
133              console.log(`${playerName} has won ${gameName}`);
134          } else {
135              socket.broadcast.to(gameName).emit('game:data', data[gameName]);
136              res(null, data[gameName]);
137          }
138      });
139      socket.on('game:roll', (x, res) => {
140          let dice = [
141              Math.floor(Math.random() * 1000) % 6 + 1,
142              Math.floor(Math.random() * 1000) % 6 + 1
143          ];
144          data[gameName].dice = dice;
145          data[gameName].rolled = true;
146          if(dice[0] + dice[1] !== 7) {
147              for(let i = 0; i < data[gameName].tiles.length; i++) {
148                  for(let j = 0; j < data[gameName].tiles[i].length; j++) {
149                      if(data[gameName].robber[0] !== i || data[gameName].robber[1] !== j) {
150                          if(data[gameName].tiles[i][j][1] === dice[0] + dice[1]) {
151                              let adj = adjacent(i, j, 'tile', 'house');
152                              adj.forEach((house) => {
153                                  if(data[gameName].houses[house[0]][house[1]][0]) {
154                                      let houseOwner = data[gameName].houses[house[0]][house[1]][1];
155                                      let resourceType = data[gameName].tiles[i][j][0];
156                                      let quantity = data[gameName].houses[house[0]][house[1]][0];
157                                      data[gameName].players[houseOwner].hand[CONST.RESOURCE][resourceType] += quantity;
158                                  }
159                              });
160                          }
161                      }
162                  }
163              }
164          }
165          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
166          res(null, data[gameName]);
167      });
168  
169      //Building events
170      socket.on('build:house', ([i, j], res) => {
171          data[gameName].houses[i][j][0] = 1;
172          data[gameName].houses[i][j][1] = playerName;
173          if(data[gameName].gameState != CONST.SETUP) {
174              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.WOOL] -= 1;
175              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.WOOD] -= 1;
176              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.WHEAT] -= 1;
177              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.BRICK] -= 1;
178          } else {
179              if(data[gameName].turnCount >= Object.keys(data[gameName].players).length) {
180                  let tiles = adjacent(i, j, 'house', 'tile');
181                  tiles.forEach((tile) => {
182                      let resourceType = data[gameName].tiles[tile[0]][tile[1]][0];
183                      data[gameName].players[playerName].hand[CONST.RESOURCE][resourceType]++;
184                  });
185              }
186          }
187          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
188          res(null, [data[gameName], [i, j]]);
189      });
190      socket.on('build:road', ([i, j, free], res) => {
191          data[gameName].roads[i][j] = playerName;
192          if(!free) {
193              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.WOOD] -= 1;
194              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.BRICK] -= 1;
195          }
196  
197          let check = (list) => {
198              let adj = adjacent(list[list.length - 1][0], list[list.length - 1][1], 'road', 'road');
199              let branch = [0, 0, 0, 0];
200              let n = 0;
201              outer:
202              for(let [x, y] of adj) {
203                  if(data[gameName].roads[x][y] === playerName) {
204                      for(let [a, b] of list) {
205                          //If not in the list already
206                          if(a === x && b === y) {
207                              continue outer;
208                          }
209                      }
210                      if(list.length >= 2) {
211                          for(let [a, b] of adjacent(x, y, 'road', 'road')) {
212                              //If on the same end of the road as the previous road
213                              if(a === list[list.length - 2][0] && b === list[list.length - 2][1]) {
214                                  continue outer;
215                              }
216                          }
217                      }
218                      branch[n++] = check([...list, [x, y]]);
219                  }
220              }
221              if(n === 0) {
222                  return list.length;
223              }
224              let max = 0;
225              while(--n >= 0) {
226                  max = Math.max(branch[n], max);
227              }
228              return max;
229          };
230          for(let a = 0; a < data[gameName].roads.length; a++) {
231              for(let b = 0; b < data[gameName].roads[a].length; b++) {
232                  if(data[gameName].roads[a][b] === playerName) {
233                      data[gameName].players[playerName].longestRoadCount = Math.max(data[gameName].players[playerName].longestRoadCount, check([[a,b]]));
234                  }
235              }
236          }
237  
238          let max = null;
239          for(let player in data[gameName].players) {
240              if(data[gameName].players[player].longestRoad) {
241                  data[gameName].players[player].longestRoad = false;
242                  max = player;
243                  break;
244              }
245          }
246          for(let player in data[gameName].players) {
247              if(data[gameName].players[player].longestRoadCount >= 5) {
248                  if(max === null || data[gameName].players[player].longestRoadCount > data[gameName].players[max].longestRoadCount) {
249                      max = player;
250                  }
251              }
252          }
253          if(max !== null) {
254              data[gameName].players[max].longestRoad = true;
255          }
256  
257          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
258          res(null, [data[gameName], [i, j]]);
259      });
260      socket.on('build:city', ([i, j], res) => {
261          data[gameName].houses[i][j][0] = 2;
262          data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.ORE] -= 3;
263          data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.WHEAT] -= 2;
264          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
265          res(null, [data[gameName], [i, j]]);
266      });
267  
268      //Development cards
269      socket.on('devcard:buy', (x, res) => {
270          let c, err;
271          if(data[gameName].devCards.length > 0) {
272              c = data[gameName].devCards.splice(0, 1);
273              data[gameName].players[playerName].hand[CONST.DEVELOPMENT][CONST.BOUGHT][c] += 1;
274              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.ORE] -= 1;
275              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.WOOL] -= 1;
276              data[gameName].players[playerName].hand[CONST.RESOURCE][CONST.WHEAT] -= 1;
277          } else {
278              err = "There are no more development cards";
279          }
280          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
281          socket.broadcast.to(gameName).emit('notification', `${playerName} bought a development card`);
282          res(err, [data[gameName], c !== undefined ? ['Knight','VP','Monopoly','Road Building','Year Of Plenty'][c] : '']);
283      });
284      socket.on('devcard:play', (which, res) => {
285          data[gameName].players[playerName].hand[CONST.DEVELOPMENT][CONST.READY][which] -= 1;
286          let cardName;
287          if(which === CONST.KNIGHT) {
288              cardName = 'Knight';
289              data[gameName].players[playerName].knights++;
290              //Calculate the largest army
291              let max = null;
292              for(let p in data[gameName].players) {
293                  if(data[gameName].players[p].largestArmy) {
294                      data[gameName].players[p].largestArmy = false;
295                      max = p;
296                      break;
297                  }
298              }
299              for(let p in data[gameName].players) {
300                  if(data[gameName].players[p].knights >= 3) {
301                      if(max === null || data[gameName].players[p].knights > data[gameName].players[max].knights) {
302                          max = p;
303                      }
304                  }
305              }
306              if(max !== null) {
307                  data[gameName].players[max].largestArmy = true;
308              }
309          } else if(which === CONST.YEAR_OF_PLENTY) {
310              cardName = 'Year of Plenty';
311          } else if(which === CONST.ROAD_BUILDING) {
312              cardName = 'Road Building';
313          } else if(which === CONST.MONOPOLY) {
314              cardName = 'Monopoly';
315          }
316          socket.broadcast.to(gameName).emit('notification', `${playerName} played a ${cardName} card`);
317          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
318          res(null, data[gameName]);
319      });
320      socket.on('devcard:monopoly', (which, res) => {
321          for(let player in data[gameName].players) {
322              if(player != playerName) {
323                  data[gameName].players[playerName].hand[CONST.RESOURCE][which] += data[gameName].players[player].hand[CONST.RESOURCE][which];
324                  data[gameName].players[player].hand[CONST.RESOURCE][which] = 0;
325              }
326          }
327          let resourceName = ['Wool', 'Wheat', 'Wood', 'Brick', 'Ore'];
328          socket.broadcast.to(gameName).emit('notification', `${playerName} took all ${resourceName[which]} cards`);
329          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
330          res(null, data[gameName]);
331      });
332      socket.on('devcard:plenty', (which, res) => {
333          data[gameName].players[playerName].hand[CONST.RESOURCE][which[0]] += 1;
334          data[gameName].players[playerName].hand[CONST.RESOURCE][which[1]] += 1;
335          let resourceName = ['Wool', 'Wheat', 'Wood', 'Brick', 'Ore'];
336          socket.broadcast.to(gameName).emit('notification', `${playerName} took a ${resourceName[which[0]]} and a ${resourceName[which[1]]}`);
337          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
338          res(null, data[gameName]);
339      });
340  
341      //Robber events
342      socket.on('robber:start', (x, res) => {
343          for(let player in data[gameName].players) {
344              data[gameName].players[player].response.robber = false;
345          }
346          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
347          res(null, data[gameName]);
348      });
349      socket.on('robber:move', (pos, res) => {
350          data[gameName].robber = pos;
351          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
352          res(null, data[gameName]);
353      });
354      socket.on('robber:steal', (target, res) => {
355          let r;
356          do {
357              r = Math.floor((Math.random() * 10) % 5);
358          } while(data[gameName].players[target].hand[CONST.RESOURCE][r] < 1);
359          data[gameName].players[target].hand[CONST.RESOURCE][r]--;
360          data[gameName].players[playerName].hand[CONST.RESOURCE][r]++;
361          socket.broadcast.to(gameName).emit('notification', `${playerName} steals from ${target}`);
362          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
363          res(null, data[gameName]);
364      });
365      socket.on('robber:discard', (discards, res) => {
366          discards.forEach((c) => {
367              data[gameName].players[playerName].hand[CONST.RESOURCE][c]--;
368          });
369          data[gameName].players[playerName].response.robber = true;
370          let done = true;
371          Object.keys(data[gameName].players).forEach((player) => {
372              if(!data[gameName].players[player].response.robber) {
373                  done = false;
374              }
375          });
376          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
377          socket.broadcast.to(gameName).emit('robber:progress', [data[gameName], done]);
378          res(null, [data[gameName], done]);
379      });
380  
381      //Trade events
382      socket.on('trade:storage', (offer, res) => {
383          for(let c = 0; c < 5; c++) {
384              data[gameName].players[playerName].hand[CONST.RESOURCE][c] += offer.get[c];
385              data[gameName].players[playerName].hand[CONST.RESOURCE][c] -= offer.give[c];
386          }
387          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
388          res(null, data[gameName]);
389      });
390      socket.on('trade:offer', (offer, res) => {
391          for(let player in data[gameName].players) {
392              data[gameName].players[player].response.trade = null;
393          }
394          offer.player = playerName;
395          data[gameName].trade = offer;
396          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
397          res(null, [data[gameName], 'trade']);
398      });
399      socket.on('trade:respond', (offer, res) => {
400          data[gameName].players[playerName].response.trade = offer;
401          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
402          res(null, data[gameName]);
403      });
404      socket.on('trade:accept', (name, res) => {
405          let offer;
406          for(let player in data[gameName].players) {
407              if(player === name) {
408                  offer = data[gameName].players[player].response.trade;
409                  if(offer.response && offer.response === true) {
410                      offer = data[gameName].trade;
411                  }
412              }
413              data[gameName].players[player].response.trade = null;
414          }
415          data[gameName].trade = null;
416          for(let c = 0; c < 5; c++) {
417              data[gameName].players[name].hand[CONST.RESOURCE][c] -= offer.get[c];
418              data[gameName].players[name].hand[CONST.RESOURCE][c] += offer.give[c];
419              data[gameName].players[playerName].hand[CONST.RESOURCE][c] += offer.get[c];
420              data[gameName].players[playerName].hand[CONST.RESOURCE][c] -= offer.give[c];
421          }
422          socket.broadcast.to(gameName).emit('notification', `${playerName} trades with ${name}`);
423          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
424          res(null, data[gameName]);
425      });
426      socket.on('trade:reject', (x, res) => {
427          for(let player in data[gameName].players) {
428              data[gameName].players[player].response.trade = null;
429          }
430          data[gameName].trade = null;
431          socket.broadcast.to(gameName).emit('notification', `${playerName} does not accept any trade`);
432          socket.broadcast.to(gameName).emit('game:data', data[gameName]);
433          res(null, data[gameName]);
434      });
435  
436      //End game
437      socket.on('disconnect', () => {
438          if(data[gameName] !== undefined && data[gameName].players[playerName] !== undefined) {
439              //Disconnect the player
440              data[gameName].players[playerName].connected = false;
441              //Remove the game from the memory if all players are gone
442              let online = false;
443              Object.keys(data[gameName].players).forEach((player) => {
444                  if(data[gameName].players[player].connected) {
445                      online = true;
446                  }
447              });
448              if(!online) {
449                  delete data[gameName];
450              }
451          }
452      });
453      //Chat
454      socket.on('chat:post', (msg) => {
455          let swears = new RegExp("\\b((" +
456              "(bull)?s[\\s\\.]*h[\\s\\.-]*i[\\s\\.-]*t([\\s\\.]*t)?|" +
457              "(dumb)?f[\\s\\.]*u[\\s\\.]*c[\\s\\.]*k|" +
458              "b[\\s\\.]*i[\\s\\.]*t[\\s\\.]*c[\\s\\.]*h|" +
459              "(bad|smart|dumb)?a[\\s\\.]*s[\\s\\.]*s(hole)?|" +
460              "h[\\s\\.]*e[\\s\\.]*l[\\s\\.]*l|" +
461              "(bull)?c[\\s\\.]*r[\\s\\.]*a[\\s\\.]*p|" +
462              "d[\\s\\.]*i[\\s\\.]*c[\\s\\.]*k|" +
463              "d[\\s\\.]*a[\\s\\.]*m[\\s\\.]*(n|m)(i[\\s\\.]*t)?|" +
464              "p[\\s\\.]*e[\\s\\.]*n[\\s\\.]*i[\\s\\.]*s|" +
465              "t[\\s\\.]*i[\\s\\.]*t([\\s\\.]t)?" +
466          ")(ed|ing|er)?(e|ie|y)?s?)+\\b", "gi");
467          let replacements = [
468              "bubblegum", "puff", "strudel", "clouds",
469              "the entire population of France", "emu",
470              "goose", "jello", "piccolo", "spaceship",
471              "pentagon", "dandelion", "happy meal",
472              "left half of a baked potato", "true friend"];
473  
474          io.to(gameName).emit('chat:message', {
475              body: msg.replace(swears, replacements[Math.floor(Math.random() * replacements.length)]),
476              author: playerName,
477              time: Date.now()
478          });
479      });
480  });