diff --git a/.eslintrc.yml b/.eslintrc.yml index 5495807..70ae0ca 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -263,7 +263,7 @@ rules: no-trailing-spaces: error no-undef: error no-undef-init: error - no-undefined: error + no-undefined: off no-underscore-dangle: error no-unexpected-multiline: error no-unmodified-loop-condition: error diff --git a/README.md b/README.md index c3a91a2..656456e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ ## Overview +**Status: currently in development.** + This is the server-side and client-side applications for playing Junkyard Brawl in your favorite device's browser. The backend server is written in nodejs and the frontend is written with [Vue.js](https://vuejs.org/) and bundled with [webpack](https://webpack.js.org/). diff --git a/client/.pug-lintrc.js b/client/.pug-lintrc.js index 3892648..3e9aed5 100644 --- a/client/.pug-lintrc.js +++ b/client/.pug-lintrc.js @@ -38,7 +38,10 @@ module.exports = { ], requireStrictEqualityOperators: true, validateAttributeQuoteMarks: '"', - validateAttributeSeparator: ', ', + validateAttributeSeparator: { + separator: ', ', + multiLineSeparator: ',\n ' + }, validateDivTags: true, validateExtensions: true, validateIndentation: 2, diff --git a/client/.stylelintrc.yml b/client/.stylelintrc.yml index 4fd0e8a..4106e1a 100644 --- a/client/.stylelintrc.yml +++ b/client/.stylelintrc.yml @@ -22,7 +22,7 @@ rules: block-opening-brace-space-after: always-single-line block-opening-brace-space-before: always color-hex-case: lower - #color-hex-length: long + color-hex-length: null color-no-invalid-hex: true comment-empty-line-before: null comment-no-empty: true @@ -135,11 +135,7 @@ rules: selector-pseudo-element-colon-notation: single selector-pseudo-element-no-unknown: true selector-type-case: lower - selector-type-no-unknown: - - true - - ignoreTypes: - - /^w(d|m|t)-/ - - chart-trade-dropdown + selector-type-no-unknown: null shorthand-property-no-redundant-values: true string-no-newline: true unit-case: lower diff --git a/client/package.json b/client/package.json index 6de04e7..7020f6c 100644 --- a/client/package.json +++ b/client/package.json @@ -47,6 +47,7 @@ "webpack-dev-server": "^2.9.3" }, "dependencies": { + "uikit": "^3.0.0-beta.40", "vue": "^2.4.2", "vue-router": "^2.7.0" } diff --git a/client/src/components/game-info.js b/client/src/components/game-info.js new file mode 100644 index 0000000..db75266 --- /dev/null +++ b/client/src/components/game-info.js @@ -0,0 +1,8 @@ +require('./game-info.scss') + +module.exports = require('vue').component('game-info', { + props: { + score: Number + }, + template: require('./game-info.pug')() +}) diff --git a/client/src/components/game-info.pug b/client/src/components/game-info.pug new file mode 100644 index 0000000..05de999 --- /dev/null +++ b/client/src/components/game-info.pug @@ -0,0 +1,10 @@ +.game-info + div + span.label Time: + span.value 4:57 + div + span.label Turns: + span.value 23 + div + span.label Score: + span.value {{ score }} diff --git a/client/src/components/game-info.scss b/client/src/components/game-info.scss new file mode 100644 index 0000000..3d42618 --- /dev/null +++ b/client/src/components/game-info.scss @@ -0,0 +1,19 @@ +.game-info { + border-bottom: 1px solid #555; + padding-bottom: 1em; + text-align: center; + + > div { + display: inline-block; + width: 200px; + + .label { + margin-right: 5px; + } + + .value { + font-weight: bold; + } + } +} + diff --git a/client/src/components/game-modal.js b/client/src/components/game-modal.js new file mode 100644 index 0000000..6d5e59a --- /dev/null +++ b/client/src/components/game-modal.js @@ -0,0 +1,8 @@ +require('./game-modal.scss') + +module.exports = require('vue').component('game-modal', { + props: { + score: Number + }, + template: require('./game-modal.pug')() +}) diff --git a/client/src/components/game-modal.pug b/client/src/components/game-modal.pug new file mode 100644 index 0000000..b8abaae --- /dev/null +++ b/client/src/components/game-modal.pug @@ -0,0 +1,6 @@ +.game-modal + .overlay + .content + .title You won! + .score Score: {{ score }} + button.uk-button.uk-button-default(@click="newGame()", type="button") New game diff --git a/client/src/components/game-modal.scss b/client/src/components/game-modal.scss new file mode 100644 index 0000000..494e6a6 --- /dev/null +++ b/client/src/components/game-modal.scss @@ -0,0 +1,48 @@ +@import 'compass/css3/border-radius'; +@import 'compass/css3/box-shadow'; +@import 'variables'; + +.game-modal { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + + .overlay { + background-color: rgba(darken($color-background, 10%), 0.9); + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 1; + } + + .content { + background-color: rgba($color-background, 0.9); + bottom: 0; + height: 200px; + left: 0; + margin: auto; + padding: 1em; + position: absolute; + right: 0; + text-align: center; + top: 0; + width: 400px; + z-index: 2; + + @include border-radius(10px); + @include box-shadow(5px 5px 20px rgba(black, 0.8)); + + .title { + font-size: 1.8em; + padding: 0.5em; + } + + .score { + padding: 0.5em; + } + } +} diff --git a/client/src/components/game.js b/client/src/components/game.js index 24e3157..a943e1c 100644 --- a/client/src/components/game.js +++ b/client/src/components/game.js @@ -1,4 +1,4 @@ -// View model's "this +// View model's "this" let vm = null // Websocket instance let ws = null @@ -6,7 +6,18 @@ let ws = null module.exports = require('vue').component('game', { data, methods: { - + addBot: function addBot() { + console.log('(local) adding bot!') + ws.send(JSON.stringify(['player:bot'])) + }, + start: function start() { + console.log('(local) starting game!') + ws.send(JSON.stringify(['game:start'])) + }, + stop: function stop() { + console.log('(local) stopping game!') + ws.send(JSON.stringify(['game:stop'])) + } }, mounted, template: require('./game.pug')() @@ -15,6 +26,28 @@ module.exports = require('vue').component('game', { function data() { return { activityLog: [], + opponents: [ + { + name: 'Kevin', + maxHp: 10, + hp: 10, + discard: [ + { + id: 'grab', + type: 'counter' + }, + { + type: 'unknown' + } + ] + }, + { + name: 'Jimbo', + maxHp: 10, + hp: 12, + discard: [] + } + ], player: { hand: [ { @@ -32,50 +65,48 @@ function data() { id: 'a-gun', name: 'A Gun', type: 'unstoppable' - }, - { - type: 'unknown' } ], hp: 8, maxHp: 10 - } + }, + score: 12, + started: null, + stopped: null } } function mounted() { vm = this - ws = new WebSocket('ws://localhost:4000') + ws = new WebSocket(`ws://localhost:4000/${vm.$route.params.gameId}`) ws.onopen = onOpen ws.onmessage = onMessage } function onOpen() { - ws.send(JSON.stringify([ - 'player:join', - { - gameId: vm.$route.params.gameId, - player: { - name: 'Bob' + Math.floor(Math.random() * 1e3) - } + ws.send(JSON.stringify(['player:join', { + player: { + name: 'Bob' + Math.floor(Math.random() * 1e3) } - ])) + }])) } function onMessage({ data: msg }) { - const [code, message, payload] = JSON.parse(msg) + const [code, payload] = JSON.parse(msg) const codes = { - 'player:joined': () => { - ws.send(JSON.stringify(['game:start'])) - }, - 'player:status': () => { - console.log(vm) - vm.player.hand = payload.player.hand - } + 'game:no-survivors': () => (vm.stopped = payload.stopped), + 'game:started': () => (vm.started = payload.started), + 'game:stopped': () => (vm.stopped = payload.stopped), + 'game:winner': () => (vm.stopped = payload.stopped) + } + + if (payload.player) { + vm.player.hand = payload.player.hand } + if (codes[code]) { codes[code]() } - console.log(code, message, payload) + console.log(code, payload) } diff --git a/client/src/components/game.pug b/client/src/components/game.pug index 2ad5e19..c316e54 100644 --- a/client/src/components/game.pug +++ b/client/src/components/game.pug @@ -1,3 +1,10 @@ div - p game page/component + game-modal(:score="score", v-if="stopped") + game-info(:score="score") + div(v-for="opponent in opponents") + p {{ opponent.name }} + opponent-discard(:discard="opponent.discard") player-hand(:hand="player.hand") + button(@click="start", type="button", v-if="!started") Start + button(@click="stop", type="button", v-if="started && !stopped") Stop + button(@click="addBot", type="button") Add bot diff --git a/client/src/components/home.js b/client/src/components/home.js index 098741a..4d50a47 100644 --- a/client/src/components/home.js +++ b/client/src/components/home.js @@ -1,3 +1,11 @@ module.exports = require('vue').component('home', { + methods: { + randomGameName + }, template: require('./home.pug')() }) + +// Guaranteed random +function randomGameName() { + return '511fax' +} diff --git a/client/src/components/home.pug b/client/src/components/home.pug index de34eb0..b877177 100644 --- a/client/src/components/home.pug +++ b/client/src/components/home.pug @@ -1,3 +1,4 @@ div p This is the main page (written in pug) - p Create a new game + router-link(:to="{ name: 'game', params: { gameId: randomGameName() }}") + button.uk-button.uk-button-default(type="button") Create a new game diff --git a/client/src/components/opponent-card.js b/client/src/components/opponent-card.js new file mode 100644 index 0000000..cc8cd04 --- /dev/null +++ b/client/src/components/opponent-card.js @@ -0,0 +1,8 @@ +require('./opponent-card.scss') + +module.exports = require('vue').component('opponent-card', { + props: { + card: Object + }, + template: require('./opponent-card.pug')() +}) diff --git a/client/src/components/opponent-card.pug b/client/src/components/opponent-card.pug new file mode 100644 index 0000000..f5c13db --- /dev/null +++ b/client/src/components/opponent-card.pug @@ -0,0 +1,5 @@ +.opponent-card(:class="{ flipped: card.name }") + .back + .front + p {{ card.name }} + p {{ card.type.toUpperCase() }} diff --git a/client/src/components/opponent-card.scss b/client/src/components/opponent-card.scss new file mode 100644 index 0000000..aad1a31 --- /dev/null +++ b/client/src/components/opponent-card.scss @@ -0,0 +1,57 @@ +@import 'compass/css3/transform'; +@import 'compass/css3/transition'; +@import 'variables'; + +.opponent-card { + display: inline-block; + height: 150px; + margin: 1em 2em; + position: relative; + width: 100px; + + @include transition(opacity 0.5s); + + .front, .back { + background-color: $color-card-background; + border-radius: 5px; + bottom: 0; + color: $color-text-dark; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + width: 100%; + + @include backface-visibility(hidden); + @include transform(translateZ(0)); + @include transition(transform 0.6s); + @include transform-style(preserve-3d); + } + + + .back { + background-image: url('../img/card_backside.jpg'); + background-position: center; + background-repeat: no-repeat; + background-size: 100%; + font-size: 12px; + } + + .front { + @include transform(rotateY(-180deg)); + background-position: center; + background-repeat: no-repeat; + background-size: 90%; + } + + &.flipped { + .back { + @include transform(rotateY(180deg)); + } + + .front { + @include transform(rotateY(0deg)); + } + } +} diff --git a/client/src/components/opponent-discard.js b/client/src/components/opponent-discard.js new file mode 100644 index 0000000..44a818b --- /dev/null +++ b/client/src/components/opponent-discard.js @@ -0,0 +1,6 @@ +module.exports = require('vue').component('opponent-discard', { + props: { + discard: Array + }, + template: require('./opponent-discard.pug')() +}) diff --git a/client/src/components/opponent-discard.pug b/client/src/components/opponent-discard.pug new file mode 100644 index 0000000..787bd6c --- /dev/null +++ b/client/src/components/opponent-discard.pug @@ -0,0 +1,5 @@ +div + opponent-card( + :card="card", + v-for="card in discard", + :key="card.id") diff --git a/client/src/components/player-card.pug b/client/src/components/player-card.pug index 717b70a..8d68aef 100644 --- a/client/src/components/player-card.pug +++ b/client/src/components/player-card.pug @@ -1,5 +1,4 @@ -.card(:class="{ flipped: card.name, disabled: card.disabled }") - .back +.player-card(:class="{ disabled: card.disabled }") .front p {{ card.name }} p {{ card.type.toUpperCase() }} diff --git a/client/src/components/player-card.scss b/client/src/components/player-card.scss index db9c9a9..fdf7452 100644 --- a/client/src/components/player-card.scss +++ b/client/src/components/player-card.scss @@ -1,7 +1,7 @@ -@import "compass"; -@import "variables"; +@import 'compass/css3/transition'; +@import 'variables'; -.card { +.player-card { display: inline-block; height: 150px; margin: 1em 2em; @@ -10,7 +10,7 @@ @include transition(opacity 0.5s); - .front, .back { + .front { background-color: $color-card-background; border-radius: 5px; bottom: 0; @@ -21,39 +21,14 @@ right: 0; top: 0; width: 100%; - - @include backface-visibility(hidden); - @include transform(translateZ(0)); - @include transition(transform 0.6s); - @include transform-style(preserve-3d); - } - - - .back { - background-image: url('../img/card_backside.jpg'); - background-position: center; - background-repeat: no-repeat; - background-size: 100%; - font-size: 12px; } .front { - @include transform(rotateY(-180deg)); background-position: center; background-repeat: no-repeat; background-size: 90%; } - &.flipped, &.disabled { - .back { - @include transform(rotateY(180deg)); - } - - .front { - @include transform(rotateY(0deg)); - } - } - &.disabled { opacity: 0.3; } diff --git a/client/src/components/player-hand.pug b/client/src/components/player-hand.pug index ab4259a..3e7b4ee 100644 --- a/client/src/components/player-hand.pug +++ b/client/src/components/player-hand.pug @@ -1,4 +1,6 @@ div p This is the player's hand - .cards(v-for="card in hand", :key="card.id") - player-card.card(:card="card", :disabled="false") + player-card( + :card="card", + :disabled="false", + v-for="card in hand") diff --git a/client/src/index.entry.js b/client/src/index.entry.js index 939395e..c5feccd 100644 --- a/client/src/index.entry.js +++ b/client/src/index.entry.js @@ -23,12 +23,12 @@ const routerConfig = new vueRouter({ mode: 'hash', routes: [ { - name: 'Create a game', + name: 'home', path: '/', component: require('./components/home') }, { - name: 'Game in progress...', + name: 'game', path: '/games/:gameId', component: require('./components/game') } diff --git a/client/src/index.html b/client/src/index.html index 7981c43..9e6e403 100755 --- a/client/src/index.html +++ b/client/src/index.html @@ -10,7 +10,7 @@ /> - +
diff --git a/client/src/modules/event-bus.js b/client/src/modules/event-bus.js new file mode 100644 index 0000000..714ee63 --- /dev/null +++ b/client/src/modules/event-bus.js @@ -0,0 +1,30 @@ +// Generic event (callback) handler for inter-component messaging + +class EventBus { + + constructor() { + this.callbacks = {} + } + + // Invoke an array of callback for a certain callback key + emit(key, data) { + // Stop execution if false is explicitly returned by a callback + (this.callbacks[key] || []).reduce((acc, callback) => { + return acc && (callback(data) !== false) + }, true) + } + + // Register a new callback with a specific callback key + on(key, callback) { + this.callbacks[key] = this.callbacks[key] || [] + this.callbacks[key].push(callback) + } + + // Un-register an existing callback + remove(key, callback) { + this.callbacks[key] = (this.callbacks[key] || []).filter(cb => cb !== callback) + } + +} + +module.exports = new EventBus() diff --git a/client/src/stylesheets/main.scss b/client/src/stylesheets/main.scss index 399b0bf..ae82f35 100755 --- a/client/src/stylesheets/main.scss +++ b/client/src/stylesheets/main.scss @@ -1,6 +1,10 @@ -@import "compass"; -@import "variables"; -@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,900|Dosis:300,400,600,700,800|Droid+Sans:400,700|Lato:300,400,700,900|PT+Sans:400,700|Ubuntu:300,400,500,700|Open+Sans:400,300,600,700|Roboto:400,300,500,700,900|Roboto+Condensed:400,300,700|Open+Sans+Condensed:300,700|Work+Sans:400,300,700|Play:400,700|Maven+Pro:400,500,700,900&subset=latin,latin-ext"); +@import 'uikit/src/scss/variables'; +@import 'uikit/src/scss/uikit'; +@import 'uikit/src/scss/components/button'; +@import 'uikit/src/scss/components/inverse'; +@import 'compass/css3/box-sizing'; +@import 'variables'; +@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,900|Dosis:300,400,600,700,800|Droid+Sans:400,700|Lato:300,400,700,900|PT+Sans:400,700|Ubuntu:300,400,500,700|Open+Sans:400,300,600,700|Roboto:400,300,500,700,900|Roboto+Condensed:400,300,700|Open+Sans+Condensed:300,700|Work+Sans:400,300,700|Play:400,700|Maven+Pro:400,500,700,900&subset=latin,latin-ext'); * { @include box-sizing(border-box); @@ -17,91 +21,6 @@ html { font-weight: 400; } -@mixin clearfix() { - &:before, - &:after { - content: ""; - display: table; - } - &:after { - clear: both; - } -} - - #app { margin: 2em; } - -.info { - border-bottom: 1px solid #555; - padding-bottom: 1em; - text-align: center; - - > div { - display: inline-block; - width: 200px; - - .label { - margin-right: 5px; - } - - .value { - font-weight: bold; - } - } -} - -.splash { - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; - - .overlay { - background-color: rgba(#000, 0.6); - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; - } - - .content { - background-color: rgba(#333, 0.9); - bottom: 0; - height: 200px; - left: 0; - margin: auto; - padding: 1em; - position: absolute; - right: 0; - text-align: center; - top: 0; - width: 400px; - - @include border-radius(10px); - @include box-shadow(5px 5px 20px rgba(black, 0.8)); - - .title { - font-size: 1.8em; - padding: 0.5em; - } - - .score { - padding: 0.5em; - } - - button { - background-color: #444; - border: 1px solid #555; - color: white; - font-size: 1.4em; - margin-top: 1em; - padding: 5px 20px; - - @include border-radius(4px); - } - } -} diff --git a/client/webpack.config.js b/client/webpack.config.js index b511363..d83e819 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -86,7 +86,6 @@ module.exports = { modules: false, // Polyfills are only needed for the following targets targets: { - // This is our advertised list of supported browsers browsers: [ 'last 2 Chrome versions', 'last 2 Firefox versions', @@ -130,7 +129,8 @@ module.exports = { // inside the directories listed below from first to last. includePaths: [ path.resolve(__dirname, 'src', 'stylesheets'), - path.resolve(__dirname, 'node_modules', 'compass-mixins', 'lib') + path.resolve(__dirname, 'node_modules', 'compass-mixins', 'lib'), + path.resolve(__dirname, 'node_modules') ], sourceMap: true } diff --git a/server/index.js b/server/index.js index 2337d56..a3c5e0e 100644 --- a/server/index.js +++ b/server/index.js @@ -1,18 +1,30 @@ -const JunkyardBrawl = require('junkyard-brawl') -const util = require('./util') +const uuid = require('uuid/v4') +const { getGame, setSocket } = require('./state') const validator = require('./validator') const WebSocket = require('ws') const wss = new WebSocket.Server({ port: 4000 }) -// Object to store game instances in -const games = {} wss.on('connection', connection) -function connection(ws) { +function connection(ws, req) { // Keep track of which game the client is referencing - let id = null + ws.gameId = req.url.replace(/^\//, '') + if (!ws.gameId) { + ws.send(JSON.stringify([ + 'error', + 'No game ID passed on connection: "example.com/gameid"' + ])) + ws.terminate() + return + } + // Matching player ID and socket ID + ws.id = uuid() + setSocket(ws.id, ws) + ws.on('message', onMessage) + ws.on('error', removePlayer) + ws.on('close', removePlayer) function onMessage(msg) { console.log(typeof msg, msg) @@ -24,13 +36,15 @@ function connection(ws) { return } - if (!Array.isArray(parsedMsg)) { + if (!Array.isArray(parsedMsg) || !parsedMsg.length) { return } const [code, payload] = parsedMsg const codes = { - 'game:start': startGame, - 'player:join': addPlayer + 'game:start': require('./responses/game-start'), + 'player:bot': require('./responses/player-bot'), + 'player:join': require('./responses/player-join'), + 'player:language': require('./responses/player-language') } if (codes[code]) { const validation = validator(code, payload) @@ -38,58 +52,16 @@ function connection(ws) { ws.send(JSON.stringify(['error', validation.errors])) return } - codes[code](payload) - } - } - - function addPlayer({ player, gameId }) { - id = gameId - const game = games[id] - if (game) { - game.addPlayer(ws, player.name) - return - } - games[id] = new JunkyardBrawl( - ws, - player.name, - generateAnnounceCallback(id), - generateWhisperCallback(id) - ) - games[id].announce('game:created') - } - - function generateAnnounceCallback(gameId) { - return (code, message) => { - const game = games[gameId] - if (!game) { - return - } - game.players.forEach(player => player.id.send(JSON.stringify([code, message]))) - console.log(` >> ${code} ${message}`) - } - } - - function generateWhisperCallback(gameId) { - return (playerId, code, message, messageProps) => { - const game = games[gameId] - if (!game) { - return - } - playerId.send(JSON.stringify([code, message, { - player: util.scrubPlayer(messageProps.player) - }])) - console.log(` >> ${messageProps.player.name}: ${code} ${message}`) + codes[code](ws, payload) } } - function startGame() { - const game = games[id] + function removePlayer() { + const game = getGame(ws.gameId) if (game) { - game.start() - game.players.forEach((player) => { - console.log(`gonna whisper status to ${player.name}`) - game.whisperStatus(player) - }) + game.removePlayer(ws.id) + // Mark the socket for garbage collection + setSocket(ws.id, null) } } diff --git a/server/package.json b/server/package.json index 27ac964..6fbdb02 100644 --- a/server/package.json +++ b/server/package.json @@ -20,7 +20,8 @@ "test": "npm run eslint" }, "dependencies": { - "junkyard-brawl": "https://github.com/gfax/junkyard-brawl.git", + "junkyard-brawl": "^0.3.0", + "uuid": "^3.2.1", "validatorjs": "^3.13.5", "ws": "^3.2.0" }, diff --git a/server/responses/game-start.js b/server/responses/game-start.js new file mode 100644 index 0000000..149ef02 --- /dev/null +++ b/server/responses/game-start.js @@ -0,0 +1,16 @@ +const { getGame } = require('../state') + +module.exports = (socket) => { + const game = getGame(socket.gameId) + if (game) { + game.start() + game.players.forEach((player) => { + if (!player.robot) { + console.log(`gonna whisper status to ${player.name}`) + game.whisperStatus(player) + } + }) + } +} + +module.exports.validator = {} diff --git a/server/responses/player-bot.js b/server/responses/player-bot.js new file mode 100644 index 0000000..fc40b7a --- /dev/null +++ b/server/responses/player-bot.js @@ -0,0 +1,8 @@ +const { getGame } = require('../state') + +module.exports = (socket) => { + const game = getGame(socket.gameId) + if (game) { + game.addBot('Dark') + } +} diff --git a/server/responses/player-join.js b/server/responses/player-join.js new file mode 100644 index 0000000..2a6b12f --- /dev/null +++ b/server/responses/player-join.js @@ -0,0 +1,83 @@ +const JunkyardBrawl = require('junkyard-brawl') +const { getPhrase } = require('junkyard-brawl/src/language') +const { getGame, getSocket, setGame, setSocket } = require('../state') +const { scrubGameData, scrubPlayerData } = require('../util') + +module.exports = (socket, { player }) => { + let game = getGame(socket.gameId) + if (game) { + game.addPlayer(socket.id, player.name) + return + } + game = new JunkyardBrawl( + socket.id, + player.name, + generateAnnounceCallback(socket), + generateWhisperCallback(socket) + ) + game.id = socket.gameId + setGame(socket.gameId, game) + game.announce('game:created') +} + +module.exports.validator = { + 'player.name': 'required|string' +} + +function generateAnnounceCallback(socket) { + return (code, message, messageProps) => { + const game = getGame(socket.gameId) + if (!game) { + return + } + game.players.forEach((player) => { + if (!player.robot) { + const playerSocket = getSocket(player.id) + if (playerSocket) { + let newMessage = null + try { + newMessage = getPhrase(code, (playerSocket.language || game.language))(messageProps) + } catch (err) {} + + playerSocket.send(JSON.stringify([code, { + game: scrubGameData(game), + message: newMessage || message, + // Send updated personal info + player: scrubPlayerData(player) + }])) + } + } + }) + if (game.stopped) { + game.players.forEach((player) => { + const playerSocket = getSocket(player.id) + if (playerSocket) { + playerSocket.terminate() + // Mark the socket for garbage collection + setSocket(player.id, null) + } + }) + // Mark the game for garbage collection + setGame(game.id, null) + } + console.log(` >> ${code} ${message}`) + } +} + +function generateWhisperCallback(socket) { + return (playerId, code, message, messageProps) => { + const game = getGame(socket.gameId) + if (!game) { + return + } + const playerSocket = getSocket(playerId) + if (playerSocket) { + playerSocket.send(JSON.stringify(code, { + game: scrubGameData(game), + message: getPhrase(code, (playerSocket.language || game.language))(messageProps), + player: scrubPlayerData(messageProps.player) + })) + } + console.log(` >> ${messageProps.player.name}: ${code} ${message}`) + } +} diff --git a/server/responses/player-language.js b/server/responses/player-language.js new file mode 100644 index 0000000..f3ee9d4 --- /dev/null +++ b/server/responses/player-language.js @@ -0,0 +1,13 @@ +// Set the player's language +const { getSupportedLanguages } = require('junkyard-brawl/src/language') + +module.exports = (socket, { language }) => { + socket.language = language +} + +module.exports.validator = (value) => { + return getSupportedLanguages().find(lang => lang === value) +} + +const languages = getSupportedLanguages().join(', ') +module.exports.validatorMessage = `Expected a supported language: ${languages}` diff --git a/server/state.js b/server/state.js new file mode 100644 index 0000000..f868d92 --- /dev/null +++ b/server/state.js @@ -0,0 +1,38 @@ +// Object to store game instances in +const games = {} +const sockets = {} + +module.exports = { + getGame, + getSocket, + setGame, + setSocket +} + +function getGame(id) { + return games[id] +} + +function getSocket(id) { + return sockets[id] +} + +function setGame(id, game) { + if (!id) { + throw new Error('Could not set game. No ID given.') + } + if (game === undefined) { + throw new Error('Could not set game. No game given.') + } + games[id] = game || undefined +} + +function setSocket(id, socket) { + if (!id) { + throw new Error('Could not set socket. No ID given.') + } + if (socket === undefined) { + throw new Error('Could not set socket. No socket given.') + } + sockets[id] = socket || undefined +} diff --git a/server/util.js b/server/util.js index a1531dc..20d7a05 100644 --- a/server/util.js +++ b/server/util.js @@ -1,20 +1,70 @@ +const { getPhrase } = require('junkyard-brawl/src/language') +const { getGame, getSocket } = require('./state') + module.exports = { - scrubPlayer + scrubGameData, + scrubOpponentData, + scrubPlayerData } -// To slim the bandwidth, we need to return only -// the very basics over the network connection. -function scrubPlayer(player) { +// We need to remove circular references and return +// only the data needing to be sent over the socket. +function scrubGameData(game) { + if (!game) { + return game + } return { - hand: scrubHand(player.hand) + manager: { + id: game.manager.id, + name: game.manager.name + }, + dropouts: game.dropouts.map(scrubOpponentData), + players: game.players.map(scrubOpponentData), + started: game.started ? game.started.valueOf() : false, + stopped: game.stopped ? game.stopped.valueOf() : false, + turns: game.turns + } +} + +// We can know everything about the player but what is in their hand +function scrubOpponentData(player) { + if (!player) { + return player } + return { + id: player.id, + conditionCards: scrubCards(player.conditionCards), + discard: scrubCards(player.discard), + extraTurns: player.extraTurns, + name: player.name, + hand: player.hand.map(card => ({ type: 'unknown' })), + hp: player.hp, + maxHand: player.maxHand, + maxHp: player.maxHp, + missTurns: player.missTurns, + score: 12, + turns: player.turns + } +} + +// Re-use the data we get from scrubbing an opponent but also hand cards +function scrubPlayerData(player) { + if (!player) { + return player + } + const playerSocket = getSocket(player.id) + const game = getGame(playerSocket.gameId) + const language = playerSocket.language || game.language + return Object.assign(scrubOpponentData(player), { + hand: scrubCards(player.hand, language) + }) } -function scrubHand(hand) { - return hand.map((card) => { +function scrubCards(cards, language) { + return cards.map((card) => { return { id: card.id, - name: card.id, + name: getPhrase(`card:${card.id}`, language)(), type: card.type } }) diff --git a/server/validator.js b/server/validator.js index c788c51..ffaf4d1 100644 --- a/server/validator.js +++ b/server/validator.js @@ -1,16 +1,6 @@ const ValidatorJs = require('validatorjs') -const rules = { - 'game:start': {}, - 'player:join': { - gameId: 'required|string', - 'player.name': 'required|string' - } -} - module.exports = (code, payload) => { - if (!rules[code]) { - throw new Error(`Received undefined code: ${code}`) - } - return new ValidatorJs(payload, rules[code]) + const responseModule = require('./responses/' + code.replace(':', '-')) + return new ValidatorJs(payload, responseModule.validator || {}, responseModule.validatorMessage) }