/ 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 });