Mr. Chess para Intellivision

Era una noche oscura y tormentosa, y aunque podía estar viendo una película en lugar de eso decidí programar un nuevo juego para mi Intellivision. Sentí como que sería más productivo que quedarme quieto en la silla, bajando mi IQ con guiones estereotipados, escenas de acción, y efectos especiales.
Hace muchos años, cuando era un niño siempre quise escribir un programa de ajedrez. En esos tiempos estaba usando una computadora homebrew basada en el Z80, y no tenía idea de búsqueda de árboles, algoritmos minimax, cortes alfa-beta ni puntuación de tableros.
Solía pasar horas dibujando piezas de ajedrez que después ponía en la pantalla y entonces hacía el interfaz para mover piezas, después (tristemente), solo tenía un juego de ajedrez para dos jugadores porque no podía programar como hacer que la computadora jugara.
Mi primer intento para que la computadora jugara el juego de ajedrez sucedió en el año de 1989, y estaba basado simplemente en leer el tablero, intentar todos los movimientos, y llamarse a si mismo. Siendo un niño no avisoré todos los posibles caminos del código. Fui capaz de jugar emocionado unos pocos movimientos contra la computadora, y entonces la pila de memoria borró todo, y mi precioso primer juego de ajedrez con código de inteligencia artificial se perdió en las nieblas del tiempo. Tal vez la cosa más interesante fue que nadie de mi familia vio lo que hice, así que cuando les dije ¡no me creyeron!

¡Bienvenido al año 2023!

Volvamos al año 2023, 44 años después de la introducción del Intellivision, y 41 años después de la aparición de USCF Chess para Intellivision.

USCF Chess corriendo en el emulador jzintv.
El juego USCF Chess utilizaba 8x8 pixeles y los gráficos de las piezas sufrían por el pequeño espacio disponible para dibujar (a pesar que de hecho algo bonito y reconocible se puede hacer en 6x6 pixeles como demuestra mi ajedrez JS1K). Decidí dibujar las piezas de ajedrez en una matriz de 16x8 pixeles, y en esta área pude dibujar piezas de 10x8 pixeles.

Las figuras de ajedrez para mi programa de ajedrez en Intellivision.
Con el conjunto inicial de piezas, procedí a diseñar las rutinas para dibujar el tablero en la pantalla, y puse un cursor en la pantalla movible con el controlador de disco.
Ahora tenía el típico juego de ajedrez de dos jugadores, sin validación de movimientos. ¿Qué tal un motor de ajedrez?

El motor

Utilicé el código de mi Toledo Atomchess escrito originalmente para procesadores x86 y comencé a convertirlo al código ensamblador del procesador CP1610.
El desafío principal fue mantener el uso de memoria dentro de los límites del Intellivision (240 bytes y 112 palabras de 16 bits). En contraste, el USCF Chess de Mattel tenía la ventaja de 2K extra de RAM (en ese tiempo un cartucho costoso).
En mi motor de ajedrez el tablero se representa como 120 bytes, en un arreglo de 10x12 donde el tablero de 8x8 se posiciona en el centro, además se crea una pila en la memoria de 8 bits para mantener datos de la búsqueda (10 bytes por cada nivel de análisis).
Aún más, se construyó un interface de juego usando el lenguaje IntyBASIC, e IntyBASIC require un poco de memoria para sus funciones (40 bytes para soporte básico y reproductor de mùsica, y 41 palabras para soporte de visualización), además las variables requeridas por la interface para mostrar en la pantalla.
A pesar de que fue una conversión bastante directa, no fue tan fácil convertir algunas rutinas. La parte más díficil fue asignar los registros para el código y mantenerlos válidos así como ejecutaba la rutina de juego. Más de una vez erré un registro que debía ser preservado en varios pasos del motor y causé un crash.
Se usan los registros R4 y R5 para leer y escribir la memoria, y también se autoincrementan esos registros. No hay un comportamiento similar en el x86, pero por supuesto, esto hizo que varias partes del código requirieran rediseño. Por la noche ya tenía una versión funcional de este motor simple.

La primera versión funcional de mi juego de ajedrez para Intellivision.
Este motor simple no soportaba los movimientos completos del ajedrez, como al paso, enroque o coronación. Sin embargo, es tan corto este motor de prueba que como programador tengo que mostrar su belleza ¡Sólo 262 líneas de código ensamblador!

	;
	; Motor de ajedrez para Intellivision
	;
	; (c) Copyright 2023 Oscar Toledo G.
	; https://nanochess.org/
	;
	; Creación: 28-feb-2023.
	;

