Me compré el juego SHENZHEN I/O en Steam hace un tiempo. Le puse como 15 horas de juego y avancé bastante, pero lo abrí después de un par de meses y ya no me acuerdo casi nada.
Así que pensé que empezarlo de nuevo era una buena ocasión para hacer una mini guía (sobre todo para no olvidarme otra vez). También sirve como una guía básica de lenguaje ensamblador.
SHENZHEN I/O
En este juego sos un ingeniero que se muda a la ciudad de Shenzhen en China (yo lo pronuncio como shenjen pero si alguien sabe chino me va a matar) para trabajar en una empresa de microcontroladores.
Te tiran por la cabeza un manual en PDF de 47 páginas donde te explican las partes que tenés que usar y cómo funciona el lenguaje.
En este punto aviso que no es muy apto para todo público el juego, básicamente hay que escribir programas concurrentes en un lenguaje ensamblador (instrucciones para el procesador), usando la menor cantidad posible de instrucciones y sin irse de presupuesto con las partes (esta última parte es un desafío pero es opcional).
No voy a traducir las 47 páginas pero sí lo mínimo para empezar a jugar.
Introducción al lenguaje MCxxxx
Todas las partes de la familia MCxxxx que están disponibles entienden este lenguaje de programación.
Las partes necesitan dormir entre ciclos de operación para ahorrar energía, porque están diseñadas para operar en ambientes con recursos restringidos.
Saltos y condicionales
En este lenguaje ensamblador no hay if
, for
, ni while
. Lo unico que hay son los saltos y los condicionales. En los primeros lenguajes de programación de “alto nivel” esto se hacía con las infames instrucciones goto
que saltaban a cualquier línea y terminaban haciendo el código ilegible (código spaghetti). Cabe aclarar que en el MSX88 que que usé en la facultad y está basado en el Intel 8088 sí tiene saltos condicionales. Bueno, acá no hay.
Volviendo a los saltos, la idea es ir dejando etiquetas en el código con etiqueta:
que son “puntos de retorno” a los que se puede llegar con un salto.
Por ejemplo, para hacer un while (true)
basta con saltar siempre a la misma etiqueta:
etiqueta: jmp etiqueta
Esto no sirve de mucho, ¿no?
Los condicionales permiten “prender” o “apagar” líneas según si se cumple la condición o no. El formato es txx operando1 operando2
donde xx es la condición (ver Instrucciones condicionales más abajo).
txx operando1 operando2
instruccion1
+ instruccion2
- instruccion3
En el caso que se muestra arriba, instruccion1
siempre se ejecuta. instruccion2
solo se ejecuta si se cumple la condición, y instruccion3
solo en caso contrario.
Una forma de hacer un for (int i = 0; i<7; i++)
es la siguiente:
guardar_0_en_el_acumulador
etiqueta: instruccion1
instruccion2
...
test_si_acumulador_es_7
+ jmp afuera
- incrementar_acumulador
- jmp etiqueta
afuera: otras_instrucciones
Si se quiere hacer while (i<7) { ... i++}
el código es exactamente igual.
Registros
Existen cuatro tipos de registros:
acc
: registro acumulador. El lenguaje funciona con una sola dirección (la mayoría de las veces), así que cuando no se especifica un registro, el otro operando es el acumuladordat
: es un registro extra para guardar datos en algunas partes avanzadas.- registros de pines: contienen el dato que se recibe o se envía de un pin. Notar que algunos pines bloquean la lectura cuando no hay un dato (son concurrentes).
Conjunto de instrucciones
Este es el set de instrucciones. Los comentarios empiezan con #
y no se ejecutan, como en Pyhton. Los enteros van de -999
a 999
.
Instrucciones básicas
# no hacer nada (para rellenar loops o condiciones)
nop
# cargar un dato leido de un registro R o un entero I al registro R
mov R/I R
# saltar a la etiqueta L (para formar loops o esquivar instrucciones)
jmp L
# esperar la cantidad de tiempo indicada en el registro R o en el entero I
slp R/I
# esperar a que haya una entrada en el pin P
slx P
Instrucciones aritméticas
# sumar el contenido del registro R o el entero I al acumulador y guardarlo en el acumulador
add R/I
# restar el contenido del registro R o el entero I al acumulador y guardarlo en el acumulador
sub R/I
# multiplicar el contenido del registro R o el entero I con el acumulador y guardarlo en el acumulador
mul R/I
# guarda 100 en el acumulador si el acumulador está en 0. En caso contrario, guarda 0.
not
# deja en el acumulador solo el dígito indicado por el registro R o el entero I. (por ejemplo , si el acumulador tiene 32, dgt 0 hace que en el acumulador quede un 2).
dgt R/I
# guarda el valor del registro R2 o entero I2 en el dígito del acumulador indicado por el registro R1 o el entero R2.
dst R1/I1 R2/I2
Instrucciones condicionales
# chequear si los dos valores son iguales
teq R/I R/I
# ejemplo
teq 4 3
+ no_se_ejecuta
- se_ejecuta
# chequear si el primer operando es mayor que el segundo
tgt R/I R/I
# ejemplo
tgt 4 3
+ se_ejecuta
- no_se_ejecuta
# chequear si el primer operando es menor que el segundo
tlt 4 3
+ no_se_ejecuta
- se_ejecuta
# comparar los dos operandos
tcp R/I R/I
# ejemplo 1
tcp 4 3
+ se_ejecuta
- no_se_ejecuta
# ejemplo 2
tcp 3 4
+ no_se_ejecuta
- se_ejecuta
# ejemplo 3
tcp 3 3
+ no_se_ejecuta
- no_se_ejecuta
Ahora sí el juego
Bueno después de todo eso se puede empezar con el primer desafío del juego: FAKE SURVEILLANCE CAMERA (cámara de vigilancia falsa).
Esta es la pantalla de diseño. Te dan las carcasas donde tienen que entrar los chips, y hay que hacer entrar tanto los chips como las instrucciones en los chips.
active
, la primera salida
El desarrollador anterior claramente se rindió después de hacer el primer chip, así que tenemos que terminar el trabajo nosotros.
En la pestaña INFORMATION
, abajo, dice que hay que hacer que active
y network
(los dos círculos) reciban las señales que indica la pestaña VERIFICATION
:
En el gráfico, la posición baja es un 0 y la posición alta un 100.
Analicemos primero active
(el primer carril):
- está 6 tiempos abajo
- después está 6 tiempos arriba
Si volvemos al código que escribió el pobre desarrollador anterior tenemos lo siguiente:
mov 0 p0
slp 6
mov 100 p0
slp 6
En castellano:
mov 0 p0 # sacar 0 por el pin p0
slp 6 # esperar 6 tiempos
mov 100 p0 # sacar 100 por el pin p0
slp 6 # esperar 6 tiempos
Cuanto termina la ejecución vuelve a empezar, así que no hace falta un jmp
(se parece a la función loop()
de Arduino).
Podemos ver si le damos al botón SIMULATE
que cumple con lo pedido, pero en network (el segundo carril) hay una línea roja porque siempre recibe 0:
network
, la segunda salida
Ahora nos toca a nosotros.
Analicemos la salida esperada de network
(segundo carril):
- está 4 tiempos abajo
- está 2 tiempos arriba
- está 1 tiempo abajo
- está 1 tiempo arriba
¿Nos entrará todo eso en el chip?
Podemos usar el mismo tipo de chip, un MC4000 que sale 3 yuanes.
Si copiamos la misma idea del código anterior quedaría así:
mov 0 p0
slp 4
mov 100 p0
slp 2
mov 0 p0
slp 1
mov 100 p0
slp 1
No hay que olvidarse de conectar p0
con network
:
Finalmente le damos a SIMULATE
y después de comprobar varias veces que funciona, nos da por bueno el prototipo:
Si se fijan hay una forma de hacerlo con con solo 3 líneas de código… pero para eso hacen falta instrucciones que todavía no tenemos.
Cerrando
Este fue el primer prototipo del juego SHENZHEN I/O. Ayuda a entender cómo funciona el lenguaje ensamblador y es un buen ejercicio de lógica.
Si alguien sobrevivió a este post, ¡felicitaciones! hasta aquí llega :)
Por comentarios o sugerencias mi correo está al final de la página. También pueden levantar un issue en el repo de GitHub del sitio.