updated at 2021-04-10 by wojtek at bitologia.org (index)
Remake of the classic game from 1981 with as little code as possible.
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.
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