STACK_ENTRY:	EQU 10

piece_total:
	DECLE 3,3,7,3,3,7,7

piece_offsets:
	DECLE displacement+16
	DECLE displacement+20
	DECLE displacement+0
	DECLE displacement+12
	DECLE displacement+8
	DECLE displacement+8
	DECLE displacement+8

displacement:
	DECLE -21,-19,-12,-8,8,12,19,21
	DECLE -10,10,-1,1
	DECLE 9,11,-9,-11
	DECLE -11,-9,-10,-20
	DECLE 9,11,10,20

piece_scores:
	DECLE 0,1,3,3,5,9

PLAY_CHESS:	PROC
	BEGIN

	MVII #-128,R0
	MVO R0,var_BEST_SCORE

	MVI var_TURN,R0
	XORI #8,R0
	MVO R0,var_TURN

	MVII #array_BOARD+21,R3

SR7:	MVI@ R3,R1	; Lee el cuadro.
	XOR var_TURN,R1	; XOR con el lado actual.
	ANDI #$0F,R1	; Elimina bit de movido.
	DECR R1		; Translada a 0-5.
	CMPI #6,R1	; ¿Es frontera o cuadro vacío?
	BC SR17		; Sí, salta.
	TSTR R1		; ¿Es un peón?
	BNE SR8		; No, salta.
	MVI var_TURN,R0
	TSTR R0		; ¿Está jugando las negras?
	BNE SR25	; No, salta.
SR8:	INCR R1		; Invierte la dirección para el peón
SR25:
	ADDI #piece_total,R1
	MVI@ R1,R0
	MVO R0,var_TOTAL_MOVEMENTS
	ADDI #7,R1
	MVI@ R1,R1	; Offset de movimientos en R1.

SR12:	MOVR R3,R2	; Reinicia el cuadro destino.

SR9:	ADD@ R1,R2	; Desplaza en la dirección.
	MVI@ R3,R4	; Cuadro origen.
	MVI@ R2,R5	; Cuadro destino.
	ANDI #$0F,R5	; ¿Cuadro vacío?
	BEQ SR10	; Sí, salta.
	;
	; Se movió a un cuadro que contiene algo.
	;
	XOR var_TURN,R5
	SUBI #9,R5	; ¿Es una captura válida?
	CMPI #6,R5
	MVI@ R2,R5
	BC SR18		; No, salta y evita.
	CMPI #displacement+16,R1	; ¿Moviendo un peón?
	BNC SR19	; No, salta a captura.
	MVI var_TOTAL_MOVEMENTS,R0
	ANDI #2,R0	; ¿Avance recto?
	BEQ SR18	; Sí, salta y evita..
	B SR19		; No, salta y captura.

	;
	; Se movió a un cuadro vacío.
	;
SR10:
	CMPI #displacement+16,R1	; ¿Moviendo un peón?
	BNC SR19	; No, salta a movimiento.
	MVI var_TOTAL_MOVEMENTS,R0
	SARC R0,2	; ¿Movimiento diagonal?
	BNOV SR19	; No, salta a movimiento.
	MOVR R3,R0
	DECR R0
	BNC SR29
	INCR R0
	INCR R0
SR29:	CMP var_EN_PASSANT,R0	; ¿Es una captura al paso válida?
	BNE SR18		; No, salta.

	;
	; Salva el estado actual en la pila simulada
	;
SR19:	MVI var_&STACK,R5
	MVO@ R3,R5		; +0 Cuadro origen.
	MVO@ R2,R5		; +1 Cuadro destino.
	MVI@ R3,R0		; Contenido del cuadro origen.
	MVO@ R0,R5		; +2
	MVI@ R2,R0		; Contenido del cuadro destino.
	MVO@ R0,R5		; +3
	ANDI #7,R0
	CMPI #6,R0		; ¿Rey capturado?
	BNE SR20		; No, salta.
	MOVR R5,R1
	SUBI #STACK_ENTRY*1+4,R1	
	CMP var_&STACK_BASE,R1	; ¿Es la primera respuesta a la computadora?
	MVII #63,R0		; Sí, puntuación máxima más extra.
	BEQ SR26	
	MVII #47,R0		; Sí, puntuación máxima.
SR26:	B SR24

