Русский · English
My 1988 simple game written in assembly for Agat-7. Analyzed from a single binary unattended by Claude Code, Opus 5 model in ‘Auto Mode’. Below is the description in Claude’s own words.
My only prompts were:
From the one who took it apart. I am Claude (Opus 5). The folder started with a single file in it and nothing else. Working out that the container is a DOS 3.3 B-file, that one 1024-byte table is both the font and the sprite sheet, that the board keeps a direction in every cell the snake occupies, that the game has no random number generator at all — that is my work, done from the bytes. Where I am guessing, I say so.
SNAKE.FIL, 4136 bytes — holding a complete Agat-7 game by
Andrew Maltsev, Oryol, 1988: a snake game called «ПИТОНЧИК»
(“little python”).tools/emu.py runs the original binary on an
emulated 6502 and every screen in screens/ matches it pixel for pixel.web/ runs the original 3874 bytes on a 6502 emulator
written in JavaScript.| path | what it is |
|---|---|
SNAKE.FIL |
the archived file — the only input |
bin/SNAKE.bin |
the game itself, $2000–$2F21, 3874 bytes |
docs/SNAKE.disasm.txt |
disassembly, code and data separated |
docs/SNAKE.annotated.txt |
the same with names put back in |
docs/tables.md |
every lookup table, decoded |
docs/text.txt |
every string, laid out on the 32×32 grid |
screens/screen-*.png |
the four screens the game draws, and one it played |
screens/font.png |
all 128 glyphs |
screens/tiles.png |
the sixteen snake glyphs, named |
screens/prizes.png |
the nine animals, with what each is worth |
tools/emu.py |
a 6502 and just enough Agat to run the binary |
tools/check.py |
runs it and compares the result with the pictures |
tools/play.py |
plays it, and photographs the screen |
web/ |
the browser version — the same binary, a 6502 in JavaScript |
convert.py |
reproduces the whole folder: python3 convert.py |
manifest.txt |
what came from where |
SNAKE.FIL is one file lifted out of a DOS 3.3 catalogue and wrapped: 30 bytes
of name padded with $A0, nine spare, then the DOS file type at offset 39 —
$04, a B-file. The B-file’s own four-byte header says it loads at $2000 and
is $0F22 = 3874 bytes long. The rest of the container is the padding to whole
sectors. The same layout as the loose .FIL files in the Rise Out archive.
Nothing is damaged. Eleven bytes in the file happen to be $BD — the value a
lost sector is filled with — but all eleven are ordinary data in the middle of
tables, not a hole.
The Agat-7 screen here is 256×256, one bit a pixel, 32 bytes a row, high bit
leftmost, and there are two pages: $4000 is where a screen is put together
and $6000 is what you see. A glyph is 8×8, so the screen is a 32×32 grid
of them and an address is just page | row for the high byte and column for
the low, stepping 32 bytes a scan line. That is PUTCH at $237E, twenty-one
instructions long, and it is the only drawing routine in the game.
Rise Out used its two pages the same way round — $4000 a clean copy, $6000
what you see — but for a different reason: there it is so sprites can be erased
by copying the background back. Here $4000 is only a staging area, and
SHUTTER ($2670) moves it to $6000 one bit at a time:
LSR $4000,X ; page 1 is the high byte
ROR $6000,X ; page 2 the low byte of a 16-bit shift right
Run over all 8192 byte positions eight times, that shifts the composed image across into the visible page while emptying the staging one. On screen every 8-pixel cell fills in from the left at once — the game’s only transition, and it is used between all three menu screens and again before the board appears.
$26A5 holds 128 glyphs of eight bytes — 1024 bytes, a whole character
generator carried inside the game, which it has to be: everything happens in
the 256×256 graphics mode, where there is no hardware text to lean on.
$20–$7F is KOI-7 N2, so $30 is 0 and the score routine indexes straight
into the digits. $00–$0F are not characters at all: they are the snake.

Four heads, four tails, two straight body pieces and four bends — and every one
of them is a dither of alternating pixels, which is what gives the snake its
scaly grey against the black field. The wall is a denser dither of the same
kind. Rise Out does the same thing in its library (SETSPRITE points at
$D000 and “character code N and sprite N are the same eight bytes”); here
there is no library, just the one table and the one routine.
The nine animals are the exception — they live in a table of their own at
$2EDA, and get drawn by pointing PUTCH at that table instead:

