Skip to content

Commit

Permalink
New architecture for the blueprint code.
Browse files Browse the repository at this point in the history
User facing improvements:
- now animating the tile swap
- smoother animations by using Tweens

Code improvements:
- greatly reduced code duplication
- modular architecture with separate classes, each with its own responsibility
- removed recursive code calls, now all activity is always generated by user
  inputs and scheduled signals (AnimEvents), removed brittle state variables
  because of this
- abstracted away logical coordinates to screen coordinates mapping
  (CoordMapper)
- debug feature to let the user replace any tile (it also demonstrates how to
  check keyboard state)
- full jsdoc documentation

Removed features:
- currently removed score tracking as it was not being used; it's easy to add
  back if need be
  • Loading branch information
UncleZeiv committed Mar 19, 2015
1 parent 282e40d commit c4983cb
Show file tree
Hide file tree
Showing 9 changed files with 823 additions and 530 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
out
10 changes: 8 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
<meta charset="utf-8" />
<title>Match 3 Blueprint</title>
<link rel="stylesheet" type="text/css" href="assets/css/style.css" />

<!-- Libraries -->
<script src="lib/kiwi.js"></script>
<script src="lib/kiwipreloader.js"></script>

<!-- Main Kiwi States -->
<script type="text/javascript" src="src/classes/animevents.js"></script>
<script type="text/javascript" src="src/classes/board.js"></script>
<script type="text/javascript" src="src/classes/boardcoords.js"></script>
<script type="text/javascript" src="src/classes/coordmapper.js"></script>
<script type="text/javascript" src="src/classes/gamelogic.js"></script>
<script type="text/javascript" src="src/classes/tile.js"></script>
<script type="text/javascript" src="src/states/play.js"></script>
<script type="text/javascript" src="src/states/intro.js"></script>
<script type="text/javascript" src="src/states/loading.js"></script>
Expand All @@ -20,4 +26,4 @@
<div id="content" style="margin: 0 auto;"></div>

</body>
</html>
</html>
83 changes: 83 additions & 0 deletions src/classes/animevents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* A handler of animation events.
*
* Keeps track of currently ongoing animations and sends a signal to any
* registered entity when all the animations are done.
*
* @see onAnimationComplete
* @see onNextAnimationComplete
*
* @class
*/
function AnimEvents() {
this.animating = 0;
this.onNextAnimationCompleteCallbacks = [];
this.onAnimationCompleteCallbacks = [];
}

/**
* Whether any animations are ongoing. Obviously this only takes into account
* animations that are registered with this class.
*
* @see animationStarted()
* @see animationFinished()
*
* @returns {Boolean} whether any animation is ongoing
*/
AnimEvents.prototype.isAnimationOngoing = function() {
return this.animating !== 0;
}

/**
* Register the start of an animation.
*
* @see animationFinished
*/
AnimEvents.prototype.animationStarted = function() {
++this.animating;
}

/**
* Register the end of an animation.
*
* This event can trigger the execution of the registered callbacks.
*
* @see onAnimationComplete
* @see onNextAnimationComplete
* @see animationStarted
*/
AnimEvents.prototype.animationFinished = function() {
--this.animating;
if (this.animating === 0) {
while (this.onNextAnimationCompleteCallbacks.length) {
(this.onNextAnimationCompleteCallbacks.shift())();
}
this.onAnimationCompleteCallbacks.forEach(function (callback) {
callback();
});
}
}

/**
* Register a callback to be called every time animations are finished (a
* single call as soon as all the animations have stopped).
*
* @param {Function} callback - the parameterless function to call
*/
AnimEvents.prototype.onAnimationComplete = function(callback) {
this.onAnimationCompleteCallbacks.push(callback);
}

/**
* Register a callback to be called only once, the next time animations are
* finished (a single call as soon as all the animations have stopped). After
* that, the callback is automatically deregistered.
*
* NOTE: The callbacks registered with this function are executed *before* the
* permanent ones.
*
* @param {Function} callback - the parameterless function to call
*/
AnimEvents.prototype.onNextAnimationComplete = function(callback) {
this.onNextAnimationCompleteCallbacks.push(callback);
}
88 changes: 88 additions & 0 deletions src/classes/board.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* The game board, a convenience class to wrap access to the actual container
* of tiles.
*
* The related class BoardCoord is an iterator of this container.
*
* The main invariant of this class is that, after the initialization phase, at
* a given BoardCoord an object exist whose "coord" member has the same value
* as the given BoardCoord:
*
* board.getAtCoord(coord).coord === coord
*
* @see BoardCoord
*
* @class
*
* @param {Object} size - the logical size of the board expressed as the x and
* y values of the given object
*/
function Board(size) {
this.size = size;
this.columns = [];
for (var i = 0; i < size.x; i++) {
this.columns.push(new Array(size.y));
};
}

/**
* An action to perform on a given tile.
* @callback tileCallback
* @param {MatchThreeTile} tile - the tile on which to perform the action
*/

/**
* Calls the given callback for each tile. Lower tiles are guaranteed to be
* called before higher tiles.
*
* @param {tileCallback} callback - the action to perform on each tile
*/
Board.prototype.forEachTile = function(callback) {
// NOTE: loop from below
for (var x = 0; x < this.size.x; x++) {
for (var y = this.size.y - 1; y >= 0; y--) {
callback(this.columns[x][y]);
}
}
}

