256-byte DOS intro · HellMood/Desire · Revision 2026
Endbot is a complete audio-visual demo that fits in exactly 256 bytes. It runs under DOS (via DosBox-X) and renders in real-time: a robot sprite with progressive bullet damage, a growing explosion, a scrolling checkerboard landscape, and a MIDI soundtrack - all from a single tiny .com file.
You need FASM (Flat Assembler). One command produces a raw binary with no linker step:
fasm endbot.asm endbot.com
The intro writes to MIDI port 0x330 for music. Use DosBox-X (not plain DOSBox) for its proper MPU-401 emulation.
[dosbox] machine=vgaonly memsize=4 [cpu] cputype=386 cycles=auto [midi] mpu401=uart mididevice=fluidsynth # use win32 on Windows, fluidsynth on Linux/macOS fluid.soundfont= # path to a GM .sf2, e.g. /usr/share/sounds/sf2/FluidR3_GM.sf2 [sblaster] sbtype=none [mixer] rate=44100
dosbox-x -conf dosbox-x.conf endbot.com
0xFF) before exit so no notes hang open.; "Endbot" - 256b intro for DosBox-X ; presented at Revision 2026 ; by HellMood/Desire %define initbp 0x91C ; BP=Timer, use constant to ease calculations org 100h push 0xa000-20*4 ; VGA Memory Adress, slightly offset to beautify pop es ; into ES mov al,0x13 ; 13h : 320 * 200 Pixels in 256 Colors int 0x10 ; Set! mov cl,Nasenatmungsgeraeusch ; Dump the whole Code to MIDI until this point mov di,si ; Align Music Pointer with Pixel Pointer for 1st check
int 10h / AL=13h sets VGA mode 13h: 320×200, 256 colors, flat 64 000-byte framebuffer at A000:0000. CL is loaded with the byte length of the combined music+sprite block - used by rep outsb on the first frame boundary to stream it all to MIDI port 0x330 in one shot.StartFrame:
mov dx,0x330 ; Set MIDI Port
sub di,si ; Advance Pixel by 4*n ( +1 by stosb is a nice dither)
jnz StartPixel ; If not at the end of a frame, skip per-frame-stuff
mov ax,bp ;
sub al,initbp&0xFF ; hit four times, at time = 0, 256, 512, 768
jnz NoSound ; Output exactly four MIDI notes, except at start
rep outsb ; CX is 4 except the first time!
NoSound:
mov al,1193182/256/30 ; PIT frequency / High Byte / FPS
out 40h,al ; Set Timer to ~ 30 FPS
hlt ; Sync against the timer
inc bp ; Next time step
xor byte [si-Nasenatmungsgeraeusch+Colors+1-8],cl ; angry flicker!
in al,0x60 ; Key Code in AL
sub al,2 ; Will leave 0xFF in AL on Exit!
jc Quit ; ESC -> Stop Sounds -> END
40h then hlt pauses the CPU until the next hardware interrupt, locking to ~30 FPS. MIDI beats: rep outsb streams CX bytes from [SI] to the MIDI port whenever BP mod 256 == 0 - four beat events over the demo's runtime; CX is 4 on beats 2–4 and the full block length on beat 1. The keyboard check reads scancode port 60h; ESC has scancode 1, subtracting 2 leaves 0xFF with carry set → exit branch. StartPixel:
mov ax,0xcccd ; The Rrrola trick to get X,Y in DL,DH
mul di ; there we go
mov ax,bp ; get timer in AX
sub ax,(768+initbp) ; check if it's explosion time
mov cx,dx ; "DrawSprite" gets x,y in DX as CX
jc NoExplosion ; No explosion yet! (CF != ZF)
jz Flash ; The very moment of impact, flash orange once
0xCCCD and reading the high-word gives approximate X (DL) and Y (DH) without a division instruction - 5 bytes of machine code vs 8+ for a proper divide+modulo. AX = BP − (768 + initbp) gives time-since-explosion: negative (CF set) = not yet; zero (ZF set) = exact impact frame; positive = expanding circle. sub ch,al ; After impact, move Bot down
xchg bx,ax ; Save "time since impact" (TSI) in BL
mov al,dh ; Y in AL
sub al,77 ; center Y
imul al ; Y*Y
sub bl,ah ; TSI - Y*Y
mov al,dl ; X in AL
add al,128 ; center X
imul al ; X*X
cmp bl,ah ; TSI - Y*Y - X*X = TSI - R*R = Circle
Flash:
mov al,0x2a ; A beautiful orange value
jz PlotPixel ; ON the circle -> plot orange
jnl BackGround ; INSIDE -> dissolve into background
js NoExplosion ; not yet exploded, and before 128 time steps after TSI
Quit: ; (RET = Quit, top Stack = 0, [0] = int 20h = quit)
out dx,al ; Either has 0xFF (silence) from above ...
ret ; ... or outputs one trash byte after silence xD
X² + Y² = TSI defines the ring edge, growing as time increases. Pixels on the edge are plotted orange (0x2A); pixels inside dissolve into the background. The bot drifts downward after the blast (sub ch,al). Exit uses ret: the stack top holds 0, and address 0 in a .com segment contains int 20h - the DOS "program terminate" call - saves 1 byte. NoExplosion:
mov ax,bp ; get timer in AX
cmp ax, 256+initbp ; first 256 time steps = no Sprite
jl BackGround
test al,32 ; mirror
jnz NoAniFlip ; on
not al ; x-axis
NoAniFlip:
and al,63 ; filter to last 6 bits
add al,148 ; adjust X position of Sprite
add cl,al ; timebased X zig zag movement of Sprite
js BackGround ; inside signed byte range = Sprite
sub ch,36+8 ; adjust Y position of Sprite
js BackGround ; inside signed byte range = Sprite
DrawSprite: ; Draw the 128x128 Pixel Sprite
mov bx,Sprite ; Sprite data adress
mov ax,bp ; Get timer in AX
sub ax,(512+initbp) ; Time for damage already?
jc SkipDamage ; AL = Time since shooting
xor al,1010101b ; Pseudo Random Impact Location
btc word [bx],ax ; Directly flip sprite bits
SkipDamage:
shrd ax,cx,18 ; SCALE and transfer of local X,Y
test al,16 ; need to mirror?
jz NoFlip ; mirror on y-axis
not al ; if needed
NoFlip:
and ax,00011110b*256 + 00011110b ; filter : range&even
xchg cx,ax ; just to get AX to CX
add bl,ch ; offset Sprite to correct line
mov ax,[bx] ; get the line data
shr ax,cl ; shift down by (mirrored) column
mov bl,Colors-1 ; CLUT -> 0 = transparent
and al,3 ; set zero flag if "transparent" == 0
xlat ; looks up 3 Colors, discarded if 4th
jnz PlotPixel ; if not transparent, directly PlotPixel
Colors. The zigzag horizontal movement comes from toggling bit 5 of the timer with test al,32 / not al. btc (bit-test-and-complement) flips one sprite bit per frame from frame 512 onward, accumulating visible bullet holes over time. xlat performs a 1-byte palette lookup: AL = [BX+AL]. BackGround:
mov al,0x4e+72 ; a bluetiful color =)
test dh,dh ; are we over the horizon?
jns PlotPixel ; if yes, just plot the Blue
sub dh,-(128+13) ; are we wrapped around to sky again!?
js PlotPixel ; if yes, just plot the Blue
mov ax,bp ; timer as depth, *DIV DANGER*
div dh ; Constant / Y as distance in AL
xchg dx,ax ; DL=distance AL=X
add al,128 ; center X
imul dl ; X' in AH
add dx,bp ; distance += time (plane movement)
xchg dx,ax ; DH= X' AL=distance'
xor dh,al ; checker pattern
imul dh ; distortion texture
aam 9 ; irregular filter & range
add al,212 ; offset to "landscape" colors
PlotPixel:
stosb ; FINALLY, plot the damn pixel!
mov cx,4 ; for sound routines above and bot flicker
jmp StartFrame ; Rinse and Repeat
div dh for a perspective depth value (BP/Y), perspective-corrects the X coordinate, then XORs for a scrolling checker pattern. The "*DIV DANGER*" note flags that DH < 13 would cause a divide fault - the horizon checks above ensure it never reaches zero here. aam 9 (ASCII Adjust after Multiply) is a 2-byte modulo-9, providing irregular color banding across the landscape palette entries. stosb writes the final pixel to ES:[DI] and auto-increments DI; CX is restored to 4 for the music/flicker logic on the next frame boundary.Sprite: dw 0000111101110000b ; Pixels by Steffest / Desire dw 0111110111110100b ; he was so nice to anticipate dw 1111011111111100b ; i need reusable Sprite Data dw 0101111101111100b ; so he left me the following dw 0111111010110000b ; two bytes of actual MIDI data dw 1111111111110000b ; inside the middle of the Bot dw 1111111100000000b ; ************************************** dw 0111111111000000b ; 0xC07F, MIDI, set gunshot on channel 0 dw 1011011111110000b ; ************************************** dw 1011101111110000b ; this (too) is dumped to MIDI at start dw 1011101101111100b ; or is it a coincidence? dw 1011101110111100b ; a 1 in 65536 coincidence? dw 1011101110011111b ; Who knows ;) dw 0111011101111111b dw 1111111111111111b ; this gets modified by the code so ;dw 1111111111111011b ; this patch has to be applied 11 -> 10 dw 1111111111111100b MusicData: db 0xc9,56 ; set drum channel to SFX db 0x99 ; play on drum channel : db 70,0x58 ; helicopter, moderate db 81,0x7F ; wind, maximum Colors: db 0x15,0x2C,0x13 ; bright gray, yellow, dark gray MusicData2: db 0xc1,91,0x91,37,127 ; Setup Bass, Play at Maximum Burst: db 0xb3,0,9,0xc3 ; Burst noise enable plus set (part1) Nasenatmungsgeraeusch: db 125,0x93,40,127 ; set (part2) and play the robo noise Gunsound: db 0x99, 70+3,90,0 ; machine gun + free byte (4 byte align) ExplosionAndTheEnd: db 0xFF,0x90,34,127 ; Kill all sounds, then explosion sound
rep outsb. Row 8 of the sprite contain the bytes 0xC0 and 0x7F - a valid MIDI program-change command (channel 0, patch 127 = gunshot). The comment "a 1 in 65536 coincidence?" is of cause rhetorical: it was designed that way... or was it? ;) The commented-out sprite row shows the original value before the runtime code overwrites it.| Trick | Why it saves bytes |
|---|---|
push imm / pop es | 4 bytes vs 5 for mov ax,… / mov es,ax |
| BP as global clock | Reuses existing register, no dedicated counter variable |
mul 0xCCCD for X/Y | 5 bytes vs a full 16-bit divide+modulo sequence |
stosb for pixel write | 1 byte; auto-increments DI for free |
hlt for frame sync | 1 byte halt until next hardware IRQ |
xlat for color lookup | 1 byte table lookup via BX+AL |
btc for sprite damage | Bit flip + memory address in one instruction |
aam 9 for modulo | 2 byte; AH = AL÷9, AL = AL mod 9 |
| Sprite doubles as MIDI init data | Same bytes serve two purposes simultaneously |
ret to quit DOS | 1 byte; stack top = 0 = int 20h = exit |