SR20:
	MOVR R0,R4
	ADDI #piece_scores,R4
	MVI@ R4,R0		; Puntos de captura.

	MOVR R5,R4
	SUBI #STACK_ENTRY*2+4,R4		; Profundidad de búsqueda.
	CMP var_&STACK_BASE,R4
	MVI var_BEST_SCORE,R4
	BEQ SR22

	MVO@ R0,R5		; +4

	MVO@ R1,R5		; +5 Apuntador a la tabla de movimientos.
	SWAP R1
	MVO@ R1,R5		; +6

	MVI var_EN_PASSANT,R0	; Estado actual de captura al paso.
	MVO@ R0,R5		; +7
	MVI var_TOTAL_MOVEMENTS,R0	; Total de movimientos restantes.
	MVO@ R0,R5		; +8
	MVI var_BEST_SCORE,R0	; Mejor puntuación hasta ahora.
	MVO@ R0,R5		; +9

	MVO R5,var_&STACK

	CLRR R0			
	MVO R0,var_EN_PASSANT

	MVI@ R3,R4		; R4 = Contenido del cuadro origen.
;	MVI@ R2,R5		; R5 = Contenido del cuadro destino.

	CLRR R5
	MVO@ R5,R3		; Limpia el cuadro origen.
	MVO@ R4,R2		; Limpia el cuadro destino.

	CALL PLAY_CHESS
	; R0 = Puntos de la respuesta.

	MVI var_&STACK,R5
	SUBI #STACK_ENTRY,R5
	MVO R5,var_&STACK

	MVI@ R5,R3	; +0
	ADDI #$0100,R3	; Restaura el cuadro origen.
	MVI@ R5,R2	; +1
	ADDI #$0100,R2	; Restaura el cuadro destino.
	MVI@ R5,R1	; +2
	MVO@ R1,R3	; Restaura el contenido del cuadro origen.
	COMR R0		; Hace negativa la nueva puntuación.
	MVI@ R5,R1	; +3
	MVO@ R1,R2	; Restaura el contenido del cuadro destino.
	INCR R0
	ADD@ R5,R0	; +4 Sustrae de la puntuación previa.
	MVI@ R5,R1	; +5
	SWAP R1
	ADD@ R5,R1	; +6
	SWAP R1
	MVI@ R5,R4	; +7
	MVO R4,var_EN_PASSANT
	MVI@ R5,R4	; +8
	MVO R4,var_TOTAL_MOVEMENTS

	MVI@ R5,R4	; +9

SR22:
	XORI #$80,R4	; Extiende el signo.
	SUBI #$80,R4
	CMPR R4,R0
	BLT SR23
	BNE SR27
	MVI _rand,R0
	ANDI #$2A,R0
	BNE SR23
	MOVR R4,R0
SR27:
	MOVR R0,R4	; Nueva mejor puntuación.

	MVI var_&STACK,R0
	CMP var_&STACK_BASE,R0
	BNE SR23
	MOVR R3,R0
	SUBI #array_BOARD,R0
	MVO R0,var_BEST_ORIGIN
	MOVR R2,R0
	SUBI #array_BOARD,R0
	MVO R0,var_BEST_TARGET

SR23:
	MVO R4,var_BEST_SCORE

SR18:	MVI@ R3,R0
	ANDI #7,R0	; ¿Era un peón?
	DECR R0
	BEQ SR11	; Sí, verifica especial.

	CMPI #1,R0	; ¿Caballo?
	BEQ SR14	; Sí, termina movimiento.
	CMPI #5,R0	; ¿Rey?
	BEQ SR14	; Sí, termina movimiento.
	MVI@ R2,R0
	TSTR R0		; ¿A un cuadro vacío?
	BEQ SR9		; Sí, sigue la línea de cuadros.
	B SR14

SR11:	MVI@ R1,R0
	CMPI #-10,R0
	BEQ SR15
	CMPI #10,R0
	BNE SR14
SR15:	MVI@ R2,R0
	TSTR R0		; ¿Peón a cuadro vacío?
	BNE SR17	; No, cancela movimiento de doble cuadro.
	MOVR R3,R0
	SUBI #array_BOARD+41,R0
	CMPI #40,R0	; ¿En primera línea superior o inferior?
	BNC SR17	; No, cancela movimiento de doble cuadro.

SR14:	INCR R1
	MVI var_TOTAL_MOVEMENTS,R4
	DECR R4
	MVO R4,var_TOTAL_MOVEMENTS
	BPL SR12

SR17:	INCR R3
	CMPI #array_BOARD+99,R3
	BNE SR7

	MVI var_BEST_SCORE,R0
	XORI #$80,R0	; Extiende el signo.
	SUBI #$80,R0

SR24:	MVI var_TURN,R1
	XORI #8,R1
	MVO R1,var_TURN

	RETURN
	ENDP
