256字节的完整Boss战
A whole boss fight in 256 bytes

原始链接: https://hellmood.111mb.de//A_whole_boss_fight_in_256_bytes.html

## Endbot:256字节DOS演示总结 Endbot是一个完整的视听演示,以惊人的方式压缩到仅256字节中,专为DOS(通过DosBox-X)设计,并由HellMood/Desire在Revision 2026上展示。它实时渲染一个受损的机器人精灵、一个不断增长的爆炸、一个滚动的风景以及一个MIDI音轨——所有这些都来自一个.com文件。 该演示使用FASM构建,代码利用巧妙的尺寸优化技术,例如重用寄存器(BP作为全局时钟)、高效的乘法进行坐标计算,以及将数据结合用于多种目的(精灵数据也初始化MIDI)。 该演示通过将MIDI数据直接写入端口0x330来运行。主要特点包括一个数学定义的爆炸、一个使用巧妙的除法和XOR运算创建的滚动风景,以及一个通过翻转位来视觉表示损伤的精灵。 代码的紧凑尺寸是通过细致的指令选择和数据打包来实现的,展示了在DOS尺寸编码方面的出色技巧。它证明了在极端限制下,能够做到什么程度。

## 256字节Boss战演示 一个256字节的MSDOS程序,名为“Endbot”,展示了一个带有剧情、同步视觉效果和声音的“Boss战”,在网上引起了关注。该演示最初于Revision Demoparty 2026发布,突显了在严格大小限制下令人难以置信的代码优化技巧。 讨论围绕着演示的技术成就展开,将其与80年代和90年代的演示场景以及更小的作品(甚至只有8字节)进行比较。用户分享了类似演示的链接和资源,例如nanogems.demozoo.org,这是一个精心策划的小型演示合集。 对话还涉及演示不断发展的格局,将“裸机”方法与利用基于浏览器的模拟器或大量库调用的现代实现进行对比。虽然该演示对许多人来说需要DOSBox才能运行,但有人指出,在稍作修改后,它有可能在较旧的硬件上运行。作者HellMood甚至正在考虑在未来版本中添加交互性。
相关文章

原文
Endbot – 256b DOS Intro

256-byte DOS intro  ·  HellMood/Desire  ·  Revision 2026

What is this?

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.

Build - ASM → COM

You need FASM (Flat Assembler). One command produces a raw binary with no linker step:

fasm endbot.asm endbot.com

Run - DosBox-X Setup

The intro writes to MIDI port 0x330 for music. Use DosBox-X (not plain DOSBox) for its proper MPU-401 emulation.

dosbox-x.conf

[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

Press ESC to quit. The code sends MIDI reset (0xFF) before exit so no notes hang open.

Code Walkthrough

1 - File Header & Init

; "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

initbp is the time value already present when the program starts for size reasons. All timing is relative to it, so animation is consistent regardless of DosBox-X startup time. 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.

2 - Main Loop & Per-Frame Stuff

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

The outer loop runs once per pixel. DI is the pixel counter; when it wraps to zero (every 65 536 iterations = one full 320×200 frame) the per-frame block executes. Timer sync: writing to PIT port 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.

3 - Per-Pixel: X/Y Decode & Explosion Check

    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

The Rrrola trick: multiplying the linear pixel index DI by 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.

4 - Explosion Circle, Flash & Exit

            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

The explosion is a mathematically clean expanding circle: 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.

5 - Sprite: Entrance, Animation & Damage

        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

The robot sprite (by Steffest/Desire) is 16 rows of 16-bit bitmasks. Each pixel uses 2 bits → 4 values: 0 = transparent, 1–3 = lookup into 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].

6 - Background: Sky & Scrolling Landscape

        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

Pixels with positive DH (below the horizon) are solid blue sky. The landscape uses 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 &lt 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.

7 - Data: Sprite, MIDI & Colors

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

The sprite bitmap and MIDI music share one continuous memory block. At startup the entire block is streamed to the MIDI port via 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.

Size Tricks

TrickWhy it saves bytes
push imm / pop es4 bytes vs 5 for mov ax,… / mov es,ax
BP as global clockReuses existing register, no dedicated counter variable
mul 0xCCCD for X/Y5 bytes vs a full 16-bit divide+modulo sequence
stosb for pixel write1 byte; auto-increments DI for free
hlt for frame sync1 byte halt until next hardware IRQ
xlat for color lookup1 byte table lookup via BX+AL
btc for sprite damageBit flip + memory address in one instruction
aam 9 for modulo2 byte; AH = AL÷9, AL = AL mod 9
Sprite doubles as MIDI init dataSame bytes serve two purposes simultaneously
ret to quit DOS1 byte; stack top = 0 = int 20h = exit
联系我们 contact @ memedata.com