GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

2048 Game JavaScript: 9 Boards from Merges to AI

A 2048 game javascript board folds rows. A row of 2, 2, 4 pushed left ends as 4, 4, not 8.

A Game of Folding Rows, Not Adding Numbers: One-Line Compression Decides Every 2048 Move

Auto-plays · click a tile to jump to its section · all nine in one zip

Trying to compute all sixteen cells at once leads to four copies of the same loop, one per direction. The nine scenes here move tiles through one function, compressLine, that folds one row toward a wall (the 07 and 09 search slides plain numbers with its own slideValues), and for up, right and down they turn a table of cell numbers so that the wall of that direction sits on the left. The folding rule is short: drop the empty cells, then merge each pair of equal neighbors once, starting from the wall, and losing one condition from that rule changes the outcome of the whole board.

What one lock condition changes

The lock marks a tile born from a merge in this move so it cannot merge again within the same move. Across 2,000 seeded boards with roughly three empty cells in eight, each of the 7,978 moves that actually shift something was played twice, with and without the lock: 661 of them (8.3%) ended differently, and 563 boards (28%) held at least one such move. That is about one move in twelve, which is easy to miss when you test by pushing a few rows by hand.

How it differs from the other board games here

The tic tac toe boards in the same game category score each cell by assuming the opponent plays the move that hurts most, and the snake that finds its own path searches for a route to the food. In 2048 the opponent is not a person but the 2 or 4 that drops into an empty cell, so 07 and 09 average over outcomes weighted by probability instead of bracing for the worst case. Pieces that lock into one another are covered separately in the jigsaw puzzle boards.

How Do the 2048 Rules and Strategy Turn into Code for a Single Push?

Reading order is settled first, before any number moves. slideBoard takes the order table ORDERS[dir] for the direction, reads the four rows from the left wall, calls compressLine on each and writes the results back to the original cell numbers. Only then are the slides (start cell to end cell), the merges (parent ids to the new id) and the score (the sum of merged values) recorded, and playMove drops a new tile into one empty cell only when the board actually moved.

Four steps one move passes through

Step Function What gets decided
1 Reading order pushOrder · ORDERS left 0,1,2,3 · up 3,7,11,15 · right 15,14,13,12 · down 12,8,4,0 (first row)
2 Folding a row compressLine drop empty cells, then merge equal neighbors; with the lock, a merged tile does not merge again
3 Recording slideBoard slides, merges and score; the first left move in 01 makes 3 merges for +16
4 New tile spawnTile one empty cell picked by seeded random, a 4 with probability 0.1, otherwise a 2
End check equalPairs · countEmpty the game ends when touching equal pairs and empty cells both reach 0

A push in a direction that moves nothing does not count as a move. So the demo plan in 01 (left, up, right, down, left, down, right, up) skips to the next direction when one is blocked, and a full loop builds 10 moves and a score of 64. If you wanted 2048 explained at the level of code rather than rules, these five rows are the whole story.

Screens this board fits

The most common spot is a mini game on a company event page or a portfolio. The rules fit in one line of instructions, and the board lives in a box that keeps a 480:300 ratio, so it slips between paragraphs easily. An event page that shows a hint next to the board can reuse the four direction bars from 07 as the hint box.

A learning app can lift the step colors from 05 as progress tiles, and the collapsing board from 08 as a game-over and replay screen. 09 plays on its own with no input, so it suits the first screen of a landing page, and the moment a visitor touches the board the auto demo stops and the board becomes theirs.

2048 Game JavaScript: 9 Scenes — One-Line Compression Moves with a Merged-Tile Lock, Slides That Stretch and Squash with Cells Traveled, a Swipe Judge That Settles Diagonals on One Axis, an Undo Rewind Where Merged Tiles Split Apart, Colors Chosen by Power-of-Two Step with Digit Fitting, Merge Bursts and a Combo Shake, an Auto Player That Counts the Tiles Yet to Fall, a Board That Collapses When No Pairs Are Left and a Hero Scene Where the 2048 Board Plays Itself

The 2048 game source code of each scene is one index.html holding a single script block, plus a style.scss, drawn on a 2D canvas with no library, web font or image file.

Values from 24 frames over 2 seconds, each compared with the next

