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

CSS Tic Tac Toe: 9 Boards That Count Ahead

Before you place a mark on a css tic tac toe board, the computer has counted the endings left — the score heat in scene 03 and the odds ring in scene 04 draw

Play All Nine Tic Tac Toe Boards Right on This Page

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

The nine boards widen from one mark landing to a calculation that reads the whole position. Scene 01 draws a mark stroke by stroke while its cell springs, 02 tilts the board toward the pointer, and 03 and 04 lay scores and outcome shares over the cells. Scene 05 is the moment a game ends, 06 nests a board inside a board, 07 and 08 fold or branch one position, and 09 finishes with two computers playing on chalk.

CSS Tic Tac Toe: 9 Boards Where the Calculation Is Drawn on the Cells

Every board is a single canvas. The script inside index.html paints marks, cells and score colors and decides the next move, while CSS keeps only the stage size, the status pill and button above it, and the keyboard focus outline. The .tt stage is min(92vw, 160vh - 12.8vw) wide at a 480:300 ratio, and the canvas resolution is 480×300 times devicePixelRatio (capped at 2), so cell coordinates do not move when the window shrinks. A pointer position is mapped back into that 480×300 space through the getBoundingClientRect ratio.

All nine boards follow the same turn rules

You play X and the computer plays O. After your mark lands, bestCell waits 420 ms and puts O on the cell with the highest full-search score, taking the lowest-numbered cell on a tie, and any input during that wait or on a filled cell is dropped.

If you stop for 2.4 seconds (150 ticks), both sides are played by the computer. X picks with a seeded random number when more than six cells are empty or on a 30% roll and otherwise calls bestCell; scene 05 is a scripted game that finishes eight lines in a fixed order, and scene 06 has its own small-board rules and picks with the chooseMove bonus formula. Once a game is over, your next tap opens a fresh one.

Input is a cursor on the canvas, not a button per cell

The canvas carries tabindex="0" and an aria-label, and the arrow keys move a cursor over the 3×3 grid (9×9 in scene 06) while Enter or Space places the mark. No cell has its own <button>, so the only <button> in a file is the pause and play control, and the cursor cell gets a 2.5 px white outline only while the canvas has focus.

The status line is aria-live="polite" and updates only when its wording changes, such as X to move or O wins. When a keyboard cursor exists, a row-column pair is appended.

Time and randomness are pinned

A harness calls scene.step() at a fixed 16 ms step (FIXED_DT) and draws once per requestAnimationFrame. Random numbers come only from a seeded linear congruential generator, lcg(seed) (each scene has its own seed: scene 01 passes 11 to makeMatch, 03 uses lcg(31), 06 lcg(61), 07 lcg(71) and 09 lcg(91), and 09 has a separate lcg(5) for the background; scene 05 is scripted, so no seed changes it), and a grep finds zero Math.random, new Date, Date.now, fetch or localStorage in all nine files.

Where these boards fit on a page

Scene 01 suits a mobile match app or a board game intro page, and 02 a board game cafe landing or a toy shop hero. Scene 03 fits an explainer on how a game computer plays and a difficulty guide, and 04 a strategy tutorial or a move-reading practice page.

Scene 05 belongs on a victory screen or a shareable result card, and 06 in a board game portfolio or a match mode introduction. Scene 07 serves math education pages and puzzle write-ups, 08 algorithm lectures and dev blog diagrams, and 09 an education service landing or a developer event poster.

Code for Each Technique Behind an HTML CSS Tic Tac Toe Board

Each snippet is a run of consecutive lines cut straight out of the <script> in index.html, and none passes 25 lines. A shared layer of roughly 520 lines (mark drawing, harness, turn driver) comes first in every file, and the scene code starts near line 528. If you came looking for a css grid tic tac toe where each cell is a div with a click handler, these boards go a layer deeper: they show where the board computes and draws its values.

01Placing a Mark That Draws X and O Stroke by Stroke While the Cell Presses In and Springs Back

X is drawn in two strokes and O in one clockwise lap from 12 o'clock, each by shrinking a setLineDash length through lineDashOffset. The cell size follows a = -SPRING_K * (pos[i] - 1) - damp * vel[i], a damped spring that overshoots once and settles, and the snippet below integrates it four sub-steps per 16 ms tick. The render logged 19.3% cumulative area, an average intensity of 45.1 and 21/23 moving frames.

