FreeCell (WEP2 / Win16)

wep16_freecell = binaries/wep16/WEP2/FREECELL.EXE, imports CARDS (cdtInit, cdtDrawExt, cdtTerm), KERNEL, GDI, USER. The loader reads CARDS.DLL from the exe's own directory (loadWin16Dlls in lib/dll-loader.js), so it gets WEP2's copy. Tut's Tomb is the only other WEP2 game that imports CARDS.

The WEP2 CARDS.DLL fixture is corrupt — 2026-10-10

The archive's installed WEP2 volume ships a CARDS.DLL (md5 de9893a7…) that differs from the copy in WEP1, WEP4, win98-16bit/ and dlls/ (md5 14e9f672…; same 148528-byte build, same resource table) in 1407 bytes, all inside file range 0xc284–0xd1c0: a run shifted by two bytes. It breaks RT_BITMAP 678–684 and two cards:

The 2026-10-03 browser evidence (scratch/runs/20261003-wep16_freecell-gameplay-restored10, "black destination, no successful move proven") was this: the move did happen, onto a card that cannot draw.

Fix: stage the intact build (cp wep16/WEP1/CARDS.DLL wep16/WEP2/CARDS.DLL). test/test-win16-wep2-gameplay.js fails with that instruction while WEP2's copy differs from WEP1's. The corrupt copy is kept in scratch/runs/20261010T1320Z-wep16_freecell-cards-dll-d10ba697/.

CARDS.DLL cdtDrawExt (seg 1, entry 0xf8)

cdtDrawExt(hdc, x, y, dx, dy, cd, mode, rgbBgnd); the jump table at 1:0x217 picks the blit by mode:

mode meaning GDI
0, 1 face, back BitBlt / StretchBlt SRCCOPY
2 highlight NOTSRCCOPY 0x330008
3, 4 ghost, remove CreateSolidBrush(rgbBgnd) + PatBlt PATCOPY (4 returns there)
5 invisible ghost SRCAND 0x8800C6
6, 7 X, O other bitmaps

Bitmap id for a face card = (cd & 3) * 13 + (cd >> 2) % 13 + 1, so clubs are 1–13, and cd = rank * 4 + suit (A♣ = 0, A♠ = 3, 2♣ = 4, 4♣ = 12). After a face blit it rounds the corners with GetPixel/SetPixel of the target DC.

How FreeCell animates a move

All through GetDC(main hwnd) with three 71×96 memory DCs: one holds the background under the card's old position (PatBlt green, then the card above it drawn at y = -18), one the background at the new position, one the moving card drawn with rgbBgnd 0. Each step: save the screen at the new position, patch it with the overlap from the old buffer, restore the old position, blit the card, swap. A cascade step is 18 px.

Headless route

node test/run.js --app=wep16_freecell --batch-size=20000 --repaint-every=5 --input=60:keydown:113,61:keyup:113,… deals game #16813 (deterministic). The 2♣ ends cascade 1: 200:mousedown:43:300,205:mouseup:43:300 selects it, 300:mousedown:40:80,305:mouseup:40:80 moves it to free cell 1, and FreeCell then plays A♠, A♣ and the 2♣ home by itself (visible by batch 360).

At 640×480 the window opens at x = 22 and is wider than the screen, so the eighth cascade is cut at the right edge.