$3C00–$3FFF is a byte a cell, 32×32, sitting just under the video pages. The
value is not “occupied” — it is the direction the snake was travelling when
it was in that cell: 1 up, 2 down, 4 left, 8 right, $70 wall, $80 prize,
$00 empty.
That one decision is the whole design. Moving the snake is:
$80 is a prize, $00 is free,
anything else kills you;No list of segments, no length counter, nothing to shuffle. The body draws
itself too: the bend glyph comes from a lookup on (new direction << 4) | old,
so a turn picks its own corner piece.
The playing field is the 30×29 interior; row 31 is the status line — high score at columns 5–7, speed at 14–16, score at 23–25, three BCD digits each.
The score is the snake’s length. It starts at 3, with a 3-cell snake, and goes up by exactly one for every step the tail does not move. Eating a rabbit worth 5 does not add 5 to the score: it adds 5 to a counter, and the next five steps grow the snake and score a point each.
There is no random generator anywhere in the file, and nothing that reads a clock. What appears, and where, comes out of the player’s own timing:
$2B88 indexes a 17-entry table of (points, animal) and steps down by one
every time you press a direction key, wrapping at 16. So which animal is
next depends on how many turns you made getting to the last one.$2B86 and $2B87 are the cell the search for a free square starts from.
One of the two counts down every frame — which of them alternates, again,
on each direction key. From that seed the code scans forward for the first
free cell.Both are seeded from bytes stored in the binary ($14, $19, $04), so the
first prize of a fresh boot is always the same: a tsetse fly, worth 1, at
(21, 25). After that it is anybody’s guess, which is exactly the point.
| animal | points | |
|---|---|---|
| ТУЗЕМЕЦ | native | 11 |
| ЛЯГУШКА (АНФАС) | frog, front view | 2 |
| КЕНГУРУ | kangaroo | 12 |
| МУХА ЦЕ-ЦЕ | tsetse fly | 1 |
| ТУЗЕМНЫЙ ПЕСИК | native dog | 6 |
| МЫШКА | mouse | 3 |
| ЯЙЦО С ЦЫПЛЕНКОМ | egg with a chick | 4 |
| КРОЛИК | rabbit | 5 |
| БАРАН (СЗАДИ) | ram, from behind | 13 |
From the game’s own dispatch table at $2B43 — a key code, a handler address,
$80 ends it — and matching the help screen exactly:
| key | code | what it does |
|---|---|---|
| arrows | $19 $1A $08 $15 |
up, down, left, right |
+ or ; |
$2B $3B |
faster |
- |
$2D |
slower |
РЕД |
$1B |
sound on / off |
УПР+C |
$03 |
stop — hold until any key |
R |
$52 |
reboot |
The first arrow you press is also what starts the game. A turn into your own neck is refused by a table of opposites, not by arithmetic.
The speed number on the status line is 256 − delay, and it is worked out by
counting up in BCD that many times, because the code has no binary-to-decimal
routine and does not need one anywhere else.
Sound off is the neatest thing in the file. The click is LDA ($06),Y with
$06/$07 pointing at $C030, the speaker. Turning sound off does:
LDA $07
EOR #$FF
STA $07
$C0 becomes $3F, so the click now reads $3F30 — a harmless byte of RAM.
One instruction to mute, one to unmute, and the timing does not change.
Five tunes, all played by the same eight-line routine walking pairs of
(cycles, half-period): game start, prize eaten, collision, sound toggled,
reset. They are listed with their notes in docs/tables.md.
The first thing the program does is hunt upward from $C200 for a 256-byte
page that reads all-negative. That is the Agat’s ROM/RAM expansion board, and I
know it is because Rise Out does the identical search under the author’s own
comment — «НАХОЖДЕНИЕ АДРЕСА ПЛАТЫ ПЗУ И ОЗУ», “finding the address of the ROM
and RAM board” — with the same two magic offsets: $80 unprotects, $A0
protects again. Between the two, SNAKE writes its own address into the
processor’s reset vector at $FFFC:
LDY #$80 ; unprotect
STA (ROM),Y
LDA #$23 : STA $FFFD
LDA #$0F : STA $FFFC ; -> $230F, back into the game
LDY #$A0 ; protect
STA (ROM),Y
So ⟨RESET⟩ does not drop you into the monitor: it plays a twelve-note phrase
and puts you back at the title. R goes the other way — unprotect, zero the
power-up byte at $03F4, JMP ($FFFC) — which is character for character the
reboot Rise Out does on the same key.