Each scene was rendered at 480×300 for 2 seconds and captured as 24 frames, and the 23 differences between neighboring frames give the numbers below.

Scene Area covered Frames that moved Mean intensity
01 Line compression and merged-tile lock 30.4% 23/23 47.1
02 Stretch slide sized by cells traveled 32.1% 23/23 48.8
03 One-axis swipe judge 29.3% 21/23 69.7
04 Splitting undo rewind 36.7% 21/23 51.2
05 Step colors and digit fitting 24.8% 22/23 52.4
06 Merge shards and combo shake 29.2% 23/23 44.4
07 Auto player counting falling tiles 29.2% 23/23 60.7
08 Board that collapses with no pairs left 34.7% 23/23 46.6
09 Hero board that plays itself 25.4% 23/23 53.6

04, which runs three moves forward and three back, covers the widest area at 36.7%. 03 and 04 stop at 21/23, likely because the pauses between trails and between undo steps fall inside the 2-second window.

01One-Line Compression Moves with a Merged-Tile Lock

A base for a merge mini game on a company event page, where a wrong rule shows at once: from the start board "2 2 4 _ / _ 4 4 8 / 2 _ 2 _ / _ _ _ 2" it plays a move every 0.48 seconds while a small board at the bottom right turns clockwise by that move's turn count and turns back. The row comparison on the right pushes the same "2 2 4 _" every 1.2 seconds, and the locked row ends as "4 4" (+4) while the unlocked row ends as a single "8" with three parents (+8). A merged tile rides a size spring (stiffness 420, damping 18), swells to 1.159× and settles within ±2% at 0.279 seconds.

One-Line Compression Moves with a Merged-Tile Lock
Open in a new tab

01 · One-Line Compression Moves with a Merged-Tile Lock — Measured area 30.423% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TURNSFOUR_CHANCEPOP_KICK
    const ORDERS = [0, 1, 2, 3].map(pushOrder);
    function compressLine(line, lock) {
        const out = [];
        for (const t of line) {
            if (!t) {
                continue;
            }
            const last = out.length ? out[out.length - 1] : null;
            if (last && last.v === t.v && !(lock && last.merged)) {
                last.v *= 2;
                last.ids.push(t.id);
                last.merged = true;
            }
            else {
                out.push({ ids: [t.id], v: t.v, merged: false });
            }
        }
        return out;
    }

02Slides That Stretch and Squash with Cells Traveled

A slide effect for a number puzzle app: a slide lasts 0.16 seconds plus 0.04 seconds for each cell after the first, and progress accelerates as u², so a tile traveling 3 cells lands at 25 cells per second, stretched to 1.3×. The cross axis takes the inverse of the stretch, so over one loop the product of the width and height scales drifts from 1 by at most 1.1e-16, and on landing the travel axis squashes to 0.843× before a spring (600, 16) brings it back in 0.242 seconds. The small board on the right takes the same 9 inputs with no queue and drops four of them.

Slides That Stretch and Squash with Cells Traveled
Open in a new tab

02 · Slides That Stretch and Squash with Cells Traveled — Measured area 32.073% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SLIDE_PER_CELL_SSTRETCH_PER_SPEEDINPUT_QUEUE_MAX
    // stretch factor: travel-axis scale × cross-axis scale = 1
    function stretchOf(speed) { return Math.min(STRETCH_MAX, 1 + STRETCH_PER_SPEED * speed); }
    function axisScales(s, speed) {
        const along = stretchOf(speed) * Math.max(0.55, 1 + s.squash.x);
        const across = 1 / along;
        return s.axis === 0 ? [along, across] : [across, along];
    }
    function feed02(dir) {
        const dropped = dropper02.dropped;
        pushInput(dropper02, dir, sceneT);
        fates02.push(dropper02.dropped > dropped ? 1 : 0);
        const mv = pushInput(queued02, dir, sceneT);
        if (mv) {
            simStatus = moveStatus(queued02, mv);
        }
    }

03A Swipe Judge That Settles Diagonals on One Axis

