Juego Toledo Atomchess
El juego de ajedrez más pequeño del mundo en código
máquina x86 ¡en sólo 326 bytes!
Por el 28 de enero de 2015 tuve noticias de un
nuevo programa de
ajedrez en 487 bytes de código máquina x86. Ni siquiera lo probé y seguí
con otras cosas ya que estaba ocupado.
Sin embargo mis amigos del JS1K y Twitter me incitaron a
hacer algo al respecto, y tengo algunas nociones de código máquina x86.
Así que empecé a codificar mi propio programa de ajedrez
en ensamblador x86, lo acabé en 24 horas y pasé otras 24 horas
depurándolo.
Después de esto le di un vistazo a la documentación del otro
programa de ajedrez y me sorprendió que hiciera movimientos ilegales y ni
siquiera tuviera árbol de búsqueda, para mí eso es casi como no jugar
ajedrez.
Así que aquí está, mi juego se llama Toledo Atomchess y
en 481 bytes de código máquina x86 juega muy razonablemente con todas las
limitaciones.
Mi primera versión de Toledo Atomchess fue de 481 bytes de
código máquina x86 y juega muy razonablemente con todas las limitaciones,
hace poco vi oportunidades de optimización y lo hice en 456 bytes (7-oct-2015), después en 446 bytes (10-oct-2015), 392 bytes (04-mar-2016), y finalmente 326 bytes (15-dic-2019).
- Juega movimientos básicos del ajedrez (no hay al paso, coronación ni enroque)
- Introduzca sus movimientos en forma algebraica (D2D4)
- No se verifica la legalidad de los movimientos del usuario
- Profundidad de búsqueda de 3 niveles
Desarrollo posterior
Recientemente tuve tiempo para investigar la implementación
de movimientos completos de ajedrez, incluyendo validación del movimiento del
jugador, enroque, captura al paso y coronación (sólo a dama)
Lo llamé Toledo Atomchess Recargado, y requiere 831
bytes de código máquina x86. También incluye un cargador para el segundo sector
del disco así que puede arrancarse de un disco flexible.
Mientras lo desarrollaba, descubrí un pequeño defecto que
evitaba que Toledo Atomchess moviera sus alfiles en diágonales hacia arriba
y otro pequeño y no tan importante en las comparaciones de pila.
Después cambié la sintaxis a la del ensamblador nasm por
sugerencia de hellmood y lo subí a Github para controlar más fácilmente los
cambios (también dado que no actualizo frecuentemente mi sitio), aquí lo
optimicé más y tuve ayuda de qkumba (Peter Ferrie) y theshich.
Y ahora también existe
Toledo Atomchess 6502
comprimido en 1K de ROM con tablero gráfico para Atari VCS/2600
También aparece aún mejor documentado en mi libro Programming Boot Sector Games.
Como ejecutarlos
Para correrlo se necesita un disco flexible de 1.44 MB y
poner el archivo de 512 bytes en el sector raíz utilizando una utilidad como
Rawrite.
El paquete también incluye el juego como un archivo COM que se
puede ejecutar en MS-DOS o en la línea de comandos de Wind*ws.
También se puede correr con DosBox o qemu (visto en
Reddit coding)
qemu-system-x86_64 -hda toledo_atomchess_disk.bin
Código fuente de Toledo Atomchess
Este es el código fuente completo de Toledo Atomchess en
su versión de 446 bytes, para la versión más reciente vea el
git de Toledo
Atomchess.
;
; Toledo Atomchess
;
; por Óscar Toledo Gutiérrez
;
; © Copyright Óscar Toledo Gutiérrez 2015
;
; Creación: 28-ene-2015 21:00 tiempo local.
; Revisióm: 29-ene-2015 18:17 tiempo local. Terminado.
; Revisión: 30-ene-2015 13:34 tiempo local. Depuración finalizada.
; Revisión: 01-jun-2015 10:08 tiempo local. Bug solucionado que evitaba que la computadora moviera los alfiles hacia arriba.
; Revisión: 06-oct-2015 06:38 tiempo local. Optimización de inicio de tablero/visualización. Y pequeños detalles.
; Revisión: 07-oct-2015 14:47 tiempo local. Más optimización y depuración.
; Revisión: 10-oct-2015 08:21 tiempo local. Más optimización.
; Características:
; * La computadora juega movimientos básicos legales del ajedrez ;)
; * Introduzca en forma algebraica (D2D4) (no se validan movimientos)
; * Profundidad de búsqueda de 3 niveles
; * No hay coronación de peones.
; * No hay enroque.
; * No hay captura al paso.
; * Tamaño de 446 bytes (cabe en un sector de arranque)
; Nota: Soy lo suficientemente perezoso para escribir mi propio
; ensamblador en lugar de buscar uno, así que disculpará la
; sintaxis usada ;)
code16
; Cambiar a org &0100 para archivo COM
org &7c00
; Limpieza general
mov sp,stack
cld
push cs
push cs
push cs
pop ds
pop es
pop ss
; Crea el tablero
mov di,board-8
mov cx,&0108
sr1: push di
pop ax
and al,&88 ; Tablero 0x88
jz sr2
mov al,&07 ; Frontera
sr2: stosb
loop sr1
; Setup board
mov si,initial
mov di,board
mov cl,&08
sr3: lodsb ; Carga pieza
stosb ; Piezas negras
or al,8
mov [di+&6f],al ; Piezas blancas
mov byte [di+&0f],&01 ; Peón negro
mov byte [di+&5f],&09 ; Peón blanco
loop sr3
;
; Bucle principal
;
sr21: call display_board
call key2
push di
call key2
pop si
call sr28
call display_board
mov ch,&08 ; Turno actual (0=blancas, 8=negras)
call play
jmp short sr21
;
; La computadora juega :)
;
play: mov bp,-32768 ; Puntuación actual
push cx ; Lado actual
push bp ; Cuadro origen
push bp ; Cuadro destino
xor ch,8 ; Cambia lado
mov si,board
sr7: lodsb ; Lee cuadro
xor al,ch ; XOR con lado actual que juega
dec ax
cmp al,6 ; Ignora si es frontera o vacío
jnc sr6
or al,al ; ¿Es un peón?
jnz sr8
or ch,ch ; ¿Juega negras?
jnz sr25 ; No, salta
sr8: inc ax
sr25: dec si
add al,&04
mov dl,al ; Total de movimientos de pieza en dl
and dl,&0c
mov bx,offsets-4
xlat
add al,displacement&255
mov dh,al ; Offset de movimientos en dh
sr12: mov di,si ; Reinicia cuadro destino
mov bl,dh
mov cl,[bx]
sr9: add di,cx
and di,&ff
or di,board
mov al,[si] ; Contenido de: origen en al, destino en ah
mov ah,[di]
or ah,ah ; ¿Cuadro vacío?
jz sr10
xor ah,ch
sub ah,&09 ; ¿Captura válida?
cmp ah,&06
mov ah,[di]
jnc sr18 ; No, evita
cmp dh,16+displacement&255 ; ¿Peón?
jc sr19
test cl,1 ; ¿Va derecho?
je sr17 ; Si, evita y cancela movimiento de doble cuadro
jmp short sr19
sr10: cmp dh,16+displacement&255 ; ¿Peón?
jc sr19
test cl,1 ; ¿Diagonal?
jne sr18 ; Si, evita
sr19: push ax ; Salva para restaurar en futuro cercano
mov bl,scores&255
mov al,ah
and al,7
cmp al,6 ; ¿Rey capturado?
jne sr20
cmp sp,stack-(5+8+5)*2 ; Si en primera respuesta entonces...
mov bp,20000 ; ...puntos máximos (probable jaquemate/mate ahogado)
je sr26
mov bp,7811 ; Máxima puntuación
sr26: add sp,6 ; Ignora valores
pop cx ; Restaura bando
ret
sr20: xlat
cbw
; cmp sp,stack-(5+8+5+8+5+8+5+8+5)*2 ; Profundidad 4 niveles
cmp sp,stack-(5+8+5+8+5+8+5)*2 ; Profundidad 3 niveles
; cmp sp,stack-(5+8+5+8+5)*2 ; Profundidad 2 niveles
; cmp sp,stack-(5+8+5)*2 ; Profundidad 1 nivel
jbe sr22
pusha
call sr28 ; Realiza movimiento
call play
mov bx,sp
sub [bx+14],bp ; Substrae BP de AX
popa
sr22: cmp bp,ax ; ¿Mejor puntuación?
jg sr23 ; No, salta
xchg ax,bp ; Nueva mejor puntuación
jne sr23 ; ¿Misma puntuación?
in al,(&40)
cmp al,&aa ; Inserta aleatoriedad
sr23: pop ax ; Restaura el tablero
mov [si],al
mov [di],ah
jg sr18
add sp,4
push si ; Guarda el movimiento
push di
sr18: dec ax
and al,&07 ; ¿Era un peón?
jz sr11 ; Si, verificación especial
cmp al,&04 ; ¿Caballo o rey?
jnc sr14 ; Fin de secuencia, escoge siguiente movimiento
or ah,ah ; ¿A cuadro vacío?
jz sr9 ; Si, sigue línea de cuadros
sr16: jmp short sr14
sr11: and cl,&1f ; ¿Avanzó el primer cuadro?
cmp cl,&10
jnz sr14
mov ax,si ; Ya verificó que es movimiento a cuadro vacío
sub al,&20
cmp al,&40 ; ¿En línea inicial de arriba o abajo?
jb sr17 ; No, cancela movimiento de doble cuadro
sr14: inc dh
dec dl
jnz sr12
sr17: inc si
sr6: cmp si,board+120
jne sr7
pop di
pop si
pop cx
cmp sp,stack-2
jne sr24
cmp bp,-16384 ; ¿Movimiento ilegal? (siempre en jaque)
jl sr24 ; Si, no se mueve
sr28: movsb
mov byte [si-1],0
sr24: ret
; Ilustra el tablero
display_board:
mov si,board-8
mov cx,73 ; 1 frontera + 8 líneas * (8 cols + 1 frontera)
sr4: lodsb
mov bx,chars
xlat
cmp al,&0d ; ¿Es RC?
jnz sr5 ; No, salta
add si,7 ; Brinca 7 bytes de frontera
call display ; Ilustra RC
mov al,&0a ; Ahora ilustra LF
sr5: call display
loop sr4
ret
; Lee coordenada algebraica
key2: call key ; Espera letra
add ax,board+127 ; Cálcula columna del tablero
push ax
call key ; Espera dígito
pop di
shl al,4 ; Substrae dígito de línea multiplicado por 16
sub di,ax
ret
key:
mov ah,0 ; Lee el teclado
int &16 ; Llama al BIOS
display:
pusha
mov ah,&0e ; Salida de consola
mov bh,&00
int &10 ; Llama al BIOS
popa
and ax,&0f
ret
initial:
db 2,5,3,4,6,3,5,2
scores:
db 0,10,50,30,90,30
chars:
db ".prbqnk",&0d,".PRBQNK"
offsets:
db 16,20,8,12,8,0,8
displacement:
db -33,-31,-18,-14,14,18,31,33
db -16,16,-1,1
db 15,17,-15,-17
db -15,-17,-16,-32
db 15,17,16,32
; 64 bytes para decir algo
db "Toledo Atomchess 10oct2015"
db " (c)2015 Oscar Toledo G. "
db "nanochess.org"
;
; Este marcador lo requiere el BIOS para arrancar de disco flexible
;
ds &7dfe-* ; Cambiar a &02fe para archivo COM
db &55,&aa
board: ds 256
ds 256
stack:
end
Código fuente de Toledo Atomchess Recargado
Aquí está el código fuente completo para Toledo Atomchess
Recargado, incluyendo apoyo para validar el movimiento del jugador, enroque,
captura al paso y coronación a dama:
;
; Toledo Atomchess recargado
;
; por Óscar Toledo Gutiérrez
;
; © Copyright Óscar Toledo Gutiérrez 2015
;
; Creación: 28-ene-2015 21:00 tiempo local.
; Revisión: 29-ene-2015 18:17 tiempo local. Terminado.
; Revisión: 30-ene-2015 13:34 tiempo local, termina depuración.
; Revisión: 26-may-2015. Verifica movimientos ilegales. Maneja
; coronacion a reina, captura al paso y enroque.
; Revisión: 04-jun-2015. Al fin depurado.
; Características:
; * Movimientos completos del ajedrez (excepto coronación sólo a reina)
; * Introduzca movimientos en forma algebraica (D2D4) (todos sus
; movimientos son verificados)
; * Búsqueda en profundidad hasta 3 niveles
; * Tamaño de 831 bytes (cabe en dos sectores de arranque)
; Note: Soy lo bastante flojo para escribir mi propio ensamblador
; en lugar de buscar uno, así que disculpe la sintaxis ;)
code16
; Búsque "REEMPLACE" para cambios para archivo COM
org &7c00 ; REEMPLACE with ORG &0100
; Inicialización
mov sp,stack
cld
push cs
push cs
push cs
pop ds
pop es
pop ss
; Carga el segundo sector
sr0: push ds
push es
mov ax,&0201
mov bx,&7e00
mov cx,&0002
xor dx,dx
int &13 ; REEMPLACE con NOP NOP
pop es
pop ds
jb sr0
; Crea el tablero
mov bx,board
sr1: mov al,bl
and al,&88 ; Tablero 0x88
jz sr2
mov al,&07 ; Frontera
sr2: mov [bx],al
inc bl
jnz sr1
; Posiciona las piezas
mov si,initial
mov [enp],si ; Reinicia estado de captura al paso
sr3: lodsb ; Lee pieza
mov [bx],al ; Piezas negras
or al,8
mov [bx+&70],al ; Piezas blancas
mov al,&11
mov [bx+&10],al ; Peón negro
mov al,&19
mov [bx+&60],al ; Peón blanco
inc bx
cmp bl,&08
jnz sr3
;
; Bucle principal
;
sr21: call display_board
call key2
push di
call key2
pop si
mov ch,&00 ; Turno actual (0=Blanco, 8=Negro)
call play_validate
test ch,ch ; ¿Cambió de turno?
je sr21 ; No, el movimiento no era válido
call display_board
mov ch,&08 ; Turno actual (0=Blanco, 8=Negro)
dec byte [legal]
mov word [depth],stack-(4+8+4+8+4+8+4)*2
call play
call play_validate
jmp short sr21
;
; La computadora juega :)
;
play_validate:
mov byte [legal],1
mov word [depth],stack-(4+8+4)*2
play: mov bp,-32768 ; Puntuación actual
push si ; Cuadro origen
push di ; Cuadro destino
xor ch,8 ; Cambia de bando
mov si,board
sr7: lodsb ; Lee el cuadro
xor al,ch ; XOR con el bando que juega actualmente
and al,&0f ; Elimina el bit de pieza movida
dec ax ; Traslada a 0-5
cmp al,6 ; ¿Es frontera o cuadro vacío?
jnc sr6 ; Sí, salta
or al,al ; ¿Es un peón?
jnz sr8 ; No, salta
or ch,ch ; ¿Juega con negras?
jnz sr25 ; No, salta
sr8: inc ax ; Dirección revertida para peón
sr25: dec si
mov bx,offsets
push ax
xlat
mov dh,al ; Offset de movimientos en dh
pop ax
add al,total-offsets
xlat
mov dl,al ; Total de movimientos de pieza en dl
sr12: mov di,si ; Reinicia el cuadro destino
mov bx,displacement
mov al,dh
xlat
mov cl,al ; Offset de desplazamiento en cl
sr9: add di,cx
and di,&ff
or di,board
mov al,[si] ; Contenido del cuadro origen en al
mov ah,[di] ; Contenido del cuadro destino en ah
and ah,&0f ; ¿Cuadro vacío?
jz sr10 ; Sí, salta
xor ah,ch
sub ah,&09 ; ¿Es una captura válida?
cmp ah,&06
mov ah,[di]
jnc sr18 ; No, salta para evitar
cmp dh,16 ; ¿Moviendo peón?
jc sr19
test cl,1 ; ¿Avance recto?
je sr18 ; Sí, evita
jmp short sr19
sr10: cmp dh,16 ; ¿Moviendo peón?
jc sr19 ; No, salta
test cl,1 ; ¿En diagonal?
je sr19 ; No, salta
mov bx,si
dec bx
test cl,2 ; ¿Yendo a la izquierda?
jne sr29
inc bx
inc bx
sr29: cmp bx,[enp] ; ¿Es una captura válida al paso?
jne sr18 ; No, evita
sr19: push ax ; Salva los cuadros origen y destino en la pila
mov al,ah
and al,7
cmp al,6 ; ¿Se comió al rey?
jne sr20
cmp sp,stack-(4+8+4)*2 ; Si fue en la primera respuesta...
mov bp,20000 ; ...máxima puntuación (probable jaquemate/ahogado)
je sr26
mov bp,7811 ; Puntuación máxima
sr26: add sp,6 ; Ignora valores
jmp sr24
sr20: mov bx,scores
xlat
cbw ; ax = puntuación por captura (garantiza ah = 0)
mov bx,[enp] ; bx = peón actual disponible para captura al paso
cmp sp,[depth]
jbe sr22
pusha
mov [enp],ax ; Ahora captura al paso no es posible
mov al,[si] ; Lee el cuadro origen
and al,&0f ; Limpia el bit 4 (marca pieza movida)
cmp al,&0e ; ¿Es un rey?
je sr36
cmp al,&06
jne sr37 ; No, salta
sr36: mov bx,si
sub bx,di
mov bh,ch ; Crea una torre movida
xor bh,&02 ;
cmp bl,2 ; ¿Es enroque a la izquierda?
jne sr38 ; No, salta
mov [di+1],bh ; Pone junto al rey
mov [di-2],ah
jmp sr37
sr38: cmp bl,-2 ; ¿Es enroque a la derecha?
jne sr37
mov [di-1],bh ; Pone junto al rey
mov [di+1],ah
sr37:
cmp al,&09 ; ¿Es un peón?
je sr31
cmp al,&01
jne sr30 ; No, salta
sr31: mov bp,sp
mov bx,di
cmp bl,&10 ; ¿Llegando a la línea superior?
jc sr32 ; Sí, salta
cmp bl,&70 ; ¿Llegando a la línea inferior?
jc sr33 ; No, salta
sr32: xor al,&05 ; Corona a reina
add word [bp+14],90 ; Añade puntos por reina
sr33: sub bx,si
call en_passant_test
jnc sr41
mov [bx],ah ; Limpia el peón tomado al paso
add word [bp+14],10 ; Añade puntos por peón
jmp sr30
sr41: and bx,&001f ; ¿Moviéndose dos cuadros adelante?
jne sr30 ; No, salta
mov [enp],di ; Toma nota de que vale tomarlo al paso
sr30: mov [di],al
mov [si],ah ; Limpia el cuadro origen
call play
mov bx,sp
sub [bx+14],bp ; Substrae BP (puntos) de AX
popa
;
; Si llega a la máxima profundidad entonces el código
; llega aquí >sin< mover la pieza
;
sr22: mov [temp],ax
cmp sp,stack-4*2 ; ¿Primer nivel?
jnz sr28 ; No, salta
test byte [legal],255 ; ¿Buscando movimiento legal?
jz sr28 ; No, salta
mov bp,sp
cmp si,[bp+4] ; ¿El origen es igual?
jnz sr23
cmp di,[bp+2] ; ¿El destino es igual?
jnz sr23
cmp ax,-16384 ; ¿Es movimiento legal?
jl sr23
add sp,6
ret
;
; Nota: La instrucción TEST limpia la bandera de acarreo
;
en_passant_test:
test bl,1 ; ¿Diagonal?
je sr42 ; No, salta
test byte [di],255 ; ¿Captura?
jne sr42 ; Sí, salta
test bl,2 ; ¿Yendo a la izquierda?
stc ; Pone acarreo
lea bx,[si-1]
jne sr42
lea bx,[si+1]
sr42: ret
displacement:
db -33,-31,-18,-14,14,18,31,33
db -16,16,-1,1
db 15,17,-15,-17
db -15,-17,-16,-32
db 15,17,16,32
scores:
db 0,10,50,30,90,30
;
; Marcador requerido por BIOS para arrancar el disco flexible.
;
ds &7dfe-* ; REEMPLACE con nada para archivo COM
db &55,&aa ; REEMPLACE con nada para archivo COM
; Inicio del segundo sector
sr28: cmp bp,ax ; ¿Mejor puntuación?
jg sr23 ; No, salta
mov bp,ax ; Nueva mejor puntuación
jne sr27
in al,(&40)
cmp al,&55 ; Un poco de aleatoriedad
jb sr23
sr27: pop ax
add sp,4
push si ; Guarda el movimiento
push di
push ax
sr23: mov [enp],bx ; Restaura estado de captura al paso
pop ax ; Restaura el tablero
mov [si],al
mov [di],ah
mov bx,di
sub bx,si
and al,&07 ; Separa el código de pieza
cmp al,&01 ; ¿Es un peón?
jne sr43
call en_passant_test
jnc sr43
mov byte [bx],ch ; Limpia
xor byte [bx],9 ; Restaura peón del oponente
sr43: cmp al,&06 ; ¿Es un rey?
jne sr18
mov bh,ch ; Crea una torre sin mover
xor bh,&12 ;
cmp bl,-2 ; ¿Enroque a la izquierda?
jne sr40
mov [di-2],bh
mov [di+1],ah
jmp sr18
sr40: cmp bl,2 ; ¿Enroque a la derecha?
jne sr18
mov [di+1],bh
mov [di-1],ah
sr18: dec ax
and al,&07 ; ¿Es un peón?
jz sr11 ; Sí, verificación especial
cmp al,&05 ; ¿Es un rey?
jne sr34 ; No, salta
test byte [si],&10 ; ¿El rey ya se había movido?
je sr34 ; Sí, salta
cmp word [temp],-4096 ; ¿Está en jaque?
jl sr34 ; Sí, salta
or ah,ah ; ¿Se mueve a cuadro vacío?
jne sr34 ; No, salta
cmp bl,-1 ; ¿Va a la izquierda por un cuadro?
je sr44
cmp bl,1 ; ¿Va a la derecha por un cuadro?
jne sr34
mov bh,[si+3] ; Lee la torre
mov bl,[si+2] ; Lee el cuadro destino
jmp sr46
sr44: test byte [si-3],255 ; ¿Está vacío el cuadro a la der. de la torre?
jne sr34 ; No, salta
mov bh,[si-4] ; Lee la torre
mov bl,[si-2] ; Lee el cuadro destino
sr46: test bl,bl ; ¿Finalizando en cuadro vacío?
jne sr34 ; No, salta
and bh,&17
cmp bh,&12 ; ¿Torre sin mover?
je sr9 ; Sí, puede moverse dos cuadros para enroque
sr34: cmp al,&04 ; ¿Caballo o rey?
jnc sr14 ; Termina secuencia, va a siguiente movimiento
or ah,ah ; ¿A cuadro vacío?
jz sr9 ; Sí, sigue línea de cuadros
sr16: jmp short sr14
sr11: and cl,&1f ; ¿Avanzó a primer cuadro?
cmp cl,&10
jnz sr14
sr15: or ah,ah ; ¿Peón a cuadro vacío?
jnz sr17 ; No, cancela el movimiento de doble cuadro
mov ax,si
sub al,&20 ; ¿En la línea inicial superior?
cmp al,&40 ; ¿En la línea inicial inferior?
jb sr17 ; No, cancela el movimiento de doble cuadro
sr14: inc dh
dec dl
jnz sr12
sr17: inc si
sr6: cmp si,board+120
jne sr7
pop di
pop si
sr24: xor ch,8
ret
display_board:
; Ilustra el tablero
call display3
mov si,board
sr4: lodsb
and al,&0f
mov bx,chars
xlat
call display2
sr5: cmp si,board+128
jnz sr4
ret
key2:
mov di,board+127
call key
add di,ax
call key
shl al,4
sub di,ax
ret
key:
push di
mov ah,0
int &16
push ax
call display
pop ax
and ax,&0f
pop di
ret
display2:
cmp al,&0d
jnz display
display3:
add si,7
mov al,&0a
call display
mov al,&0d
display:
push si
mov ah,&0e
mov bh,&00
int &10
pop si
ret
chars:
db ".prbqnk",&0d,".PRBQNK"
initial:
db &12,&15,&13,&14,&16,&13,&15,&12
offsets:
db 16,20,8,12,8,0,8
total:
db 4, 4,4, 4,8,8,8
; Bytes to say something
db "Toledo Atomchess reloaded"
db "nanochess.org"
ds &8000-* ; REEMPLACE con &0500 para archivo COM
board: ds 256
depth: ds 2 ; Profundidad de búsqueda
enp: ds 2 ; Captura al paso
temp: ds 2 ; Puntuación temporal
legal: ds 1 ; Bandera que indica validación de movimiento legal
ds 249
stack:
end
Ligas relacionadas