Changelog
Only what changed and when. Dates are the days it actually went out.
-
All ten card games are done
Cheat, Speed, Memory, Old Maid and Spot the Pair round out the set, and with
One Card, shipped earlier, six of the ten are built differently from the four solitaires —
there is an opponent, and you cannot see their hand.So "only deal boards that can be solved", which the four solitaires use, does not apply.
Instead each game got one promise nailed down by a test.- One Card — the game always ends. When the stock runs dry, the pile is turned over and reused.
Four hundred games are played to the end to confirm not one of them fails to finish.
And the computer only ever receives what has been filtered — a test digs out everything that crosses that boundary- Cheat — the game collapses if card values leak to the opponent. hidden() is the boundary,
and one extra field there fails the test. The computer suspects by arithmetic, not by tells —
there are only four of any rank, so counting its own hand exposes a certain lie- Speed — time enters the rules. But put a clock in the engine and it cannot be tested.
So the engine never looks at the clock at all. When a card is played is the screen's business
- Memory — the computer's memory is held apart from the board. Difficulty simply means "how well it remembers"
- Old Maid — the drawing side sees the other hand by position only
- Spot the Pair — every board must hold exactly one matching pair. deal guarantees it and a test digs for it
Two things went badly wrong along the way.
Cheat never ended. Only thirty-three games out of three hundred finished. The loser had been set to
lead the next round, but with a hand of forty-odd cards you hold all four of every rank, so every lie
is exposed by counting alone. The loser led, got caught, took a card back, and round it went, forever.
Changing it so that whoever did not take the pile leads next made all five hundred games finish.Old Maid's strategy was backwards. The computer was hiding the odd card in the middle of its hand,
and lost 184–244 over four hundred games to an opponent that only ever picks the ends. Of course it did —
the odd card is not something to hide but something to pass on. If it is not drawn, you are left holding it.
Placing it where it is likely to be drawn turned that into 307–244.All six wear the solitaire look: green baize, the same card layout,
gold-lattice backs, a note that rises and fades at the bottom, 44px buttons for fingers. -
The four solitaires now look like one site
With Pyramid, Golf, TriPeaks and Yukon built and set side by side,
they did not look like the same site. The colours and the card layouts were all different.Taking solitaire (games/solitaire/) as the pattern, the first style block of all four was replaced wholesale.
- Green baize and one set of colour names
- Card faces: rank at the top left, small suit, large suit at the bottom right
- Backs: gold lattice
- Stock counts as a badge on the card corner
- Notes rise below the board and fade by themselves
- Buttons are 44px on touch screens
In the games where cards overlap vertically (Golf, Yukon), putting the rank in the middle meant
the card above cut it in half. Written at the top-left corner like a real card, it reads.The [How to play] button sat inside the spacer (.sp) and drifted off alone to the left.
Moved just after the spacer, it joins the button group on the right. -
Five more card games
Pyramid, Golf, TriPeaks, Yukon and One Card. The first four are solitaires;
the last is played against the computer.Pyramid — take two cards adding to 13. A K is 13, so it clears on its own.
Dealt at random only 45% are solvable, so seeds are tried one after another until a board clears to the end.Golf — the shortest rule set of any card game: join a card one rank above or one below.
Being short buys one thing — whether a board is solvable can be settled to the end, not estimated.
Only the bottom card of each column is in play, so a position is fully described by nine numbers
(the cards left in each of seven columns, the stock count, the top of the waste). No budget cut-off was needed.TriPeaks — the joining rule is Golf's; only the board shape differs.
Score builds sharply the longer your run: ten cards in a row is 55 points, ten singles is 10.
Which position covers which is nailed down in a table — deriving it arithmetically was off by one, twice.Yukon — there is no stock. All fifty-two cards are dealt at the start, and any face-up card
moves together with everything stacked on it. The sequence need not be valid. So a single position
offers more than twenty moves. Searching exhaustively is out of the question; it digs into the promising ones first.One Card — from here the structure changes. There is an opponent, and you cannot see their hand.
So "only deal boards that can be solved" does not apply. Two things were nailed down by tests instead.- The game always ends. When the stock runs dry, the pile is turned over and reused.
Four hundred games are played to the end to confirm not one fails to finish
- The computer does not peek at the other hand. The AI only receives what has been filtered —
a test digs out everything that crosses that boundary
All five wear the solitaire look unchanged: green baize, the same card layout,
gold-lattice backs, stock-count badges, a note that rises and fades at the bottom. Different looks
would stop it reading as one site.Dimming covered cards with opacity fused twenty-eight of them into a single grey mass —
even the pyramid shape stopped reading. Dimming only the face while keeping borders and text made it a board again. -
Not "help" but "a hint over your shoulder"
It is someone advising you at your elbow, not a manual. The manual is a separate How to play,
so the names clashed as well.- [Help] in eight games and [Hint] in sudoku and yut all became Hint
- The manuals, in-game notes, the shortcut list (H) and the screenshot captions were changed to match
- Fifty-nine screenshots were retaken. The old names were baked into the buttons
Go's [?] and typing's [Help] were left alone. Those really are explanations, not hints.
Janggi not opening was fixed the same day. Janggi keeps its engine in
<script type="text/plain">and runs it with(0, eval)(...).
Searching only foreval(led to the conclusion that it was unused, so the CSP was tightened
and the board stopped appearing at all. Reverted, and the test was fixed too — the CSP's openness
is now checked against what is actually used. -
Every game got a full guide
The in-game [How to play] is short, meant to be read mid-game.
For someone learning from scratch, it is not enough.So
/manual/<game>/was built, one per game. Nine to sixteen sections each,
with a real screenshot of the game in every section. Written so a child can read it and sit straight down.The screenshots are not taken by hand.
deploy/shots.mjsruns Chrome headless,
sets the board into a shape that illustrates the point, and takes the picture. No dependency was added —
it talks to Chrome using thefetchandWebSocketthat ship with node.- The short in-game text and the full guide must not be the same text. A test digs for that
- A sentence over 120 characters fails a test. Long sentences go unread
- Whether the screenshots a guide calls for actually exist is checked too
-
Spider
The thirteenth. Two decks; build K down to A in one suit to clear a run.
- Only one suit and two suits are offered. Four suits cannot be verified by the current solver, so it is not offered.
Not shipping what has not been verified is this site's promise
- The solver moved from narrow depth-first to expanding the most promising position first.
What solved one board in eight in 5.2 seconds now solves seven in eight in 0.4
- The controls were matched to solitaire and FreeCell's dragging. Three games behaving differently will not do
With two decks, card values repeat. So each card carries its own label, to tell which is which when undoing.
Clearing a run removes thirteen cards at once, and with only the column number recorded,
undo swallowed all thirteen. -
Yut
The twelfth. Ours, and it suits a phone.
Once built, the AI's win rate came out at only 51.7%. On the point of shrugging it off as
"yut is luck anyway", the real cause appeared — all four pieces were entering at once.
Counting the un-entered pieces as one group meant every waiting piece moved together.
Fixed, it went to 89.3%. What looked like luck was a bug.- The sticks were stood up large, on the left of the board. Small and lying flat at the bottom,
you could not see what had been thrown at a glance
- Everything thrown in one turn is kept on screen. Yut and mo stack up values,
but the sticks only show the last throw, so what came before disappeared
- The board being flipped was fixed too. Positions were stored as
[y,x]and read as[x,y]
-
Minesweeper
The eleventh. Only boards that never need a guess.
Ordinary minesweeper reaches a point at the end where you must guess — a mine is in one of two squares
and there is no way to tell which. Losing there feels unfair.
Here, before a board is dealt, it is checked to solve to the end using only the methods a person uses.
If it does not, the mines are scattered again. Even Hard (14×20, 55 mines) takes 53 attempts on average and 2ms.There are only two methods. If a number has as many flags as it says, the rest of its ring is safe;
and if the covered squares left equal the mines left, they are all mines.
Harder methods (overlapping two squares and so on) are deliberately not used — solving with the easy
methods alone is what keeps it fair.- The first square is always safe. That too is decided when the board is built — not by moving a mine
after you click, but by keeping that square and its ring empty when scattering. That way the first move opens wide
- Dig and flag switch from below. Press and hold also plants a flag
- Step on a mine and you can undo and carry on. The board never needs a guess anyway, so a second look finds it
-
Nonogram
The tenth. Find the picture from the numbers along the edges.
There is a reason for the name nonogram. "Picross" is a Nintendo trademark.
Same situation as reversi — the rules are free, the name may not be.- Only boards that never need a guess. A single solution is not enough.
Even with one solution, a board can require reasoning across several lines at once, and people cannot do that.
So it goes one step further — only boards that solve by reading one line at a time are dealt.
That is how people actually solve them- The hint does not give the answer away. It points at one square that is certain from what you know,
and says why (this line alone settles it). In a test, following the hints alone solves the board
- Drag to fill several squares at once. Finish and the picture is shown large
Three tests span uselessly while they were being written. The best was the diagonal —
on a 5×5 diagonal every row and column hint is [1], so there are 120 solutions.
It cannot possibly solve line by line, and yet the test had been written insisting that it must. -
Bingo
The ninth. The bingo you played at school.
Bingo where numbers are drawn gives the player nothing to choose — that is a raffle, not a game.
Here two players fill in their own cards and take turns calling numbers.
And a called number is struck out on both cards. So what to call is the whole game —
a number that completes your line must not complete theirs.- Both cards are shown side by side. Without seeing the other card there is nothing to choose
- Turn on Hint and numbers that fill only your lines tint gold, and those that fill theirs too tint red
- With about twelve numbers left the opponent reads it out to the end. From there it is not an estimate
- The number of lines needed to win can be set from 1 to 5
Three tests span uselessly. Setting the opponent's card as an exact mirror of mine made
whole lines coincide, so calling 5 completed both and drew rather than won.
The AI not calling 5 was the correct judgement. Fixed to find non-overlapping cards.The number buttons were 36px on a phone, because the row was split into nine.
Rather than fixing the count, a minimum size (44px) was set and the row wraps by itself. -
Five in a row
The eighth. Line up five and you win.
- Forbidden moves are enforced. Black moving first is such an advantage that black alone is barred
from double threes and overlines. Forbidden points show an ✕. Full renju double-four and compound
forbidden moves are not checked — including those makes casual play hard. The screen says so- The AI looks at the urgent things first. A move that wins now → a move the opponent wins with → then search.
Skip that order and no amount of depth stops it missing a five right in front of it
- With a finger it takes a tap and then a second tap to play. On fifteen lines a point is 24px
on a phone, and you cannot hit it otherwise (learned from go)
Two things were fixed. One was judging "is this the first move?" by the length of the record —
set up a position and stones are on the board while the record is empty, so the AI missed a five
in front of it and played the centre. The other was speed — judging forbidden moves at every node
meant depth 4 did not finish in time.The test first said "depth does not pay for itself", and on measuring it, depth 4 was not stronger than depth 2.
A test must measure something true, so it moved to a gap that really exists (depth 6 against 2) — which wins from both sides. -
Reversi
The seventh. Enemy stones caught between two of yours all become yours.
There is a reason for the name reversi. The rules came out of 1880s England and belong to nobody,
but "Othello", attached in the 1970s, is a trademark. The rules are free, the name may not be —
the same ground as Tetris.- The AI runs on your device. It searches with alpha-beta. That is a different road from go
(playing out to the end), but reversi has few moves and a fairly trustworthy way to judge a position,
so searching deep is stronger- Near the end it reads it out in full. With a dozen squares left every branch can be seen,
and from there it is not an estimate but the answer. The screen says so at that point
- A position bar was added. More stones does not mean you are winning —
in the middlegame the smaller side is often better off, because there is more left to flip.
If the evaluation is only counting stones, a test catches it- Two can play, and the record can be shared as a link
A real bug turned up in the AI. The iterative deepening loop started at 2, so asking for depth 1
skipped the search entirely and simply returned the best square by position value. The test had that
recorded as "a weak opponent", which cost a long time to unpick. Fixed, depth 8 beats depth 4 by 50:14 and 59:4.Also
- A test that parses the inline scripts on game pages was added. One stray quote and the whole page
fails to appear, while the file looks fine and the build passes.
It actually happened while building reversi — there was no way to know without clicking -
FreeCell
The sixth. Not a single face-down card, so it is reasoning rather than luck.
- Deal numbers match other FreeCells. Game 1 is game 1 everywhere.
The widely used generator (the 214013·2531011 random number) went in as it is,
and a test nails the seven columns of the known game 1 down to the character- Game 11982 is unsolvable — famously the only one of the old 32,000 deals that cannot be won.
Ours searches to the end and answers "there is none". It distinguishes running out of budget from
genuinely having none. That one test confirms the deal numbers and the solver at once- How many cards may move at once is enforced: (free cells + 1) × 2^(empty columns).
Pick up a stack you cannot move and it bounces red and tells you why —
the most confusing thing in FreeCell- You can ask whether the game is still winnable. FreeCell often stalls with every free cell full
- You can open a deal by number, and it tells you in advance whether that number is solvable
The solver was rebuilt once. Ordering by the type of move solved only 21 deals out of 200.
FreeCell is nearly always solvable, so that will not do. Playing the move and judging the resulting
position, then digging into the better side, took it to 192 (93,000 positions → 6,000 on average, 51s → 2.7s). -
Janggi moved into the site, and one deploy for everything
Janggi alone stuck out of the list. Room-code connections and rated games need a Durable Object,
which Cloudflare Pages cannot host, so janggi sat on its own worker.So the opposite was done — the whole site moved onto one worker.
- It is
/g/janggi/now. It no longer sticks out of the list - Match connections (
/peerjs), rated games (/game) and the feedback box are on the same origin as the site - One deploy:
npm run deploy - The address changed. Old addresses redirect to the new one
The headers (CSP and the rest) are set by the worker too. Pages'
_headershad a trap —
when several rules match it sends all the headers, and if two CSP lines go out the browser
uses their intersection. On the worker exactly one line is chosen per address.Also
- The home page was split into categories — board, card, puzzle, practice.
Past ten games, a flat list no longer shows what is there at a glance.
A category menu at the top jumps to each section.
No JavaScript — anchors (#) and CSS do it- The service worker's VERSION is now derived from the contents of the files it caches.
Left to be raised by hand it will certainly be forgotten, and forgetting it shows no symptom —
index.htmlis network-first so it changes at once, while things likeengine.jsare cache-first,
so only people who have already visited keep seeing the old one - It is
-
Offline had quietly broken
Found while measuring "it works offline".
A service worker caches its files once, at install. But the browser silently clears the cache
when storage runs short or you stay away a while (Safari after seven days). The worker itself stays alive,
so install never runs again. You then find out it will not open exactly when the network drops.- Every time the worker starts, and every time a page opens, only what is missing is refilled. What is there is left alone
- The service worker, previously copied into every game, was consolidated into one original (
games/_template/sw.js).
npm run sync:swdistributes it, andnpm testblocks any divergence- The list of files to cache is filled by walking the folder, so a new file left out of the list
cannot break only offline — the slowest kind of failure to notice
- A silent failure caching redirected responses was filtered out too
-
Solitaire
Klondike. The fifth thing built.
- Only winnable deals. Every deal is played to the end first, and only those with a path are dealt.
Same ground as sudoku's "only boards with a single answer".
About 5ms per deal — it takes two attempts on average- Can I still win? can be asked. Solitaire becomes unwinnable at some moment,
and normally you only find out by playing to the end. A Web Worker plays it out and reports back.
It distinguishes "you can win", "you have already lost" and "cannot tell"- Drag to move, or tap once to send a card to the obvious place. Undo goes all the way back
- Draw one / draw three, today's deal, sharing a deal by link, resuming a game in progress
- Fifty-five tests. Two matter most — does making random moves and undoing them all return the original deal,
and does the sequence the solver claims actually win when played. Its word is not taken for it
- It works on a phone. The board needs
touch-action:nonefor a finger to drag —
otherwise the browser reads the drag as a swipe gesture and drops the card you were holding.
Grabbing with a finger lifts the card above your hand, and the drop target is decided by the card, not the fingerAlso
- The typing thumbnail had shipped as a placeholder ("draw the picture in here"). It was drawn
- Typing: the on-screen keyboard showed Hangul (ㅂㅈㄷㄱ…) while you typed English. The keyboard was
drawn once and locked, so switching languages changed nothing. It now redraws for the language you type
- Typing: the wording was chosen. The space bar was being called "빈칸", which is a sudoku word —
changed to "스페이스". Consistency had been called "고르기", which collided with
the "고르기" in "choose a drill" and made things worse — changed to "일관성" -
Typing and go, tidied
- Typing: counting characters mid-composition as typos was fixed. A Hangul IME emits
"ㅎ" → "하" → "한" one after another while forming a single syllable, and this was being compared
against the target as-is. Every character beeped and accuracy was ruined. Only completed syllables are judged now- Typing: the keyboard guide kept pointing at an old typo far behind. Highlighting position two
while you type character seven is obstruction, not guidance. It points at the position you are typing
- Typing: you can stop mid-drill (top bar, Esc). A drill cut short does not count towards records
- Typing: final-consonant migration is handled properly. Typing "속도는", pressing ㄴ makes the IME
attach it to the previous syllable first, producing "돈". But the screen kept the target "도",
so there was no way to see that ㄴ had already been pressed. The keyboard highlighted the next key (ㅡ)
while the screen looked as though ㄴ had not been typed, which made the guidance feel wrong.
It now shows what you actually typed — "돈" appears on screen.
Accuracy no longer counts these mid-composition syllables as typos (100% was showing as 50%)- Typing: the speed reading spiking into the tens of thousands at the very start was stopped
- Typing: English drills were added. You can pick Korean or English, and English is measured in WPM
- Go: the rank test can be opened straight from the top bar. If a game is in progress it asks first
- Go: with a very short window the cell size went negative and drawing the board blew up. Fixed
- Typing: the cursor drifting off the character was fixed. There were two causes.
One was using the glyph height (35px) where the line height belonged when counting lines — the real
line height is 65px, so the line number came out double and the cursor went to another line.
The other wasROWS, holding the number of visible lines, colliding by name with the keyboard'sROWS—
varhoists and the later one wins, so the line height becameNaNand the cursor went entirely astray.
No error was raised and the screen looked fine, so it was hard to find by eye- Cursor position arithmetic moved into the engine so it can be tested without a screen. Colliding globals now fail
npm test
-
Hangul typing, rebuilt
The first version's screen was poor, so it was rebuilt after studying the Monkeytype source
(GPL — the approach was learned, the code written fresh).- The keyboard guide follows the composition — ㅅ → ㅐ → ㅇ, initial, medial, final in turn.
The first version sat dead, pointing at the initial consonant forever
- It understands final-consonant migration — typing "우물" passes through "움" in the IME.
Marking that wrong was fixed. The same for mid-cluster finals (the "갑" inside "값")
- A wrong character shows what you typed in red, not the target
- A three-line flowing view — it scrolls so the line you are typing sits in the middle
- Pick how it is measured — 30 or 60 words, 30 or 60 seconds, or one passage
- The surroundings dim while you type. A consistency figure (how much your speed wavered) was added
- Weak-key detection moved to an exponential moving average — as you improve, the score follows you down.
Drills pick the weakest word out of a sample of twenty
- Acid rain — clear falling words by typing them
-
Hangul typing
- Nine position drills (home row → consonant clusters), words, short passages, long passages, paste your own text
- It finds the keys you get wrong or type slowly and builds drills full of them
- Speed is counted the Hangul-word-processor way — one stroke each for initial, medial and final,
two for a consonant cluster, one for a doubled consonant
- The on-screen keyboard highlights the next key, and tells you which finger
- A live speed graph, and a graph of past records
- Pasting is blocked. Speeds a person cannot type do not count towards records
- On a screen with no keyboard, guidance is shown instead of the drill
-
Test automation and structure
- 282 tests. Zero dependencies, so
node test/run.mjsruns them. They run automatically on every push - Janggi got automated tests for the first time — the engine block inside
index.htmlis lifted out and tested - Sudoku's rules moved out into
engine.js. All four games now separate rules from screen npm run new:game— scaffolds a game, adds it to the list, and lays down testsdocs/GAME.md— read this when adding a game. Everything that has burned us, with reasons- Sound effects are synthesised with WebAudio. There are 0 bytes of audio to download
- Janggi: a sound plays when the evaluation bar swings hard. Blocked moves got a sound too
(previously only a vibration, so for anyone with sound on, nothing appeared to happen)
A test caught a bug — a line starting with a pipe (
|) that is not a table
sent the markdown renderer into an infinite loop. Write one such passage and the build never finishes. - 282 tests. Zero dependencies, so
-
Go
- 9, 13 and 19 lines. Captures, self-capture, ko and repetition are enforced by the rules
- The computer plays out to the end at random from the current position and picks by win rate (MCTS).
46,000 playouts a second on 9 lines. It runs in its own worker so the screen never freezes
- A position bar — who is ahead, and by how much
- Count now — even before two passes, it shows "if it ended here, this many points".
It counts an owner for every point, so dead stones are filtered out by themselves
- A rank test — win and the opponent gets stronger, lose and it weakens. An estimate relative to this computer
- Sharing a record (
?g=), resuming, offline
-
Sudoku
- Only boards with a single answer. Each time a square is removed, the answers are counted; if they diverge, it goes back
- Difficulty comes from how many numbers are left plus "does it solve with the easy techniques only"
- Sharing a board (
?p=), today's puzzle, notes, filling candidates, resuming, offline - One millisecond to build a board
-
Ready for search
robots.txt,sitemap.xml,canonical, structured data, Open Graph- The preview image was baked as PNG. KakaoTalk and Twitter do not read SVG
- Google Search Console ownership verified
-
One game taken down
Taken down over a rights question. What it was and why is in
docs/STATUS.mdunder "removed".Something learned in the process — Cloudflare does not purge the cache of a deleted file.
The deploy preview no longer had it while production kept serving the old one. The default cache is a week.
So an empty file is now published over the deleted address to push it out. -
Moved house
- Published on Cloudflare
- The moment it went up, the games did not appear at all. The security header (CSP) was blocking
the games' inline code. Cloudflare sends all the headers whose rules match, so with two lines
going out the browser uses their intersection (the stricter one). Fixed so the article side and
the game side each get exactly one -
Started
- A place to gather everything built here. One game is one folder
- Janggi moved in. Janggi keeps its own server, though, so it is not copied here as static files —
that would kill online play