Placing a Mark That Draws X and O Stroke by Stroke While the Cell Presses In and Springs Back
Open in a new tab

01 · Placing a Mark That Draws X and O Stroke by Stroke While the Cell Presses In and Springs Back — Measured area 19.281% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

MARK_MSOVERSHOOTSTROKE_WSPRING_K
                const dt = FIXED_DT / 1000;
                for (let i = 0; i < 9; i += 1) {
                    if (age[i] < 0) {
                        continue;
                    }
                    age[i] += FIXED_DT;
                    // split the 16 ms step into four sub-steps; one big step gives a smaller overshoot than the formula targets
                    for (let q = 0; q < 4; q += 1) {
                        const a = -SPRING_K * (pos[i] - 1) - damp * vel[i];
                        vel[i] += (a * dt) / 4;
                        pos[i] += (vel[i] * dt) / 4;
                    }
                }

02A Solid Board That Tilts in Perspective With the Pointer and Casts a Longer, Softer Shadow While a Mark Falls Into Place

The board is a 252 px square, and the pointer feeds the tilt through lerp(ax, tx, LERP_K) each tick, which reaches 95% of the target after 32 ticks (512 ms). A shadow stretches and spreads with the mark's height, yet the spread is four overlapping strokes of different widths instead of a real blur: a resting mark gets length 9.9 px and an outer stroke near 17.5 px, while a mark at 90 px up gets 59.4 px and near 56 px. This board measured 26.7% cumulative area, an average intensity of 45.6 and 23/23 moving frames.

A Solid Board That Tilts in Perspective With the Pointer and Casts a Longer, Softer Shadow While a Mark Falls Into Place
Open in a new tab

02 · A Solid Board That Tilts in Perspective With the Pointer and Casts a Longer, Softer Shadow While a Mark Falls Into Place — Measured area 26.728% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TILT_MAXDEPTH_PXSHADOW_LENLERP_K
                for (let i = 0; i < 9; i += 1) {
                    if (who[i] === 0) {
                        continue;
                    }
                    const hgt = zb[i] + DEPTH_PX;
                    const off = hgt * SHADOW_LEN;
                    const blur = 1 + hgt * 0.16;
                    const alpha = clamp(0.5 - hgt * 0.0016, 0.18, 0.5);
                    for (let pass = 3; pass >= 0; pass -= 1) {
                        strokeMark(ctx, who[i], i, 0.5, 'rgba(10,8,22,' + (alpha / 4).toFixed(3) + ')', 7 + blur * pass * 0.9, off * 0.5, off * 0.8);
                    }
                }

03A Board Where Every Empty Cell Is Tinted by a Score Read Out to the Very End and the Best Cell Lights Up

For each empty cell, solve plays the resulting position out to the end and passes up SCORE_WIN - ply when X wins, the negative of that when X loses and 0 for a draw. Positive scores tint mint and negative ones pink, and on an empty board every first move scores 0, so no number appears and only the ratio of the side to move's wins minus losses among the endings to the ending count separates the pale tints; the best cell starts a border pulse after 700 ms. The measurement shows 37.2% cumulative area, an average intensity of 30.2 and 21/23 moving frames.

A Board Where Every Empty Cell Is Tinted by a Score Read Out to the Very End and the Best Cell Lights Up
Open in a new tab

03 · A Board Where Every Empty Cell Is Tinted by a Score Read Out to the Very End and the Best Cell Lights Up — Measured area 37.219% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

SEARCH_DEPTHSCORE_WINHEAT_ALPHATHINK_MS
        let best = turn === 1 ? -1000 : 1000;
        let any = false;
        for (let i = 0; i < 9; i += 1) {
            if (b[i] !== 0) {
                continue;
            }
            b[i] = turn;
            const v = solve(b, 3 - turn, ply + 1, depth - 1, cnt);
            b[i] = 0;
            any = true;
            if (turn === 1 ? v > best : v < best) {
                best = v;
            }
        }
        return any ? best : 0;

04A Board Where Hovering a Cell Spreads a Ring of Win, Draw and Loss Shares Around a Ghost Mark