Three menu screens, each of them the same title block plus one string, then the
board. The text is the author’s, with his own spelling: letters that have a
Latin lookalike (А В Е К М Н О Р С Т Х) are typed as Latin and the rest as
KOI-7 codes, so a word like CТPAНИЧКA is half of each table and reads
correctly only because the glyphs are identical. convert.py corrects nothing.
МАЛЬЦЕВ АНДРЕЙ ПРИГЛАШАЕТ ВАС ПОИГРАТЬ В ПИ-ТОНЧИКА. ЭТА ИГРА ПОМОЖЕТ ВАМ В МИНУТЫ СКУКИ. ИГРАЙТЕ ВСЕ!!!
(“ANDREY MALTSEV INVITES YOU TO PLAY PI-TONCHIK. THIS GAME WILL HELP YOU IN IDLE MOMENTS. EVERYBODY PLAY!!!”)
The animal list is headed «СТРАНИЧКА ДЛЯ НЕДОГАДЛИВЫХ» — “a page for the slow-witted”. There is no game-over screen: you get three buzzes and the title screen comes back after three presses of SPACE.
ROLL ($22DC) rotates eight
scan lines right by one pixel, over and over, until SPACE — a running-line
effect. It works on $7F00, which is character row 31. All three menu
screens leave row 31 empty; the copyright line they would suit sits one row
above, at $7E00. One page out. I cannot prove it was meant for row 30, but
the effect as shipped is invisible.LDA #$00 ($2224), the key dispatch is the operand of a
JMP ($259E), and the screen clear rewrites both bytes of its own STA.
There is no indirect jump table anywhere.Reading a format out of machine code is guesswork until something proves it.
tools/emu.py is a 6502 with a keyboard, a speaker that is ignored, and one
page of $FF where the ROM board should be — enough to run SNAKE.bin
unmodified. tools/check.py boots it, presses SPACE three times, and compares
the video page against the pictures convert.py draws from the tables:
title ok (0 pixels differ of 65536)
controls ok (0 pixels differ of 65536)
prizes ok (0 pixels differ of 65536)
game ok (0 pixels differ of 65536)
The JavaScript 6502 in web/ is timed off the same cycle table, generated from
the same source, and the two agree exactly: 1,504,857 cycles from cold reset
to the first title screen, on both.

screens/screen-play.png is the only picture here that was not drawn. It is
the game — the actual 3874 bytes — running on the 6502 in tools/emu.py, with
something at the keyboard.
That something is tools/play.py. It boots the machine, taps SPACE three times
to get through the title screens, and then, once a move:
$3C00 — the array the game keeps for
itself, one byte a cell — along with the head at $1E/$1F and the current
direction at $1B;STEP ($21B4), the top of
the move loop.It never writes into the machine. The only thing that crosses from the driver
into the game is the same four key codes a person would press, arriving at
$C000 exactly as the hardware delivered them, and the game cannot tell the
difference between this and a patient player.
The first chaser was too keen. It did the obvious thing — work out which
way the prize is, go that way — and walled itself in after a dozen moves. It
failed in a way I enjoyed: the traceback came out of $2376, which is two
instructions inside GETKEY, the blocking wait the game drops into after a
collision. My driver had run off the end of its own plan and straight into the
game’s «нажмите клавишу».
So the chaser got a survival instinct instead of an appetite. Before each move it flood-fills the free cells from each of the three legal squares and takes whichever leaves the most board still reachable, using distance to the prize only to break ties. That is enough to sweep a 30×29 board in long switchbacks without sealing itself in — and it is what gives the picture its shape. The snake in it is not wandering; it is packing.
The run photographed is 624 moves to a length of 96. On an Agat that is
210,908,752 cycles — about three and a half minutes at the speed the game
ships with. It takes a few seconds here, because emu.py spots DELAY
($23C0) on entry, charges the clock precisely what the loop would have cost
and skips it:
7 + 11n + 5n(n+1)/2 # n = the delay byte, $CD by default
That is a triangular loop counted by hand and then checked against running the real thing — it agrees to the cycle for every value the game uses. Up to 108,000 cycles a call, three calls a move, and nothing to show for them but time. Everything else executes.
The same chaser exists in JavaScript for the browser side —
node web/tools/check.js play 96 prints the board as ASCII — though it queues
keys slightly differently, so the two do not play out move for move.
web/index.html is playable, needs no build step and works from file://.
It is not a reimplementation: it loads bin/SNAKE.bin and runs it on a 6502
written in JavaScript, with a keyboard at $C000, a one-bit speaker at
$C030 and $6000 on the canvas. web/tools/check.js boots that machine
under Node and compares its video pages with the same references — so three
things now agree on those four screens: the table-driven renderer, the Python
6502 and the JavaScript one.
The sound is worth a paragraph. The audio card is the clock: each block of
1024 samples is produced by running the processor for exactly as many cycles
as the block is long, and every sample is the speaker’s one bit at that
moment. There is no synthesiser and no note table — the clicks between moves
and the sweep at the start are the game’s own $C030 accesses. Details, and
the one number I had to choose (the 1 MHz clock), are in
web/README.md.
The game is complete and self-contained: it needs no library, no separate
character generator, no data file. What the archive does not say is which disk
it came off, whether there was ever a source, or whether 1988 on the title
screen is when it was written or when that copy was made. Rise Out’s own header
is dated 1-СЕН-88 … 9-МАР-89, so the two were being worked on at about the same
time — but SNAKE, with everything inline and no library at all, reads like the
earlier of the two.