A JavaScript and Canvas card game
Published: 2022-07-30 18:50:00


<p><span style="font-weight: 400;">One day in my leisure time I came across the game </span><a 

href="https://solitairesocial.com/"><span style="font-weight: 400;">Solitaire Social</span></a><span style="font-weight: 400;">, started thinking in my head about how to arrange my own card game, and </span><span style="font-weight: 400;">came up with, without thinking long, created its electronic version. And then I added four more games, including Texas Hold'em poker. And to make it even more interesting, I built in a kind of progression: if you win one game, you open a new one. I plan to put my game on two social networks and in the Windows Store as a html5-js application. As well as possibly create more builds for Android and iOs. You can play through the server with other people, or you can play offline with the AI.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">As always I didn't use any third-party engines and libraries, I even didn't need jQuery. Only vanilla JavaScript features, including canvas functionality. The canvas in the game is the basis for all game graphics. WebGL, this time, was not necessary, so the zoo of supported browsers expanded. The programming environment, as usual, was an advanced notepad. The game is 3.8 MB, of which 3 MB are seven sprite maps in png format. The game starts with an HTML file. The server is in PHP. In the case of the single-player game (i.e. with AI), requests to the server are not sent and all calculations are performed on the client.</span><span style="font-weight: 400;"><br /><br /></span></p>
<h3><strong>GUI</strong></h3>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">It didn't take me very long to write the graphical code because I had already developed a JavaScript menu engine for my previous games that used canvas tags and loaded sprites in jpg and png format, or, in other words, a GUI. All I had to do was to take that code and define all the active areas, i.e. the maps and menu buttons, as on-screen buttons using an associative array. And then to sketch out the sprites themselves, that is, to create a design. The main time spent, in fact, on the description of the game logic of all five games. And a little later - to create a multiplayer version for two social networks and a Windows store.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">I did lie a little bit about the fact that my canvas menu was so completely done. Most importantly, I wanted the cards not to lie in a boring straight line, but to lay out in a fun fan, no matter how many of them there were. However, my menu didn't support rotating "buttons." So it was time to program this mess once and for all. For starters.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /><br /></span></p>
<h3><strong>Welcome to Vegas</strong></h3>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">I decided that the curvature would be given by some parameter &ldquo;d&rdquo;, which denotes how much the center of the map line extends upward.</span><span style="font-weight: 400;"><br /><br /></span></p>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">So, given: the number of cards (n), the width of the rectangle (w) bounded by the centers of the extreme, boring, "unbent" cards, and the upward shift of the middle of the card line (d), making them fun. You need to find the center points and angles of each card so that you get a fan like in the picture. It's hard to say how good my method is, but it's what I can remember from geometry. We will write it in JavaScript.</span>

<pre>
  <code>
var L=0, h=0, dx=0, hx=0, hy=0;

//Find the unknown sides of the triangle (1...3...n) around which the circle will be circumscribed

//Let us denote by the variable a the equal sides 1. 3 и 3.. n

var a = Math.sqrt ( d*d+(w*w/4) );

//Find the radius R of the circumcircle by the formula

var p = 0.5*(a+w+a);

var R = a*w*a / ( 4*Math.sqrt( p*(p-a)*(p-w)*(p-a) ) );

//The angle L0 for the deviation of the first and last maps from the horizontal axis will be

var L0 = Math.asin( 1 - (d/R) );

//the angle dL, by which the cards will be indented from each other on the circle

var dL = ( Math.PI-(2*L0) ) / (n-1);

//Cycle all the cards

for (var i=0; i< n; i++) {

    //Find the total angle of deviation for the map from the horizontal coordinate axis

    L = L0 + (dL*i) + rot;

    //here rot is another input parameter that sets the overall angle of the entire fan, if we want the line w to be at an angle

    //Find the vertical and horizontal axis indents for each map, from which you can derive the coordinates of the centers of each map

    hx = -R*Math.cos(L) - R*Math.sin(rot);

    hy = d + R*Math.sin(L) - R*Math.cos(rot);

    //Memorizing these parameters in the array, we get all the coordinates and angles

};
</code>
</pre>
<p><span style="font-weight: 400;">Vegas is getting close.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">The code for displaying all the sprites on canvas using a generated array of points and corners is trivial and uses only standard canvas functions. The scheme is simple: you save the canvas state, move to where the center of the sprite will be, rotate the canvas to the desired angle, display the sprite, and then restore the canvas state. By the way, the half width (w2) and half height (h2) of the sprite don't have to be calculated every time, they can be predefined. In general, it's something like this:
<pre>
  <code>