With a mark pretended into the hovered empty cell, enumerate walks the endings depth first and turns the X win, draw and O win shares into ring arc lengths. Pointing at a corner cell on an empty board visits 59,705 positions, which passes the ENUM_CAP of 20,000, so only 9,074 of the 27,732 endings are counted; a dotted outer ring appears and the status line adds a cap note. The render gave 22.5% cumulative area, an average intensity of 115.8 and 21/23 moving frames.

A Board Where Hovering a Cell Spreads a Ring of Win, Draw and Loss Shares Around a Ghost Mark
Open in a new tab

04 · A Board Where Hovering a Cell Spreads a Ring of Win, Draw and Loss Shares Around a Ghost Mark — Measured area 22.503% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

ODDS_RADIUSHOVER_LERPGHOST_ALPHAENUM_CAP
    function enumerate(b, turn, t, cap) {
        if (t.capped) {
            return;
        }
        t.nodes += 1;
        if (t.nodes > cap) {
            t.capped = true;
            return;
        }
        const w = lineOf(b);
        if (w >= 0) {
            if (b[LINES[w][0]] === 1) {
                t.x += 1;
            }
            else {
                t.o += 1;
            }
            return;
        }

05A Win Scene Where a Line Sweeps Through the Centers of Three Matching Cells and Its Glow Swells and Fades

After every move the eight line combinations are checked, and a winning line is extended 0.3 of a cell past both end centers and grown over LINE_MS, 420 ms. The glow gives shadowBlur the value GLOW_BLUR times progress while the line grows, then Math.cos(fade * Math.PI / 2) shrinks it to 0 over 900 ms, and because shadow blur ignores the canvas transform the code multiplies it by devicePixelRatio. The scene measured 11.4% cumulative area, an average intensity of 97.6 and 14/23 moving frames.

A Win Scene Where a Line Sweeps Through the Centers of Three Matching Cells and Its Glow Swells and Fades
Open in a new tab

05 · A Win Scene Where a Line Sweeps Through the Centers of Three Matching Cells and Its Glow Swells and Fades — Measured area 11.423% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

LINE_MSGLOW_BLURWIN_LINESFADE_ALPHA
                    x0 -= ux * CELL * 0.3;
                    y0 -= uy * CELL * 0.3;
                    x1 += ux * CELL * 0.3;
                    y1 += uy * CELL * 0.3;
                    const prog = smooth(winAge / LINE_MS);
                    // glow radius: grows while the line draws, then shrinks along a sine curve once it is done
                    const fade = clamp((winAge - LINE_MS) / GLOW_FADE_MS, 0, 1);
                    const glow = GLOW_BLUR * (winAge < LINE_MS ? prog : Math.cos(fade * Math.PI / 2)) * dprG;
                    ctx.save();
                    ctx.shadowColor = rgba(MINT, 1);
                    ctx.shadowBlur = glow;
                    ctx.strokeStyle = rgba(mix(MINT, CREAM, 0.35), 1);
                    ctx.lineWidth = 9;
                    ctx.lineCap = 'round';
                    ctx.beginPath();
                    ctx.moveTo(x0, y0);
                    ctx.lineTo(lerp(x0, x1, prog), lerp(y0, y1, prog));
                    ctx.stroke();
                    ctx.restore();

06A Board Inside a Board Where the Cell You Pick Chooses the Next Board and the View Springs In to It

The index of the cell you pick becomes the index of the small board to play next, and if that board is already won or full you may choose any small board. Camera position and scale each chase the target from goal(), a small-board center at ZOOM_K 2.6x, through their own springs, so the scale peaks at 2.725 and settles within 2% of the travel distance in 576 ms. This nested board covers 59.0% cumulative area, the widest of the nine, with an average intensity of 32.3 and 19/23 moving frames.

A Board Inside a Board Where the Cell You Pick Chooses the Next Board and the View Springs In to It
Open in a new tab

06 · A Board Inside a Board Where the Cell You Pick Chooses the Next Board and the View Springs In to It — Measured area 59.026% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

ZOOM_KFOCUS_MSDIM_ALPHASPRING_ZETA
        // camera target: zoom to the dictated small board, otherwise the full view
        function goal() {
            if (fb >= 0) {
                return [(fb % 3) * SMALL + SMALL / 2, Math.floor(fb / 3) * SMALL + SMALL / 2, ZOOM_K];
            }
            return [BIG / 2, BIG / 2, 1];
        }

07A Symmetry Fold View That Merges Positions Equal Under Rotation and Flip Into One Board

Four rotations times two reflections give eight transforms, which buildSym stores as cell-permutation tables; the position is permuted eight times, joined into strings, and the smallest string becomes its canonical form. Eight translucent boards open into a 104 px ring around the center and fold into one spot over 800 ms while the status line reports how many distinct shapes remain out of 8. This scene measured 29.1% cumulative area, an average intensity of 70.0 and 21/23 moving frames.

A Symmetry Fold View That Merges Positions Equal Under Rotation and Flip Into One Board
Open in a new tab

07 · A Symmetry Fold View That Merges Positions Equal Under Rotation and Flip Into One Board — Measured area 29.139% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

SYM_COUNTFOLD_MSSPREAD_RMERGE_ALPHA
    function buildSym() {
        const out = [];
        for (let f = 0; f < 2; f += 1) {
            for (let r = 0; r < 4; r += 1) {
                const perm = [];
                for (let i = 0; i < 9; i += 1) {
                    let row = Math.floor(i / 3);
                    let col = i % 3;
                    for (let q = 0; q < r; q += 1) {
                        const nr = col;
                        col = 2 - row;
                        row = nr;
                    }
                    if (f === 1) {
                        col = 2 - col;
                    }
                    perm.push(row * 3 + col);
                }
                out.push(perm);
            }
        }
        return out;
    }

08A Tree View Where Moves Branch From the Current Position and Branch Thickness Follows the Number of Endings Below

The root is a 54 px mini board for the current position, each empty cell becomes a 26 px child board on a 70 px radius circle with a 55 ms stagger, and grandchild dots follow 160 ms later. Branch width is 1 plus BRANCH_W (5) times the log ratio of endings below, colored sky blue for a positive played-out score, pink for a negative one and cream for 0. The tree logged 18.6% cumulative area, an average intensity of 75.0 and 16/23 moving frames.

A Tree View Where Moves Branch From the Current Position and Branch Thickness Follows the Number of Endings Below
Open in a new tab

08 · A Tree View Where Moves Branch From the Current Position and Branch Thickness Follows the Number of Endings Below — Measured area 18.648% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

RING_RNODE_RBRANCH_WEXPAND_MS
        function branch(ctx, x0, y0, x1, y1, leaves, mx, prog, col, hot) {
            const w = 1 + BRANCH_W * (Math.log(1 + leaves) / Math.log(1 + mx));
            ctx.strokeStyle = rgba(col, hot ? 0.95 : 0.55);
            ctx.lineWidth = w;
            ctx.lineCap = 'round';
            ctx.beginPath();
            ctx.moveTo(x0, y0);
            ctx.lineTo(lerp(x0, x1, prog), lerp(y0, y1, prog));
            ctx.stroke();
        }

09A Hero Where Two AIs Play Each Other on a Chalk-Textured Board, Dust Drifts, and an Eraser Wipes It Clean

A chalk line is a straight segment cut every 5 px and nudged sideways by value noise times 1.8 px, and dust comes from a pool of 90 particles, two per drawing tick, under gravity and wind. Four hundred ms after the last move an eraser sweeps from x 74 to x 406 over 800 ms and clears the board, redrawing marks only to the right of the band so the swept area looks empty. The blackboard registered 45.6% cumulative area, an average intensity of 85.6 and 22/23 moving frames.

A Hero Where Two AIs Play Each Other on a Chalk-Textured Board, Dust Drifts, and an Eraser Wipes It Clean
Open in a new tab

09 · A Hero Where Two AIs Play Each Other on a Chalk-Textured Board, Dust Drifts, and an Eraser Wipes It Clean — Measured area 45.573% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

CHALK_NOISEDUST_NTURN_MSERASE_MS
        function chalkLine(x0, y0, x1, y1, seed) {
            const len = Math.hypot(x1 - x0, y1 - y0);
            const n = Math.max(2, Math.ceil(len / 5));
            const nx = -(y1 - y0) / len;
            const ny = (x1 - x0) / len;
            const out = [];
            for (let k = 0; k <= n; k += 1) {
                const t = k / n;
                const off = (noise2(t * len * 0.045, seed * 7.31, 3) - 0.5) * 2 * CHALK_NOISE;
                out.push([lerp(x0, x1, t) + nx * off, lerp(y0, y1, t) + ny * off]);
            }
            return out;
        }

Where the Values Change the Feel — Spring Overshoot, Search Depth, Glow Radius

These three numbers did not come from a screenshot: they were counted by calling the functions in the source directly in node. The values measured on rendered frames are gathered in the table that follows.

Raising the overshoot from 1.1 to 1.6 stretches settling from 368 ms to 1,424 ms

With scene 01's OVERSHOOT at 1.1 the damping ratio is 0.591, and a cell peaks at 1.093x after 240 ms and settles within 2% by 368 ms. At 1.6 the damping ratio falls to 0.160, the peak reaches 1.598x at 192 ms and settling takes 1,424 ms. Since O answers after 420 ms, a next mark can land while the earlier one is still wobbling.

Lowering the search depth from 9 to 3 flattens the heat map

Scene 03's SEARCH_DEPTH of 9 visits 549,945 positions on an empty board (about 13 ms per cellScores call in node), and 3 visits only 585. Counting the 81 positions with one or two marks placed, which give 576 candidate cells, a signed score appears on 276 cells at depth 9 and on none at depth 3.

Raising the glow blur from 6 to 20 makes the lit area about 2.3 times larger

With scene 05's GLOW_BLUR at 6, one 280 px line 9 px thick on a 480×300 canvas lights pixels out to 10 px from the center line, 5,900 pixels in total. At 20 the reach is 22 px and 13,348 pixels, roughly 2.3 times the area, which is enough for the light to spill into the gaps between cells.

The renders of all nine scenes gave the numbers below. Moving frames run from 14 to 23 across the scenes, and this measurement alone could not tell why scene 05 sits lowest at 14/23.

Scene Cumulative area Average intensity Moving frames
01 Spring mark placement 19.3% 45.1 21/23
02 Solid board 26.7% 45.6 23/23
03 Score heat 37.2% 30.2 21/23
04 Odds ring 22.5% 115.8 21/23
05 Win line 11.4% 97.6 14/23
06 Nested board 59.0% 32.3 19/23
07 Symmetry fold 29.1% 70.0 21/23
08 Game tree 18.6% 75.0 16/23
09 Chalkboard self-play 45.6% 85.6 22/23

Where It Breaks — Rapid Clicks, After the Game Ends, Reduced-Motion Settings

Rapid clicks vanish while O is answering

In all nine boards turn !== 1 holds for 420 ms after your move, so new input is discarded during that stretch. Rapid tapping can look dead for that window, and a press on an occupied cell is ignored for the same reason.

Scene 02 can show a falling mark away from the cell you must click

The hit test hit uses the footprint at z = 0, not the mark's height. At the maximum tilt of 0.42 rad a mark 90 px up appears higher on screen: roughly 37 px if you ignore perspective, and about 43 px on the middle row once proj() applies it (31 to 52 px depending on the row). So while it falls, the drawn mark and the cell you have to press are offset.

Scene 04's empty-board shares lean toward the early cells

A count stopped at the 20,000 cap goes depth first, so branches under the low-numbered cells are counted first. With one X in the center and the first empty cell hovered, the visit count is 6,812, under the cap, and the endings are counted all the way.

Scene 05 cuts its effect short if you tap after the game ends

Whether the line is still growing or the glow is fading, a tap starts a new game and the effect disappears at once. A draw has neither a line nor a glow.

Scene 06's cells are small in the full view

On the canvas a cell is 30 px in the full view and 78 px zoomed in. At a 320×200 viewport .tt shrinks to about 279 px wide and those become roughly 17 px and 45 px, which is small for a touch target in the full view. When no small board is dictated the camera zooms back out to the full view, yet a click is still resolved in cell coordinates.

Scene 07 ignores taps while folding

Input is ignored for the 1,060 ms of folding and settling, because the center board turns into the canonical shape and no longer lines up with the real cells. The 5,478 reachable positions fold into 765 canonical ones, and a corner X with an edge O has eight variants that are all different, so the shape count there goes up to 8 of 8.

Scene 08 shows almost no thickness difference on an empty board

Below an empty board there are 255,168 endings, and each child holds 25,872 to 29,592, so widths cluster between 5.9 and 6.0 px. With two marks down, the seven children hold 441 to 576 endings each and widths differ by about 0.2 px; the real difference shows at the grandchild layer.

Scene 09's background is blurry on a high-density screen

The offscreen canvas (480×300) that bakes the background blur is not multiplied by devicePixelRatio, so it is stretched twice at dpr 2 and looks soft. When the dust pool fills to 90, the oldest particle disappears first.

To edit the three values by hand, take the archive on the card below. It unzips with the password gvrueby4, and every board in it comes as an HTML and SCSS pair along with a React version.

Three Variants and Accessibility

Name Value Feel
Change the spring overshoot 01 OVERSHOOT 1.1 → 1.6 The overshoot grows. The damping ratio drops from 0.59 to 0.16, the peak rises from 1.09x to 1.60x, and the time to settle within 2% stretches from about 370 ms to about 1,420 ms, so the mark wobbles for a good while.
Change the search depth 03 SEARCH_DEPTH 9 → 3 The heat map flattens. Across the 576 candidate cells in all 81 positions with one or two marks placed, 276 carry a non-zero score at depth 9 and 0 at depth 3, and the positions searched on an empty board drop from 549,945 to 585, so the thinking time nearly vanishes.
Change the glow radius 05 GLOW_BLUR 6 → 20 The glow spreads wide. For one 280 px line drawn on a 480x300 canvas, the distance the light reaches from the center line grows from 10 px to 22 px and the lit area from 5,900 px to 13,348 px, about 2.3 times, so it bleeds across the gaps between cells.

These figures come from calling the source functions in node, as in the values section above.

All nine files share one input skeleton. Each has exactly one <button>, a canvas with tabindex="0" and an aria-label, and one aria-live status line. preventDefault is called in just two places, the arrow-key branch and the Enter and Space branch, so Tab is never blocked and focus moves from the canvas to the button, and none of the nine uses setPointerCapture.

prefers-reduced-motion is read in one matchMedia call in the script and one place in CSS. When it is on, no requestAnimationFrame loop starts and a still frame with three marks is drawn; input redraws at once through show() without interpolation and O does not wait to reply, and the button turns into a next-scene control. Scene 01 shows the final size and finished strokes with no spring, 02 a tilt of 0 and a drop height of 0, 04 a ring already at its target shares, and 06 a camera already on target. Scene 07 pins the eight variants on the ring with no fold, 08 opens its branches instantly, and 09 has no dust or eraser.

Is HTML CSS JavaScript Tic Tac Toe Enough Without a Library?

The nine files use no library, CDN or WebGL, and <script src and http each appear zero times, so one file is enough to run. The score search, the odds count, the symmetry fold and the game tree are all computed by JavaScript inside that file, which is what a tic tac toe game html css js project needs for these views.

What it lacks is the product layer. There is no localStorage for win records, no AudioContext for sound, and no mode for two people at one screen. The reflection angles of a ball and bricks are covered in Brick Breaker Game JavaScript — 9 Physics Patterns, and a snake that plans its route is in CSS Snake Game: 9 Boards That Find Their Own Path. For a wider sweep of genres, open 9 CSS Game Demos — Mini Games in HTML & CSS and HTML CSS JS mini games: 9 copy-paste mini apps next.

Frequently Asked Questions Before Reusing Tic Tac Toe Game HTML CSS JS Code

Can I paste this html css tic tac toe source straight into my site?

The files call no outside address, so there is no dependency list to carry along. Each scene is one index.html holding its styles and script, and seeded randomness makes the same game reproduce. The on-screen wording follows the archive you take: the Korean archive holds Korean text and the English archive holds English, so change it to your site's language either way.

Where does the computer choose its move?

After your move, bestCell picks the O reply in scenes 01 to 05 and 07 to 09: the cell with the best full-search score, the lowest number on a tie. Only scene 06 has small boards, so chooseMove decides there with bonuses such as +100 for completing a line, +40 for blocking the opponent's line and +6 for the center, minus 30 for handing the opponent an immediate win, plus lcg noise. The scores in 03 are full-search values and the shares in 04 are a capped count of endings, so both show how the board is read and neither promises a win.

How do I get a different game every time?

X's first moves in the self-playing scenes come from an lcg seed, so changing the seed number gives another game (scene 05 is scripted, so a seed does not change it). To randomize on every load you would have to supply that value yourself, because these files never generate one.

Enter the archive password

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