/**
* An action to perform on a given board coord.
* @callback coordCallback
* @param {BoardCoord} coord - the coord on which to perform the action
*/

/**
* Calls the given callback for each board coord.
*
* @param {coordCallback} callback - the action to perform on each coord
*/
Board.prototype.forEachCoord = function(callback) {
for (var x = 0; x < this.size.x; x++) {
for (var y = 0; y < this.size.y; y++) {
callback(new BoardCoord(this, x, y));
}
}
}

/**
* Access a tile given a board coord.
*
* @param {BoardCoord} coord - the board coordinate of the tile
* @returns {MatchThreeTile|undefined} the desired tile or undefined if out of
* range
*/
Board.prototype.getAtCoord = function(coord) {
return (this.columns[coord.x] || {})[coord.y];
}

/**
* Sets a tile at the given board coord.
*
* @param {BoardCoord} coord - the desired board coordinate of the tile
* @param {MatchThreeTile} tile - the given tile
*/
Board.prototype.setAtCoord = function(coord, tile) {
this.columns[coord.x][coord.y] = tile;
tile.coord = coord;
}
63 changes: 63 additions & 0 deletions src/classes/boardcoords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* This is an iterator for the Board class.
*
* Note: y goes downward
*
* @param {Board} board - the board this coordinate iterates on
* @param {Integer} x - the logical horizontal position on the board
* @param {Integer} y - the logical vertical position on the board (starting
* from the top)
*
* @class
*/
function BoardCoord(board, x, y) {
this.board = board;
this.x = x;
this.y = y;
}

/**
* @return {BoardCoord|undefined} the board coordinate left of the current one
* or undefined if out of bounds
*/
BoardCoord.prototype.left = function() {
return this.board.getAtCoord({x: this.x - 1, y: this.y});
}

/**
* @return {BoardCoord|undefined} the board coordinate right of the current one
* or undefined if out of bounds
*/
BoardCoord.prototype.right = function() {
return this.board.getAtCoord({x: this.x + 1, y: this.y});
}

/**
* @return {BoardCoord|undefined} the board coordinate above of the current one
* or undefined if out of bounds
*/
BoardCoord.prototype.above = function() {
return this.board.getAtCoord({x: this.x, y: this.y - 1});
}

/**
* @return {BoardCoord|undefined} the board coordinate below of the current one
* or undefined if out of bounds
*/
BoardCoord.prototype.below = function() {
return this.board.getAtCoord({x: this.x, y: this.y + 1});
}

/**
* @param {BoardCoord} other - the board coordinate to check against
* @return {Boolean} whether the two board coordinates are adjacent
*/
BoardCoord.prototype.isAdjacent = function(other) {
if (this.x != other.x && this.y != other.y) {
return false;
}
return 1 >= Math.max(
Math.abs(other.x - this.x),
Math.abs(other.y - this.y)
);
}
81 changes: 81 additions & 0 deletions src/classes/coordmapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Encapsulates the conversion between screen coordinates and board coordinates
* taking tile size and board offset into account.
*
* @class
*
* @param {Object} size - object with x y size of a single tile, in screen
* coordinates
* @param {Object} offset - object with x y top-left offset of a board, in
* screen coordinates
*/
function CoordMapper(size, offset) {
this.size = size;
this.offset = offset;
}

/**
* Converts a board coordinate (typically a BoardCoord) into its equivalent
* screen coordinate given the current mapping.
*
* This method works on a single axis.
*
* @param {String} axis - "x" or "y"
* @param {Integer} axisCoord - the board coordinate along that axis
*
* @returns {Number} the screen coordinate along the given axis
*/
CoordMapper.prototype.boardToScreenAxis = function(axis, axisCoord) {
return axisCoord * this.size[axis] + this.offset[axis];
};

/**
* Converts board coordinates (typically a BoardCoord) into their equivalent
* screen coordinates given the current mapping.
*
* @param {Object} coord - the board coordinates as x and y values of the given
* object
*
* @returns {Object} the screen coordinates as x and y values
*/
CoordMapper.prototype.boardToScreen = function(coord) {
return {
x: this.boardToScreenAxis('x', coord.x),
y: this.boardToScreenAxis('y', coord.y),
};
};

/**
* Converts a screen coordinate into its equivalent board coordinate given the
* current mapping. The resulting coordinate isn't necessarily valid but can be
* validated through other means, e.g. by creating a BoardCoord object out of
* it.
*
* This method works on a single axis.
*
* @param {String} axis - "x" or "y"
* @param {Number} axisCoord - the screen coordinate along that axis
*
* @returns {Integer} the board coordinate along the given axis
*/
CoordMapper.prototype.screenToBoardAxis = function(axis, axisCoord) {
return Math.floor((axisCoord - this.offset[axis]) / this.size[axis]);
};

/**
* Converts screen coordinates into their equivalent board coordinates given
* the current mapping. The resulting coordinates aren't necessarily valid but
* can be validated through other means, e.g. by creating a BoardCoord object
* out of them.
*
* @param {Object} coord - the screen coordinates as x and y values of the
* given object
*
* @returns {Object} the board coordinates as x and y values
*/
CoordMapper.prototype.screenToBoard = function(coord) {
return {
x: this.screenToBoardAxis('x', coord.x),
y: this.screenToBoardAxis('y', coord.y),
};
};
Loading

0 comments on commit c4983cb

Please sign in to comment.