From 8160fed6393d977dd7c9dc5e4c34780954ad7b93 Mon Sep 17 00:00:00 2001 From: Evert Date: Wed, 5 Apr 2017 23:14:57 +0300 Subject: [PATCH] put it up on github --- .gitignore | 1 + client/index.css | 194 +++++++++++++++ client/index.html | 72 ++++++ client/index.js | 621 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 20 ++ server.js | 454 +++++++++++++++++++++++++++++++++ 6 files changed, 1362 insertions(+) create mode 100644 .gitignore create mode 100644 client/index.css create mode 100644 client/index.html create mode 100644 client/index.js create mode 100644 package.json create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/client/index.css b/client/index.css new file mode 100644 index 0000000..3de75e1 --- /dev/null +++ b/client/index.css @@ -0,0 +1,194 @@ +body { + font-family: "Open Sans"; + margin: 0; + color: black; +} +.wrapper { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + background: -moz-radial-gradient(center, ellipse cover, #ffffff 0%, #e5e5e5 100%); + background: -webkit-radial-gradient(center, ellipse cover, #ffffff 0%,#e5e5e5 100%); + background: radial-gradient(ellipse at center, #ffffff 0%,#e5e5e5 100%); + background-repeat: no-repeat; +} +.screen { + position: absolute; + width: 100%; + height: 100%; +} +.dialog { + position: absolute; + width: 260px; + padding: 20px; + left: 0; + right: 0; + margin: auto; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 10px; + top: 20%; +} +h1, h2, h3 { + margin-top: 0; +} +input[type="text"] { + background-color: #f9f9f9; + border: 2px solid #ddd; + border-radius: 2px; + font-size: 125%; + padding: 2px; + box-shadow: inset 1px 1px 10px #ddd; + color: black; +} +.dialog input { + margin-bottom: 10px; + width: 95%; +} +button { + background: #ffffff; + background: -moz-linear-gradient(top, #ffffff 0%, #e5e5e5 100%); + background: -webkit-linear-gradient(top, #ffffff 0%,#e5e5e5 100%); + background: linear-gradient(to bottom, #ffffff 0%,#e5e5e5 100%); + border: 1px solid #ddd; + padding: 5px 15px; + border-radius: 5px; + cursor: pointer; + color: black; +} +button:hover { + background: -moz-linear-gradient(top, #ffffff 0%, #e8e8e8 100%); + background: -webkit-linear-gradient(top, #ffffff 0%,#e8e8e8 100%); + background: linear-gradient(to bottom, #ffffff 0%,#e8e8e8 100%); +} +canvas { + background-color: #e0e0e0; + margin: 10px; +} +.boxlayout { + text-align: center; +} +.box { + text-align: left; + width: 25%; + display: inline-block; + height: 400px; + margin: 20px; + background-color: #fff; + padding: 20px; + min-width: 280px; + overflow-y: auto; + border: 1px solid #ddd; + box-shadow: 4px 4px 10px #ddd; + vertical-align: top; +} +.stat { + display: block; +} +#waitlist { + margin-bottom: 5px; +} +#waitlist .red, #waitlist .green { + padding: 10px; +} +#waitlist .red { + background-color: #ffd5d5; + border: 1px solid #ff5c5c; +} +#waitlist .green { + background-color: #daffda; + border: 1px solid #00f700; +} +.waitlistInstance { + background-color: #f5f5f5; + padding: 5px; + max-height: 240px; + overflow-y: auto; +} +.waitlistInstance:nth-child(odd) { + background-color: #f9f9f9; +} +.joinBtn { + float: right; + color: #03A9F4; + text-decoration: none; + font-weight: bold; + padding: 0px 10px; +} +.joinBtn:hover { + text-decoration: underline; +} +.header { + height: 40px; + line-height: 2.5; + padding: 5px 20px; + background-color: #fff; + border-bottom: 1px solid #ddd; +} +button#leave { + float: right; + line-height: 2; +} +.sidebar { + float: left; + background-color: #fff; + padding: 10px; + position: absolute; + top: 51px; + bottom: 0; +} +#g_s_stat { + color: #119286; + font-weight: bold; + padding-top: 5px; + width: 200px; + height: 50px; +} +#game_canvas { + margin-left: 300px; +} +.endresult { + text-align: center; +} +.result { + width: 530px; + display: inline-block; +} +button#lobby { + display: inline-block; + vertical-align: middle; + margin-left: 15px; +} +.bigstat { + font-size: 200%; + font-weight: bold; + margin-bottom: 50px; + margin-top: 10px; +} +.message.t_event { + color: #b5b5b5; +} +.message .sender { + color: red; +} +.message.me .sender { + color: blue !important; +} +.chatbox { + margin-top: 50px; + padding: 5px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; +} +#messages { + height: 250px; + background-color: #fff; + border-radius: 5px; + overflow-y: scroll; + word-wrap: break-word; + white-space: pre-line; + max-width: 254px; +} diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..c37abcc --- /dev/null +++ b/client/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + HTML5 Connect Four + + +
+
+
+

Connect Four

+

+ + +
+
+ + +
+ + + diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..a7df8c3 --- /dev/null +++ b/client/index.js @@ -0,0 +1,621 @@ +(function ($) { + let io = window.io.connect() + let Connect4 = { + DOM: {}, + playerName: '', + playerID: '', + verified: null, + locked: false, + waitlist: [], + played: 0, + renderTick: false, + Game: { + gameId: null, + myTurn: false, + myColor: '', + opponentID: '', + opponentName: '', + places: [[],[],[],[],[],[],[],[],[]] + }, + color: { + blue: '#102aed', + red: '#ed1010' + } + } + + window.requestAnimFrame = (function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60) + } + })() + + function mustacheTempl (tmlTag, data) { + let html = '' + const tag = document.querySelector('#' + tmlTag) + + if (!tag) return '' + html = tag.innerHTML + html = window.Mustache.to_html(html, data) + return html + } + + function pointerOnCanvas (e) { + let x + let y + + if (e.changedTouches) { + let touch = e.changedTouches[0] + if (touch) { + e.pageX = touch.pageX + e.pageY = touch.pageY + } + } + + if (e.pageX || e.pageY) { + x = e.pageX + y = e.pageY + } else { + x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft + y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop + } + + x -= Connect4.DOM.canvas.offsetLeft + y -= Connect4.DOM.canvas.offsetTop + + return {x: x, y: y} + } + + let GameDrawer = { + drawMyBoard: true, + boardStaticState: null, + + mX: 0, + mY: 0, + + padding: 32, + + gridX: 0, + gridY: 0, + + gridSize: 64, + mouseOn: false, + + bw: 576, + bh: 576, + + startGame: () => { + Connect4.Game.places = [[],[],[],[],[],[],[],[],[]] + Connect4.ctx.clearRect(0, 0, Connect4.canvasW, Connect4.canvasH) + Connect4.Game.myTurn = true + + let p = GameDrawer.padding + + Connect4.ctx.beginPath() + for (let x = 0; x <= GameDrawer.bw; x += GameDrawer.gridSize) { + Connect4.ctx.moveTo(0.5 + x + p, p) + Connect4.ctx.lineTo(0.5 + x + p, GameDrawer.bh + p) + } + + for (let x = 0; x <= GameDrawer.bh; x += GameDrawer.gridSize) { + Connect4.ctx.moveTo(p, 0.5 + x + p) + Connect4.ctx.lineTo(GameDrawer.bw + p, 0.5 + x + p) + } + Connect4.ctx.closePath() + + Connect4.ctx.lineWidth = 1 + Connect4.ctx.strokeStyle = "black" + Connect4.ctx.stroke() + + GameDrawer.boardStaticState = new Image() + GameDrawer.boardStaticState.src = Connect4.DOM.canvas.toDataURL() + GameDrawer.boardStaticState.onload = () => { + Connect4.renderTick = true + GameDrawer.gameLoop() + } + }, + + possible: (column) => { + let inTable = Connect4.Game.places[column] + if (inTable.length === 9) { + return false + } + return true + }, + + click: () => { + if (!Connect4.Game.gameId) return + + if (Connect4.Game.myTurn && GameDrawer.mouseOn) { + let column = GameDrawer.gridX - 1 + if (GameDrawer.possible(column)) { + io.emit('place_at', {column: column, gameId: Connect4.Game.gameId}) + } + } + }, + + updater: () => { + for (let i in Connect4.Game.places) { + let col = Connect4.Game.places[i] + for (let p in col) { + let piece = col[p] + if (piece.dy < piece.y) { + piece.dy += 0.5 + } else { + piece.dy = piece.y + } + Connect4.ctx.fillStyle = Connect4.color[piece.color] + Connect4.ctx.fillRect((parseInt(i) * GameDrawer.gridSize) + GameDrawer.padding, + (piece.dy * GameDrawer.gridSize) + GameDrawer.padding, GameDrawer.gridSize, GameDrawer.gridSize) + } + } + if (!Connect4.renderTick || !Connect4.Game.gameId) return + if (Connect4.Game.myTurn) { + if (GameDrawer.mouseOn) { + let pos = (GameDrawer.gridX * GameDrawer.gridSize) + + Connect4.ctx.beginPath() + Connect4.ctx.moveTo(pos - 8, (GameDrawer.padding / 2) - 4) + Connect4.ctx.lineTo(pos, (GameDrawer.padding / 2) + 4) + Connect4.ctx.lineTo(pos + 8, (GameDrawer.padding / 2) - 4) + Connect4.ctx.closePath() + + Connect4.ctx.lineWidth = 10 + Connect4.ctx.strokeStyle = Connect4.color[Connect4.Game.myColor] + Connect4.ctx.stroke() + } + } + }, + + gameLoop: () => { + Connect4.ctx.clearRect(0, 0, Connect4.canvasW, Connect4.canvasH) + if (!Connect4.renderTick) return + + GameDrawer.updater() + + Connect4.ctx.drawImage(GameDrawer.boardStaticState, 0, 0) + requestAnimFrame(GameDrawer.gameLoop) + }, + + initialize: () => { + Connect4.DOM.canvas.addEventListener('mousemove', (e) => { + let p = pointerOnCanvas(e) + + if (p.x > GameDrawer.padding && p.y > GameDrawer.padding) { + let gridX = Math.floor((p.x + GameDrawer.padding) / GameDrawer.gridSize) + let gridY = Math.floor((p.y + GameDrawer.padding) / GameDrawer.gridSize) + + if (gridX <= 9 && gridY <= 9) { + GameDrawer.mouseOn = true + + GameDrawer.gridX = gridX + GameDrawer.gridY = gridY + } else { + GameDrawer.mouseOn = false + } + } else { + GameDrawer.mouseOn = false + } + }) + + Connect4.DOM.canvas.addEventListener('mouseleave', (e) => { + GameDrawer.mouseOn = false + }) + + Connect4.DOM.canvas.addEventListener('click', (e) => { + GameDrawer.click() + }) + + document.addEventListener('keydown', (e) => { + if (GameDrawer.placingShips && e.keyCode === 82) { + if (GameDrawer.shipOrientation === 0) { + GameDrawer.shipOrientation = 1 + } else { + GameDrawer.shipOrientation = 0 + } + } + }) + } + } + + function getStored (variable) { + let result = null + if (!window.localStorage) { + return null + } + + if (window.localStorage.game_store) { + try { + let obj = JSON.parse(window.localStorage.game_store) + if (obj[variable] != null) { + result = obj[variable] + } + } catch (e) { + result = null + } + } + + return result + } + + function storeVar (variable, value) { + if (!window.localStorage) { + return null + } + + if (window.localStorage.game_store) { + try { + let obj = JSON.parse(window.localStorage.game_store) + obj[variable] = value + window.localStorage.game_store = JSON.stringify(obj) + } catch (e) { + return null + } + } else { + let obj = {} + obj[variable] = value + window.localStorage.game_store = JSON.stringify(obj) + } + } + + function playerNameValidation (name) { + if (/^([A-Z0-9_\-@]{3,20})$/i.test(name)) { + return true + } + return false + } + + function logWarning (msg) { + Connect4.DOM.joinWarn.innerHTML = msg + } + + function logStatus (msg) { + Connect4.DOM.statusCurrent.innerHTML = msg + } + + function joinGame (game) { + Connect4.played += 1 + + alert('Game has started!') + //io.emit('leave_game', {gameId: Connect4.Game.gameId}) + Connect4.Game.gameId = game.gameId + Connect4.Game.opponentID = game.opponentId + Connect4.Game.opponentName = game.opponentName + Connect4.Game.myColor = game.color + Connect4.DOM.opponentName.innerHTML = game.opponentName + + Connect4.DOM.chatbox.innerHTML = '' + + io.emit('game_poll', {gameId: Connect4.Game.gameId}) + + Connect4.DOM.gameScreen.style.display = 'block' + Connect4.DOM.selectionScreen.style.display = 'none' + Connect4.DOM.waitlistBtns.style.display = 'block' + Connect4.DOM.waitlistQuit.style.display = 'none' + GameDrawer.startGame() + addChatMessage('event', null, 'Game started!') + } + + function attemptJoin (name) { + if (Connect4.locked) return + if (!io.connected) { + return logWarning('Disconnected from server socket.') + } + + if (playerNameValidation(name) == false) { + return logWarning('Username not allowed.') + } + + logWarning('Attempting to join..') + Connect4.locked = true + io.emit('session_create', {name: name}) + } + + function joinSuccess (data) { + Connect4.playerName = data.name + Connect4.playerID = data.uid + Connect4.DOM.selectionScreen.style.display = 'block' + + storeVar('name', data.name) + io.emit('poll_games') + Connect4.locked = false + } + + function joinResponse (data) { + if (data.success !== true) { + Connect4.locked = false + return logWarning(data.message) + } + + Connect4.DOM.startScreen.style.display = 'none' + + joinSuccess(data) + } + + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min + } + + function constructWaitList() { + let finalML = '' + for (let i in Connect4.waitlist) { + let game = Connect4.waitlist[i] + finalML += mustacheTempl('waitlistInstance', game) + } + waitlist.innerHTML = finalML + } + + window.joinWaiting = (gameId) => { + if (Connect4.Game.gameId) return + io.emit('game_attempt_join', {gameId: gameId}) + } + + function gameEnds (reason, winner) { + if (reason === 1) { + if (winner === true) { + alert('You won!') + logStatus('You won!') + } else { + alert('You lost.') + logStatus('You lost.') + } + } + + if (reason === 0 && winner === true) { + alert('Your opponent left the game.') + Connect4.DOM.gameScreen.style.display = 'none' + Connect4.DOM.selectionScreen.style.display = 'block' + Connect4.renderTick = false + } + + if (reason === 2) { + alert('You tied!') + logStatus('It\'s a tie!.') + } + + Connect4.locked = false + Connect4.Game.gameId = null + Connect4.Game.myTurn = false + io.emit('poll_games') + + //Connect4.DOM.gameScreen.style.display = 'none' + //Connect4.DOM.selectionScreen.style.display = 'block' + + Connect4.DOM.waitlistBtns.style.display = 'block' + Connect4.DOM.waitlistQuit.style.display = 'none' + addChatMessage('event', null, 'Disconnected') + } + + function forceRelogin () { + logWarning('Please log in again.') + Connect4.DOM.gameScreen.style.display = 'none' + Connect4.DOM.selectionScreen.style.display = 'none' + Connect4.DOM.startScreen.style.display = 'block' + Connect4.DOM.resultScreen.style.display = 'none' + + Connect4.locked = false + Connect4.playerName = '' + Connect4.Game.gameId = null + } + + function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + } + + function addChatMessage (type, senderName, message) { + let msgElem = '
' + if (senderName) { + msgElem += '' + senderName + ' ' + } + msgElem += '' + escapeHtml(message) + '' + + Connect4.DOM.chatbox.innerHTML += msgElem + Connect4.DOM.chatbox.scrollTop = Connect4.DOM.chatbox.scrollHeight + } + + window.onload = () => { + const startScreen = Connect4.DOM.startScreen = $.querySelector('#start') + const selectionScreen = Connect4.DOM.selectionScreen = $.querySelector('#selection') + const gameScreen = Connect4.DOM.gameScreen = $.querySelector('#game') + + const warning = Connect4.DOM.joinWarn = startScreen.querySelector('#warning_message') + const playerName = startScreen.querySelector('#player_name') + const startButton = startScreen.querySelector('#sock_player_init') + + const waitlist = Connect4.DOM.waitlist = selectionScreen.querySelector('#waitlist') + const random = selectionScreen.querySelector('#waitlist_join_random') + const newGame = selectionScreen.querySelector('#waitlist_join') + const refresh = selectionScreen.querySelector('#waitlist_join_refresh') + + const waitlistQuit = Connect4.DOM.waitlistQuit = selectionScreen.querySelector('#waitlist_quit') + const waitlistBtns = Connect4.DOM.waitlistBtns = selectionScreen.querySelector('.idbuttons') + + const stat_ingame = selectionScreen.querySelector('#stats_players') + const stat_total = selectionScreen.querySelector('#stats_games') + const stat_client = selectionScreen.querySelector('#stats_clientgames') + + const leaveBtn = gameScreen.querySelector('#leave') + const opponentName = Connect4.DOM.opponentName = gameScreen.querySelector('#opponent_name') + + let canvas = Connect4.DOM.canvas = gameScreen.querySelector('#game_canvas') + let ctx = Connect4.ctx = canvas.getContext('2d') + + Connect4.canvasW = canvas.width + Connect4.canvasH = canvas.height + + Connect4.DOM.statusCurrent = gameScreen.querySelector('#g_s_stat') + + const chatbox = Connect4.DOM.chatbox = gameScreen.querySelector('#messages') + const chatfield = Connect4.DOM.chatfield = gameScreen.querySelector('#message_send') + + GameDrawer.initialize() + + let uname = getStored('name') + if (uname) { + playerName.value = uname + } + + playerName.addEventListener('keydown', (e) => { + if (e.keyCode === 13) { + attemptJoin(playerName.value) + } + }, false) + + chatfield.addEventListener('keydown', (e) => { + if (e.keyCode === 13 && Connect4.Game.gameId) { + if (chatfield.value != '') { + io.emit('chat_send', {message: chatfield.value, gameId: Connect4.Game.gameId}) + addChatMessage('chat me', Connect4.playerName, chatfield.value) + chatfield.value = '' + } + } + }) + + startButton.addEventListener('click', (e) => { + attemptJoin(playerName.value) + }, false) + + newGame.addEventListener('click', (e) => { + if (Connect4.locked) return + if (Connect4.Game.gameId) return + io.emit('new_game') + Connect4.locked = true + }) + + refresh.addEventListener('click', (e) => { + if (Connect4.locked) return + io.emit('poll_games') + }) + + waitlistQuit.addEventListener('click', (e) => { + io.emit('leave_game', {gameId: Connect4.Game.gameId}) + }) + + leaveBtn.addEventListener('click', (e) => { + if (Connect4.Game.gameId) { + io.emit('leave_game', {gameId: Connect4.Game.gameId}) + } + Connect4.DOM.gameScreen.style.display = 'none' + Connect4.DOM.selectionScreen.style.display = 'block' + Connect4.renderTick = false + }) + + random.addEventListener('click', (e) => { + Connect4.joinRandomWhenDone = true + io.emit('poll_games') + }) + + io.on('chat', (data) => { + addChatMessage('chat', data.name, data.message) + }) + + io.on('infmessage', (message) => { + logStatus(message) + }) + + io.on('game_start', (data) => { + leaveBtn.innerHTML = 'Leave game' + joinGame(data) + }) + + io.on('left_success', () => { + gameEnds(0, null) + }) + + io.on('turn', (val) => { + if (val === true) { + Connect4.Game.myTurn = true + logStatus('Your turn.') + } else { + Connect4.Game.myTurn = false + logStatus('Your opponent\'s turn.') + } + }) + + io.on('place', (data) => { + let col = Connect4.Game.places[data.column] + col.push({y: 8 - col.length, color: data.color, dy: 0}) + }) + + io.on('game_error', (data) => { + alert(data.message) + gameEnds(0, null) + io.emit('poll_games') + }) + + io.on('force_relog', () => { + forceRelogin() + }) + + io.on('game_end', (data) => { + gameEnds(data.result, data.win) + leaveBtn.innerHTML = 'Back to lobby' + }) + + io.on('game_new_done', (data) => { + Connect4.locked = true + Connect4.DOM.waitlist.innerHTML = '
Waiting for an opponent..
' + Connect4.DOM.waitlistBtns.style.display = 'none' + Connect4.DOM.waitlistQuit.style.display = 'block' + Connect4.Game.gameId = data.gameId + }) + + io.on('current_stats', (data) => { + dataOpponentDestroyed.innerHTML = data.opponentShipsLeft + dataMineDestroyed.innerHTML = data.myShipsLeft + }) + + io.on('login_status', joinResponse) + io.on('poll_games_res', (data) => { + Connect4.DOM.waitlistQuit.style.display = 'none' + + let list = data.list + + if (data.sessions != null) { + stat_ingame.innerHTML = data.sessions + } + + if (data.totalGames != null) { + stat_total.innerHTML = data.totalGames + } + + stat_client.innerHTML = Connect4.played + + if (!list.length) { + waitlist.innerHTML = '
No people currently waiting, press Join Wait List to enter.
' + Connect4.waitlist = [] + + if(Connect4.joinRandomWhenDone) { + delete Connect4.joinRandomWhenDone + } + + return + } + + Connect4.waitlist = list + + if (Connect4.joinRandomWhenDone && Connect4.waitlist.length) { + delete Connect4.joinRandomWhenDone + + let rand = getRandomInt(1, Connect4.waitlist.length) + + io.emit('game_attempt_join', {gameId: Connect4.waitlist[rand - 1].gameId}) + } + + constructWaitList() + }) + + io.on('disconnect', () => { + gameEnds(0, null) + forceRelogin() + logWarning('Server disconnected') + }) + } +})(document) diff --git a/package.json b/package.json new file mode 100644 index 0000000..fce2a18 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "battleship.js", + "version": "0.0.0", + "description": "Battleship game in the browser", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "battleship", + "game", + "html5" + ], + "author": "Evert", + "license": "MIT", + "dependencies": { + "express": "^4.15.2", + "socketio": "^1.0.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..f5cab21 --- /dev/null +++ b/server.js @@ -0,0 +1,454 @@ +const express = require('express') +const socketio = require('socket.io') +const http = require('http') +const path = require('path') + +let app = express() +let server = http.createServer(app) +let io = socketio(server) + +app.enable('trust proxy') +app.disable('x-powered-by') + +app.use('/', express.static(path.join(__dirname, '/client/'))) + +function playerNameValidation (name) { + if (/^([A-Z0-9_\-@]{3,20})$/i.test(name)) { + return true + } + return false +} + +let clients = {} +let games = {} + +let totalGames = 0 + +// Generate a random int betweem two ints +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +// Generate random string of characters +function nuid(len) { + let buf = [], + chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + charlen = chars.length + + for (let i = 0; i < len; ++i) { + buf.push(chars[getRandomInt(0, charlen - 1)]) + } + + return buf.join('') +} + +function clientsBySocketID (id) { + let result = null + + for (let uid in clients) { + let client = clients[uid] + if (client.sockID === id) { + result = uid + } + } + + return result +} + +function determineOpponent (myIndex) { + let opponent = 'red' + + if (myIndex === 'red') { + opponent = 'blue' + } + + return opponent +} + +function killGamesClientIsIn (uid) { + for (let gameId in games) { + let game = games[gameId] + if (game.blue && game.blue === uid) { + if (!game.isWaiting && game.red) { + clients[game.red].socket.emit('game_end', {win: true, result: 0}) + } + } else if (game.red && game.red === uid) { + if (clients[game.blue]) { + clients[game.blue].socket.emit('game_end', {win: true, result: 0}) + } + } else { + continue + } + delete games[gameId] + console.log(gameId + ' was ended abruptly on ' + uid + '\'s demand.') + } +} + +function createNewGame (uid) { + let client = clients[uid] + let gameId = nuid(16) + + client.socket.emit('game_new_done', {gameId: gameId}) + + console.log(client.name + ' has started a new game. ID: ' + gameId) + + games[gameId] = { + blue: uid, + red: null, + isWaiting: true, + turn: 1, + places: [[],[],[],[],[],[],[],[],[]], + created: new Date(), + started: null + } +} + +function joinGame (uid, gameId) { + let me = clients[uid] + + if (!games[gameId]) { + return me.socket.emit('game_error', {message: 'That game has ended!'}) + } + + if (games[gameId].red != null) { + return me.socket.emit('game_error', {message: 'That game has already started!'}) + } + + if (!clients[games[gameId].blue]) { + return me.socket.emit('game_error', {message: 'That game has ended!'}) + } + + let game = games[gameId] + + game.red = uid + + game.isWaiting = false + game.started = new Date() + + let opponent = clients[game.blue] + + if (!opponent) { + return me.socket.emit('game_error', {message: 'Your opponent abruptly dissappeared, what?'}) + } + + opponent.socket.emit('game_start', {gameId: gameId, opponentId: uid, opponentName: me.name, color: 'blue'}) + me.socket.emit('game_start', {gameId: gameId, opponentId: opponent.uid, opponentName: opponent.name, color: 'red'}) + + game.turn = 'blue' + clients[game.blue].socket.emit('turn', true) + clients[uid].socket.emit('turn', false) + + totalGames += 1 +} + +function endGame (gameId, victoryId, loserId, status) { + if (clients[victoryId]) { + clients[victoryId].socket.emit('game_end', {win: true, result: status}) + } + + if (clients[loserId]) { + clients[loserId].socket.emit('game_end', {win: false, result: status}) + } + + delete games[gameId] + console.log(gameId + ' ended with ' + victoryId + '\'s victory.') +} + +function waitingGamesList (uid) { + let result = [] + let cap = 0 + + let gamesInSession = 0 + for (let i in games) { + let game = games[i] + if (!game.isWaiting) { + gamesInSession += 1 + } + } + + for (let gameId in games) { + if (cap >= 20) break + + let game = games[gameId] + + if (game.isWaiting) { + let userName = clients[game.blue].name + if (uid && game.blue === uid) continue + + result.push({ + gameId: gameId, + name: userName, + started: game.started + }) + + cap += 1 + } + } + + return { + sessions: gamesInSession, + totalGames: totalGames, + list: result + } +} + +function determinePlayerById (gameId, uid) { + let game = games[gameId] + + if (!game) return null + + if (game.blue && game.blue === uid) { + return 'blue' + } else if (game.red && game.red === uid) { + return 'red' + } + + return null +} + +function getPiece (game, col, index) { + if (col > 8 || index > 8) return + col = game.places[col] + + if (!col) return + + if (!col.length) { + return + } + + let match = null + for (let i in col) { + if (col[i].y === index) { + match = col[i] + break + } + } + return match +} + +function detectWin (color, game) { + let win = false + for (let c in game.places) { + let col = game.places[c] + for (let p in col) { + let piece = col[p] + let matches = 0 + for (let i = 0; i < 4; i++) { + let pAt = getPiece(game, parseInt(c) + i, piece.y) + if (pAt && pAt.color === color) { + console.log(pAt) + matches += 1 + } else { + matches = 0 + } + } + if (matches >= 4) { + win = true + console.log('horizontal win') + break + } + matches = 0 + for (let i = 0; i < 4; i++) { + let pAt = getPiece(game, parseInt(c), piece.y + i) + if (pAt && pAt.color === color) { + matches += 1 + } else { + matches = 0 + } + } + if (matches >= 4) { + console.log('vertical win') + win = true + break + } + matches = 0 + for (let i = 0; i < 4; i++) { + let pAt = getPiece(game, parseInt(c) + i, piece.y - i) + if (pAt && pAt.color === color) { + matches += 1 + } else { + matches = 0 + } + } + if (matches >= 4) { + console.log('diagonal right win') + win = true + break + } + matches = 0 + for (let i = 0; i < 4; i++) { + let pAt = getPiece(game, parseInt(c) - i, piece.y - i) + if (pAt && pAt.color === color) { + matches += 1 + } else { + matches = 0 + } + } + if (matches >= 4) { + console.log('diagonal left win') + win = true + break + } + } + } + return win +} + +function detectTie (game) { + let tie = true + for (let c in game.places) { + let col = game.places[c] + if (col.length !== 9) { + tie = false + break + } + } + return tie +} + +io.on('connection', (socket) => { + socket.on('session_create', (data) => { + if (!data.name) { + return socket.emit('login_status', {success: false, message: 'Invalid name.'}) + } + + if (!playerNameValidation(data.name)) { + return socket.emit('login_status', {success: false, message: 'Invalid name.'}) + } + + let playerUid = nuid(32) + + socket.emit('login_status', {success: true, uid: playerUid, name: data.name}) + clients[playerUid] = { + socket: socket, + name: data.name, + sockID: socket.conn.id + } + + console.log('New player: "' + data.name + '" with uid ' + playerUid) + }) + + socket.on('poll_games', () => { + let client = clientsBySocketID(socket.conn.id) + socket.emit('poll_games_res', waitingGamesList(client)) + }) + + socket.on('game_attempt_join', (data) => { + let client = clientsBySocketID(socket.conn.id) + + if (!client) { + socket.emit('game_error', {message: 'You are not logged in properly!'}) + socket.emit('force_relog') + return + } + + if (!data.gameId) return + + joinGame(client, data.gameId) + }) + + socket.on('leave_game', (data) => { + let client = clientsBySocketID(socket.conn.id) + + if (!client) return + killGamesClientIsIn(client) + + socket.emit('left_success') + }) + + socket.on('new_game', () => { + let client = clientsBySocketID(socket.conn.id) + + if (!client) { + socket.emit('game_error', {message: 'You are not logged in properly!'}) + socket.emit('force_relog') + return + } + + createNewGame(client) + }) + + socket.on('chat_send', (data) => { + let client = clientsBySocketID(socket.conn.id) + + if (!client) { + socket.emit('game_error', {message: 'You are not logged in properly!'}) + socket.emit('force_relog') + return + } + + let game = games[data.gameId] + let playerInGame = determinePlayerById(data.gameId, client) + + if (!playerInGame) { + socket.emit('game_error', {message: 'unexpected error. code: 763'}) + return + } + + let opponent = determineOpponent(playerInGame) + let opponentObj = game[opponent] + let me = game[playerInGame] + + clients[opponentObj].socket.emit('chat', {name: clients[me].name, message: data.message}) + }) + + socket.on('place_at', (data) => { + let client = clientsBySocketID(socket.conn.id) + + if (!client) { + socket.emit('game_error', {message: 'You are not logged in properly!'}) + socket.emit('force_relog') + return + } + + let game = games[data.gameId] + let playerInGame = determinePlayerById(data.gameId, client) + + if (!playerInGame) { + socket.emit('game_error', {message: 'unexpected error. code: 763'}) + return + } + + if (data.column == null || data.column > 9) { + socket.emit('game_error', {message: 'Unexpected column'}) + return + } + + let opponent = determineOpponent(playerInGame) + opponent = game[opponent] + + let me = game[playerInGame] + + clients[me].socket.emit('place', {column: data.column, color: playerInGame}) + clients[opponent].socket.emit('place', {column: data.column, color: playerInGame}) + + game.places[data.column].push({color: playerInGame, y: 8 - game.places[data.column].length}) + console.log(game.places[data.column]) + + if (detectWin(playerInGame, game)) { + endGame(data.gameId, me, opponent, 1) + return + } + + if (detectTie(game)) { + endGame(data.gameId, me, opponent, 2) + return + } + + clients[me].socket.emit('turn', false) + clients[opponent].socket.emit('turn', true) + }) + + socket.on('disconnect', () => { + let client = clientsBySocketID(socket.conn.id) + if (!client) return + + killGamesClientIsIn(client) + + console.log('Player uid ' + client + ' left.') + + delete clients[client] + }) +}) + +server.listen(8245)