A judge for the swipe area of a mobile web game: it measures length, speed and angle from the start and end points alone, drops trails within 10° of a 45° border or shorter than 24px, and still accepts a flick faster than 450px per second from 8px up. Of the 7 demo trails, 52° and 228° sit 7° and 3° from a border and are dropped as diagonal, the 16px, 140ms trail is dropped as short, and the 18px, 30ms flick is accepted as left, for 4 accepted and 3 dropped per loop. Each trail bends sideways in the middle, but the verdict uses only the dashed straight line joining its two ends.

A Swipe Judge That Settles Diagonals on One Axis
Open in a new tab

03 · A Swipe Judge That Settles Diagonals on One Axis — Measured area 29.258% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

SWIPE_MIN_PXFLICK_SPEED_PX_SDEAD_ZONE_DEG
    // screen angle (0° right, 90° down) into wedges: the nearest multiple of 90° is the direction
    const ANGLE_TO_DIR = [DIR_RIGHT, DIR_DOWN, DIR_LEFT, DIR_UP];
    function judgeSwipe(dx, dy, ms) {
        const length = Math.sqrt(dx * dx + dy * dy);
        const speed = length / Math.max(1, ms) * 1000;
        const angle = (deg(Math.atan2(dy, dx)) + 360) % 360;
        const base = { dir: -1, angle: angle, length: length, speed: speed, reason: VERDICT_SHORT };
        const flick = length >= FLICK_MIN_PX && speed >= FLICK_SPEED_PX_S;
        if (length < SWIPE_MIN_PX && !flick) {
            return base;
        }
        // angle left to the border: distance from 45° within the span that repeats every 90°
        const toBorder = Math.abs((angle % 90) - 45);
        if (toBorder < DEAD_ZONE_DEG) {
            base.reason = VERDICT_DIAGONAL;
            return base;
        }
        base.dir = ANGLE_TO_DIR[Math.round(angle / 90) % 4];
        base.reason = length < SWIPE_MIN_PX ? VERDICT_FLICK : VERDICT_OK;
        return base;
    }

04An Undo Rewind Where Merged Tiles Split Apart

Like a one-move takeback on a learning puzzle site, every move records tile ids and merge parents, and at 1.45, 1.85 and 2.25 seconds the record is read backward: merged tiles split into their two parents and return to their start cells while the new tile shrinks away over 0.22 seconds. After three undos the last one, the left move, splits 4 tiles, the score returns to 0 and the board matches the start board. Undoing mid-slide first sets the current board down so no tile shows twice, and with focus on the board Backspace undoes as well.

An Undo Rewind Where Merged Tiles Split Apart
Open in a new tab

04 · An Undo Rewind Where Merged Tiles Split Apart — Measured area 36.685% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

FORWARD_ATUNDO_ATSHRINK_S
    // undo: pop the top of the history, put the board back to the pre-move board and subtract that move's gain from the score
    function undo04() {
        const mv = board04.history.pop();
        if (!mv) {
            simStatus = STATUS_NOTHING;
            return;
        }
        // if a slide is still running, settle the current board first and then rewind, so the same tile does not show twice
        if (sceneT < board04.busyUntil) {
            settleView(board04.view, board04.game.tiles);
        }
        board04.game.tiles = copyTiles(mv.before);
        board04.game.score -= mv.gain;
        board04.busyUntil = animateUndo(board04.view, mv, sceneT);
        board04.queue = [];
        undone04.push(mv);
        simStatus = STATUS_UNDO.replace('{d}', DIR_NAMES[mv.dir]).replace('{m}', String(mv.merges.length)).replace('{s}', String(board04.game.score));
    }

05Colors Chosen by Power-of-Two Step with Digit Fitting

This fits progress tiles that show a learning grade: the step is how many times 2 was doubled (log2), seven anchor colors sit at steps 1, 3, 5, 7, 9, 11 and 13, and the colors between are blended linearly on each of the three oklab values, while the visible step chases a value that doubles every 0.15 seconds and closes 95% of a one-step gap in 0.214 seconds. Digits start at half the cell width and shrink by the overflow ratio once the measured width passes 80% of the cell, so 3 digits draw at 92% and 4 digits at 69%, which puts a 4-digit number at 29.7px type in an 86px cell. The two bands at the top and bottom set oklab blending beside sRGB blending, and between these pastel anchors they differ by at most 1 on any RGB channel, so the bands appear to compare the two formulas rather than show a visible color gap.

