updated at 2021-04-10 by wojtek at bitologia.org (index)


倉庫番

Remake of the classic game from 1981 with as little code as possible.

Fig. 1. Level designed by author.
Above PNG file is approximately 6.8 times bigger than the engine of the game it depicts!

Game is made in the mode 13h (320 × 200 × 256) and each level (map) must fit in an array of 20 × 12 cells, each of 16 × 16 pixels. Small margin of 8 pixels at the bottom is left for a future to display number of moves and level's metadata.

There are only four elements: a cursor, box, slot (for a box) and riveted wall. Two movable objects, the cursor and box, are shaped so that they do not overlap with slots when going through. Graphics is intentionally monochromatic to follow the minimalistic style.

All collision conditions between the cursor, box and wall and also the number of filled slots is calculated instantly from data taken directly from the RAM of VGA. This roughly explains the localisation of rivets on the walls and the special form of boxes, slots and the cursor.

Fig. 2. Four game elements in one picture.

On the Fig. 2. all four elements are magnified (175/8 times) and superimposed. One readily sees that there are four pixels (other configurations are also possible but the anti-diagonal alignment does the job) which, if appropriately combined, can identify a given relation uniquely. For example:

	[1]==0 and [2]==1 and [3]==1 then box is in slot

	[3]==0 and [4]==1            then cell is occupied by wall

	[3]==1 and [4]==1            then cell is occupied by box
	
	[4]==0                       then cell is "empty" (empty from a box or cursor's perspective)

[edit 2021-05-11] actually this can be further simplified!

Note that the cursor (rotated by any angle: π/2, π, 3π/2) avoids these control spots perfectly.

That is all. The objective of the game is to push all boxes into slots. Space Bar reloads the map in case of failure and Escape returns to DOS.

Compiled using Turbo Assembler 2.0. Tested on DOSBox, 80386-DX33, P166-MMX and P133-S.


The source still can be compressed so that the main engine (without LEVEL_DATA and ELEMENTs) can be less than 469 bytes!

        ; wojtek[at]bitologia.org
        ;
        ; 2021-03-31 idea of the minimal sokoban game returns
        ; 2021-04-02 first attempt
        ; 2021-04-03 binary file with data and levels
        ; 2021-04-04 graphics design :)
        ; 2021-04-05 k/b module (what about sokoban on a torus?)
        ; 2021-04-06 full engine and 1st playable version!
        ;            data merged with source
        ; 2021-04-07 code clean, debugging, simplification
        ; 2021-04-08 optimization
        ; 2021-04-09 optimization - engine (without data) has only 495B!
        ; 2021-04-10 optimization - engine (without data) has only 469B!
        ; -----------------------------------------------------------------------------
        .model tiny
        .code
        .386
	        MAX_LEVEL equ 2 ; maximal level number - must coincide with LEVEL_DATA

	        org 100h
        start:  mov ax, 0013h
	        int 10h
	        mov ax, 0a000h
	        mov es, ax

        next_level:
	        xor ax, ax
	        mov cx, 0ffffh
	        rep stosw ; clear screen

	        lea si, LEVEL_DATA
	        mov cl, cs:[level]
        get_offset:
	        add si, 240d
	        loop get_offset
	        xor ax, ax
        draw_map:
	        lodsb
	        or al, al
	        jz short skip_element
	        dec al
	        cmp al, 3
	        jg short not_cursor
	        mov cs:[cursor_position], ah
	        mov cs:[cursor_status], al
        not_cursor:
	        call draw_element
        skip_element:
	        inc ah
	        cmp ah, 240d ; level volume
	        jne short draw_map

        main:   mov ah, 1
	        int 16h
	        jz short kp
	        mov byte ptr cs:[key_pressed], ah
	        xor ah, ah
	        int 16h
	        call check_cursor_status
	        xor bp, bp
	        call check_slot
	        dec bp
	        jz short next_level
        kp:     cmp byte ptr cs:[key_pressed], 1 ; ESC
	        jne short main

	        mov ax, 0003
	        int 10h
	        mov ah, 4ch
	        int 21h
        ;------------------------------------------------------------------------------
        check_slot:
	        mov bx, 320
	        mov cx, 20*12
	        xor di, di
        check:  cmp byte ptr es:[di+bx+8+7*320], 0  ; center of slot
	        jne short not_slot
	        cmp byte ptr es:[di+bx+9+6*320], 0  ; border of slot
	        je short not_slot
	        cmp byte ptr es:[di+bx+12+3*320], 0 ; box area
	        je short slot_not_filled
        not_slot:
                sub bx, 16
                jns short CRLF
	        add di, 320*16
                mov bx, 320
        CRLF:   loop short check

        ; if here then level completed!

	        mov bx, 1024
	        mov al, 0b6h
	        out 43h, al
	        mov al, bl
	        out 42h, al
	        mov al, bh
	        out 42h, al
	        in al, 61h
	        or al, 11b
	        out 61h, al
        beep:   hlt
                hlt
	        in al, 61h
	        and al, 11111100b
	        out 61h, al

	        inc bp ; TODO: this is lousy, a FLAG must be used for this!
	        inc cs:[level]
	        cmp cs:[level], MAX_LEVEL
	        jne short slot_not_filled
	        mov byte ptr cs:[level], 0
        slot_not_filled:
	        ret
        ;------------------------------------------------------------------------------
        check_cursor_status:
	        cmp ah, '9' ; press space to reload level
	        je next_level
	        cmp ah, 'H'; 48h ; up
	        jne short p1
	        mov cl, 1
	        jmp short p0
        p1:     cmp ah, 'M'; 4dh ; left
                jne short p2
	        mov cl, 2
	        jmp short p0
        p2:     cmp ah, 'P'; 50h ; down
	        jne short p3
	        mov cl, 3
	        jmp short p0
        p3:     cmp ah, 'K'; 4bh ; right
	        jne short ignore_other_keys
	        xor cl, cl

        p0:     cmp cl, cs:[cursor_status]
	        mov ah, cs:[cursor_position] ; set the cursor's orientation
	        mov al, cs:[cursor_status]
	        je short old_position
	        call draw_element
	        mov al, cl
	        call draw_element
	        mov cs:[cursor_status], cl
	        ret
        old_position: ; move the cursor (with box) if possible
	        call draw_element
	        mov al, 5 ; box is the only thing (except cursor) that is movable

	        shl cl, 3
	        mov si, cx
	        mov ch, byte ptr cs:[cursor_data+si+6]

	        mov bx, cs:[cursor_data+si]
	        cmp byte ptr es:[di+bx+1], 0   ; wall
	        jne short s0
	        mov bx, cs:[cursor_data+si+2]
	        cmp byte ptr es:[di+bx+4], 0   ; box
	        je short b1                    ; move only cursor
	        mov bx, cs:[cursor_data+si+4]
	        cmp byte ptr es:[di+bx+3], 0   ; is box blocked by box or wall?
	        jne short s0

	        mov ah, cs:[cursor_position]
	        add ah, ch
	        call draw_element              ; remove box
	        add ah, ch
	        call draw_element              ; draw box
        b1:	add cs:[cursor_position], ch
        s0:     mov ah, cs:[cursor_position]   ; TODO: maybe no need to use AX?
	        mov al, cs:[cursor_status]
	        call draw_element	
        ignore_other_keys:
	        ret
        ;------------------------------------------------------------------------------
        draw_element: ; ah=position, al=#element(offset)
	        push si
	        push ax
	        push bx
	        push cx

	        lea si, ELEMENT
	        xor cx, cx
	        mov cl, al
	        shl cx, 5
	        add si, cx

	        mov al, ah
	        xor dx, dx
	        xor ah, ah
	        mov di, 20d
	        div di
	        push dx ; store remainder
	        mov di, 320*16
	        mul di
	        mov di, ax
	        pop ax ; restore remainder
	        shl ax, 4
	        add di, ax
	        mov cx, 16d
        load_:  lodsw
	        push cx
	        mov cx, 16d
        rotate: push ax
	        and ax, 1
	        shl ax, 1
	        xor ax, es:[di] ; get pixel
	        stosb           ; put pixel
	        pop ax
	        ror ax, 1
	        loop rotate
	        add di, 304d
	        pop cx
	        loop load_

	        pop cx
	        pop bx
	        pop ax
	        pop si
	        ret
        ;------------------------------------------------------------------------------

        level           DB 0 ; enumerate levels from zero to simplify offset calculation
        cursor_position DB ?
        cursor_status   DB ?
        key_pressed     DB 0

        cursor_data DW -160*32-16, -160*32+320*3-16, -160*32+320*2-32, -1
	        DW -320*32, -320*29, -320*30-160*32, -20
	        DW -160*32+16, -160*32+16, -160*32+320+32, 1
	        DW 0000, 0000, 320*16, 20

        ELEMENT DB 000h, 001h, 080h, 001h, 0c0h, 001h, 0e0h, 001h, 0f0h, 001h, 038h, 000h, 03ch, 000h, 03eh, 000h ; cursor
	        DB 03ch, 000h, 038h, 000h, 0f0h, 001h, 0e0h, 001h, 0c0h, 001h, 080h, 001h, 000h, 001h, 000h, 000h

	        DB 000h, 001h, 080h, 003h, 0c0h, 007h, 0e0h, 00fh, 0f0h, 01fh, 038h, 038h, 03ch, 078h, 03eh, 0f8h ; cursor
	        DB 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h

	        DB 000h, 001h, 000h, 003h, 000h, 007h, 000h, 00fh, 000h, 01fh, 000h, 038h, 000h, 078h, 000h, 0f8h ; cursor
	        DB 000h, 078h, 000h, 038h, 000h, 01fh, 000h, 00fh, 000h, 007h, 000h, 003h, 000h, 001h, 000h, 000h

	        DB 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 03eh, 0f8h ; cursor
	        DB 03ch, 078h, 038h, 038h, 0f0h, 01fh, 0e0h, 00fh, 0c0h, 007h, 080h, 003h, 000h, 001h, 000h, 000h

	        DB 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 080h, 003h, 080h, 002h ; slot
	        DB 080h, 003h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h, 000h

	        DB 038h, 038h, 038h, 038h, 0feh, 0ffh, 0feh, 0ffh, 0feh, 0ffh, 038h, 038h, 038h, 038h, 038h, 038h ; box
	        DB 038h, 038h, 038h, 038h, 0feh, 0ffh, 0feh, 0ffh, 0feh, 0ffh, 038h, 038h, 038h, 038h, 000h, 000h

	        DB 0feh, 0ffh, 0fah, 0efh, 0fah, 0efh, 0e2h, 08fh, 0feh, 0ffh, 0feh, 0ffh, 0feh, 0ffh, 0feh, 0ffh ; wall
	        DB 0feh, 0ffh, 0feh, 0ffh, 0feh, 0ffh, 0fah, 0efh, 0fah, 0efh, 0e2h, 08fh, 0feh, 0ffh, 000h, 000h

        LEVEL_DATA DB 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 5, 6, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 2, 5, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 5, 6, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7
	        DB 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7
	        DB 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7


	        DB 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
	        DB 7, 0, 4, 7, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 7, 7, 7, 7, 7
	        DB 7, 0, 6, 0, 6, 7, 7, 7, 7, 0, 7, 0, 7, 7, 0, 7, 7, 7, 7, 7
	        DB 7, 7, 0, 6, 0, 7, 0, 0, 7, 0, 7, 0, 7, 7, 0, 7, 7, 7, 7, 7
	        DB 7, 0, 6, 6, 0, 6, 6, 0, 0, 5, 5, 5, 0, 6, 0, 7, 7, 7, 7, 7
	        DB 7, 0, 0, 6, 0, 6, 0, 7, 7, 5, 6, 5, 7, 6, 0, 7, 7, 7, 7, 7
	        DB 7, 0, 0, 6, 0, 0, 0, 5, 5, 5, 5, 5, 0, 6, 0, 7, 7, 7, 7, 7
	        DB 7, 7, 7, 0, 7, 7, 7, 5, 7, 5, 7, 0, 7, 0, 0, 7, 7, 7, 7, 7
	        DB 7, 7, 7, 0, 7, 7, 7, 5, 5, 5, 6, 0, 7, 7, 7, 7, 7, 7, 7, 7
	        DB 7, 7, 7, 0, 7, 7, 7, 0, 7, 0, 7, 0, 7, 7, 7, 7, 7, 7, 7, 7
	        DB 7, 7, 7, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7
	        DB 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7

        end start