In this tutorial we're going to create a simple online multiplayer game on Derby.js
The game
The professor creates a question: “Guess the number of jelly beans in the jar.” Everyone has to enter. Once the professor closes the game, the average is displayed to everyone. No more people can enter the game. People can enter the game without a login.
Generating project
Create a project using generator-derby
:
$ sudo install -g yo
$ sudo install -g generator-derby
$ mkdir my-app
$ cd my-app
$ yo derby --coffee
$ npm start
Creating game
We want to create a game which has a name and a question in it. Create two inputs and a ‘Create new game!’ button, wrapped in a form which has a ‘post’ method.
home.jade
form(method='post', action='/create_game')
p Type the name of the game:
input(type='text', name='name')
p Type the question:
input(type='text', name='question')
br
br
button Create new game!
Write a controller that handles /create_game
action.
index.coffee
app.post '/create_game', (page, model, params) ->
model.add 'games', {
name: params.body.name
question: params.body.question
players: {}
userIds: []
}, ->
page.redirect '/'
Here we create a game: add a new games
document with a name
, a question
, an empty object with players
and an array with userIds
(we need them to easily loop through all players in the game). model.add also generates an id for each game. As for form variables (name
, question
), we get them from params.body
in the controller. We redirect to /
page, because we want to stay on the same page after creating the game. Let’s display the games on the home page. Each game will be a link.
List of games
To be able to loop through the games in games
collection (in order to display the list of games) we need to create a local db out of games
, convert it to an array and make a reference in private _page
collection.
index.coffee
app.get '/', (page, model) ->
model.subscribe 'games', ->
model.at('games').filter().ref '_page.games'
page.render 'home'
We are subscribing for games
collection to make it accessible on /
page. model.at
creates a local games
db,filter()
converts games
to an array and ref()
creates a reference from this array. Now we can loop through the array of games in views:
home.jade
p Games:
each #root._page.games as #game
a(href='/game/{{#game.id}}') {{#game.name}}
We are displaying each game as a link which will redirect us to the game page. id and name are the fields in game object. Let’s add a game.
Deleting games
To delete all the games add a ‘Delete all games!’ input.
home.jade
input(on-click='deleteGames()', type='submit',
value='Delete all games!')
When clicking it, deleteGames
function is triggered.
index.coffee
app.proto.deleteGames = ->
@model.subscribe 'games', =>
games = @model.get 'games'
for key of games
@model.del "games.#{key}"
We need to loop through games deleting each game from games
collection. It is necessary to subscribe for games
and set it into a variable. for of
loop goes through each game and model.del
deletes each game. Notice that in the last line we used double quotes. String in double quotes supports interpolation. In this case, we interpolate key, that is game id.
Adding player’s name
Let’s add player’s name.
home.jade
p Player's name:
form(on-submit='setUserName()')
input(type='text', value='{{#root._page.userName}}'')
br
br
button(type=’submit’) Save!
We are binding the value of the input. Submitting the form triggers setUserName
function.
index.coffee
app.proto.setUserName = ->
userId = this.model.get '_session.userId'
userName = this.model.get '_page.userName'
if userName?
userName = userName.trim()
if userName
this.model.set 'users.' + userId + '.userName', userName
this.model.del '_page.userName'
else
alert 'Name cannot be empty!'
We get userName
from _page.userName
, trim and set it to users
collection. If an empty input is submitted we show "Name cannot be empty!" alert message Also, let’s greet the player. To do this, we subscribe for current user (not for players, because name is stored in users
collection). We also need to get userId, which is stored in private collection_session.userId
and set it in a variable to be able to subscribe for current user.
index.coffee
app.get '/', (page, model) ->
userId = model.get ‘_session.userId’
model.subscribe 'games', ‘users.’ + userId, ->
model.ref ‘_page.user’, ‘users.’ + userId
model.at('games').filter().ref '_page.games'
page.render 'home'
There is a reference (model.ref
) on the current user and now we can check in views if the user has name. If he does, the greeting appears on the page.
p Player's name:
form(on-submit='setUserName()')
input(type='text', value='{{#root._page.userName}}'')
br
br
button(type=’submit’) Save!
if #root._page.user.userName
p Hello, {{#root.users[#root._session.userId].userName}}
Game page
Add game.jade
file to /views/app
and import it to index.jade
game.jade
index.jade
import:(src='./home')
import:(src='./game')
Title:
| {{_page.title}}
Body:
view(name='header')
view(name='{{$render.ns}}')
view(name='footer', year='2014')
header:
h1 Header
footer:
p Footer {{@year}}
Create a controller that handles game page.
index.coffee
app.get '/game/:gameId', (page, model, params) ->
page.render ‘game’
Display the question
We want to display the question in the game. To do that, subscribe for the game (and make a reference for easy access), set the game id and make a reference, so that we can access the game id in views.
app.get '/game/:gameId', (page, model, params) ->
model.subscribe ‘games.’ + params.gameId, ->
model.set ‘_page.gameId’, params.gameId
page.render ‘game’
Here comes the views.
game.jade
index:
|+ p {{#root.games[#root._page.gameId].question}}
Add input for the answer
game.jade
index:
p {{#root.games[#root._page.gameId].question}}
|+ form(on-submit='setAnswer()')
|+ input(type='text', value='{{#root._page.answer}}')
|+ br
|+ button Save
Let’s subscribe for the users in the game (because we want to display players in the game) and make references to players, current user, current game and current player. To subscribe for users in the game, we need to make a query request.
app.get '/game/:gameId', (page, model, params) ->
model.subscribe ‘games.’ + params.gameId, ->
model.set ‘_page.gameId’, params.gameId
|+ usersInGame = model.query 'users', 'games.' + params.gameId +
|+ '.userIds'
|+ userId = model.get '_session.userId'
|+ model.subscribe usersInGame, 'users.' + userId, ->
|+ model.set '_page.gameId', params.gameId
|+ model.ref '_page.user', 'users.' + userId
|+ model.ref '_page.game', 'games.' + params.gameId
|+ model.ref '_page.players', 'games.' + params.gameId + '.players'
|+ model.ref '_page.player', 'games.' + params.gameId +
|+ '.players.' + userId
page.render 'game'
Add player to the game
index.coffee
app.get '/game/:gameId', (page, model, params) ->
model.subscribe ‘games.’ + params.gameId, ->
model.set '_page.gameId', params.gameId
usersInGame = model.query 'users', 'games.' + params.gameId +
'.userIds'
userId = model.get '_session.userId'
model.subscribe usersInGame, 'users.' + userId, ->
model.set '_page.gameId', params.gameId
model.ref '_page.user', 'users.' + userId
model.ref '_page.game', 'games.' + params.gameId
model.ref '_page.players', 'games.' + params.gameId + '.players'
model.ref '_page.player', 'games.' + params.gameId +
'.players.' + userId
|+ unless model.get '_page.player'
|+ model.add '_page.players', { id: userId }
|+ model.push '_page.game.userIds', userId
page.render 'game'
When opening the game page, the player is added to the game, unless he is already there. model.add adds a new document with random id. It is possible to specify an id. We want players’ ids to be the same as users’ ids, that is why we set id as userId. When the player is added, his id is saved into game’s userIds array.
Save the answer
index.coffee
app.proto.setAnswer = ->
answer = this.model.get '_page.answer'
unless answer?
answer = ""
answer = parseInt( answer.trim() )
if isNaN( answer )
alert 'The answer must be a number!'
else
this.model.set '_page.player.answer', answer
this.model.del '_page.answer'
We are get the answer from _page.answer
and set it to answer, checking whether it is not undefined
or null
and parsing it to integer. If the answer is NaN
, we alert "The answer must be a number!". Otherwise the answer is set to games
collection.
Since we’ve made a reference, we don’t need to write the full path ('games.' + params.gameId + '.players.' + userId
) — we are setting the answer to _page.player.answer
. We also want to display the answer on the game page. When the player changes his mind and enters a different guess, the answer on the page will be changed instantly. The answer should be displayed only if there is one, so we make an if-statement checking if there is an answer.
game.jade
if #root._page.player.answer
p Your answer is {{#root._page.player.answer}}
List of players in the game
Let’s display all players in the game.
game.jade
p Players in the game
each #root._page.game.userIds as #userId
p {{#root.users[#userId].userName}}
Giving control to prof
Only the professor should be able to create a game, not the students. Add an input (type checkbox). If it is checked, prof
field (this field is created in _page.user
when the checkbox is checked) becomes true
. Only when it is true
the game can be created.
home.jade
index:
label
input(type='checkbox', checked='{{#root._page.user.prof}}')
| Professor
if #root._page.user.prof
form(method='post', action='/create_game')
p Type the name of the game:
input(type='text', name='name')
p Type the question:
input(type='text', name='question')
br
br
button Create new game!
Besides, only the professor should be able to finish the game. We have already subscribed for the current user and made a reference (_page.user
).
Write an if
-statement which will check if the user is the professor. If he is, then the "Finish the game!" button will be displayed.
game.jade
if #root._page.user.prof
input(on-click='finishGame()', type='submit',
value='Finish the game!')
Finish the game
Now let’s write the controller that handles finishing the game. The aim here is to collect the answers, strike an average and finish the game. To collect the answers, use the for of
loop. The controller sets _page.game.finish
to true
.
index.coffee
app.proto.finishGame = ->
players = this.model.get '_page.players'
average = 0
for key, value of players
average += value.answer
numberOfPlayers = this.model.get('_page.game.userIds').length
average = average / numberOfPlayers
this.model.set '_page.game.average', average
this.model.set '_page.game.finish', true
No going back
When the game is finished by professor, we want all the players inside the game to automatically go to the results page. To do that, we need to create a component from the view we render. For convenience, the name of the component should match the name of the template it’s associated with.
We use component’s create
method to execute code right after this component (in our case it’s the whole page) has been rendered. When the professor clicks "Finish the game!", _page.game.finish
becomes true
. So we want to listen for_page.game.finish
variable changes. And if the game is finished, we want players to be redirected to the page with the results.
index.coffee
app.component 'game', class Game
create: ->
gameId = this.model.root.get '_page.gameId'
this.model.root.on 'change', '_page.game.finish', (value) ->
app.history.replace '/game/' + gameId + '/results'
New players shouldn’t be able to enter the game if it has been finished, so when going to the game page, we are checking whether the game is finished or not.
Results
index.coffee
app.get '/game/:gameId', (page, model, params) ->
|+ model.subscribe 'games.' + params.gameId, ->
|+ if model.get 'games.' + params.gameId + '.finish'
|+ page.redirect '/game/' + params.gameId + '/results'
|+ return
Add results.jade
file to /views/app
and import it to index.jade
:
result.jade
index.jade
Here is the controller that handles results page.
index.coffee
app.get '/game/:gameId/results', (page, model, params) ->
model.subscribe 'games.' + params.gameId, ->
model.ref '_page.average', 'games.' + params.gameId + '.average'
page.render 'results'
We subscribe for the game and make a reference to the average, which is stored in games
collection. And, finally, display the results on the results page.
results.jade
index:
p The average is {{#root._page.average}}
Final code
index.coffee
derby = require 'derby'
app = module.exports = derby.createApp 'app', __filename
global.app = app unless derby.util.isProduction
app.serverUse module, 'derby-jade'
app.serverUse module, 'derby-stylus'
app.loadViews __dirname + '/../../views/app'
app.loadStyles __dirname + '/../../styles/app'
app.get '/', (page, model) ->
userId = model.get '_session.userId'
model.subscribe 'games', 'users.' + userId, ->
model.ref '_page.user', 'users.' + userId
model.at('games').filter().ref '_page.games'
page.render 'home'
app.post '/create_game', (page, model, params) ->
model.add 'games', {
name: params.body.name
question: params.body.question
players: {}
# to have access to all players in the game
userIds: []
}, ->
page.redirect '/'
app.get '/game/:gameId', (page, model, params) ->
model.subscribe 'games.' + params.gameId, ->
if model.get 'games.' + params.gameId + '.finish'
page.redirect '/game/' + params.gameId + '/results'
return
model.set '_page.gameId', params.gameId
usersInGame = model.query 'users', 'games.' + params.gameId +
'.userIds'
userId = model.get '_session.userId'
model.subscribe usersInGame, 'users.' + userId, ->
userId = model.get '_session.userId'
model.set '_page.gameId', params.gameId
model.ref '_page.user', 'users.' + userId
model.ref '_page.game', 'games.' + params.gameId
model.ref '_page.players', 'games.' + params.gameId + '.players'
model.ref '_page.player', 'games.' + params.gameId +
'.players.' + userId
unless model.get '_page.player'
model.add '_page.players', { id: userId }
model.push '_page.game.userIds', userId
page.render 'game'
app.get '/game/:gameId/results', (page, model, params) ->
model.subscribe 'games.' + params.gameId, ->
model.ref '_page.average', 'games.' + params.gameId + '.average'
page.render 'results'
app.proto.deleteGames = ->
@model.subscribe 'games', =>
games = @model.get 'games'
for key of games
@model.del "games.#{key}"
app.proto.setUserName = ->
userId = this.model.get '_session.userId'
userName = this.model.get '_page.userName'
if userName?
userName = userName.trim()
if userName
this.model.set 'users.' + userId + '.userName', userName
this.model.del '_page.userName'
else
alert 'Name cannot be empty!'
app.proto.setAnswer = ->
answer = this.model.get '_page.answer'
unless answer?
answer = ""
console.log answer
answer = parseInt( answer.trim() )
if isNaN( answer )
alert 'The answer must be a number!'
else
this.model.set '_page.player.answer', answer
this.model.del '_page.answer'
app.proto.finishGame = ->
players = this.model.get '_page.players'
average = 0
for key, value of players
average += value.answer
numberOfPlayers = this.model.get('_page.game.userIds').length
average = average / numberOfPlayers
this.model.set '_page.game.average', average
this.model.set '_page.game.finish', true
app.component 'game', class Game
create: ->
gameId = this.model.root.get '_page.gameId'
this.model.root.on 'change', '_page.game.finish', (value) ->
app.history.replace '/game/' + gameId + '/results'
home.jade
index:
label
input(type='checkbox', checked='{{#root._page.user.prof}}')
| Professor
if #root._page.user.prof
form(method='post', action='/create_game')
p Type the name of the game:
input(type='text', name='name')
p Type the question:
input(type='text', name='question')
br
br
button Create new game!
p Player's name:
form(on-submit='setUserName()')
input(type='text', value='{{#root._page.userName}}', name='name')
br
br
input(type='submit', value='Save!')
if #root._page.user.userName
p Hello, {{#root.users[#root._session.userId].userName}}
p Games:
each #root._page.games as #game
a(href='/game/{{#game.id}}') {{#game.name}}
br
br
input(on-click='deleteGames()', type='submit',
value='Delete all games!')
game.jade
index:
p Game
p {{#root.games[#root._page.gameId].question}}
form(on-submit='setAnswer()')
input(type='text', value='{{#root._page.answer}}')
br
button Save
br
if #root._page.player.answer
p Your answer is {{#root._page.player.answer}}
p Players in the game
each #root._page.game.userIds as #userId
p {{#root.users[#userId].userName}}
if #root._page.user.prof
input(on-click='finishGame()', type='submit',
value='Finish the game!')
results.jade
index:
p The average is {{#root._page.average}}