Colors Chosen by Power-of-Two Step, with Digit Fitting
Open in a new tab

05 · Colors Chosen by Power-of-Two Step, with Digit Fitting — Measured area 24.76% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

ANCHOR_STEPSDIGIT_FILLCOLOR_RATE
    // digit size fitted: start at cell width × DIGIT_PX_RATIO, and if the measured digit width passes cell width × DIGIT_FILL, shrink by that ratio
    const DIGIT_PX_RATIO = 0.5;
    const DIGIT_FILL = 0.8;
    const DIGIT_WEIGHT = 800;
    function fitDigits(text, cell, m) {
        const px0 = cell * DIGIT_PX_RATIO;
        const raw = textWidth(text, px0, DIGIT_WEIGHT, m);
        const room = cell * DIGIT_FILL;
        const scale = raw > room ? room / raw : 1;
        return { px: px0 * scale, width: raw * scale, rawWidth: raw, scale: scale };
    }

06Merge Bursts and a Combo Shake

For a score game on a launch page where merges need some punch: each merge throws step × 3 shards at step × 34 × 0.55 to 1 pixels per second, pulled by gravity 520 and drag 2.4, and the first left move stacks 7 merges at once for 63 shards and an 11.2px shake. The shake adds 1.6px per merge to whatever strength is left, then fades as e^(−6t), halving every 0.116 seconds while it trembles 16 times a second. A shard leaving sideways at 170px per second travels only 60.1px in its 0.8-second life because of drag, against 136px without it.

Merge Bursts and a Combo Shake
Open in a new tab

06 · Merge Bursts and a Combo Shake — Measured area 29.176% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BURST_PER_STEPBURST_DRAGSHAKE_PER_MERGE
    // current shake strength (px): the last strength set, shakeAmp0, reduced since then by e^(−SHAKE_DECAY·t)
    function shakeAmp(t) { return t < shakeAt ? 0 : shakeAmp0 * Math.exp(-SHAKE_DECAY * (t - shakeAt)); }
    function shakeOffset(t) {
        const a = shakeAmp(t);
        const ph = TAU * SHAKE_HZ * (t - shakeAt);
        return [a * Math.sin(ph), a * 0.6 * Math.cos(ph * 1.3)];
    }
    // cell centers measured from the board's top-left: where shards burst (same values as F06 on the drawing side)
    const F06_X = 24;
    const F06_Y = 22;
    const F06_CELL = 46;
    const F06_GAP = 6;
    function burst(cell, v, t) {
        const k = stepOf(v);
        const n = BURST_PER_STEP * k;
        const cx = F06_X + F06_GAP + colOf(cell) * (F06_CELL + F06_GAP) + F06_CELL / 2;
        const cy = F06_Y + F06_GAP + rowOf(cell) * (F06_CELL + F06_GAP) + F06_CELL / 2;
        for (let i = 0; i < n; i += 1) {
            const a = burstRng() * TAU;
            const sp = BURST_SPEED_PER_STEP * k * (0.55 + 0.45 * burstRng());
            shards.push({ x: cx, y: cy, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - BURST_LIFT, born: t, rot: burstRng() * TAU, spin: (burstRng() - 0.5) * 14, size: 3 + 3 * burstRng(), c: tileColor(k) });
        }
        return n;
    }

07An Auto Player That Counts the Tiles Yet to Fall

For a next-move hint in a puzzle app that suggests one direction: on each of the four pushed boards it places a 2 (probability 0.9) and a 4 (0.1) in every empty cell, averages the scores, then runs one more round of its own move and the falling tile, a two-layer search. On the first board "2 _ _ 2 / 4 2 _ _ / 8 16 4 2 / 64 32 8 4" it gets left 8.5, up 1.98, right 9.43 and down 9.6, picks down, and that single choice scores 1,660 end boards. The bars grow for 0.3 seconds before the move is played, and the board pushed in the chosen direction, before its new tile falls, shows faded underneath.

An Auto Player That Counts the Tiles Yet to Fall
Open in a new tab

