03Prior work
Almost nothing here is a new idea. It is mostly other people's ideas pushed one step further, so the chain is worth spelling out.
LeanChess, Dmitry Shechtman, 2019
This is the one that mattered. It took the record from a 487-byte boot sector down to a 288-byte DOS .COM, and did it with a design that is genuinely readable: a padded 0x88 board, a piece encoding that doubles as its own move-generation index, and one recursive routine that both makes a move and finds the best reply. AttoChess is a derivative work and keeps Shechtman's copyright and MIT license intact. If any of this interests you, read his source before mine.
Toledo Atomchess, Óscar Toledo G., 2015
Held the DOS .COM record between BootChess and LeanChess, and got refined over several years down to 352 bytes (356 as a bootable sector, 326 in its stripped “HACK” build). Like everything at this size, AttoChess included, it leaves out castling, en passant, and promotion. Write-ups of this record tend to jump straight from 487 to 288 and skip it. Source on GitHub.
BootChess, Olivier Poudade, 2015
Fit a chess game into a 512-byte boot sector, 487 bytes of it code. This is roughly where the modern version of the contest starts. Poudade's page still hosts it.
1K ZX Chess, David Horne, 1982
Squeezed a playable game into 672 bytes on a Sinclair ZX81, and it held for over thirty years.
05Optimizations
The 276-byte phase removed twelve bytes from LeanChess through display, input, depth-lifetime, pawn-direction, and addressing work. The current release adds four compositionally checked one-byte reductions. The final page-pointer rewrite depends on the new pawn undo freeing BH.
01 · Display
The board draws itself
This one saves the most. The original renders the position into a separate buffer: it walks the board, transforms each square into a printable character, stores it, appends a $ terminator, and prints the whole string with DOS function 09h (int 21h). That path costs a buffer pointer setup, the copy loop's store, the terminator write, and, because the buffer lives after the board, a reserved board array in the image.
AttoChess drops all of it. The board's border columns are laid out as CR, LF, CR, LF (0Dh, 0Ah, 0Dh, 0Ah) rather than the 08h filler LeanChess used. CR and LF both have bit 3 set, so every existing border/color mask test fires exactly as before, except now the raw board bytes are already a printable frame. The display loop streams each byte straight to the console with int 29h (DOS fast console output): borders become newlines, empty squares become NULs, and only real pieces take the ASCII transform.
main_loop:
mov si, board_db + 24 ; row 2 (black back rank), col 0
mov cl, 98 ; 8 rank rows + final CR,LF (CH=0)
disp_loop:
lodsb ; read square contents
test al, 30h ; piece?
jz disp_cont ; no: emit raw (CR / LF / NUL)
inc ax ; zero-align king
and al, 27h ; isolate piece type + black/lowercase bit
add al, 4Bh ; K, N, B, P, Q, R (upper/lower by color)
disp_cont:
int 29h ; fast console output of AL
loop disp_loop
That takes out the render buffer, its pointer setup, the $ terminator, the int 21h/09h string print, and the reserved board array in the file image.
02 · Startup
No BIOS mode-set
The original opens with int 10h to force BIOS display mode 0. AttoChess drops it and makes its two genuine entry assumptions explicit instead, which is both smaller overall and correct regardless of how the program is launched:
start:
cld ; DF is not guaranteed clear at entry
mov cx, 13 ; row count (entry CX is not guaranteed)
Streaming through int 29h works in whatever video mode you happen to be in, so the mode-set is not needed.
03 · Input
The input decoder folds every constant into one base address
Reading a move means turning two typed characters (a file and a rank) into a board address. The original does this in stages: read the file char, add it, read the rank char, mask it down with and al, 0Fh, load 12 into ah, mul to get the row offset, and subtract.
AttoChess collapses the arithmetic by pre-folding the ASCII bias constants into the base address and letting 16-bit pointer math wrap around mod 64K. The normalization step and the separate multiply setup both disappear:
read_sub:
mov bp, di
mov di, board_db + 123 + 0CE0h ; base pre-folds the ASCII offsets
mov ah, 01h
int 21h ; read file char
add di, ax ; AX = 0100h + file char
int 21h ; read rank char
imul ax, 12 ; AX = 12 * (0130h + rank digit)
sub di, ax ; land on the target square
imul ax, 12 (an 80186 immediate-form multiply) replaces the mov ah,12 + mul ah pair, and the wrap-around base makes the explicit and al, 0Fh input mask unnecessary.
04 · Search
The source loop leaves CX alone, so depth is never reloaded
Inside the recursive search, the original scans candidate source squares with a counted loop (mov cl, 92 ... loop src_loop). That reuses CX as the loop counter, which clobbers the search depth living there, so every recursive call has to re-read the depth back off the stack frame (mov cx, [si + 32]) before decrementing it.
AttoChess walks the source squares by comparing the pointer against the end of the board instead:
src_cont:
inc bp
cmp bp, board_db + 120 ; past the last square?
jnz src_loop
CX is never touched, so it stays the live depth counter for the whole scan. The recursive call site then just does dec cx, and the stack reload of depth is gone entirely.
05 · Pawns
Pawn direction folded into the color bit
The original's pawn logic isolates the vector's sign bit, shifts it into alignment with the color bit, XORs against the side-to-move, and branches on parity, which is several instructions of bit-shuffling. AttoChess folds the forward/backward test straight into color bit 5 with a single xor al, dh, and reuses vector parity (odd offset means diagonal) to tell captures from pushes:
pawn:
push ax
xor al, dh ; bit 5 := vector sign XOR side to move
test al, 20h ; forward for the moving color?
pop ax ; POP leaves flags intact
jz vec_cont ; backward: reject
test al, 1 ; odd offset (+/-11, +/-13) = diagonal?
jnz pawn_cont ; diagonal: must capture
xor ah, 30h ; straight (+/-12): invert dest color for the empty test
pawn_cont:
test ah, dl
jz vec_cont
Because the direction test now keys off the side-to-move color rather than an absolute sign, pawns move correctly for both colors from the one code path.
06 · Addressing
Piece type held in BX, so the move-table address folds into one lea
Contributed by Peter Ferrie.
The source loop reads a square, masks it down to a piece type, and uses that as an index into the move-vector metadata. Holding that index in AX costs a separate copy into BL and a two-instruction address calculation:
mov al, [bp]
and ax, 07h
mov bl, al ; save piece type
mov si, offset moves_knight - 2 ; base
add si, ax ; + index
Reading straight into BL means the index is already where the slider test later needs it, and the whole address computation collapses into a single lea:
mov bl, [bp]
and bx, 07h ; also zeroes BH
lea si, [bx + moves_knight - 2]
lodsb
cbw ; AH is no longer zeroed by the AND
There is one subtlety. The original and ax, 07h was doing double duty: it also cleared AH, so the following add si, ax treated the loaded byte as a 16-bit value. Masking BX leaves AH untouched, so a one-byte cbw has to restore that invariant. Net saving: 2 bytes.
07 · Prologue
Test the source after exchanging it into AL
The old routine compares [BP] to CH before clearing AX. Search depth is 0–4, so CH=0. Exchanging the source into AL first and testing that byte preserves the continuing state; an empty source receives an idempotent zero store before the same deliberate self-loop.
; before ; after
cmp [bp], ch xor ax, ax
jz $ xchg al, [bp]
xor ax, ax test al, al
xchg al, [bp] jz $
Encoded saving: one byte.
08 · Pawn predicate
Map occupancy and vector parity into PF
The new predicate places destination occupancy in carry, rotates it beside vector parity, and tests the resulting two low bits. Parity is even exactly when occupancy agrees with diagonal/straight movement. POP restores AX without changing PF.
push ax
and ah, dl
neg ah
rcl al, 1
test al, 3
pop ax
jpo vec_cont
Because AH remains available for memory undo, XCHG AH,[DI] replaces the BH clone path and removes MOV BH,AH. Net saving: one byte.
09 · Data overlap
Make the king value initialize the hidden border
eval_db[7]=2Eh is also init_db[0]. The evaluation lookup is unchanged. In the hidden top border, both the old 09h and new 2Eh block both color masks, and those bytes are outside display, source scanning, and initializer self-feed reads. Saving: one byte.
10 · Page addressing
Keep the common vector page in BH
The AH-based undo frees BH. Since evaluation data, metadata, and vectors all occupy runtime page 01xx, BH=01h can remain live. Six absolute low-byte pointers move into the unreachable island after RET, permitting a disp8 rather than disp16 LEA:
and bl, 07h
lea si, [byte bx + 53h] ; 8D 77 53
lodsb
mov ah, bh
xchg ax, si
The metadata bytes are E4 EA E9 ED EF ED, selecting full pointers 01E4h, 01EAh, 01E9h, 01EDh, 01EFh, 01EDh for types 2–7. Saving: one byte. This reduction depends on the preceding pawn/undo rewrite.
06Verification evidence
The exact 272-byte artifact is accompanied by eight standalone SMT-LIB mismatch queries. Z3 4.16 returned UNSAT for every obligation: the move prologue, pawn predicate, recursive-call dead inputs, memory undo, contextual live projection, table overlap, pointer metadata, and rejected-path state.
Binary audits also verify exact 80186 instruction boundaries, byte-identical re-encoding, the two executable spans, exclusion of control flow from the metadata island, BH=01h dominance across nested PUSHA/POPA, and AH/BH read-before-kill on all three reject paths.
An independent Unicorn 2.1.4 harness compared complete modeled DOS event traces and relocated board state across all 20 legal openings plus three two-turn sequences. All 23 passed; the canonical report SHA-256 is ab632e7a55ed9ce43f26dae4803080e17c0b625b072a7fa23976f30b537b59f3.
Replay the exact queries and inspect the native proof output on GitHub.
Scope
The formulas establish their stated local and contextual contracts. They do not prove global minimality among every x86 byte string or a mechanically composed unbounded whole-program bisimulation. The contextual pawn theorem uses equal recursive score as an explicit composition hypothesis.
07Building and running
AttoChess is written in NASM syntax targeting the 80186.
Assemble with NASM
nasm -f bin -w+error -o ATTOCHES.COM AttoChess.asm
This yields ATTOCHES.COM, a 272-byte DOS executable.
Run under DOSBox
dosbox
Then, at the DOSBox prompt:
mount c: .
c:
ATTOCHES.COM
Any real or emulated 16-bit DOS environment works (DOSBox, PCem, 86Box, or actual hardware), since the program uses only standard DOS int 21h/int 29h calls.
Playing
You play White. The computer plays Black and answers on its own. Enter a move as four characters, source file and rank followed by destination file and rank, for example e2e4. The board redraws after each pair of moves. AttoChess searches four plies deep.