var w2 = Math.floor(spriteW/2), h2 = Math.floor(spriteH/2);

var x = spriteX+w2, y = spriteY+h2;

ctx.save();

ctx.translate(x, y);

ctx.rotate(spriteL - Math.PI/2);

ctx.drawImage(spriteMapImage, spriteMapX, spriteMapY, spriteW, spriteH, -w2, -h2, spriteW, spriteH);

ctx.restore();
</code>
</pre>

<img src="https://i.ibb.co/VqdWCCy/unnamed-28.jpg" alt="unnamed-28" border="0">
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">Yay! Now the cards are fan-folded, just like in Vegas - I've seen it in movies. The disadvantage, or rather, the flaw so far is that, although the card-button sprites are now placed at an angle, the clickable areas remain unrotated, even if they are properly displaced. That is, they only have the centers aligned with the sprites. For this game, however, that is quite enough. The card deflection angle will be quite small, and you are unlikely to accidentally click on a neighboring card. Here, if there will be such circles of cards as in this picture, they won't be clickable, but just for effect. There will be far fewer curves for card players.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">There's always something to aim for. You can count sines and cosines in the mouse pointer handler to check if it enters the clickable area so that you can position the tilted button more accurately. Or you can rotate the canvas as the mouse moves. But all of this will seem to me to be a heavy and unreasonable burden on the processor. After all, we would need to sine-cosine all "screen buttons" every time the mouse moves to find out if the cursor is in a certain clickable area. You could, of course, work out the angles just for the click event, but then you wouldn't get that nice change of look of the pointer when it comes in and out of the clickable area. I'll think about how to solve this problem. For now, let's move on.</span></p>
<h3><strong>AI</strong></h3>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">After reading this subtitle, one would have thought that here it was, at last, artificial intelligence had been invented. What all the brightest minds in civilization have been striving for so long has come to fruition. I'll disappoint you, however. In this case, under the pathological acronym AI there is an ordinary random number generator. Okay, just kidding, not an ordinary one. Our gaming AI does have some minimal logic. At poker, I almost always lose. But maybe I don't know how to play it. </span><span style="font-weight: 400;"><br /><br /></span></p>
<h3><strong>Map animation</strong></h3>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">Let's make a sweet game. Without card animation, everything works kind of abruptly and ugly. Let's add another graphic layer (read - transparent canvas) on top of the main layer and arrange "races" of cards on it between the deck and the table, the table and the players, the deck and the players. With rotation, songs, and dances. Cards, which you want to animate, will simply be moved to the outer canvas, and there will move freely, without fear of being an elephant in a china shop, so as not to accidentally erase their neighbors and the background. And at the end of the way, it will fall back to the lower canvas and then behave decently. The animation can also be turned off via the game menu if you don't need it. By the way, we'll put click handlers right on the outer canvas.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">The map movement on the canvas will be defined by the following input parameters: velocity V and points, initial (X0;Y0) and final (X,Y). As we move, we will draw the sprite in the position it should be in after the time &ldquo;ti&rdquo; has elapsed since we started the movement. And beforehand, of course, erase the sprite from the old position. We will call the next drawing as soon as the previous one is finished. In this way, we will provide as smooth motion as the device on which the code will be executed will allow. And binding to the time will make the whole movement smooth, regardless of possible short-term lags of the browser. The principle of calculation is this:</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">At the beginning of the motion call function, let's define some parameters for the motion</span>
<pre>
  <code>
//Remember the start time
var t0 = Date. now() / 100;
//Full movements on horizontal and vertical axes
var Sx = X - X0;
var Sy = Y - Y0;
//Find the total distance according to the formula
var S = Math. sqrt (Sx*Sx + Sy*Sy);
//The time elapsed since the beginning of motion (ti) refers to the total time of motion (t) as the corresponding distances on the x-axis, e.g., x, that is, ti / t = dxi / Sx. It follows that the coordinate increment for the horizontal axis dxi = Sx * ti / t, or dxi = Sx * ti * V / S. The velocity is given as a motion parameter, and we have already calculated the distances. Similarly for the vertical axis.
//Let's derive for this animation some constant-coefficients, which follow from the formula above and which will be used to calculate horizontal and vertical coordinates in the loop, so that we don't have to calculate these values at each iteration. That is, for now let's take without time ti
var constX = V * Sx / S;
var constY = V * Sy / S;
</code>
</pre>
<p><span style="font-weight: 400;"></span><span style="font-weight: 400;">And in the cycle of motion, we will each time calculate the coordinates already by time</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></p>
<pre>
  <code>
