commit 73893b6cb67fa975c629bdbc3c5ea94fc65e8a71 Author: Evert Date: Mon Apr 3 23:44:36 2017 +0300 code 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..9ab203e --- /dev/null +++ b/client/index.css @@ -0,0 +1,155 @@ +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 { + width: 512px; + height: 512px; + background-color: #03A9F4; + margin: 10px; + /*border: 5px solid #2196F3;*/ +} +.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 { + margin-top: 20px; + border-top: 1px solid #ddd; + color: #119286; + font-weight: bold; + padding-top: 5px; + width: 200px; +} +#game_canvas { + margin-left: 265px; +} diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..37c73bc --- /dev/null +++ b/client/index.html @@ -0,0 +1,73 @@ + + + + + + + + + + + + HTML5 Battleship (simplified version) + + +
+
+
+

Enter your name

+

+ + +
+
+ + +
+ + + diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..01fc9fa --- /dev/null +++ b/client/index.js @@ -0,0 +1,689 @@ +(function ($) { + let io = window.io.connect() + let Battleship = { + DOM: {}, + playerName: '', + playerID: '', + verified: null, + locked: false, + waitlist: [], + played: 0, + Game: { + gameId: null, + inGame: false, + myTurn: true, + opponentID: '', + opponentName: '', + ships: 0, + oShips: 0, + gridHome: {ships: [], strikes: []}, + gridOpponent: [] + } + } + + 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 -= Battleship.DOM.canvas.offsetLeft + y -= Battleship.DOM.canvas.offsetTop + + return {x: x, y: y} + } + + let GameDrawer = { + drawMyBoard: true, + boardStaticState: null, + + mX: 0, + mY: 0, + + gridX: 0, + gridY: 0, + + gridSize: 32, + mouseOn: false, + + bw: 512, + bh: 512, + + placingShips: true, + canPlace: false, + shipIndex: 0, + shipOrientation: 0, + shipTiles: [], + + startGame: () => { + GameDrawer.placingShips = true + GameDrawer.canPlace = false + GameDrawer.shipIndex = 0 + GameDrawer.shipOrientation = 0 + GameDrawer.shipTiles = [] + + Battleship.ctx.clearRect(0, 0, Battleship.canvasW, Battleship.canvasH) + Battleship.Game.myTurn = true + + let p = 0 + let context = Battleship.ctx + + for (let x = 0; x <= GameDrawer.bw; x += GameDrawer.gridSize) { + context.moveTo(0.5 + x + p, p) + context.lineTo(0.5 + x + p, GameDrawer.bh + p) + } + + for (let x = 0; x <= GameDrawer.bh; x += GameDrawer.gridSize) { + context.moveTo(p, 0.5 + x + p) + context.lineTo(GameDrawer.bw + p, 0.5 + x + p) + } + + context.strokeStyle = "black" + context.stroke() + + GameDrawer.boardStaticState = new Image() + GameDrawer.boardStaticState.src = Battleship.DOM.canvas.toDataURL() + GameDrawer.boardStaticState.onload = () => { + GameDrawer.gameLoop() + } + }, + + click: () => { + if (GameDrawer.placingShips && GameDrawer.canPlace && GameDrawer.shipIndex != -1) { + let shipData = Battleship.ships[GameDrawer.shipIndex] + let placed = { + name: shipData.name, + sunken: false, + cells: GameDrawer.shipTiles + } + + io.emit('ship_place', {ship: placed, gameId: Battleship.Game.gameId}) + + if (GameDrawer.shipIndex + 1 < Battleship.ships.length) { + GameDrawer.shipIndex += 1 + } else { + GameDrawer.shipIndex = -1 + logStatus('Waiting for opponent') + } + } else if (!GameDrawer.placingShips && Battleship.Game.myTurn) { + io.emit('set_bomb', {x: GameDrawer.gridX, y: GameDrawer.gridY, gameId: Battleship.Game.gameId}) + } + }, + + intersectsExisting: (cx, cy) => { + let is = false + for (let i in Battleship.Game.gridHome.ships) { + let ship = Battleship.Game.gridHome.ships[i] + for (let j in ship.cells) { + let cell = ship.cells[j] + if (cell.x === cx && cell.y === cy) { + is = true + } + } + } + return is + }, + + updater: () => { + if (Battleship.Game.myTurn && !GameDrawer.placingShips) { + if (GameDrawer.mouseOn) { + Battleship.ctx.fillStyle = '#f4b002' + Battleship.ctx.fillRect(GameDrawer.gridX * GameDrawer.gridSize, GameDrawer.gridY * GameDrawer.gridSize, GameDrawer.gridSize, GameDrawer.gridSize) + } + + for (let i in Battleship.Game.gridHome.strikes) { + let strike = Battleship.Game.gridHome.strikes[i] + if (strike.destroy) { + Battleship.ctx.fillStyle = '#ff0000' + } else { + Battleship.ctx.fillStyle = '#dddd00' + } + Battleship.ctx.fillRect(strike.x * GameDrawer.gridSize, strike.y * GameDrawer.gridSize, GameDrawer.gridSize, GameDrawer.gridSize) + } + } else if ((!Battleship.Game.myTurn && !GameDrawer.placingShips) || (Battleship.Game.myTurn && GameDrawer.placingShips)) { + for (let i in Battleship.Game.gridOpponent) { + let strike = Battleship.Game.gridOpponent[i] + Battleship.ctx.fillStyle = '#dddd00' + Battleship.ctx.fillRect(strike.x * GameDrawer.gridSize, strike.y * GameDrawer.gridSize, GameDrawer.gridSize, GameDrawer.gridSize) + } + + for (let i in Battleship.Game.gridHome.ships) { + let ship = Battleship.Game.gridHome.ships[i] + for (let j in ship.cells) { + let cell = ship.cells[j] + let color = '#dddddd' + + if (cell.destroyed || ship.sunken) { + color = '#ff0000' + } + + Battleship.ctx.fillStyle = color + Battleship.ctx.fillRect(cell.x * GameDrawer.gridSize, cell.y * GameDrawer.gridSize, GameDrawer.gridSize, GameDrawer.gridSize) + } + } + } + + if (Battleship.Game.myTurn && GameDrawer.placingShips) { + let shipData = Battleship.ships[GameDrawer.shipIndex] + + if (!shipData) return + + let shipCenter = Math.floor(shipData.tiles / 2) + + let cellsOff = 0 + let positions = [] + let color = '#00dd00' + + for (let i = 0; i < shipData.tiles; i++) { + let sx = 0 + let sy = 0 + i = parseInt(i) + + let rx = 0 + let ry = 0 + + if (i < shipCenter) { + if (GameDrawer.shipOrientation === 0) { + sx = GameDrawer.gridX - (shipCenter - i) + sy = GameDrawer.gridY + } else { + sx = GameDrawer.gridX + sy = GameDrawer.gridY - (shipCenter - i) + } + } else if (i === shipCenter) { + sx = GameDrawer.gridX + sy = GameDrawer.gridY + } else { + if (GameDrawer.shipOrientation === 0) { + sx = GameDrawer.gridX + (i - shipCenter) + sy = GameDrawer.gridY + } else { + sx = GameDrawer.gridX + sy = GameDrawer.gridY + (i - shipCenter) + } + } + + if (sx < 0 || sy < 0 || sx > 15 || sy > 15) { + cellsOff += 1 + } + + if (GameDrawer.intersectsExisting(sx, sy)) { + cellsOff += 1 + } + + positions.push({x: sx, y: sy, destroyed: false}) + } + + for (let i in positions) { + let pos = positions[i] + + if (cellsOff > 0) { + color = '#dd0000' + GameDrawer.canPlace = false + } else { + GameDrawer.canPlace = true + } + + Battleship.ctx.fillStyle = color + Battleship.ctx.fillRect(pos.x * GameDrawer.gridSize, pos.y * GameDrawer.gridSize, GameDrawer.gridSize, GameDrawer.gridSize) + } + + GameDrawer.shipTiles = positions + } + }, + + clearCanvas: () => { + Battleship.ctx.clearRect(0, 0, Battleship.canvasW, Battleship.canvasH) + Battleship.ctx.drawImage(GameDrawer.boardStaticState, 0, 0) + }, + + gameLoop: () => { + GameDrawer.clearCanvas() + if (!Battleship.Game.gameId) return + + GameDrawer.updater() + + requestAnimFrame(GameDrawer.gameLoop) + }, + + initialize: () => { + Battleship.DOM.canvas.addEventListener('mousemove', (e) => { + let posOnCanvas = pointerOnCanvas(e) + GameDrawer.mX = posOnCanvas.x + GameDrawer.mY = posOnCanvas.y + GameDrawer.mouseOn = true + + let rowCount = GameDrawer.gridSize + + GameDrawer.gridX = Math.floor(GameDrawer.mX / rowCount) + GameDrawer.gridY = Math.floor(GameDrawer.mY / rowCount) + }) + + Battleship.DOM.canvas.addEventListener('mouseleave', (e) => { + GameDrawer.mouseOn = false + }) + + Battleship.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) { + Battleship.DOM.joinWarn.innerHTML = msg + } + + function logStatus (msg) { + Battleship.DOM.statusCurrent.innerHTML = msg + } + + function joinGame (game) { + Battleship.played += 1 + + alert('Game has started!') + //io.emit('leave_game', {gameId: Battleship.Game.gameId}) + Battleship.Game.gameId = game.gameId + Battleship.Game.opponentID = game.opponentId + Battleship.Game.opponentName = game.opponentName + Battleship.DOM.opponentName.innerHTML = game.opponentName + + io.emit('game_poll', {gameId: Battleship.Game.gameId}) + + logStatus('Place your ships onto the board.
Press `r` to rotate') + + Battleship.DOM.gameScreen.style.display = 'block' + Battleship.DOM.selectionScreen.style.display = 'none' + Battleship.DOM.waitlistBtns.style.display = 'block' + Battleship.DOM.waitlistQuit.style.display = 'none' + GameDrawer.startGame() + } + + function attemptJoin (name) { + if (Battleship.locked) return + if (!io.connected) { + return logWarning('Disconnected from server socket.') + } + + if (playerNameValidation(name) == false) { + return logWarning('Username not allowed.') + } + + logWarning('Attempting to join..') + Battleship.locked = true + io.emit('session_create', {name: name}) + } + + function joinSuccess (data) { + Battleship.playerName = data.name + Battleship.playerID = data.uid + Battleship.DOM.selectionScreen.style.display = 'block' + + storeVar('name', data.name) + io.emit('poll_games') + Battleship.locked = false + } + + function joinResponse (data) { + if (data.success !== true) { + Battleship.locked = false + return logWarning(data.message) + } + + Battleship.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 Battleship.waitlist) { + let game = Battleship.waitlist[i] + finalML += mustacheTempl('waitlistInstance', game) + } + waitlist.innerHTML = finalML + } + + window.joinWaiting = (gameId) => { + if (Battleship.Game.gameId) return + io.emit('game_attempt_join', {gameId: gameId}) + } + + function gameEnds (reason, winner) { + if (reason === 1) { + if (winner === true) { + alert('You won!') + } else { + alert('You lost.') + } + } + + if (reason === 0 && winner === true) { + alert('Your opponent left the game.') + } + + Battleship.locked = false + Battleship.Game.gameId = null + io.emit('poll_games') + Battleship.DOM.gameScreen.style.display = 'none' + Battleship.DOM.selectionScreen.style.display = 'block' + Battleship.DOM.waitlistBtns.style.display = 'block' + Battleship.DOM.waitlistQuit.style.display = 'none' + + Battleship.Game.gridHome = {ships: [], strikes: []} + Battleship.Game.gridOpponent = [] + + Battleship.DOM.dataOpponentDestroyed.innerHTML = '0' + Battleship.DOM.dataMineDestroyed.innerHTML = '0' + } + + function forceRelogin () { + logWarning('Please log in again.') + Battleship.DOM.gameScreen.style.display = 'none' + Battleship.DOM.selectionScreen.style.display = 'none' + Battleship.DOM.startScreen.style.display = 'block' + + Battleship.locked = false + Battleship.playerName = '' + Battleship.Game.gameId = null + } + + window.onload = () => { + const startScreen = Battleship.DOM.startScreen = $.querySelector('#start') + const selectionScreen = Battleship.DOM.selectionScreen = $.querySelector('#selection') + const gameScreen = Battleship.DOM.gameScreen = $.querySelector('#game') + + const warning = Battleship.DOM.joinWarn = startScreen.querySelector('#warning_message') + const playerName = startScreen.querySelector('#player_name') + const startButton = startScreen.querySelector('#sock_player_init') + + const waitlist = Battleship.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 = Battleship.DOM.waitlistQuit = selectionScreen.querySelector('#waitlist_quit') + const waitlistBtns = Battleship.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 = Battleship.DOM.opponentName = gameScreen.querySelector('#opponent_name') + + let canvas = Battleship.DOM.canvas = gameScreen.querySelector('#game_canvas') + let ctx = Battleship.ctx = canvas.getContext('2d') + + Battleship.canvasW = canvas.width + Battleship.canvasH = canvas.height + + const dataOpponentDestroyed = Battleship.DOM.dataOpponentDestroyed = gameScreen.querySelector('#o_s_num') + const dataMineDestroyed = Battleship.DOM.dataMineDestroyed = gameScreen.querySelector('#g_s_num') + Battleship.DOM.statusCurrent = gameScreen.querySelector('#g_s_stat') + + GameDrawer.initialize() + + let uname = getStored('name') + if (uname) { + playerName.value = uname + } + + playerName.addEventListener('keydown', (e) => { + if (e.keyCode === 13) { + attemptJoin(playerName.value) + } + }, false) + + startButton.addEventListener('click', (e) => { + attemptJoin(playerName.value) + }, false) + + newGame.addEventListener('click', (e) => { + if (Battleship.locked) return + if (Battleship.Game.gameId) return + io.emit('new_game') + Battleship.locked = true + }) + + refresh.addEventListener('click', (e) => { + if (Battleship.locked) return + io.emit('poll_games') + }) + + waitlistQuit.addEventListener('click', (e) => { + io.emit('leave_game', {gameId: Battleship.Game.gameId}) + }) + + leaveBtn.addEventListener('click', (e) => { + io.emit('leave_game', {gameId: Battleship.Game.gameId}) + }) + + random.addEventListener('click', (e) => { + Battleship.joinRandomWhenDone = true + io.emit('poll_games') + }) + + io.on('destroy_turn', (val) => { + GameDrawer.placingShips = false + if (val === true) { + Battleship.Game.myTurn = true + logStatus('It\'s your turn!
Click anywhere on the grid to attempt to destroy enemy ships.') + } else { + Battleship.Game.myTurn = false + logStatus('Your opponent\'s turn.') + } + }) + + io.on('update_hits', (data) => { + if (data.me) { + Battleship.Game.gridHome.strikes = data.strikes + } else { + Battleship.Game.gridOpponent = data.strikes + } + }) + + io.on('updateShip', (dship) => { + for (let i in Battleship.Game.gridHome.ships) { + let ship = Battleship.Game.gridHome.ships[i] + if (ship.name === dship.name) { + for (let j in ship.cells) { + let cell = ship.cells[j] + cell.destroyed = dship.cells[j].destroyed + } + ship.sunken = dship.sunken + + if (ship.sunken) { + logStatus('Opponent sunk one of your ships!') + } + } + } + }) + + io.on('infmessage', (message) => { + logStatus(message) + }) + + io.on('game_settings', (data) => { + Battleship.ships = data.ships + }) + + io.on('verified_place', (ship) => { + Battleship.Game.gridHome.ships.push(ship) + }) + + io.on('game_start', (data) => { + joinGame(data) + }) + + io.on('left_success', () => { + gameEnds(0, null) + }) + + 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) + }) + + io.on('game_new_done', (data) => { + Battleship.locked = true + Battleship.DOM.waitlist.innerHTML = '
Waiting for an opponent..
' + Battleship.DOM.waitlistBtns.style.display = 'none' + Battleship.DOM.waitlistQuit.style.display = 'block' + Battleship.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) => { + Battleship.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 = Battleship.played + + if (!list.length) { + waitlist.innerHTML = '
No people currently waiting, press Join Wait List to enter.
' + Battleship.waitlist = [] + + if(Battleship.joinRandomWhenDone) { + delete Battleship.joinRandomWhenDone + } + + return + } + + Battleship.waitlist = list + + if (Battleship.joinRandomWhenDone && Battleship.waitlist.length) { + delete Battleship.joinRandomWhenDone + + let rand = getRandomInt(1, Battleship.waitlist.length) + + io.emit('game_attempt_join', {gameId: Battleship.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..b4451b0 --- /dev/null +++ b/server.js @@ -0,0 +1,517 @@ +const express = require('express') +const socketio = require('socket.io') +const http = require('http') +const path = require('path') + +// TODO LIST: +// * More ships +// * Timer +// * Chat box +// * Side-by-side board display + +const ships = [ + {name: 'biggest', tiles: 5, destCount: 4}, + {name: 'bigger', tiles: 4, destCount: 3}, + {name: 'medium', tiles: 3, destCount: 2}, + {name: 'smaller', tiles: 3, destCount: 2}, + {name: 'smallest', tiles: 2, destCount: 1} +] + +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 killGamesClientIsIn (uid) { + for (let gameId in games) { + let game = games[gameId] + if (game.player1 && game.player1.uid === uid) { + if (!game.isWaiting && game.player2) { + clients[game.player2.uid].socket.emit('game_end', {win: true, result: 0}) + } + } else if (game.player2 && game.player2.uid === uid) { + if (clients[game.player1.uid]) { + clients[game.player1.uid].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] = { + player1: { + uid: uid, + ships: [], + strikes: [], + placed: false, + destructions: 0 + }, + player2: null, + isWaiting: true, + turn: 1, + started: new Date() + } +} + +function joinGame (uid, gameId) { + let me = clients[uid] + + if (!games[gameId]) { + return socket.emit('game_error', {message: 'That game has ended!'}) + } + + if (games[gameId].player2 != null) { + return socket.emit('game_error', {message: 'That game has already started!'}) + } + + if (!clients[games[gameId].player1.uid]) { + return socket.emit('game_error', {message: 'That game has ended!'}) + } + + games[gameId].player2 = { + uid: uid, + ships: [], + strikes: [], + placed: false, + destructions: 0 + } + + games[gameId].isWaiting = false + + let opponent = clients[games[gameId].player1.uid] + + if (!opponent) { + return socket.emit('game_error', {message: 'Your opponent abruptly dissappeared, what?'}) + } + + opponent.socket.emit('game_start', {gameId: gameId, opponentId: uid, opponentName: me.name}) + me.socket.emit('game_start', {gameId: gameId, opponentId: opponent.uid, opponentName: opponent.name}) + + totalGames += 1 +} + +function endGame (gameId, victoryId, loserId) { + if (clients[victoryId]) { + clients[victoryId].socket.emit('game_end', {win: true, result: 1}) + } + + if (clients[loserId]) { + clients[loserId].socket.emit('game_end', {win: false, result: 1}) + } + + 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.player1.uid].name + if (uid && game.player1.uid === uid) continue + + result.push({ + gameId: gameId, + name: userName, + started: game.started + }) + + cap += 1 + } + } + + return { + sessions: gamesInSession, + totalGames: totalGames, + list: result + } +} + +function bombTile (playerIndex, gameId, xTile, yTile) { + +} + +function determinePlayerById (gameId, uid) { + let game = games[gameId] + + if (game.player1 && game.player1.uid === uid) { + return 'player1' + } else if (game.player2 && game.player2.uid === uid) { + return 'player2' + } + + return null +} + +function getShipByName (name) { + let ship = null + for (let i in ships) { + if (ships[i].name === name) { + ship = ships[i] + } + } + return ship +} + +function integrityCheck (shipDataProvided) { + if (!shipDataProvided.name || shipDataProvided.sunken == null || !shipDataProvided.cells) { + return false + } + + let shipActual = getShipByName(shipDataProvided.name) + + if (shipActual === null) { + return false + } + + if (shipDataProvided.cells.length !== shipActual.tiles) { + return false + } + + return true +} + +function getDestroyedTiles (ship) { + let count = 0 + for (let i in ship.cells) { + let cell = ship.cells[i] + if (cell.destroyed === true) { + count += 1 + } + } + return count +} + +function markAllTilesDestroyed (ship, myStrikes) { + for (let i in ship.cells) { + let cell = ship.cells[i] + cell.destroyed = true + myStrikes.push({x: cell.x, y: cell.y, destroy: true}) + } + ship.sunken = true +} + +function attemptToBombTile (playerIndex, opponent, game, x, y) { + let me = game[playerIndex] + let opponentObj = game[opponent] + + let tileMatch = null + let shipMatch = null + + for (let i in opponentObj.ships) { + let ship = opponentObj.ships[i] + for (let j in ship.cells) { + let cell = ship.cells[j] + if (cell.x === x && cell.y === y) { + tileMatch = cell + shipMatch = ship + } + } + } + + if (!tileMatch) { + me.strikes.push({x: x, y: y, destroy: false}) + //opponentObj.strikes.push({x: x, y: y, destroy: false}) + return {event: 2, ship: null} + } + + if (tileMatch.destroyed === true) { + return {event: 5, ship: shipMatch} + } + + tileMatch.destroyed = true + me.strikes.push({x: x, y: y, destroy: true}) + + let shipActual = getShipByName(shipMatch.name) + let destroyedTileCount = getDestroyedTiles(shipMatch) + + if (destroyedTileCount >= shipActual.destCount) { + markAllTilesDestroyed(shipMatch, me.strikes) + opponentObj.destructions += 1 + + if (opponentObj.destructions === ships.length) { + console.log('winner: ' + me.uid) + return {event: 6, ship: shipMatch} + } + + return {event: 1, ship: shipMatch} + } + + return {event: 0, ship: shipMatch} +} + +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}) + socket.emit('game_settings', {ships: ships}) + 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('ship_place', (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 || !data.ship) 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 meObj = game[playerInGame] + if (meObj.placed === true) { + socket.emit('game_error', {message: 'Please don\'t cheat.'}) + return + } + + if (!integrityCheck(data.ship)) { + socket.emit('game_error', {message: 'Something went wrong when you tried placing a ship.'}) + return + } + + meObj.ships.push({ + name: data.ship.name, + sunken: false, + cells: data.ship.cells + }) + + socket.emit('verified_place', data.ship) + + if (meObj.ships.length === ships.length) { + meObj.placed = true + let opponent = 'player2' + + if (playerInGame === 'player2') { + opponent = 'player1' + } + + if (game[opponent].placed) { + game.turn = 'player1' + clients[game.player1.uid].socket.emit('destroy_turn', true) + clients[game.player2.uid].socket.emit('destroy_turn', false) + } + + clients[meObj.uid].socket.emit('current_stats', { + opponentShipsLeft: ships.length - game[opponent].destructions, + myShipsLeft: ships.length - meObj.destructions + }) + clients[game[opponent].uid].socket.emit('current_stats', { + opponentShipsLeft: ships.length - meObj.destructions, + myShipsLeft: ships.length - game[opponent].destructions + }) + } + }) + + socket.on('set_bomb', (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 = 'player2' + + if (playerInGame === 'player2') { + opponent = 'player1' + } + + let result = attemptToBombTile(playerInGame, opponent, game, data.x, data.y) + + let opponentObj = game[opponent] + let me = game[playerInGame] + + if (result.ship) { + clients[opponentObj.uid].socket.emit('updateShip', result.ship) + } + + clients[me.uid].socket.emit('update_hits', {me: true, strikes: me.strikes}) + clients[me.uid].socket.emit('update_hits', {me: false, strikes: opponentObj.strikes}) + + switch (result.event) { + case 5: + case 1: + clients[me.uid].socket.emit('infmessage', 'You sunk a ship!') + break + case 0: + clients[me.uid].socket.emit('infmessage', 'You destroyed some of the opponents ship!') + break + case 2: + game.turn = opponent + clients[me.uid].socket.emit('destroy_turn', false) + clients[opponentObj.uid].socket.emit('destroy_turn', true) + clients[me.uid].socket.emit('infmessage', 'You missed!') + break + case 6: + endGame(data.gameId, client, opponentObj.uid) + break + } + + clients[me.uid].socket.emit('current_stats', { + opponentShipsLeft: ships.length - opponentObj.destructions, + myShipsLeft: ships.length - me.destructions + }) + clients[opponentObj.uid].socket.emit('current_stats', { + opponentShipsLeft: ships.length - me.destructions, + myShipsLeft: ships.length - opponentObj.destructions + }) + }) + + 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(8000)