emberpress.js | |
---|---|
EmberPressThis is a clone of the popular Letterpress game. It is meant to show off the client side powers of the EmberJS framework and was originally created for Toronto EmberJS meetup by Evil Trout. Mucho thanks go out Loren Brichter for the inspiration. Thanks to the EmberJS team for making an awesome framework that makes client side development fun. The dictionary was imported from EOWL. The icons are from Font Awesome. The complete source code can be found on Github. | |
EmberPress source code | /*global window, Ember, $, EmberPressDictionary, alert*/
(function () {
'use strict'; |
All Ember applications need to be an instance of | var EmberPress = window.EmberPress = Ember.Application.create({}); |
ModelsOur models are delcared as extensions of | |
Letter: A simple object to represent a letter on the board. | EmberPress.Letter = Ember.Object.extend({}); |
Word: A word that has been played in the game. | EmberPress.Word = Ember.Object.extend({}); |
Player: A player of the game. There will be two instances of this, for p1 and p2. | EmberPress.Player = Ember.Object.extend({ |
Players start with a score of 0. | score: 0, |
During a turn, all scores are automatically updated to show
how they will be affected should the player finished their
turn. We consider this the | possibleScore: function() { |
If it's a player's turn, their | var result = this.get('score');
if (this.get('isTurn')) {
result += this.get('board.score');
} else {
result -= this.get('board.stolenScore');
}
return result;
}.property('board.score', 'board.stolenScore', 'isTurn'), |
Is it this player's turn? | isTurn: function() {
return this.get('board.currentPlayer') === this;
}.property('board.currentPlayer'), |
When a turn finishes, | updateScore: function() {
this.set('score', this.get('possibleScore'));
}
}); |
Board: The current game board and all associated data. | EmberPress.Board = Ember.Object.extend({ |
The dimensions of the board. It's always square so we just need one size. | SIZE: 5, |
Start a new game on the board. | restart: function() { |
When a game begins, there is no winner. | this.set('winner', null); |
There are two players. We'll identify them as p1 and p2 | this.set('player1', EmberPress.Player.create({id: 'p1', board: this}));
this.set('player2', EmberPress.Player.create({id: 'p2', board: this})); |
The first turn always goes to p1 | this.set('currentPlayer', this.get('player1')); |
Clear the current word being built by the players. | this.clearWord(); |
| this.set('played', Ember.A()); |
| this.set('rows', Ember.A()); |
Assemble a board of random | var letterId = 0;
for(var j=0; j< this.SIZE; j += 1) {
var row = Ember.A();
for(var i=0; i<this.SIZE; i += 1) {
var letter = EmberPress.Letter.create({
id: letterId,
letter: String.fromCharCode(65 + Math.round(Math.random() * 25))
});
row.pushObject(letter);
letterId += 1;
}
this.rows.pushObject(row);
}
}, |
The game has history once at least one word has been played. We use this to determine whether to show the list of previous words at the bottom of the interface. | hasHistory: function() {
return this.get('played').length > 0;
}.property('played.@each'), |
Add a letter to the word being built. | addLetter: function(letter) {
this.get('word').pushObject(letter);
}, |
Remove a letter from the word being built. | removeLetter: function(letter) {
this.get('word').removeObject(letter);
}, |
Remove all letters from the word. | clearWord: function() {
this.set('word', Ember.A());
}, |
Switch to the next player's turn and clear the current word in progress. | nextTurn: function() {
this.clearWord();
this.set('currentPlayer', this.get('otherPlayer'));
}, |
Our current word is a collection of | wordAsString: function() {
var result = "";
this.get('word').forEach(function (letter) {
result += letter.get('letter');
});
return result;
}.property('word.@each'), |
| otherPlayer: function() {
if (this.get('currentPlayer') === this.get('player1')) return this.get('player2');
return this.get('player1');
}.property('currentPlayer'), |
If the current word is being made up of letters belonging to the other player, we consider them stolen. To correctly display the other player's possible score we need to calculate how many points have been stolen. fortified letters cannot be stolen. | stolenScore: function() {
var result = 0,
otherPlayer = this.get('otherPlayer');
this.get('word').forEach(function (letter) {
if (letter.get('owner') === otherPlayer && !letter.get('fortified')) {
result += 1;
}
});
return result;
}.property('word.@each'), |
The score of the current word. Each | score: function() {
var result = 0,
currentPlayer = this.get('currentPlayer');
this.get('word').forEach(function (letter) {
if ((letter.get('owner') !== currentPlayer) && (!letter.get('fortified'))) {
result += 1;
}
});
return result;
}.property('word.@each'), |
Finish the current game. | finishGame: function(resigned) { |
If a player resigned, the other player automatically wins. | if (resigned) {
this.set('winner', this.get('otherPlayer'));
} else { |
Otherwise, the winner is simply the player with the larger score. | var diff = this.get('player1.score') - this.get('player2.score');
if (diff > 0) {
this.set('winner', this.get('player1'));
} else if (diff < 0) {
this.set('winner', this.get('player2'));
}
}
}, |
Submit the current word in play. | submitWord: function() { |
We call | var currentPlayer = this.get('currentPlayer');
currentPlayer.updateScore();
this.get('otherPlayer').updateScore(); |
Give ownership of each | this.get('word').forEach(function (letter) { |
Change the color unless it's fortified | if (!letter.get('fortified')) {
letter.set('owner', currentPlayer);
}
}); |
We need to iterate through every | var boardFull = true;
for(var y=0; y<this.SIZE; y++) {
for (var x=0; x<this.SIZE; x++) {
var letter = this.rows[y][x];
var owner = letter.get('owner.id'); |
By default we remove fortification (it will be applied again if still valid.) | letter.set('fortified', false);
if (owner) { |
Check the NESW neighbors of the tile | if ((y > 0) && (this.rows[y-1][x].get('owner.id') != owner)) continue;
if ((y < this.SIZE-1) && (this.rows[y+1][x].get('owner.id') != owner)) continue;
if ((x > 0) && (this.rows[y][x-1].get('owner.id') != owner)) continue;
if ((x < this.SIZE-1) && (this.rows[y][x+1].get('owner.id') != owner)) continue; |
If all neighbours are the same colour, fortify it. | letter.set('fortified', true);
} else { |
If a single tile has no owner, we don't consider the board full. | boardFull = false;
}
}
} |
Add the word to the played list | this.get('played').addObject(EmberPress.Word.create({
value: this.get('wordAsString'),
playedBy: this.get('currentPlayer')
})); |
If the board is full, finish the game. | if (boardFull) {
this.finishGame(false);
} else { |
Otherwise, skip to the next player's turn. | this.nextTurn();
}
}
}); |
Controllers | |
ApplicationController: Handles controls at the application level. | EmberPress.ApplicationController = Ember.Controller.extend({ |
Whether the instructions are being displayed. | instructionsVisible: false, |
Toggle displaying the instructions. | toggleInstructions: function() {
this.toggleProperty('instructionsVisible');
}
}); |
BoardController: handles all interaction with the game board. | EmberPress.BoardController = Ember.ObjectController.extend({ |
By default, there is no game in progress. | inProgress: true, |
Do we want to show 'CLEAR' button? | showClearWord: function() { |
The word needs to have at least one letter to be cleared. | return this.get('word').length > 0;
}.property('word.@each'), |
Do we want to show the 'SUBMIT' button? | showSubmitWord: function() { |
Word needs to be at least 2 letters long | return (this.get('word').length > 1);
}.property('word.@each'), |
| resign: function() {
this.get('model').finishGame(true);
}, |
If we have a winner, the game is over | winnerChanged: function() {
if (this.get('winner')) {
this.set('inProgress', false);
}
}.observes('winner'), |
| submitWord: function() {
var w = this.get('wordAsString').toLowerCase(); |
First, we need to see if the word is in our game's dictionary. We use jQuery's handy $.inArray for this. | if ($.inArray(w, EmberPressDictionary) == -1) {
alert("Sorry, that word isn't in the dictionary");
return;
} |
Secondly, we need to consider whether that word has already
been played. We unfortunately have to use a | var unplayedWord = true;
this.get('played').forEach(function (word) {
if (word.get('value').toLowerCase().indexOf(w) === 0) {
alert("That word can't be played.");
unplayedWord = false;
return false;
}
});
if (!unplayedWord) return; |
Note that this turn wasn't skipped. | this.set('skipped', false); |
Finally, submit the word to the | this.get('model').submitWord();
}, |
When a user chooses to skip their turn. | skipTurn: function() {
if (this.get('skipped')) { |
If the previous player also skipped their turn, the game is now over. | this.get('model').finishGame();
} else { |
Otherwise, skip to the next turn. | this.set('skipped', true);
this.get('model').nextTurn();
}
}, |
When we want to start a new game on this board. | reset: function() {
this.set('skipped', false);
this.get('model').restart();
this.set('inProgress', true);
}
}); |
Views | |
BoardView: Used to render the board from a template. | EmberPress.BoardView = Ember.View.extend({templateName: 'board'}); |
LetterView: Represents a | EmberPress.LetterView = Ember.View.extend({
classNameBindings: [':letter', 'chosen', 'ownerClass', 'content.fortified'],
boardBinding: 'controller.content', |
Set the CSS class to be the id of the current | ownerClass: function() {
var owner = this.get('content.owner');
if (!owner) { return null; }
return owner.get('id');
}.property('content.owner'), |
No need for a template for one letter! | render: function(buffer) {
buffer.push(this.get('content.letter'));
}
}); |
WordLetterView: is the word being assembled. | EmberPress.WordLetterView = EmberPress.LetterView.extend({ |
If the player clicks a letter in the word, we remove it. | click: function() {
this.get('board').removeLetter(this.get('content'));
}
}); |
BoardLetterView: A | EmberPress.BoardLetterView = EmberPress.LetterView.extend({ |
Has this letter been chosen? | chosen: function() {
return this.get('board.word').findProperty('id', this.get('content.id')) ? true : false;
}.property('board.word.@each.id'), |
The player clicked on a letter, so we want to add it to our word. | click: function() {
if (this.get('chosen')) return;
this.get('board').addLetter(this.get('content'));
}
}); |
PlayerView: Render a player with their scores. | EmberPress.PlayerView = Ember.View.extend({
classNameBindings: [':player', 'content.id', 'content.isTurn'],
templateName: 'player'
}); |
Boilerplate below initializes the game. Routers make more sense when there is more than one URL :) | EmberPress.IndexRoute = Ember.Route.extend({
model: function() {
return EmberPress.Board.create();
},
setupController: function(controller, board) {
board.restart();
this.controllerFor('board').set('model', board);
},
renderTemplate: function() {
this.render('board');
}
});
}());
|