//The time elapsed since the beginning of the movement is equal to
var ti = Date.now()/100 - t0;
//Current coordinates (Xi; Yi) depending on this time
var Xi = constX * ti + X0;
var Yi = constY * ti + Y0;
  </code>
</pre>
<span style="font-weight: 400;">Finally, these coordinates will be checked for belonging to the motion segment. That is, Xi must be between X0 and X, and Yi must be between Y0 and Y. As long as this condition is met, we draw the sprite at (Xi; Yi) and loop the motion function. The rest of the code is clear and there is no need to describe it in detail. I have explained the whole principle.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">Now for the rotation angle of the sprite (ai). If the initial angle (a0) and the final angle (a) specified as input parameters are different, then we calculate the current angle ai in the motion function as follows. The increment of the angle, i.e., (ai) refers to the increment of the path, for example, along the horizontal axis ( which we have equal to (Xi - X0) ), as the total angle of rotation (a - a0) refers to the total path along the horizontal axis (Sx), i.e:</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">ai / (Xi - X0) = (a - a0) / Sx</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /><br /></span></p>
<pre>
  <code>
//And where we calculate the current sprite coordinates in the loop, let's add also the calculation of the current rotation angle, remembering to add the initial angle (a).</span></em></p>
<p><span style="font-weight: 400;">var </span><span style="font-weight: 400;">ai = (a-a0) * </span><span style="font-weight: 400;">Math</span><span style="font-weight: 400;">.abs (Xi - X0) / </span><span style="font-weight: 400;">Math</span><span style="font-weight: 400;">.abs(Sx) + a;</span></p>
<p><em><span style="font-weight: 400;">//You can put a constant value (a-a0) / Math. abs(Sx) out of the loop brackets;
</code>
</pre>
<p><span style="font-weight: 400;">We draw the sprite with the standard drawImage and pre-turn the canvas with the standard JS functions translate and rotate, I think everything is clear here.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">Yes, this algorithm does not take into account some special cases. When distance equals 0, for example, we get division by 0 error. But we won't set the cards to zero motion, so why would we need it? Therefore, it is unnecessary to check for such extremes in this case.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /><br /></span></p>
<p>&nbsp;</p>
<h3><strong>Design</strong></h3>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">Design is not my thing. In the process of creating the game, it was, as usual, redesigned and adjusted many times. In the end, by the time of the game's release, I settled on the black-and-red version that suddenly came to my mind, with the layout of the main menu on the suits. I thought for a long time about which of the two types to keep and then decided to have both by switching between them.</span><span style="font-weight: 400;"><br /><br /></span></p>
<h3><strong>Sounds</strong></h3>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">The sounds for the game were partly generated with a synthesizer program, partly taken from free Assets, and connected with the audio tag. There is not much to say about it. There was only one unpleasant thing, connected with the fact that if the current sound did not end, the new sound was skipped and was not played at all. Therefore, I had to shorten the sounds so that they would not "overlap" each other in some situations.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /><br /></span></p>
<h3><strong>Subtotal</strong></h3>
<p><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">At the moment the game is moderated in the Windows Store as a single-user UWP app x86 and x64 for Windows 8.1 - 10. I also plan to make builds for Android and iOs. And also, perhaps, just zip-archive to run the single-player version of the game at the index. HTML for all operating systems.</span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;"><br /></span><span style="font-weight: 400;">In general, the archive with html-js files seems to me the best way to distribute such applications, because there is no need to build something with "heavy" development tools, there is no need to create an installer, there is no need to include in the assembly of any library, html can be opened in any OS with any browser of your choice and use the one in which the application will work best. It's a shame that app stores don't allow apps to be distributed this way, and operating systems don't install and create shortcuts to html as they do with executable files, for example. Unless Windows 8.1 - 10 is somehow moving in that direction, the assembled package for the Windows Store is only 4.4 MB. Browser makers could also turn towards html-js applications and provide some kind of launch mode without showing the browser interface, as is done, for example, in node-WebKit, when the browser engine is used. Then you wouldn't, for example, have to include the engine in the build of a "classic" application and "drag" the engine with each such application. After all, web applications have potential: available 3D graphics WebGL, bytecode, and soon, perhaps, the browser code will be even closer to the native one in terms of speed.</span></p>