Chesslings






4.99/5 (43 votes)
An isometric chess game using Paper JS
Table of Contents
- Introduction
- System Requirements
- Paper JS
- Tiny Chess
- Chesslings
- Movement Detection
- Animation
- Final Considerations
- History
Introduction
As a programmer, I always thought that developing a chess game would be a big milestone and I would be so proud of it. And here I am, presenting you with a chess game. And yes, I'm a little proud of it. But wait. Have I really developed a chess game?
First of all, creating a chess game means nothing less than developing the chess engine, which is the smarty-pants piece of software sitting behind the board, trying to come up with the best moves and pretending to be a human. I have always been a poor chess player, and my first thought was that the best thing I could do to develop a chess engine would be porting from some existing and working C chess engine. The target language would be one of the few which I feel comfortable with: C# or JavaScript.
Much to my surprise, recently I discovered that a brilliant Mexican guy named Óscar Toledo Gutiérrez had already developed a JavaScript chess engine, after having developed a C and Java versions of it. Not only that: Toledo managed to squeeze his C chess engine, named Nano Chess, in only 1257 characters, becoming the world's smallest C chess engine. Toledo perfected it further and his Pico Chess C code amounts to less than 1Kb. He is five times winner of the IOCCC (International Obfuscated C Code Contest). After some thought, I decided not to write the chess engine by myself.
Chesslings is a chess game based on Toledo's Tiny Chess. That is, literally based on it. Tiny Chess actually runs silently behind the scenes, providing all the game logic we need. What Chesslings really does is provide a different look and feel by isometric projection and animations for each move.
System Requirements
This article contains the code needed to run a website relying on no programming languages other than JavaScript, and consequently without references to assemblies or C# code, so it doesn't need to be compiled. You can simply download the source code and open the HTML file in the browser of your preference.
Paper JS
For this project, we use Paper JS as the rendering framework. Paper.js is an open source vector graphics scripting framework that runs on top of the HTML5 Canvas.
This project will not use the most noticeable feature of Paper JS, i.e. the vector graphics, but takes advantage of the Paper JS's built-in render loop and ability to
render images on the html5 canvas
.
Throughout the code, you will notice many references to types defined by the Paper JS framework. Here goes a short explanation of how we use them in the project:
- paper.setup: Sets up an empty project for us. If a canvas is provided, it also creates a View for it, both linked to this scope.
The "project" in question is an instance of the
project
created internally by the Paper JS framework. It's enough to say that it accepts the canvas residing on our page as a parameter, and manage all output to that canvas (like rendering images) and control user interactions such as keyboard strokes. - paper.Point: a two-dimensional object representing a point on the canvas. It is extensively employed in our project to hold and calculate positions on the screen.
- paper.Rectangle:
- paper.view.center: the center of the
paper.view
object, which also means the point at the center of our canvas. - paper.Key.isDown: a boolean function that checks whether the keyboard Down key is pressed at that particular moment.
- paper.Raster: a raster is a specialized kind of
Item
object that represents a image in the Paper JS project. We use it to create independent images for each of our chess pieces. - paper.view.onFrame: Item level handler function to be called on each frame of an animation. This function gives us information about how many times the event was fired, the total amount of time passed since the first frame event, and the time passed since the last frame occurred. It is extensively used by our chess animations.
- paper.tool.onMouseUp: The function to be called when the mouse button is released over the item. The function receives a MouseEvent object which contains information about the mouse event.
Toledo's Tiny Chess
As I mentioned in the article introduction, Óscar Toledo ported his C code "Nano Chess" to JavaScript short after he had won the IOCCC (International Obfuscated C Code Competition) prize, and this resulted in what he called Tiny Chess. As expected, the code is heavily obfuscated, and surprisingly short for a chess engine. Here is the full JavaScript source of Tiny Chess:
//(c)2009 Oscar Toledo G.
var B,i,y,u,b,I=[],G=120,x=10,z=15,M=1e4,l=[5,3,4,6,2,4,3,5,1,1,1,1,1,1,1,1,9,9
,9,9,9,9,9,9,13,11,12,14,10,12,11,13,0,99,0,306,297,495,846,-1,0,1,2,2,1,0,-1,-
1,1,-10,10,-11,-9,9,11,10,20,-9,-11,-10,-20,-21,-19,-12,-8,8,12,19,21];function
X(w,c,h,e,S,s){var t,o,L,E,d,O=e,N=-M*M,K=78-h<<x,p,g,n,m,A,q,r,C,J,a=y?-x:x;
y^=8;G++;d=w||s&&s>=h&&X(0,0,0,21,0,0)>M;do{if(o=I[p=O]){q=o&z^y;if(q<7){A=q--&
2?8:4;C=o-9&z?[53,47,61,51,47,47][q]:57;do{r=I[p+=l[C]];if(!w|p==w){g=q|p+a-S?0
:S;if(!r&(!!q|A<3||!!g)||(r+1&z^y)>9&&q|A>2){if(m=!(r-2&7))return y^=8,I[G--]=
O,K;J=n=o&z;E=I[p-a]&z;t=q|E-7?n:(n+=2,6^y);while(n<=t){L=r?l[r&7|32]-h-q:0;if(
s)L+=(1-q?l[(p-p%x)/x+37]-l[(O-O%x)/x+37]+l[p%x+38]*(q?1:2)-l[O%x+38]+(o&16)/2:
!!m*9)+(!q?!(I[p-1]^n)+!(I[p+1]^n)+l[n&7|32]-99+!!g*99+(A<2):0)+!(E^y^9);if(s>h
||1<s&s==h&&L>z|d){I[p]=n,I[O]=m?(I[g]=I[m],I[m]=0):g?I[g]=0:0;L-=X(s>h|d?0:p,L
-N,h+1,I[G+1],J=q|A>1?0:p,s);if(!(h||s-1|B-O|i-n|p-b|L<-M))return W(),G--,u=J;
J=q-1|A<7||m||!s|d|r|o<z||X(0,0,0,21,0,0)>M;I[O]=o;I[p]=r;m?(I[m]=I[g],I[g]=0):
g?I[g]=9^y:0;}if(L>N||s>1&&L==N&&!h&&Math.random()<.5){I[G]=O;if(s>1){if(h&&c-L
<0)return y^=8,G--,L;if(!h)i=n,B=O,b=p;}N=L;}n+=J||(g=p,m=p<O?g-3:g+2,I[m]<z|I[
m+O-p]||I[p+=p-O])?1:0;}}}}while(!r&q>2||(p=O,q|A>2|o>z&!r&&++C*--A));}}}while(
++O>98?O=20:e-O);return y^=8,G--,N+M*M&&N>-K+1924|d?N:0;}B=i=y=u=0;while(B++<
120)I[B-1]=B%x?B/x%x<2|B%x<2?7:B/x&4?0:l[i++]|16:7;for(a=
"<table cellspacing=0 align=center>",i=18;i<100;a+=++i%10-9?
"<th width=40 height=40 onclick=Y("+i+") style='border:2px solid #aae' id=o"+i+
" bgcolor=#"+(i*.9&1?"9090d0>":"c0c0ff>"):(i++,"<tr>"));
a+="<th colspan=8><select id=t><option>Q<option>R<option>B";
document.write(a+"<option>N</select></table>");
function W(){B=b;for(p=21;p<99;++p)if(q=document.getElementById("o"+p)){q.
innerHTML="<img width=40 src="+(I[p]&z)+".gif>";q.
style.borderColor=p==B?"#ff0":"#aae";}}W();
function Y(s){i=(I[s]^y)&z;if(i>8){b=s;W();}else if(B&&i<9){b=s;i=I[B]&z;if((i&
7)==1&(b<29|b>90))i=14-document.getElementById("t").selectedIndex^y;X(0,0,0,21,
u,1);if(y)setTimeout("X(0,0,0,21,u,2/*ply*/),X(0,0,0,21,u,1)",250);}}
That´s it. That is all you need to run a full chess game in JavaScript. Now, where does our Chesslings game enter?
First, we create a dedicated div
for Tiny Chess, and give it an id equals to "toledoTable":
<div id="toledoTable" style="position: absolute; top: 0; display: none;"></<div>
Now we change Tiny Chess code a little, just enough to inject some notifications to our Chesslings game whenever a new game movement occurs, and renaming some functions and variables for the sake of readability. This is needed because, in fact, all game movements are actually made by the background running Tiny Chess code.
function renderChess() {
i = "<table>";
for (u = 18;
u < 98;
render()
)
B = selectedTileNum;
if (game) {
game.processMovement(I);
}
}
renderChess()
function render() {
$('#toledoTable').html(i += ++u % x - 9 ?
"<th width=30 height=30 onclick='clickTile(" + u + ")' style='font-size:25px'bgcolor=#" + (u - B ? u * .9 & 1 || 9 :
"d") + "0f0e0>&#" + (I[u] ? 9808 + l[67 + I[u]] : 160) + ";" : u++ && "<tr>");
}
function clickTile(u) {
game.onTileClick(u);
I[selectedTileNum = u] > 8 ? renderChess() : X(0, 0, 1);
}
After these little tweaks in the original code, we can proceed to our game development
Chesslings
Chesslings is a JavaScript game that reacts to the movements made by the underlying Tiny Chess code, as explained before.
However, we will not be talking about each and every aspect of the code. Instead, let's focus on the key features of it.
The first entry point for the Chesslings is the onTileClick
function exposed by the game
instance:
onTileClick: function (tileNum) {
//here goes the code that highlights the selected tile.
},
The next entry point is the processMovement
function, responsible for acquiring and comparing the current board
state with the previous board state, so that we can find out which chess piece was moved, and which one was possibly lost in
the movement.
processMovement: function (tableArray) {
//here goes the code that processes the game board's current state.
},
The board state is represented by a matrix of 10x12 positions, which contains a smaller 8x8 chess board in its center. The reason for the matrix dimensions being greater than a regular chess board is that only the central area of the matrix is occupied by the chess board itself, where the margin positions are kept so to facilitate the Tiny Chess movement calculations.
As we can see, each position in the table array is occupied by a chess piece containing a distinct number representing the piece type, except for the pawns, the blank spaces and the off-board positions (they have the same piece numbers). We ported the piece type codes to our game to later show each piece according to its meaning.
Chess.PieceTypes = {
SPACE: 0,
BLACK_PAWN: 1,
BLACK_KING: 2,
BLACK_HORSE: 3,
BLACK_BISHOP: 4,
BLACK_ROOK: 5,
BLACK_QUEEN: 6,
OFF_BOARD: 7,
WHITE_PAWN: 9,
WHITE_KING: 10,
WHITE_HORSE: 11,
WHITE_BISHOP: 12,
WHITE_ROOK: 13,
WHITE_QUEEN: 14
};
As expected, the game starts with a set of 32 pieces. The code below shows how we instantiate them and populate the array of pieces. For this we consider only the top two rows and the bottom two rows (y < 2 || y > 5) and ignore the rest of the board. Each piece has a unique key assigned to it, along with the name of the Spritesheet representing that category of pieces.
var keys = ('BRL,BNL,BBL,BQ,BK,BBR,BNR,BRR,BP1,BP2,BP3,BP4,BP5,BP6,BP7,BP8,' +
'WP1,WP2,WP3,WP4,WP5,WP6,WP7,WP8,WRL,WNL,WBL,WQ,WK,WBR,WNR,WRR').split(',');
for (var y = 0; y <= 7; y++) {
for (var x = 0; x <= 7; x++) {
if (y < 2 || y > 5) {
var key = keys[self.pieces.length];
var ss;
if (y < 2)
ss = window['spriteSheetS_' + key[1]];
else
ss = window['spriteSheetN_' + key[1]];
self.pieces.push(new Chess.Piece(
key,
{ x: x, y: y }, ss));
}
}
}
The names "spriteShetN" and "spriteShetS" represent both "white" and "black" teams respectively, where "N" and "S" mean "North-facing pieces" vs. "South-facing pieces". Every category of piece ([P]awn, [R]ook, [B]ishop, k[N]ight, [Q]ueen, [K]ing) has a "north-facing" and "south-facing" version, as we can see in the images below:
Piece | Back View | Front View |
Pawn | ||
Rook | ||
Bishop | ||
Knight | ||
Queen | ||
King |
Notice how all chesslings have the same colors. In order to differentiate them, we assume that the back-facing pieces are "white" pieces while the front-facing pieces are the "black" ones. This means that when the pieces walk back, they will also walk backwards.
Note: "Chesslings" is also the name I gave to the pieces of the game. Just like "earthlings" are people living on Earth, "chesslings" are these weird looking inhabitants of the Chess world.
Movement Detection
Every time the underlying Tiny Chess code calls the processMovement
function, the Chesslings code compares
the state of the table array with the last processed array state. Some loops are required to check the changed positions, others
are intended to find out which pieces are affected, and then a animation is enqueued in a AnimationManager
to
process pending animations one after another. The processMovement
function structure is shown below:
processMovement: function (tableArray) {
var self = this;
//Let's proceed only when something changed
if (self.lastTableArray != tableArray) {
var fromPosition; //the position from where some piece moved
var toPosition; //the position to where the piece moved
$(self.lastTableArray).each(function (index, item) {
//some code to set the "fromPosition" variable
});
//Let's proceed only if some movement was detected
if (fromPosition) {
$(tableArray).each(function (index, item) {
var row = parseInt(index / 10) - 2;
var col = (index % 10) - 1;
//If this position was last time empty but now is occupied by a piece,
//then the piece moved to here
if (self.lastTableArray[index] == Chess.PieceTypes.SPACE &&
tableArray[index] != Chess.PieceTypes.SPACE) {
//some code to set the "toPosition" variable
}
//If this position was last time occupied by a piece but now is replaced by another one,
//then this last piece was lost
else if (self.lastTableArray[index] != Chess.PieceTypes.SPACE &&
tableArray[index] != Chess.PieceTypes.SPACE &&
self.lastTableArray[index] != tableArray[index]) {
//some code to set the "toPosition" variable
}
});
//This loop will decide which piece moved, and take action accordingly
$(game.pieces).each(function (index, piece) {
if (piece.isActive &&
piece.currentTile.x == fromPosition.x &&
piece.currentTile.y == fromPosition.y) {
//The animation will move "piece" from "fromPosition" to "toPosition" in 1000 milliseconds,
//starting 200 milliseconds after the start of the animation
var animation = new Chess.PointAnimation(piece, fromPosition, toPosition, 200, 1000);
animation.onFinish = function () {
//When the animation ends, we inactivate (make invisible) the piece that
//may have been lost to the attacking one.
$(game.pieces).each(function (index2, piece2) {
if (piece2.isActive &&
piece2.currentTile.x == toPosition.x &&
piece2.currentTile.y == toPosition.y &&
piece2.key != piece.key) {
//setting a chess piece to "inactive" actually makes it invisible
piece2.setInactive(false);
}
});
}
//we enqueue the animation, so that it will be processed only when required
self.animationManager.enqueueAnimation(animation);
}
});
}
}
self.lastTableArray = tableArray.slice();
self.processCount++;
},
Animation
Animation is a very important aspect of the code. We don't want to move the pieces from point A to point B right away. Instead, we want the piece to
move from A to B in a given T time. And this is accomplished by processing animations. Our AnimationManager
processes a queue of
instances of the PointAnimation
type, and this way we can assure that two or more animations occur concurrently.
Each instance of the PointAnimation
type is created with a set of parameters:
- piece: an instance of the
Chess.Piece
type - startPosition: the
Paper.Point
instance with the (x, y) coordinates for the animation's start position - endPosition: the
Paper.Point
instance with the (x, y) coordinates for the animation's end position - startTimeInMS: the time delay time until the animation starts, in milliseconds
- totalTimeInMS: the total animation duration in milliseconds
The main function of PointAnimation
, onFrame
, is the core of the object. This function is called many times during the
animation's lifetime. Given a deltaTimeInSec
parameter, the function calculates the relative position, taking in consideration the
total animation duration and the ellapsed time, to produce the delta vector, i.e., the D (dx, dy) vector difference between point A (ax, ay) and
point B (bx, by). The function returns true
until the total animation duration is completed.
//********************
//Chess.PointAnimation
//********************
//piece: an instance of the Chess.Piece type
//startPosition: the Paper.Point instance with the (x, y) coordinates for the animation's start position
//endPosition: the Paper.Point instance with the (x, y) coordinates for the animation's end position
//startTimeInMS: the time delay time until the animation starts, in milliseconds
//totalTimeInMS: the animation duration in milliseconds
Chess.PointAnimation = function (piece, startPosition, endPosition, startTimeInMS, totalTimeInMS) {
this.init(piece, startPosition, endPosition, startTimeInMS, totalTimeInMS)
}
$.extend(Chess.PointAnimation.prototype, {
init: function (piece, startPosition, endPosition, startTimeInMS, totalTimeInMS) {
this.piece = piece;
this.startTimeInMS = startTimeInMS;
this.startPosition = startPosition;
this.endPosition = endPosition;
this.totalTimeInMS = totalTimeInMS;
this.ellapsedTimeInMS = 0; //
this.isActive = true; //
this.point = startPosition; //
},
onFrame: function (deltaTimeInSec) {
var self = this;
//the delta time, in seconds
var deltaTimeInMS = deltaTimeInSec * 1000;
//adds to the total ellapsed time for this animation instance
self.ellapsedTimeInMS += deltaTimeInMS;
if (self.ellapsedTimeInMS < self.startTimeInMS) {
//the animation is active but still not started, so do nothing
return true;
}
else if (self.ellapsedTimeInMS <= self.totalTimeInMS) {
//the current point is calculated taking in consideration the total animation duration, the ellapsed time,
//the start and the end position.
var timeFraction = self.ellapsedTimeInMS / self.totalTimeInMS;
var diffVector = new paper.Point(self.endPosition.x - self.startPosition.x, self.endPosition.y - self.startPosition.y);
self.piece.currentTile =
self.point = new paper.Point(self.startPosition.x + diffVector.x * timeFraction, self.startPosition.y + diffVector.y * timeFraction);
return true;
}
else {
//when the total animation duration is completed, we force the piece to assume the end position.
//the onFinish callback function is invoked when needed.
self.isActive = false;
self.piece.currentTile =
self.point = self.endPosition;
consoleLog('end of: ' + self.toString());
if (self.onFinish)
self.onFinish();
return false;
}
},
toString: function () {
return 'pa (from: ' + this.startPosition + ' to:' + this.endPosition + ')';
}
});
The logic behind the animation sequencing lies in the Chess.AnimationManager
code. It is basically a queue implementation for
instances of Chess.PointAnimation
type:
//********************
//Chess.AnimationManager
//********************
Chess.AnimationId = 1;
Chess.AnimationManager = function () {
this.init();
}
$.extend(Chess.AnimationManager.prototype, {
currentAnimation: null,
queue: [],
init: function () {
this.id = Chess.AnimationId++;
},
enqueueAnimation: function (animation) {
consoleLog('enqueueing ' + animation.piece.key + ':' + animation.toString());
var self = this;
//positioning the animation at the end of the queue
self.queue.push(animation);
},
dequeueAnimation: function () {
var self = this;
//taking the first element of the queue
var animation = self.queue[0];
consoleLog('dequeueing ' + animation.piece.key + ':' + animation.toString());
//deleting the first element
self.queue = self.queue.slice(1, self.queue.length)
self.currentAnimation = null;
return animation;
},
getCurrentAnimation: function () {
var self = this;
if (!self.currentAnimation) {
if (self.queue.length > 0) {
self.currentAnimation = self.queue[0];
}
}
return self.currentAnimation;
}
})
Back to the processMovement
code, we notice how the instance of Chess.AnimationManager
is invoked. First
we discover which piece moved, then we enqueue an animation starting from the original table position and ending in the destination
position.
We also subscribe an onFinish
event, that will make the lost piece invisible if it´s the case, and such callback event will
take place only when the animation ends. This ensures that the processing respect the sequence of events.
processMovement: function (tableArray) {
*** HERE WE INSTANTIATE THE fromPosition AND toPosition VARIABLES ***
//This loop will decide which piece moved, and take action accordingly
$(game.pieces).each(function (index, piece) {
if (piece.isActive &&
piece.currentTile.x == fromPosition.x &&
piece.currentTile.y == fromPosition.y) {
//The animation will move "piece" from "fromPosition" to "toPosition" in 1000 milliseconds,
//starting 200 milliseconds after the start of the animation
var animation = new Chess.PointAnimation(piece, fromPosition, toPosition, 200, 1000);
animation.onFinish = function () {
//When the animation ends, we inactivate (make invisible) the piece that
//may have been lost to the attacking one.
$(game.pieces).each(function (index2, piece2) {
if (piece2.isActive &&
piece2.currentTile.x == toPosition.x &&
piece2.currentTile.y == toPosition.y &&
piece2.key != piece.key) {
piece2.setInactive(false);
}
});
}
//we enqueue the animation, so that it will be processed only when required
self.animationManager.enqueueAnimation(animation);
}
});
*** SOME POST-PROCESSING CODE HERE ***
},
Final Considerations
As you have seen, rather than an original work, this project would not have been possible without Óscar Toledo and his amazing work with chess. If you had the trouble to read to these final lines, thanks very much for your patience. I hope this article may be useful for you in some way, regarding game development, isometric projection and javascript-based animation.
History
- 2013-08-31: Initial version.
- 2013-09-05: New mouse controls.