07 · An Auto Player That Counts the Tiles Yet to Fall — Measured area 29.16% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SEARCH_DEPTHEMPTY_WEIGHTTHINK_S
    // chance turn: the average over every empty cell of placing a 2 (probability 1 − FOUR_CHANCE) or a 4 (FOUR_CHANCE). each cell in empty is picked with equal probability
    function chanceNode(vals, depth) {
        const empty = [];
        for (let i = 0; i < CELLS; i += 1) {
            if (!vals[i]) {
                empty.push(i);
            }
        }
        if (!empty.length) {
            return depth > 1 ? moveNode(vals, depth - 1) : evaluate(vals);
        }
        let total = 0;
        for (const cell of empty) {
            for (const v of [2, 4]) {
                const p = v === 2 ? 1 - FOUR_CHANCE : FOUR_CHANCE;
                const next = vals.slice();
                next[cell] = v;
                total += p * (depth > 1 ? moveNode(next, depth - 1) : evaluate(next));
            }
        }
        return total / empty.length;
    }

08A Board That Collapses When No Pairs Are Left

For a game-over and replay screen: every move counts the empty cells and the touching equal pairs, links the pairs left with violet lines, and treats the board as finished the moment both reach 0. From the start board two pushes left bring the pairs to 0, the collapse begins at 0.967 seconds, and the 16 tiles, each delayed by 0 to 0.3 seconds, fall under gravity of 1600px/s² and bounce 56 times in all with restitution 0.42 before resting at 2.25 seconds. Then the first board's tiles drop from 120px above, bottom row first at 0.035-second intervals, and a new board begins at 3.588 seconds.

A Board That Collapses When No Pairs Are Left
Open in a new tab

08 · A Board That Collapses When No Pairs Are Left — Measured area 34.712% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FALL_GRAVITYBOUNCECOLLAPSE_SPREAD_S
    function equalPairs(vals) {
        const out = [];
        for (let r = 0; r < SIZE; r += 1) {
            for (let c = 0; c < SIZE; c += 1) {
                const i = r * SIZE + c;
                if (!vals[i]) {
                    continue;
                }
                if (c + 1 < SIZE && vals[i + 1] === vals[i]) {
                    out.push({ a: i, b: i + 1 });
                }
                if (r + 1 < SIZE && vals[i + SIZE] === vals[i]) {
                    out.push({ a: i, b: i + SIZE });
                }
            }
        }
        return out;
    }
    function countEmpty(vals) { let n = 0; for (const v of vals) {
        if (!v) {
            n += 1;
        }
    } return n; }
    function isStuck(vals) { return countEmpty(vals) === 0 && equalPairs(vals).length === 0; }

09A Hero Scene Where the 2048 Board Plays Itself

For the first screen of a learning app's landing page, where the board has to look alive untouched: the two-layer search from 07 picks a direction, a 70px, 130ms pretend swipe is drawn and judged by judgeSwipe from 03, and the stretch from 02, the shards and shake from 06 and the collapse from 08 follow with the same constants. The board collapses and starts over when it gets stuck or after 16 moves, and the demo ends a loop at 6.646 seconds with a score of 368 and a top tile of 256, then returns to the first board at 8.946 seconds. ROUND_MOVES exists because the search dodges dead ends so well that without it the board plays 95 moves over 40 seconds and is still not stuck.

A Hero Scene Where the 2048 Board Plays Itself
Open in a new tab

09 · A Hero Scene Where the 2048 Board Plays Itself — Measured area 25.374% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ROUND_MOVESSWIPE_MSMOVE_GAP_09
    function afterMove9(mv) {
        if (!mv || !mv.moved) {
            return;
        }
        moves9 += 1;
        simStatus = STATUS_HERO.replace('{d}', DIR_NAMES[mv.dir]).replace('{m}', String(mv.merges.length)).replace('{s}', String(hero.game.score));
        let first = NO_HIDE;
        for (const s of hero.view.sprites) {
            for (const m of mv.merges) {
                if (s.id === m.id) {
                    first = Math.min(first, s.show);
                    pend9.push({ at: s.show, cell: m.cell, v: m.v });
                }
            }
        }
        if (mv.merges.length) {
            shakeAmp9 = shake9(first) + SHAKE_PER_MERGE * mv.merges.length;
            shakeAt9 = first;
        }
        stuckReal9 = isStuck(valuesOf(hero.game.tiles));
        if (stuckReal9 || moves9 >= ROUND_MOVES) {
            stuckAt9 = hero.busyUntil + STUCK_HOLD_S;
        }
    }

