Game 2048 in 5 minutes

About

2048 is a single player sliding puzzle. Goal of the game is to combine tiles with powers of 2 and get to the tile containing 2048 (that is 2 to the 10-th power).

Link to Wikipedia: https://en.wikipedia.org/wiki/2048_(video_game)

Live coding

Here is a live coding session that shows how it can be done.

Duration: 5:00

Disclaimer: never use this code in production. It was created for fun.

Breakdown

Let's break down the solution and comment on some complex or interesting things.
This is by far the most complex mini-project so far involving a number of non-trivial solutions and tricks.
Technically the game is built using JavaScript without any libraries and should work in any modern browser.

0:13 - as usual starting with HTML template. Our game field with be a HTML table. Right now we are defining only the top-level table element. All rows and cells will be created later by means of JavaScript function - this way it will be easier to deal with cells later.

0:21 - quite routine HTML operations are done here. We need to create table rows and cells programmatically. We do it this way because we need to apply some styling for all of them. Also we would benefit from storing references to the HTML elements - we'll need them in near future for manipulations.
I feel that it is quite annoying that in most of the projects where we deal with rectangular game field more or less the same code is used. Maybe it makes sense to extract it to some shared library.
Pay attention to "cell" class set on each cell - we'll define the style of cell later and it will be one for all cells.
In the end we'll get htmlElements array initialized and it contains references to all cells on game field.

0:52 - initialization function will be used to prepare the game. Right now it only creates game field elements.

0:55 - define the styles. Also not doing anything complex here. Each cell will be rectangular with a black border around. Make sure that the text is aligned to the middle of the cell.

1:07 - right now we have only visual part. Now we need to define internal model. It will be an array containing numbers that should be displayed in the cells. For convenience using 0 as empty cell.
Initially the game field is empty.
Also for the game we need to create new numbers in empty cells. In original game there was a rule that in 90% of the cases new number should be 2 and in remaining 10% it is 4. Let's implement the same. For that purpose generate 2 random numbers for X and Y coordinate and check if the cell is still empty.
The game will start with 3 filled cells.

1:30 - draw function will be used to synchronize the internal state with the HTML elements. Remember that we'll be working only with cells array and then using draw function to draw the results of every move.
Here htmlElements array comes into play - we have references to all table cells and it makes it easy to find the proper cell.

1:50 - now we have game model ready and next step would be to implement the actions that will happen when player will press up, down, left and right arrows.
The cells should slide to corresponding direction.
First of all we need to define functions that do the moves. For simplicity we define separate function for each direction.
Also coding the event listener for key down events. It will be triggered every time a user presses any key (with a focus on HTML document). Some magic numbers are used here for the key codes. You can either google them or do an intermediate step to output values of keyCode to console and play around a bit.
Right now key press should trigger one of 4 functions.

2:11 - probably the most important and complex step of the project. We are getting to implementation of move logics.
For complex tasks it makes sense to break it down into smaller manageable part. This approach will be taken also here.
Imagine that we need to slide only one single row to the left.
Let's take the following row as an example and let's see how it behaves during sliding operation.

This is exactly the logics done in slide function. And it will be our building block for all moves.

2:48 - having slide function at hand the moveLeft is trivial - it's basically just slide of each and every row.

2:58 - moving or sliding to the right requires a trick. Of course we can implement the same sliding but to the right, but actually we can just mirror the board and reuse existing sliding functionality.
Let's review it on an example:

The implementation for moveRight function is using exactly this approach.

3:22 - moving up and down also can be done with a little trick - quite similar to moveRight function. But instead of mirroring we'll use transposing - it is mirroring the board against the main diagonal.
Let's take the same example and see how it works with moving up:

Also moveDown function can be implemented as combination of mirror, transpose and moveLeft. Or just transpose and moveRight. You can manually check that either of these approaches work.
After all the move functions are implemented our main game logics is completed.
And finally after every move we should drop a new number into empty cell.

3:47 - we have a minor flaw in our logics. There can be situations when some move (e.g. move up) does not change the game field. In other words not all moves are possible.
Let's examine the following situation:

8420
0000
0000
0000
Obviously slides to the left and to the top are not possible as they are not changing the game field. However moves down and to the right are possible.
We need to implement a logics that checks if a move has actually changed something or not.
For that purpose we can adjust slide function and check if it changes a row or not. And then propagate this changed flag to all move functions.
There are many ways how to compare if 2 JavaScript arrays have the same elements, but for shortness I decided to go with the following one-liner:
arrayA.join(',') == arrayB.join(',')
Basically we are converting each array to comma-separated list of elements and compare two strings.
Also in key press event handler we need to check if it was a valid move and only in this case generate new number in empty cell.

4:13 - currently it's quite hard to distinguish the cells. We can use different colors for different number cells as it was in original game.
We'll use different colors and different algorithm how to calculate colors. The formula is based on HSL (hue, saturation, lightness) color model:
hsl(20 + 24 * Math.log2(2048 / v), 100%, 50%)
Saturation is 100% to get bright color, it's ok to have 50% lighness. The most interesting part is hue component.
For hue we are choosing an interval from 20 to 260. Smallest value on the field will be 2 meaning that hue value will be 260. For 2048 hue value will be 20.
Refer to the following table for used colors.

2 4 8 16 32 64 128 256 512 1024 2048
You can see nice gradient from blue to red over green.

4:32 - last bit of the solution - check when the game is over and it is not possible to do any move. There are 2 cases when game can continue:

Implementing isGameOver function to check both scenarios. And update key press handler to check if game is over after each move.
And that concludes the solution.

As you see this mini-project contains some non-trivial solutions and tricks. However the implementation is quite straight-forward.
What makes is so long is the need to iterate across whole game field in multiple functions.

Resources

Sources: https://github.com/5minute/examples/tree/main/2048

See live results:https://jsfiddle.net/uft4jgd9/