En caso de que quieras probar este ejemplo temprano y muy básico de un juego de ajedrez, puedes descargar el código fuente para esto (requieres IntyBASIC para compilarlo):
El código para los movimientos completos del ajedrez vino de mi motor Toledo Atomchess Reloaded.
Posteriormente las optimizaciones también causaron dolores de cabeza, por ejemplo, cuando se elimina un cálculo, o se limitaba una expresión a cierto registro, y ese mismo registro era usado después en una forma diferente. Mi favorito fue cuando al integrar el enroque, al paso y coronación, el nuevo código dependía de las piezas de ajedrez con un bit para indicar si se habían movido de su posición original, pero el código viejo todavía hacía comparaciones con el valor completo, así que no podía capturar ninguna pieza que no se hubiera movido ¡ya que el patrón de bits no concordaba con una pieza válida!
Documenté mi progreso con mi juego de ajedrez en Atariage en este hilo https://forums.atariage.com/topic/348824-maybe-another-chess-game/

Modo de torneo

Justo cuando mostraba el juego de ajedrez, Psycho Stormtrooper sugirió que podía existir un modo de torneo. ¡Y me encendió la proverbial luz en la cabeza! Por supuesto, desde hace mucho los juegos japoneses de ajedrez para consolas Gameboy tienen un modo de torneo. Es batallar la consola en diferentes niveles donde cada nivel es representado por un personaje diferente.
Así que cree a Midori, la retadora, y sus enemigos: Monkey, Sue y el profesor Shifu. Dibujé algunas pantallas mostrando a Midori cuando entra al torneo, otra cuando gana el torneo, y una más para cuando pierde el torneo.

Midori.
Ese fue también el momento en que el juego cambió su título a Mr. Chess ya que pensé que tenía más pegue.

La pantalla de título final.
Después comencé a pensar que los enemigos también debían tener su propia pantalla de introducción, y dibujé los gráficos y textos para cada oponente. Es bastante difícil hacer gráficos bonitos en Intellivision, pero me las arreglé para meter todo en el contexto de las limitaciones gráficas. William Moeller contribuyó una mejor (y más divertida) descripción para Monkey que la que escribí primero.

Monkey.
El juego tiene varias melodías agradables para cada personaje, y también para la pantalla de título, la pantalla de ganar y derrotado. Todas estas fueron compuestas por mi hermano Adán Toledo.

Midori vs Monkey.

Desarrollo posterior

Hice más fuerte el motor siguiendo una cadena de recapturas. Supongamos que el motor captura la reina con la reina, pero el jugador toma represalias capturando con un peón, que entonces sería ignorada; pero supongamos que el motor hace cualquier movimiento, el jugador hace otro, y el motor captura la reina (tercer nivel de profundidad) y la búsqueda se detiene. El Intellivision "creería" que puede capturar la reina, y lo integraría como el mejor movimiento posible, pero si añadimos una cadena de recaptura entonces detectaría inmediatamente que la reina es capturada por el peón ¡y lo ignoraría! Esta simple técnica hace más fuerte el motor y evita que caiga en errores simples.
Esta mejora hizo el motor aún más lento. Me pareció que debía ser más rápido, pero no. Algo lo estaba haciendo lento en ciertas posiciones "simples" que involucraban capturas que debían ser fáciles para un motor de ajedrez. Revisé varias veces el algoritmo de corte Alfa-Beta y todo se veía correcto. Al final mi mente "conectó" y noté que la puntuación usada para comparar no tenía el signo extendido, así que el movimiento estaba perdiendo puntos, pero no era eliminado porque parecía un valor altísimo ¡y el Intellivision seguía explorando porque lucía bien! Después de esta corrección, la velocidad subió por tres veces.
Otro error detectado durante las pruebas causaba que el programa de ajedrez permitiera al jugador poner el rey en jaque, y como esta misma rutina era usada por el análisis de recapturas, no detectaba ciertas capturas. Después de la corrección, el juego jugó mejor, y por supuesto, no al nivel de un maestro, pero razonable para un procesador de 16 bits de 895 khz.

Edición física

La edición fisica de Mr. Chess para Intellivision (caja, cartucho, manual y overlays) puede ser adquirida de www.intellivisioncollector.com y también en la tienda eBay de los Blue Sky Rangers.

Foto de Mr. Chess.

Mr. Chess for Intellivision - Análisis de MGG Homebrew

Mr. Chess for Intellivision - Análisis de Gray Defender

Ligas relacionadas

Última modificación: 06-feb-2024