Rotate the Board to Solve One Direction, Split Swipes into Wedges, Pick the Next Move by Expected Value: Three Calculations Behind Each Move

How many clockwise turns bring that wall to the left

rotateCW turns the cell-number table clockwise once, placing the original cell at row 3 − c, column r into the new row r, column c. The turn counts in TURNS are left 0, up 3, right 2 and down 1, so when pushing up, the first row of the turned table read from the left is the rightmost column read top to bottom: 3, 7, 11, 15. Only the reading order is prepared four times and the board itself stays put, so for the tiles on screen a rule like the lock lives in one place, compressLine. Inside the search, 07 and 09 skip compressLine and use a separate slideValues on plain number arrays that carries its own locked flag. The small board at the bottom right of 01, which springs (260, 24) to TURNS × 90° and back to 0°, is this table made visible.

The share one wedge accepts, and the flick threshold

03 drops an angle whose remainder after dividing by 90° lies within 10° of 45°, so of each 90° wedge, 70° is accepted: (90 − 2 × 10) ÷ 90 = 77.8%. A trail shorter than 24px still counts at 450px per second or faster, and covering 24px at that speed takes 24 ÷ 450 × 1000 = 53.3ms or less. Feeding in 1,000 seeded trails (4 to 120px, 30 to 260ms, random angle) gives 627 accepted (624 straight, 3 flicks), 180 short and 193 diagonal. For another screen driven by swipes, see the image slider post.

The expected value of empty cells and a three-term score

chanceNode weights the result of a 2 placed in each empty cell by 0.9 and a 4 by 0.1, adds them up and divides by the number of empty cells. With no empty cell it goes straight to the next own-move layer, and at the bottom layer evaluate scores the board. The score is empty × 2.7 + monotonicity × 1.0 + smoothness × 0.1; the first board of 07 has 4 empty cells, monotonicity −3 and smoothness −20, so it comes to 10.8 − 3 − 2 = 5.8. The formula looks like the familiar advice of leaving cells free and keeping big numbers lined up in order, written as three terms. Two layers look at 1,660 end boards and a third layer swells that to 66,350, so depth should rise only as far as one frame can carry it.

Where a 2048 Board Breaks: a Tile Merging Twice in One Move, a Swipe Arriving Mid-Slide and Four-Digit Numbers Spilling Out of the Cell

A tile with three or four parents in an unlocked row

Calling compressLine([2,2,4,_]) without the lock turns 2 and 2 into 4, then lets that 4 merge with the next 4, leaving one 8 with three parents. "2 2 4 8" becomes "4 · 4 · 8" with the lock and a single "16" with four parents without it, and "4 4 8 _" becomes "16" instead of "8 · 8". "2 2 2 2" pairs from the front, so it comes out as "4 · 4" on both boards, which is how this bug hides from a quick test by hand. The fix is one condition in compressLine: !(lock && last.merged).

A swipe arriving during the 0.2-second slide

Four of the 9 demo inputs in 02 arrive 0.1 seconds after the previous move starts sliding. The board with a one-slot queue (INPUT_QUEUE_MAX 1) plays all nine and reaches a score of 48, while the board without a queue drops the second inputs at 0.3, 1.0, 2.3 and 3.0 seconds, plays only five moves and stops at 16. The final boards differ as well, so for someone swiping fast in a row the board would likely feel as if it swallowed input. Capping the queue at one slot and dropping input once it is full appears meant to keep the board from playing several moves on its own after the finger lifts.

Four-digit numbers at 115.9% of the cell width

The digits 0 to 9 in this font share one width at weight 800, so the width of a number depends only on how many digits it has. With the type fixed at cell × 0.5, a 4-digit number is 4 × 0.5796 × 0.5 = 115.9% of the cell, spilling 7.3px past a 46px board cell and 13.7px past an 86px cell. To pin the scale that fitDigits picks to 1 in the index.html of folder 05 and compare the spilled tiles in the large cells yourself, unzip the package with j4u24xfe as the password. With fitting on, numbers shrink from 3 digits up once they pass cell × 0.8, so a 4-digit number that would spill 23.9px even in a 150px cell stays inside.

