Estaba limpiando un área de trabajo antigua cuando vi mi viejo cuaderno azul que usé de 1986 a 1989. Había sufrido el paso del tiempo y polvo.
Mi viejo cuaderno de 1988
Pensé que era el mismo cuaderno que había visto antes, porque resulta que tengo dos cuadernos azules del mismo modelo, pero perdí uno. Para mi sorpresa, descubrí que era el segundo cuaderno perdido ¡Donde puse mi primer programa en ensamblador Z80!
Muy contento, de inmediato tomé fotos de las páginas importantes, en caso de que perdiera el cuaderno de nuevo.
Y los recuerdos comenzaron a volver lentamente...
La historia
Tenía 9 años y quería escribir juegos sorprendentes. Por supuesto, era demasiado niño para crear algo al nivel de los juegos que veía en las revistas como Compute! o Input MSX.
Después de cuatro años de escribir programas en lenguaje BASIC, sentí que me detenía la lentitud inherente de un lenguaje interpretado.
Este juego fue escrito en junio de 1988, mi padre daba una clase de programación del procesador VDP TMS9128. Este procesador de video tenía amplia disponibilidad en ese momento (via Digikey) debido al crash de las computadoras personales.
Mis notas sobre la estructura de los 16k de VRAM del VDP. Note los números de registros.
Los estudiantes habían estado soldando sus computadoras basadas en Z80 sobre un tablero de cobre perforado con agujeros espaciados por 2.54mm.
Desafortunadamente mis manos no eran precisas para sostener un cautín de soldar, así que había estado observando aburrido como los alumnos disfrutaban construir sus computadoras, como lograban hacerlas funcionar, y como solucionaban los bugs.
Esto me concedió toneladas de tiempo para pensar acerca del concepto de programación del ensamblador Z80. Había sido incapaz de atrapar el concepto de como poner juntas las instrucciones. Las semanas previas antes de ese domingo (puedo recordar que era domingo), mientras observaba a mi padre poner el código para la EPROM de monitor/VDP/teclado para la computadora estudiantil, finalmente se me prendió el foco. ¡Por supuesto! Varias instrucciones componen una sentencia de lenguaje BASIC, así que era una cuestión de replicar lo mismo en ensamblador.
Esa mañana de domingo antes de la clase, comencé a escribir furiosamente sobre mi cuaderno un juego de un karateka que luchaba contra una mujer que lanzaba cuchillos. Tenía la idea completa en mi cabeza, ahora me doy cuenta por primera vez de que tenía un algoritmo en BASIC muy claro en mi mente. Era solo cuestión de trasladarlo a ensamblador. Me había dado cuenta de que la separación de líneas del lenguaje BASIC era invisible en ensamblador, pero cada dirección podía relacionarse con un número imaginario de línea del lenguaje BASIC.
Puedes imaginar la sorpresa de toda la clase cuando les mostré orgullosamente mi juego. Recuerdo que tres alumnos copiaron manualmente el código de un vaciado de la RAM y después hicieron copias fotostáticas para otros estudiantes.
Haciendolo funcionar en 2021
Aquí están las fotos de mi cuaderno. Tendrás que perdonar las horribles letras de niño. Apenas algunos comentarios aquí y allá.
Mientras comentaba cada página (¡y analizaba a mi yo de 9 años!), trasladé este código a tniASM v0.44 para MSX y Colecovision (solo porque sí). Se requiere la siguiente capa de apoyo y traducción porque no disponemos de la ROM original de la computadora estudiantil:
;
; Karateka
;
; por Oscar Toledo G.
; (c) Copyright Oscar Toledo G. 1988-2021
; https://nanochess.org/
;
; Creación: Jun/1988. Tenía 9 años.
; Revisión: May/12/2021. Portado a MSX/Colecovision.
;
COLECO: EQU 0 ; Defina esto a 0 para MSX, 1 para Colecovision
RAM_BASE: EQU $E000-$7000*COLECO
VDP: EQU $98+$26*COLECO
KEYSEL: EQU $80
JOYSEL: EQU $C0
JOY1: EQU $FC
JOY2: EQU $FF
if COLECO
fname "KARATECV.ROM"
org $8000,$9fff
dw $aa55 ; Sin pantalla de título de BIOS
dw 0
dw 0
dw 0
dw 0
dw START
jp 0 ; RST $08
jp 0 ; RST $10
jp 0 ; RST $18
jp 0 ; RST $20
jp 0 ; RST $28
jp 0 ; RST $30
jp 0 ; RST $38
jp 0 ; Sin manejo de NMI
else
fname "KARATMSX.ROM"
org $4000,$5fff
dw $4241
dw START
dw 0
dw 0
dw 0
dw 0
dw 0
dw 0
SNSMAT: equ $0141
endif
WRTVDP:
ld a,b
out (VDP+1),a
ld a,c
or $80
out (VDP+1),a
ret
SETWRT:
ld a,l
out (VDP+1),a
ld a,h
or $40
out (VDP+1),a
ret
WRTVRM:
push af
call SETWRT
pop af
out (VDP),a
ret
FILVRM:
push af
call SETWRT
.1: pop af
out (VDP),a
push af
dec bc
ld a,b
or c
jp nz,.1
pop af
ret
; Configura el VDP antes del juego
setup_vdp:
LD BC,$0200
CALL WRTVDP
LD BC,$C201 ; Sin interrupciones
CALL WRTVDP
LD BC,$0F02 ; $3C00 para tabla de caracteres
CALL WRTVDP
LD BC,$FF03 ; $2000 para tabla de color
CALL WRTVDP
LD BC,$0304 ; $0000 para tabla de bitmaps
CALL WRTVDP
LD BC,$3605 ; $1b00 para tabla de atributos de sprites
CALL WRTVDP
LD BC,$0706 ; $3800 para bitmaps de sprites
CALL WRTVDP
LD BC,$0407 ; Borde azul
CALL WRTVDP
IF COLECO
LD HL,($006C) ; Caracteres BIOS
LD DE,-128
ADD HL,DE
ELSE
LD HL,($0004) ; Caracteres BIOS
INC H
ENDIF
PUSH HL
LD DE,$0100
LD BC,$0300
CALL LDIRVM
POP HL
PUSH HL
LD DE,$0900
LD BC,$0300
CALL LDIRVM
POP HL
LD DE,$1100
LD BC,$0300
CALL LDIRVM
LD HL,$2000
LD BC,$1800
LD A,$F4
CALL FILVRM
RET
LDIRVM:
EX DE,HL
.1: LD A,(DE)
CALL WRTVRM
INC DE
INC HL
DEC BC
LD A,B
OR C
JR NZ,.1
RET
GTTRIG:
if COLECO
out (KEYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
ld c,a
in a,(JOY2)
and c
ld c,a
out (JOYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
and c
ld c,a
in a,(JOY2)
and c
rlca
rlca
ccf
ld a,0
sbc a,a
ret
else
xor a
call $00d8
or a
ret nz
ld a,1
call $00d8
or a
ret nz
ld a,2
call $00d8
or a
ret nz
ld a,3
call $00d8
or a
ret nz
ld a,4
call $00d8
ret
endif
;
; Obtiene la dirección del joystick
; 0 - Sin movimiento
; 1 - Arriba
; 2 - Arriba + derecha
; 3 - Derecha
; 4 - Derecha + abajo
; 5 - Abajo
; 6 - Abajo + izquierda
; 7 - Izquierda
; 8 - Izquierda + arriba
;
GTSTCK:
if COLECO
out (JOYSEL),a
ex (sp),hl
ex (sp),hl
in a,(JOY1)
ld b,a
in a,(JOY2)
and b
and $0f
ld c,a
ld b,0
ld hl,joy_map
add hl,bc
ld a,(hl)
ret
joy_map:
db 0,0,0,6,0,0,8,7,0,4,0,5,2,3,1,0
else
xor a
call $00d5
or a
ret nz
ld a,1
call $00d5
or a
ret nz
ld a,2
jp $00d5
endif
; Rutinas ROM olvidadas
; Limpia la pantalla
LIMPIA: ; $04cc
LD HL,$3C00
LD BC,$0300
XOR A
JP FILVRM
; Copia cadena al VDP
INMEDIATO: ; $0169
EX (SP),HL
.0: LD A,(HL)
INC HL
OR A
JR Z,.1
PUSH AF
POP AF
OUT (VDP),A
JR .0
.1: EX (SP),HL
RET
;
; Inicio del juego
;
START: ; 8000
DI
LD SP,RAM_BASE+256
Cada trozo extra de código será agregado al final del código previo.
Página 1 de mi juego Z80, click derecho para mayor resolución.
Esta es la página 1 de mi juego Z80. El código fuente a la derecha fue escrito pensando en ponerlo dentro de un programa ensamblador que iba a programar (imagina eso, un niño de 9 años pensando ambiciosamente en ¡escribir un ensamblador!)
Para entrar este código en la computadora de los estudiantes, uno solo necesita entrar el microcódigo a la izquierda. Si encuentras un error del lado derecho es porque estaba pensado solo para referencia.
El código comienza por inicializar el juego y crear un bucle principal. Puse NAVAJA como NEVAJA. Nota como reemplacé PONEHL por una rutina MSX similar WRTVDP. La rutina INMEDIATO copia los siguientes bytes directamente al VDP hasta que encuentra un byte cero.
JUEGO:
CALL setup_vdp ; Requerido para iniciar VDP
CALL LIMPIA ; Limpia la pantalla
CALL PRESENTACION ; Pantalla de título
CALL GRAFICOS ; Pone gráficos
PRIN: ; 8009
CALL PATCH ; Parche para el salto
CALL KARATEKA ; Mueve karateka
CALL MALORA ; Mueve enemigo
CALL NEVAJA ; Mueve cuchillos
JP PRIN ; Repite el bucle principal
; Pantalla de título
PRESENTACION: ; 8018
LD HL,$3C4D
CALL SETWRT
CALL INMEDIATO
Página 2 de mi juego Z80.
Por supuesto, no tenía idea de directivas excepto que poner un byte en cada línea. Esta es la pantalla de título. "PULSE ESPACIO" indica que se debe presionar la barra de espacio. Lo cambié para ser un botón de joystick del MSX o Colecovision.
DB "KARATEKA",0
LD HL,$3ECA
CALL SETWRT
CALL INMEDIATO
DB "PULSE ESPACIO",0
Página 3 de mi juego Z80.
Utiliza la rutina de BIOS interna $04f5 para leer la matriz del teclado, esto fue reemplazado por una llamada GTTRIG de MSX (o en Colecovision lee los botones). Entonces pone los gráficos para el karateka, el cuchillo y la judoka.
; Cambiado del original
TEC: XOR A
CALL GTTRIG
OR A
RET NZ
JP TEC
; Pone los gráficos para el Karateka, cuchillo y Judoka
GRAFICOS:
LD HL,$3800
LD DE,GRAFICOS_8600
LD B,$00
.1: LD A,(DE)
CALL WRTVRM
INC HL
INC DE
DJNZ .1
LD HL,$0B08
LD DE,GRAFICOS_85D8
LD B,$28
.2: LD A,(DE)
Página 4 de mi juego Z80.
Ahora pone algunas variables para la posición del karateka y los cuchillos disparados por el enemigo. También reinicia la variable kicks_given. Comienza a llenar el color para el cielo y la hierba.
Entonces limpia la pantalla y comienza a poner los caracteres en la pantalla para representar visualmente el cielo y el pasto. Y sí, embarazosamente el niño de 9 años esta repitiendo código como loco.
Por alguna razón desconocida pongo de nuevo el registro 1 del VDP, entonces pone la posición de inicio para el jugador. Note que las microclaves dicen 3E 68 corrigiendo el código fuente a la derecha que dice LD A,70 poniendo el jugador debajo del piso.
El movimiento es a la izquierda y derecha, arriba para golpes con la mano, abajo para patadas. De nuevo la lectura del teclado es reemplazada por GTSTCK de MSX (y trasladado para Colecovision).
Creo que iba en una compilación mental de BASIC a ensamblador, es lo único que puede explicar porque el código esta tan mal optimizado. Optimizarlo un poco me habría ahorrado mucho tiempo al volver a entrar el código.
Mover el karateka a la izquierda se lograba simplemente sustrayendo 8 de la coordenada X, y actualizando también en la tabla de atributos de sprites. También hace animación del jugador en dos cuadros.
CP $05
JP Z,GODPI
RET
; Mover karateka a la izquierda
MIK: ; 8149
LD A,(x_karateka)
SUB $08
LD (x_karateka),A
LD HL,$1b01
CALL WRTVRM
LD A,(spr_karateka)
CP $00
JP Z,SP2
LD A,$00
LD (spr_karateka),A
JP SPR
SP2: ; 8167
LD A,$08
LD (spr_karateka),A
SPR: LD HL,$1b02
CALL WRTVRM
RET
Página 9 de mi juego Z80.
El movimiento a la derecha replica esencialmente el mismo código que el movimiento a la izquierda. Pude haber usado una subrutina para pasar los parametros en los registro DE o BC..
Como no tiene idea de la dirección del jugador, tiene que leer el cuadro del sprite para saber en que dirección va. Otra variable lo haría más fácil.
El código para golpear a la izquierda (GAI) y derecha (GAD), simplemente actualiza el sprite al cuadro de golpe, y llama a NADEO (donde mira si el enemigo es golpeado), entonces realiza un retardo (¡deteniendo el juego completo!) y restaura el sprite original. Todavía necesitaba entender el concepto de objetos independientes.
JP Z,GAD
CP $0C
JP Z,GAD
RET
GAI: ; 81B5
LD HL,$1B02
LD A,$10
ZON: CALL NADEO
LD BC,$4000
.1: DEC BC
LD A,B
OR C
JR NZ,.1
LD A,(spr_karateka)
JP WRTVRM
GAD: ; 81CB
LD HL,$1B02
LD A,$14
JP ZON
Página 11 de mi juego Z80.
El código para manejar patadas es esencialmente una copia del código previo. BOLAS es una exclamación, muy similar a ¡Ay Caramba!
GODPI: ; 81D3 Olvidé la etiqueta en el código
LD A,(spr_karateka)
CP $00
JP Z,IZ
CP $08
JP Z,IZ
CP $04
JP Z,DER
CP $0C
JP Z,DER
RET
IZ: ; 81EB
LD HL,$1B02
LD A,$18
HOLA: CALL NADEO
LD BC,$4000
BOLAS: DEC BC
LD A,B
OR C
JR NZ,BOLAS
LD A,(spr_karateka)
JP WRTVRM
DER: ; 8201
LD HL,$1B02
LD A,$1C
JP HOLA
Página 12 de mi juego Z80.
Después de ver que no había forma de evitar los cuchillos, implementé el salto para el karateka.
Esto se ve más pensado, como llamar NEVAJA para mantener los cuchillos en movimiento. Pero todavía son dos subrutinas separadas, una para mover el jugador arriba, y otra para moverlo abajo. Cada una con su propio retardo.
; Salto del karateka
SALTO: ; 8209
LD A,(y_karateka)
LD B,$04
OSC: LD HL,$1B00
SUB $04
CALL WRTVRM
PUSH HL
PUSH AF
CALL NEVAJA
PUSH BC
LD BC,$4000
.1: DEC BC
LD A,B
OR C
JR NZ,.1
POP BC
POP AF
POP HL
DJNZ OSC
LD B,$04
KAR: LD HL,$1B00
ADD A,$04
CALL WRTVRM
PUSH HL
PUSH AF
CALL NEVAJA
PUSH BC
Página 13 de mi juego Z80.
Mover los cuchillos es un contador simple de izquierda a derecha, actualizando la pantalla.
El niño no pudo solucionar como mover los cuchillos más cerca del enemigo que los lanzaba, porque los cuchillos borraban con espacios detrás suyo así que borrarían la mitad derecha del enemigo.
LD BC,$4000
BOCHA: DEC BC
LD A,B
OR C
JR NZ,BOCHA
POP BC
POP AF
POP HL
DJNZ KAR
RET
NEVAJA: ; 8247
LD A,(knife1)
ADD A,$A5
LD H,$3D
LD L,A
LD A,$65
CALL WRTVRM
LD A,(knife1)
ADD A,$A4
LD H,$3D
LD L,A
LD A,$60
CALL WRTVRM
LD A,(knife1)
INC A
Página 14 de mi juego Z80.
Un soprendente gasto de bytes repitiendo el mismo código para mover un cuchillo paralelo en la misma columna usando una variable diferente.
De nuevo un gasto de bytes borrando cada cuchillo por separado, cuando ambos vienen juntos. Tal vez estaba pensando en hacerlos aparecer en momentos diferentes, pero no pude lograrlo. La función MALORA (una variante de malo) mira si los cuchillos tocan al jugador, de nuevo comparando cada cuchillo por separado.
LD L,A
LD A,$60
CALL WRTVRM
LD A,$00
LD (knife1),A
RET
B2: DEC A
ADD A,$C5
LD H,$3D
LD L,A
LD A,$60
CALL WRTVRM
LD A,$00
LD (knife2),A
RET
MALORA: ; 82B9
LD A,(x_karateka)
RRCA
RRCA
RRCA
LD B,A
LD A,(knife1)
ADD A,$05
CP B
JP Z,MUERTO
LD A,(knife2)
Página 16 de mi juego Z80.
La función MUERTO simplemente llena la tabla de color completa con colores aleatorios (¡Hey! ¡Un efecto visual especial!) y se detiene antes de regresar al monitor interno. No podía insertar un salto al juego principal porque la instrucción RST 00 utiliza un solo byte, y un salto usa 3 bytes (recuerda que no estaba usando un ensamblador) así que para esta vez ahora si puse el salto al juego principal.
ADD A,$05
CP B
JP Z,MUERTO
RET
MUERTO: ; 82D3
LD HL,$2000
LD BC,$1800
M2: LD A,R
CALL WRTVRM
INC HL
DEC BC
LD A,B
OR C
JR NZ,M2
LD BC,$0000
M3: DEC BC
LD A,B
OR C
JR NZ,M3
JP JUEGO ; No en el original porque hubiera tenido que reubicar
Página 17 de mi juego Z80.
Finalmente mi primera subrutina de ayuda NADEO, llamada 4 veces para ver si el enemigo es tocado. No me imagino que intentaba abreviar aquí. La comprobación es simple, si el jugador esta parado exactamente donde salen los cuchillos, le pegará al enemigo, por supuesto tratando de hacer que al jugador lo toquen los cuchillos. Desafortunadamente esta posición pega en el aire, y no hay indicación visual del golpe...
NADEO: ; 82ED
CALL WRTVRM
PUSH AF
PUSH HL
PUSH BC
PUSH DE
LD A,(x_karateka)
CP $28
CALL Z,GOLPES
POP DE
POP BC
POP HL
POP AF
RET
GOLPES: ; 8301
LD A,(kicks_given)
INC A
LD (kicks_given),A
CP $14
JP Z,GANAR
RET
GANAR: ; 830E
CALL LIMPIA
LD HL,$3C03
CALL SETWRT
CALL INMEDIATO
Página 18 de mi juego Z80.
Esta parte es sencilla. Solo un mensaje al ganar (BIEN!)
DB "BIEN!",0
LD BC,$0000
BION: DEC BC
LD A,B
OR C
JR NZ,BION
JP JUEGO
Página 19 de mi juego Z80.
Obviamente escribí el juego completo de mi mente al papel, y no pensé en un retardo para hacer el juego correr a una velocidad manejable. Así que este código hace un retardo y también maneja el salto.
Nota que no usé interrupciones. No sabía de las interrupciones entonces, pero la simple idea de tener una constante de tiempo continua referida en términos de cuadros de video ¡hubiera hecho estallar mi mente!
PATCH:
LD BC,$4000
.1: DEC BC
LD A,B
OR C
JR NZ,.1
XOR A
CALL GTTRIG
OR A
JP NZ,SALTO
RET
Página 20 de mi juego Z80.
Esta página no tuvo que portarse ya que VPOKE se reemplazó por la rutina WRTVRM. Y no se usa VPEEK. Por cierto, esto muestra la influencia que tenía en mí la revista Input MSX.
Página 21 de mi juego Z80.
Página 22 de mi juego Z80.
Estas páginas contienen los gráficos para el juego. Debe existir en alguna parte papel cuadriculado con los dibujos originales pero no los he encontrado.
GRAFICOS_85D8:
DB $00,$03,$06,$0D,$1B,$17,$03,$01 ; JUDOKA
DB $00,$C0,$00,$C0,$40,$C0,$80,$00
DB $0F,$F3,$00,$07,$07,$02,$1C,$30
DB $F0,$CF,$00,$E0,$E0,$40,$38,$0C
DB $10,$10,$20,$FF,$FF,$20,$10,$10 ; CUCHILLO
GRAFICOS_8600:
DB $03,$07,$07,$03,$01,$03,$03,$03 ; KARATEKA IZQ.
DB $03,$03,$03,$01,$01,$01,$03,$0F
DB $C0,$E0,$E0,$C0,$80,$C0,$C0,$C0
DB $C0,$C0,$C0,$80,$80,$80,$80,$80
DB $03,$07,$07,$03,$01,$03,$03,$03 ; KARATEKA DER.
DB $03,$03,$03,$01,$01,$01,$01,$01
DB $C0,$E0,$E0,$C0,$80,$C0,$C0,$C0
DB $C0,$C0,$C0,$80,$80,$80,$C0,$F0
DB $03,$07,$07,$03,$01,$03,$03,$03 ; KARATEKA CAMINA IZQ.
DB $03,$03,$03,$01,$02,$04,$0C,$3D
DB $C0,$E0,$E0,$C0,$80,$C0,$C0,$C0
DB $C0,$C0,$C0,$80,$40,$20,$60,$E0
DB $03,$07,$07,$03,$01,$03,$03,$03 ; KARATEKA CAMINA DER.
DB $03,$03,$03,$01,$02,$04,$06,$07
DB $C0,$E0,$E0,$C0,$80,$C0,$C0,$C0
DB $C0,$C0,$C0,$80,$40,$20,$30,$BC
DB $03,$07,$07,$03,$01,$FF,$FF,$03 ; KARATEKA GOLPE IZQ.
DB $03,$03,$03,$01,$01,$01,$03,$0F
DB $C0,$E0,$E0,$C0,$80,$C0,$C0,$C0
DB $C0,$C0,$C0,$80,$80,$80,$80,$80
DB $03,$07,$07,$03,$01,$03,$03,$03 ; KARATEKA GOLPE DER.
DB $03,$03,$03,$01,$01,$01,$01,$01
DB $C0,$E0,$E0,$C0,$80,$FF,$FF,$C0
DB $C0,$C0,$C0,$80,$80,$80,$C0,$F0
DB $03,$07,$07,$03,$01,$03,$C3,$C3 ; KARATEKA PATEA IZQ.
DB $33,$33,$0F,$07,$00,$00,$01,$07
DB $C0,$E0,$E0,$C0,$80,$C0,$C0,$C0
DB $C0,$C0,$C0,$80,$80,$80,$80,$80
DB $03,$07,$07,$03,$01,$03,$03,$03 ; KARATEKA PATEA DER.
DB $03,$03,$03,$01,$01,$01,$01,$01
DB $C0,$E0,$E0,$C0,$80,$C0,$C3,$C3
DB $CC,$CC,$F0,$E0,$00,$00,$80,$E0
Página 23 de mi juego Z80.
La última página contiene documentación de las variables y la mitad está desincronizada, y el cuchillo 3 no se implementó. Es un ejemplo práctico de documentación real :P
Karateka.zip fuentes y archivos ROM para Colecovision y MSX (5 kb)
Karate2.zip archivos ROM para Colecovision y MSX (17 kb), programado en un día con todas mis herramientas. Es el juego como lucia en la mente de mis 9 años.
Extras
Solo me tomó casi tres años (11-feb-2024) descubrir que los dibujos para el juego estaban de hecho en el mismo cuaderno, pero las dos páginas estaban pegadas entre si.
El siguiente dibujo no fue usado en el juego debido a que lucía demasiado diferente de mis dibujos base, pero recuerdo que la pierna en diagonal se inspiró en ese dibujo. Por supuesto, me tomó años descubrir que un movimiento de patada no es simplemente una pierna levantada (no lo repitas).
Ligas relacionadas
Viboritas: Un juego en código máquina Z80 escrito en 2K cuando tenía 11 años.
Cubos: Un juego en código máquina Z80 escrito en 3K cuando tenía 12 años.