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