What If More 4s Fall? Four-Tile Odds, Search Depth and Minimum Swipe Length Side by Side

Name Changed value Feel
More 4s falling 01 FOUR_CHANCE 0.1 → 0.25 Of 10,000 new tiles, 4s go from 1,001 to 2,523 and the mean value from 2.2 to 2.505; with the same fixed policy, 300 seeded boards get stuck after 155.3 moves on average instead of 200.9
Looking one move further 07 SEARCH_DEPTH 2 → 3 End boards for the first board go from 1,660 to 66,350 and the score after 40 moves from 560 to 620; depth 3 took 189.7ms per move on average and 339.4ms at the longest (measured once during development)
A longer minimum swipe 03 SWIPE_MIN_PX 24 → 48 Of 1,000 trails, accepted 627 → 504, dropped as short 180 → 344, accepted as flicks 3 → 45

Numbers to check before changing a value

Raising the chance of a 4 to 0.25 brings the stuck point about 45 moves earlier and drops the mean top tile at the end from 181.3 to 153.6, which makes it a handy difficulty knob. Depth 3 earns 60 more points but multiplies the end boards by 40, and since the search runs synchronously inside one frame, the screen hitches for as long as the longest moves take. Milliseconds vary by machine, so compare end-board counts instead. A 48px swipe threshold leaves the flick threshold at 450px per second, so short trails drawn within 106.7ms come in as flicks more often, and the 7 demo trails keep their results because the two short ones are 16 and 18px.

The Same Move from the Arrow Keys: a Sentence Reading Each Result, a Pause Button and Reduced Motion for Accessibility

Keys only while the board has focus

The board box carries tabindex="0" and role="application" with the name "2048 board: click it, then use the arrow keys or swipe to move", and its keydown handler calls preventDefault() on arrow keys only when Alt, Ctrl and Meta are not held. Arrow keys pressed outside the board keep their default action in all nine scenes, so page scrolling is left alone, while 04 adds "Backspace to undo" to the name and 05 names Up or Right for raising the step and Down or Left for lowering it. The status line starts as aria-live="off", switches to polite after the first input, and reads each move as one short sentence in the form {d} · merges {m} · score {s}. Short notices like this get their own treatment in the toast notification post.

Reduced motion and the pause button

Under prefers-reduced-motion: reduce the loop stops and one still board is drawn, with zero pixel difference between the frames at 0.4 and 1.2 seconds, and a key press then jumps straight to the result board without sliding and draws it once. Shards, shake and collapse are left off the still board, and the loop runs again once the setting is lifted. The pause <button> in each scene switches to aria-pressed="true" when pressed, and each scene halts while its tab is hidden and continues on return.

Questions on 2048 Game Code in JavaScript: Board Size, Saving a Game in Progress and Phone Swipes

Where do I change things to make the board 5×5

On the rules side, only SIZE. CELLS, rotateCW, pushOrder, slideBoard and equalPairs all count cells through SIZE, so changing 4 to 5 brings the order tables and the pair count along. The drawing side, though, fixes the cell size per scene in constants (such as F06_CELL 46 in 06), so cells and gaps need new values to fit inside 480×300, and more empty cells also grow the end-board count of the 07 search.

If I add save and resume, does the 2048 game source code give the same next tile

The demos use no storage API, so a fresh load starts from the first board. Saving the tiles (id, value, cell), the score and the next tile id restores the board, but makeRng keeps its random state inside a closure, so matching the next falling tile as well would likely need that state exposed for saving. Undo in 04 shows the same gap: undo04 restores the tiles and the score but not g.rng, so playing the same direction again would likely drop a different new tile, since spawnTile keeps drawing from that advanced state.

Does swiping the board on a phone also scroll the page

touch-action: none applies only while the board has focus (.cf__board:focus), so after one tap to focus, swipes go to the board, and without focus a touch scroll goes to the page. Swipes feed the coordinates and time gap of pointerdown and pointerup into the same judgeSwipe as 03, so all nine scenes judge by one rule. For a screen that uses the scroll position itself in its math, see the PDF viewer post. The event order of pointer input is documented in MDN Pointer events, and the color space used for blending in MDN oklab().

Enter the archive password

The password is inside this article. You will find it as you read.