cursos:z88dk:z88dklife

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Ambos lados, revisión anterior Revisión previa
Próxima revisión
Revisión previa
cursos:z88dk:z88dklife [09-08-2007 12:23] sromerocursos:z88dk:z88dklife [24-02-2020 19:24] (actual) falvarez
Línea 1: Línea 1:
 +Publicado originalmente en [[https://magazinezx.speccy.org/10/z88dk.html|MagazineZX número 10]] (noviembre 2004)
  
 ====== El juego de la vida de John Conway ====== ====== El juego de la vida de John Conway ======
  
 Hasta el momento hemos aprendido a instalar y compilar programas con z88dk, utilizando para ellos ejemplos prácticos basados en la creación de una Aventura Conversacional de Texto. En nuestros anteriores números, y gracias a los ejemplos de aventuras conversacionales, hemos descubierto cómo z88dk nos permite utilizar las diferentes funciones estándar de ANSI C para realizar nuestros pequeños programas. Hasta el momento hemos aprendido a instalar y compilar programas con z88dk, utilizando para ellos ejemplos prácticos basados en la creación de una Aventura Conversacional de Texto. En nuestros anteriores números, y gracias a los ejemplos de aventuras conversacionales, hemos descubierto cómo z88dk nos permite utilizar las diferentes funciones estándar de ANSI C para realizar nuestros pequeños programas.
 +
 +
  
 ===== El Juego de la Vida ===== ===== El Juego de la Vida =====
Línea 13: Línea 16:
 La mejor definición del Juego de la Vida de John Conway la podemos encontrar en la Wikipedia: La mejor definición del Juego de la Vida de John Conway la podemos encontrar en la Wikipedia:
  
------------------------- 
  
-//El juego de la vida es un autómata celular diseñado por el matemático británico John Horton Conway en 1970. Es el mejor ejemplo de un autómata celular. Hizo su primera aparición pública en el número de octubre de 1970 de la revista Scientific American, en la columna de juegos matemáticos de Martin Gardner. Desde un punto de vista teórico, es interesante porque es equivalente a una máquina universal de Turing, es decir, todo lo que se puede computar algorítmicamente se puede computar en el juego de la vida. +//El juego de la vida es un autómata celular diseñado por el matemático británico John Horton Conway en 1970. Es el mejor ejemplo de un autómata celular. Hizo su primera aparición pública en el número de octubre de 1970 de la revista Scientific American, en la columna de juegos matemáticos de Martin Gardner. Desde un punto de vista teórico, es interesante porque es equivalente a una máquina universal de Turing, es decir, todo lo que se puede computar algorítmicamente se puede computar en el juego de la vida.//
-\\  +
-Desde su publicación, ha atraído mucho interés debido a la gran variabilidad de la evolución de los patrones. La vida es un ejemplo de emergencia y autoorganización. Es interesante para los científicos, matemáticos, economistas y otros observar cómo patrones complejos pueden provenir de la implementación de reglas muy sencillas. +
-\\  +
-La vida tiene una variedad de patrones reconocidos que provienen de determinadas posiciones iniciales. Poco después de la publicación, se descubrieron el pentominó R y el planeador (en inglés glider), lo que atrajo un mayor interés hacia el juego. Contribuyó a su popularidad el hecho de que se publicó justo cuando se estaba lanzando al mercado una nueva generación de miniordenadores baratos, lo que significaba que se podía jugar durante horas en máquinas que, por otro lado, no se utilizarían por la noche. Para muchos aficionados, el juego de la vida sólo era un desafío de programación y una manera divertida de usar ciclos de la CPU. Para otros, sin embargo, el juego adquirió más connotaciones filosóficas. Desarrolló un seguimiento casi fanático a lo largo de los años 1970 hasta mediados de los 80. +
-\\  +
-Cabe decir que el "Juego de la Vida" causó furor en 1970, hasta el punto en que se convirtió en el divertimento de muchos "hackers programadores" la implementación del Juego de la Vida en los potentes mainframes de Universidades y Centros de Cálculo. Hoy en día sigue siendo un buen ejercicio de programación para aquellos que empiezan a dar clases de Informática, ya que la programación es muy sencilla y no requiere grandes conocimientos del lenguaje de programación utilizado. +
-//+
  
-------------------------+//Desde su publicación, ha atraído mucho interés debido a la gran variabilidad de la evolución de los patrones. La vida es un ejemplo de emergencia y autoorganización. Es interesante para los científicos, matemáticos, economistas y otros observar cómo patrones complejos pueden provenir de la implementación de reglas muy sencillas.//
  
 +//La vida tiene una variedad de patrones reconocidos que provienen de determinadas posiciones iniciales. Poco después de la publicación, se descubrieron el pentominó R y el planeador (en inglés glider), lo que atrajo un mayor interés hacia el juego. Contribuyó a su popularidad el hecho de que se publicó justo cuando se estaba lanzando al mercado una nueva generación de miniordenadores baratos, lo que significaba que se podía jugar durante horas en máquinas que, por otro lado, no se utilizarían por la noche. Para muchos aficionados, el juego de la vida sólo era un desafío de programación y una manera divertida de usar ciclos de la CPU. Para otros, sin embargo, el juego adquirió más connotaciones filosóficas. Desarrolló un seguimiento casi fanático a lo largo de los años 1970 hasta mediados de los 80.//
 +
 +//Cabe decir que el "Juego de la Vida" causó furor en 1970, hasta el punto en que se convirtió en el divertimento de muchos "hackers programadores" la implementación del Juego de la Vida en los potentes mainframes de Universidades y Centros de Cálculo. Hoy en día sigue siendo un buen ejercicio de programación para aquellos que empiezan a dar clases de Informática, ya que la programación es muy sencilla y no requiere grandes conocimientos del lenguaje de programación utilizado.//
  
 ===== Reglas básicas de El Juego de la Vida ===== ===== Reglas básicas de El Juego de la Vida =====
Línea 31: Línea 29:
  
 El Juego de la Vida se basa en una matriz de un tamaño determinado (como por ejemplo, 50x50, ó 32x32), que podríamos considerar nuestro "caldo de cultivo", en la cual mueren y se crean células. De forma efectiva, una célula es un 1 en una posición determinada de la cuadrícula mientras que un "espacio vacío" se representa mediante un cero. El Juego de la Vida se basa en una matriz de un tamaño determinado (como por ejemplo, 50x50, ó 32x32), que podríamos considerar nuestro "caldo de cultivo", en la cual mueren y se crean células. De forma efectiva, una célula es un 1 en una posición determinada de la cuadrícula mientras que un "espacio vacío" se representa mediante un cero.
-Células vivas en la cuadrícula + 
-Células vivas en la cuadrícula+{{ cursos:z88dk:z88dk_cuadric.png |Células vivas en la cuadrícula}} 
  
 El Juego de la Vida no requiere interacción por parte del usuario: a partir de un estado inicial (células diseminadas por el caldo de cultivo) se aplican una serie de reglas y se obtiene una nueva generación de células en dicho "caldo". Esta nueva generación será la entrada para volver a aplicar las reglas, y así sucesivamente. El Juego de la Vida no requiere interacción por parte del usuario: a partir de un estado inicial (células diseminadas por el caldo de cultivo) se aplican una serie de reglas y se obtiene una nueva generación de células en dicho "caldo". Esta nueva generación será la entrada para volver a aplicar las reglas, y así sucesivamente.
Línea 45: Línea 44:
 Con estas sencillas reglas se obtienen unos resultados sorprendentes, ya que aparecen patrones de evolución que se cambian, realizan formas y caminos determinados, etc. Así, aparece el "planeador o caminador" (un conjunto de células que se desplazan), el "explosionador" (conjunto de células que parecen formar la onda expansiva de una explosión), etc. Con estas sencillas reglas se obtienen unos resultados sorprendentes, ya que aparecen patrones de evolución que se cambian, realizan formas y caminos determinados, etc. Así, aparece el "planeador o caminador" (un conjunto de células que se desplazan), el "explosionador" (conjunto de células que parecen formar la onda expansiva de una explosión), etc.
  
-Implementación del Juego de la Vida en el Spectrum+===== Implementación del Juego de la Vida en el Spectrum =====
  
 Antes de codificar propiamente en C lo que es el "juego", veamos cómo sería el pseudocódigo que implementaría el diseño definido por Conway: Antes de codificar propiamente en C lo que es el "juego", veamos cómo sería el pseudocódigo que implementaría el diseño definido por Conway:
  
 +<code>
 tablero[ANCHO][ALTO] tablero[ANCHO][ALTO]
  
Línea 59: Línea 59:
          Crear_Generación_Aleatoria()          Crear_Generación_Aleatoria()
   fin repetir   fin repetir
 +</code>
  
 El anterior sería el esqueleto de la función principal, que como podréis ver en el código, se corresponde con la siguiente función main(): El anterior sería el esqueleto de la función principal, que como podréis ver en el código, se corresponde con la siguiente función main():
  
 +<code c>
 //--- Funcion principal main() ---------------------------- //--- Funcion principal main() ----------------------------
 int main( void ) int main( void )
Línea 79: Línea 81:
    return(0);    return(0);
 } }
 +</code>
  
 Con esto, el programa principal realizaría la impresión en pantalla de la generación actual de células (para que podamos ver la evolución visualmente) mediante la función Dibujar_Generación_Actual(). Tras esto, se calcularía la siguiente generación de células aplicando las reglas anteriormente explicadas, dentro de la función Calcular_Siguiente_Generacion() . Si en cualquier momento se pulsa la tecla 'r' (en nuestro programa) se modificará de nuevo el tablero aleatoriamente para añadir nuevas células y que en el siguiente paso del bucle comience de nuevo la simulación. Con esto, el programa principal realizaría la impresión en pantalla de la generación actual de células (para que podamos ver la evolución visualmente) mediante la función Dibujar_Generación_Actual(). Tras esto, se calcularía la siguiente generación de células aplicando las reglas anteriormente explicadas, dentro de la función Calcular_Siguiente_Generacion() . Si en cualquier momento se pulsa la tecla 'r' (en nuestro programa) se modificará de nuevo el tablero aleatoriamente para añadir nuevas células y que en el siguiente paso del bucle comience de nuevo la simulación.
  
-Función Crear_Generación_Aleatoria()+===== Crear_Gen_Aleatoria() ===== 
  
 La función Crear_Generación_Aleatoria() se encargaría de vaciar el "tablero de juego" o "caldo de cultivo" (poner todos sus elementos a cero), y rellenar algunas celdillas aleatorias con células (valores 1). Dicha función la hemos definido en pseudocódigo de la siguiente forma: La función Crear_Generación_Aleatoria() se encargaría de vaciar el "tablero de juego" o "caldo de cultivo" (poner todos sus elementos a cero), y rellenar algunas celdillas aleatorias con células (valores 1). Dicha función la hemos definido en pseudocódigo de la siguiente forma:
  
 +<code>
 Crear_Generacion_Aleatoria: Crear_Generacion_Aleatoria:
    Para todo 'x' y todo 'y':    Para todo 'x' y todo 'y':
Línea 95: Línea 100:
      ycel = y_aleatoria()      ycel = y_aleatoria()
      tablero_temporal[ xcel ][ ycel ] = 1      tablero_temporal[ xcel ][ ycel ] = 1
 +</code>
  
 En nuestro pseudocódigo utilizamos una matriz [ancho*alto] para representar el caldo de cultivo. Dentro de esta matriz, cada posición matriz[x][y] puede contener o no una célula mediante los valores de 0 y 1 respectivamente. Así, en un tablero de 32x32, podemos poner una célula justo en la mitad del tablero ejecutando "matriz[16][16] = 1". En nuestro pseudocódigo utilizamos una matriz [ancho*alto] para representar el caldo de cultivo. Dentro de esta matriz, cada posición matriz[x][y] puede contener o no una célula mediante los valores de 0 y 1 respectivamente. Así, en un tablero de 32x32, podemos poner una célula justo en la mitad del tablero ejecutando "matriz[16][16] = 1".
  
 +<code>
     dimensiones:      ancho = 32     dimensiones:      ancho = 32
                       alto  = 16                       alto  = 16
Línea 104: Línea 111:
     Crear célula:     mapa[x][y] = 1     Crear célula:     mapa[x][y] = 1
     Borrar célula:    mapa[x][y] = 0     Borrar célula:    mapa[x][y] = 0
 +</code>
  
 En el caso de z88dk, en lugar de utilizar matrices bidimensionales del tipo [ancho][alto] necesitaremos utilizar un vector de tamaño [ancho*alto], ya que la versión actual de z88dk no soporta el primer tipo de matrices. En el caso de z88dk, en lugar de utilizar matrices bidimensionales del tipo [ancho][alto] necesitaremos utilizar un vector de tamaño [ancho*alto], ya que la versión actual de z88dk no soporta el primer tipo de matrices.
Línea 109: Línea 117:
 De esta forma, ahora los accesos al mapa de células quedarían así: De esta forma, ahora los accesos al mapa de células quedarían así:
  
 +<code>
     Tablero:          mapa[ancho*alto]     Tablero:          mapa[ancho*alto]
     Temporal:         temp[ancho*alto]     Temporal:         temp[ancho*alto]
     Crear célula:     mapa[(y*ancho) + x] = 1     Crear célula:     mapa[(y*ancho) + x] = 1
     Borrar célula:    mapa[(y*ancho) + x] = 0     Borrar célula:    mapa[(y*ancho) + x] = 0
 +</code>
  
 Es decir, que un array unidimensional se puede tratar como un array bidimensional donde cada una de las líneas se coloca a continuación de la anterior, y acceder a cada uno de sus elementos mediante "posición = (y * ancho_fila) + x". Es decir, que un array unidimensional se puede tratar como un array bidimensional donde cada una de las líneas se coloca a continuación de la anterior, y acceder a cada uno de sus elementos mediante "posición = (y * ancho_fila) + x".
Línea 118: Línea 128:
 Para facilitar el tratamiento de los datos, en el código definimos los siguientes macros o #defines (que hacen las veces de funciones, pero que en lugar de ser llamadas son "incluidas", evitando un CALL con sus PUSHes, POPs y sus RETs): Para facilitar el tratamiento de los datos, en el código definimos los siguientes macros o #defines (que hacen las veces de funciones, pero que en lugar de ser llamadas son "incluidas", evitando un CALL con sus PUSHes, POPs y sus RETs):
  
 +<code c>
 #define Celula(x,y)     (mapa[((y)*32)+(x)]) #define Celula(x,y)     (mapa[((y)*32)+(x)])
 #define TempCelula(x,y) (temp[((y)*32)+(x)]) #define TempCelula(x,y) (temp[((y)*32)+(x)])
 +</code>
  
 De esta forma en nuestro código podemos hacer simplemente: De esta forma en nuestro código podemos hacer simplemente:
  
 +<code c>
  valor = Celula( 10, 10 );  valor = Celula( 10, 10 );
  Celula( 11, 12 ) = 1;  Celula( 11, 12 ) = 1;
- +</code> 
  
 Y al compilar, este código será sustituído por: Y al compilar, este código será sustituído por:
  
 +<code c>
  valor = (mapa[((10)*32)+(10)]);  valor = (mapa[((10)*32)+(10)]);
  (mapa[((12)*32)+(11)]) = 1;  (mapa[((12)*32)+(11)]) = 1;
 +</code>
  
 Obviamente el código utilizando nuestros #defines es mucho más claro y más fácil de mantener. Si encontramos una manera más óptima de acceder a las células, sólo hará falta modificarlo en el #define para que al recompilar, el cambio se aplique en todo el código fuente (en lugar de tener que ir cambiándolo llamada a llamada, como nos ocurriría en caso de no haber usado #defines). Y como ejemplo de "mejora" de nuestro define, vamos a acelerar la velocidad del cálculo de la posición del array cambiando la multiplicación por 32 por un desplazamiento de bits a la izquierda, ya que en un número almacenado de forma binaria, multiplicar por una potencia n-sima de 2 equivale a desplazar n veces a la izquierda el número que queríamos multiplicar. De esta, forma como 32 es 2 elevado a la quinta potencia, nos queda que: Obviamente el código utilizando nuestros #defines es mucho más claro y más fácil de mantener. Si encontramos una manera más óptima de acceder a las células, sólo hará falta modificarlo en el #define para que al recompilar, el cambio se aplique en todo el código fuente (en lugar de tener que ir cambiándolo llamada a llamada, como nos ocurriría en caso de no haber usado #defines). Y como ejemplo de "mejora" de nuestro define, vamos a acelerar la velocidad del cálculo de la posición del array cambiando la multiplicación por 32 por un desplazamiento de bits a la izquierda, ya que en un número almacenado de forma binaria, multiplicar por una potencia n-sima de 2 equivale a desplazar n veces a la izquierda el número que queríamos multiplicar. De esta, forma como 32 es 2 elevado a la quinta potencia, nos queda que:
  
 +<code>
  x * 32 = x << 5  x * 32 = x << 5
 +</code>
  
 Reemplazando esto en nuestro #define: Reemplazando esto en nuestro #define:
  
 +<code>
 #define Celula(x,y)     (mapa[((y)<<5)+(x)]) #define Celula(x,y)     (mapa[((y)<<5)+(x)])
 #define TempCelula(x,y) (temp[((y)<<5)+(x)]) #define TempCelula(x,y) (temp[((y)<<5)+(x)])
 +</code>
  
 Se deja al lector como ejercicio realizar la prueba de reemplazar <<5 por *32 en el código fuente y verificar que efectivamente, el desplazamiento de bits es mucho más rápido que la multiplicación (puede apreciarse visiblemente en la velocidad de la simulación). Se deja al lector como ejercicio realizar la prueba de reemplazar <<5 por *32 en el código fuente y verificar que efectivamente, el desplazamiento de bits es mucho más rápido que la multiplicación (puede apreciarse visiblemente en la velocidad de la simulación).
Línea 145: Línea 164:
 El código final correspondiente para nuestra función de generación aleatoria de estados iniciales es el siguiente: El código final correspondiente para nuestra función de generación aleatoria de estados iniciales es el siguiente:
  
 +<code c>
 //--- Rellenar el tablero con valores aleat. -------------- //--- Rellenar el tablero con valores aleat. --------------
 void GenLife( void ) void GenLife( void )
Línea 165: Línea 185:
       TempCelula(x,y) = Celula(x,y) = 1;       TempCelula(x,y) = Celula(x,y) = 1;
    }    }
- 
 } }
 +</code>
  
 Como cosas que destacar de esta función tenemos: Como cosas que destacar de esta función tenemos:
  
-    * srand(clock()) : Inicializa la semilla de números aleatorios usando como base el reloj del Spectrum (por reloj del Spectrum consideramos el contador que tiene el sistema y que contabiliza el número de segundos transcurridos desde el inicio del ordenador). Esto asegura que cada vez que ejecutemos el programa (en un Spectrum real o un emulador que no tenga carga automática del .tap al abrirlo con él), tengamos una secuencia de números aleatorios diferentes y no obtengamos siempre la misma generación inicial de células. Los números aleatorios los obtendremos posteriormente con rand(). +    * **srand(clock())** : Inicializa la semilla de números aleatorios usando como base el reloj del Spectrum (por reloj del Spectrum consideramos el contador que tiene el sistema y que contabiliza el número de segundos transcurridos desde el inicio del ordenador). Esto asegura que cada vez que ejecutemos el programa (en un Spectrum real o un emulador que no tenga carga automática del .tap al abrirlo con él), tengamos una secuencia de números aleatorios diferentes y no obtengamos siempre la misma generación inicial de células. Los números aleatorios los obtendremos posteriormente con rand(). 
-    * BORDER() y CLS(): Estas 2 funciones cambian el color del borde y borran la pantalla respectivamente. Están implementadas en ensamblador, como puede verse en el código fuente de ZXlife . La primera cambia el borde mediante un OUT del valor del borde en el puerto que se utiliza para ello, mientras que la segunda utiliza LDIR para borrar la zona de pantalla de memoria. Este ejemplo nos permite ver lo sencillo que es integrar ensamblador en z88dk, embebiendo el código ASM dentro del propio programa. +    * **BORDER() y CLS()**: Estas 2 funciones cambian el color del borde y borran la pantalla respectivamente. Están implementadas en ensamblador, como puede verse en el código fuente de ZXlife . La primera cambia el borde mediante un OUT del valor del borde en el puerto que se utiliza para ello, mientras que la segunda utiliza LDIR para borrar la zona de pantalla de memoria. Este ejemplo nos permite ver lo sencillo que es integrar ensamblador en z88dk, embebiendo el código ASM dentro del propio programa. 
-    * printf( "\x1B[%u;%uH",(21),(1)): Este comando es el equivalente ANSI de gotoxy(x,y), es decir, posiciona el cursor en la posición (1,21) de pantalla para que el próximo printf comience a trazar las letras en esa posición. En este caso lo utilizamos para posicionar en cursor en la parte baja de la pantalla, donde escribimos el título del programa posteriormente.+    * **printf( "\x1B[%u;%uH",(21),(1))**: Este comando es el equivalente ANSI de gotoxy(x,y), es decir, posiciona el cursor en la posición (1,21) de pantalla para que el próximo printf comience a trazar las letras en esa posición. En este caso lo utilizamos para posicionar en cursor en la parte baja de la pantalla, donde escribimos el título del programa posteriormente. 
 + 
 + 
 +===== Dibujar_Gen_Actual() =====
  
-La función Dibujar_Generación_Actual() 
  
 En cada paso del bucle principal tenemos que redibujar la colonia actual de células. El pseudocódigo para hacer esto es: En cada paso del bucle principal tenemos que redibujar la colonia actual de células. El pseudocódigo para hacer esto es:
  
 +<code>
 Dibujar_Generacion_Actual: Dibujar_Generacion_Actual:
    Para todo 'x' y todo 'y':    Para todo 'x' y todo 'y':
      Si tablero[x][y] = 0:  Dibujar Blanco en (x,y)      Si tablero[x][y] = 0:  Dibujar Blanco en (x,y)
      Si tablero[x][y] = 1:  Dibujar Célula en (x,y)      Si tablero[x][y] = 1:  Dibujar Célula en (x,y)
 +</code>
  
 Traducido a código C: Traducido a código C:
  
 +<code c>
 #define DrawCell(x,y,val) \ #define DrawCell(x,y,val) \
   *((unsigned char *) (0x4000 + 6144 + ((y)<<5) + (x))) = (val)<<3 ;   *((unsigned char *) (0x4000 + 6144 + ((y)<<5) + (x))) = (val)<<3 ;
Línea 199: Línea 224:
       }       }
 } }
 +</code>
  
 La clave de esta función está en la macro DrawCell, que es la que efectivamente pinta en pantalla las células. Lo interesante de la función es que no dibuja nada en pantalla, sino que modifica los atributos de la videomemoria para cambiar los "espacios en blanco" que hay en pantalla entre 2 colores diferentes (negro y azul). Concretamente, esta macro lo que hace es modificar los atributos (tinta/papel) de los caracteres de 0,0 a 32,16, accediendo directamente a videomemoria, en la zona de los atributos. La clave de esta función está en la macro DrawCell, que es la que efectivamente pinta en pantalla las células. Lo interesante de la función es que no dibuja nada en pantalla, sino que modifica los atributos de la videomemoria para cambiar los "espacios en blanco" que hay en pantalla entre 2 colores diferentes (negro y azul). Concretamente, esta macro lo que hace es modificar los atributos (tinta/papel) de los caracteres de 0,0 a 32,16, accediendo directamente a videomemoria, en la zona de los atributos.
Línea 208: Línea 234:
 Cuando escribimos un byte en una de esas posiciones estaremos modificando los atributos del caracter (x,y) de la pantalla de forma que: Cuando escribimos un byte en una de esas posiciones estaremos modificando los atributos del caracter (x,y) de la pantalla de forma que:
  
 +<code>
  Direccion_atributos(x,y) = 22528 + (y*32) + x  Direccion_atributos(x,y) = 22528 + (y*32) + x
 +</code>
  
 El byte que escribamos define los atributos con el siguiente formato: El byte que escribamos define los atributos con el siguiente formato:
  
-   bits 0...2tinta (0 a 7, orden de los colores del Spectrum) +^ Bits ^ Significado ^ 
-   bits 3...5papel (0 a 7, orden de los colores del Spectrum) +0...2 tinta (0 a 7, orden de los colores del Spectrum) | 
-   bit      6brillo (1 ó 0, con o sin brillo) +3...5 papel (0 a 7, orden de los colores del Spectrum) | 
-   bit      7flash (1 ó 09, con o sin parpadeo)+brillo (1 ó 0, con o sin brillo) | 
 +flash (1 ó 09, con o sin parpadeo) |
  
 Los colores están definidos igual que se detalla en el manual del Spectrum, es decir: Los colores están definidos igual que se detalla en el manual del Spectrum, es decir:
  
-   0. - negro +^ Valor ^ Color ^ 
-   1. - azul +negro  
-   2. - rojo +azul | 
-   3. - púrpura o magenta +rojo | 
-   4. - verde +púrpura o magenta | 
-   5. - cyan +verde | 
-   6. - amarillo +cyan | 
-   7. - blanco+amarillo | 
 +blanco |
  
 Con los bits indicandos anteriormente, un atributo se construiría con el siguiente código: Con los bits indicandos anteriormente, un atributo se construiría con el siguiente código:
  
 +<code c>
  atributo = (flash<<7) + (brillo<<6) + (papel<<3) + (tinta);  atributo = (flash<<7) + (brillo<<6) + (papel<<3) + (tinta);
 +</code>
  
 Por ejemplo, para establecer el caracter (2,5) con color verde (4) sobre fondo rojo (2) y con flash, podemos utilizar el siguiente código: Por ejemplo, para establecer el caracter (2,5) con color verde (4) sobre fondo rojo (2) y con flash, podemos utilizar el siguiente código:
  
 +<code c>
  // memaddr = 22528 + (y*32) + x  // memaddr = 22528 + (y*32) + x
  memaddr = 22528 + (5*32) + 2;  memaddr = 22528 + (5*32) + 2;
Línea 239: Línea 272:
  // escribir en memaddr (flash<<7)+(brillo<<6)+(papel<<3)+tinta.  // escribir en memaddr (flash<<7)+(brillo<<6)+(papel<<3)+tinta.
  *memaddr = (1<<7) + (2<<3) + (4);  *memaddr = (1<<7) + (2<<3) + (4);
 +</code>
  
 De este modo podemos activar y desactivar cuadros completos de pantalla modificando su tinta y papel. Este método es mucho más rápido para nuestro programa que dibujar los 8x8 pixels de cada carácter para dibujar o apagar las células (una sóla escritura en memoria modifica el estado de 64 píxeles simultáneamente), y puede servirnos de ejemplo para mostrar cómo modificar los atributos. De este modo podemos activar y desactivar cuadros completos de pantalla modificando su tinta y papel. Este método es mucho más rápido para nuestro programa que dibujar los 8x8 pixels de cada carácter para dibujar o apagar las células (una sóla escritura en memoria modifica el estado de 64 píxeles simultáneamente), y puede servirnos de ejemplo para mostrar cómo modificar los atributos.
  
-La función Calcular_Siguiente_Generación()+===== Calcular_Siguiente_Gen() ===== 
  
 En este momento ya tenemos una función que nos genera una colonia inicial de células (aleatoria), y un bucle principal que redibuja la colonia de células actual en memoria. Lo que falta a continuación es implementar la esencia del algoritmo de John Conway para modificar la colonia de células actual (array mapa[]) y obtener el nuevo estado (array temp[]) para, repitiendo el ciclo una y otra vez, realizar la simulación. En este momento ya tenemos una función que nos genera una colonia inicial de células (aleatoria), y un bucle principal que redibuja la colonia de células actual en memoria. Lo que falta a continuación es implementar la esencia del algoritmo de John Conway para modificar la colonia de células actual (array mapa[]) y obtener el nuevo estado (array temp[]) para, repitiendo el ciclo una y otra vez, realizar la simulación.
Línea 248: Línea 283:
 El pseudocódigo es el siguiente: El pseudocódigo es el siguiente:
  
 +<code>
 Calcular_Siguiente_Generación: Calcular_Siguiente_Generación:
    Para todo 'x' y todo 'y':    Para todo 'x' y todo 'y':
Línea 263: Línea 299:
         Si no está habitada:         Si no está habitada:
            Si tiene 2 ó 3 vecinas : Creamos una célula            Si tiene 2 ó 3 vecinas : Creamos una célula
 +</code>
  
 El código en C que implemente este algoritmo es: El código en C que implemente este algoritmo es:
  
 +<code c>
 //--- Funcion donde se simula la vida --------------------- //--- Funcion donde se simula la vida ---------------------
 void Life( void ) void Life( void )
Línea 318: Línea 356:
    } // fin for y    } // fin for y
 } }
 +</code>
  
 Para compilar el programa puede utilizarse el siguiente comando: Para compilar el programa puede utilizarse el siguiente comando:
  
 +<code>
   zcc +zxansi -vn -O1 zxlife.c -o zxlife.bin -lndos   zcc +zxansi -vn -O1 zxlife.c -o zxlife.bin -lndos
   bin2tap zxlife.bin zxlife.tap   bin2tap zxlife.bin zxlife.tap
   rm -f zcc_opt.def   rm -f zcc_opt.def
 +</code>
  
 Si ejecutamos el programa en nuestro Spectrum (o en un emulador) veremos la evolución de las células en tiempo real en nuestra pantalla: Si ejecutamos el programa en nuestro Spectrum (o en un emulador) veremos la evolución de las células en tiempo real en nuestra pantalla:
-Simulaciones en nuestro Spectrum + 
-Simulaciones en nuestro Spectrum +{{ cursos:z88dk:z88dk_zxlife1.png |Simulaciones en nuestro Spectrum}} 
-Simulaciones en nuestro Spectrum + 
-Simulaciones en nuestro Spectrum+ 
 +{{ cursos:z88dk:z88dk_zxlife2.png |Simulaciones en nuestro Spectrum}} 
  
 Cada vez que pulsemos 'r' se generará una nueva "remesa" de células para volver a aplicar el algoritmo y ver su evolución. Cada vez que pulsemos 'r' se generará una nueva "remesa" de células para volver a aplicar el algoritmo y ver su evolución.
  
-Optimizaciones del Juego de la Vida+ 
 +===== Optimizaciones del Juego de la Vida ===== 
  
 Todos los programas pueden ser optimizados, y zxlife no es una excepción. Como ya se ha mostrado en el ejemplo del cálculo de la posición (x,y) en el array, cambiando una multiplicación por 32 por un desplazamiento binario 5 veces a la izquierda obtenemos un gran incremento de velocidad de ejecución. Este incremento de velocidad es tal porque esa función es llamada muchas veces durante la ejecución del programa. Se demuestra con esto que no por el mero hecho de utilizar C o Ensamblador el programa será más o menos rápido: La velocidad de ejecución del programa reside en que utilicemos los algoritmos adecuados a cada problema; así, desplazar binariamente 5 veces es mucho más rápido que multiplicar por 32, y esa multiplicación sería igual de lenta si la programáramos en ensamblador. De ahí la importancia del diseño del programa y los algoritmos empleados en su implementación. Todos los programas pueden ser optimizados, y zxlife no es una excepción. Como ya se ha mostrado en el ejemplo del cálculo de la posición (x,y) en el array, cambiando una multiplicación por 32 por un desplazamiento binario 5 veces a la izquierda obtenemos un gran incremento de velocidad de ejecución. Este incremento de velocidad es tal porque esa función es llamada muchas veces durante la ejecución del programa. Se demuestra con esto que no por el mero hecho de utilizar C o Ensamblador el programa será más o menos rápido: La velocidad de ejecución del programa reside en que utilicemos los algoritmos adecuados a cada problema; así, desplazar binariamente 5 veces es mucho más rápido que multiplicar por 32, y esa multiplicación sería igual de lenta si la programáramos en ensamblador. De ahí la importancia del diseño del programa y los algoritmos empleados en su implementación.
Línea 339: Línea 384:
 En el caso de zxlife, podemos también optimizar el código de obtención de nuevas generaciones evitando cálculos innecesarios. Concretamente, cuando contamos las células vecinas podemos evitar calcular la posición de cada célula en cada caso. El código original sin optimizar es: En el caso de zxlife, podemos también optimizar el código de obtención de nuevas generaciones evitando cálculos innecesarios. Concretamente, cuando contamos las células vecinas podemos evitar calcular la posición de cada célula en cada caso. El código original sin optimizar es:
  
 +<code c>
    // Obtenemos el numero de celulas vecinas    // Obtenemos el numero de celulas vecinas
    vecinos = 0;    vecinos = 0;
Línea 349: Línea 395:
    vecinos += Celula(x+1,y-1);    vecinos += Celula(x+1,y-1);
    vecinos += Celula(x+1,y+1);    vecinos += Celula(x+1,y+1);
 +</code>
  
 Cada vez que llamamos a Celula(x,y) estamos realizando el cálculo de la posición absoluta de la célula dentro del vector (en nuestra conversión bidimensional a unidimensional) mediante una serie de operaciones matemáticas. Si tenemos en cuenta que todas las células vecinas están a una distancia fija de -1, +2, -33, -32, -31 y +31, +32 y +33 de cada célula, podemos convertir esto a: Cada vez que llamamos a Celula(x,y) estamos realizando el cálculo de la posición absoluta de la célula dentro del vector (en nuestra conversión bidimensional a unidimensional) mediante una serie de operaciones matemáticas. Si tenemos en cuenta que todas las células vecinas están a una distancia fija de -1, +2, -33, -32, -31 y +31, +32 y +33 de cada célula, podemos convertir esto a:
  
 +<code c>
    offset = (y<<5)+x;    offset = (y<<5)+x;
  
Línea 359: Línea 407:
              mapa[ offset+1  ] + mapa[ offset+31 ] +              mapa[ offset+1  ] + mapa[ offset+31 ] +
              mapa[ offset+32 ] + mapa[ offset+33 ];              mapa[ offset+32 ] + mapa[ offset+33 ];
 +</code>
  
 Esto es así porque como podemos ver en la siguiente figura, podemos obtener las 8 células vecinal a partir de un mismo offset calculado: Esto es así porque como podemos ver en la siguiente figura, podemos obtener las 8 células vecinal a partir de un mismo offset calculado:
-Offset de las 8 células vecinas de una dada + 
-Offset de las 8 células vecinas de una dada+{{ cursos:z88dk:z88dk_offsets.png |Offset de las 8 células vecinas de una dada}} 
  
 El código resultante de la optimización sería el siguiente: El código resultante de la optimización sería el siguiente:
  
 +<code c>
 //--- Funcion donde se simula la vida --------------------- //--- Funcion donde se simula la vida ---------------------
 void Life( void ) void Life( void )
Línea 414: Línea 465:
    } // fin for y    } // fin for y
 } }
 +</code>
  
 Aparte de la optimización de la función de cálculo de nuevas generaciones, también podemos optimizar la función que dibuja en pantalla de forma que en lugar de realizar el cálculo de posición del atributo en cada célula, lo realice una sola vez y vaya incrementándolo (algo parecido a lo que hemos hecho con el cálculo de células vecinas). Podemos ver la versión optimizada a continuación: Aparte de la optimización de la función de cálculo de nuevas generaciones, también podemos optimizar la función que dibuja en pantalla de forma que en lugar de realizar el cálculo de posición del atributo en cada célula, lo realice una sola vez y vaya incrementándolo (algo parecido a lo que hemos hecho con el cálculo de células vecinas). Podemos ver la versión optimizada a continuación:
  
 +<code c>
 //--- Dibujar en pantalla el array de celulas ------------- //--- Dibujar en pantalla el array de celulas -------------
 void DrawLife( void ) void DrawLife( void )
Línea 435: Línea 488:
    }    }
 } }
 +</code>
  
 Lo que hacemos en el ejemplo anterior es apuntar nuestra variable puntero memaddr a la posición de memoria donde comienzan los atributos (memaddr = posición de memoria del atributo del caracter 0,0 de pantalla), tras lo cual podemos escribir el atributo e incrementar el puntero para pasar al siguiente atributo que vamos a modificar. De este modo podemos redibujar nuestros 32x16 caracteres sin tener que recalcular memaddr para cada célula, como se hacía en el caso anterior. Lo que hacemos en el ejemplo anterior es apuntar nuestra variable puntero memaddr a la posición de memoria donde comienzan los atributos (memaddr = posición de memoria del atributo del caracter 0,0 de pantalla), tras lo cual podemos escribir el atributo e incrementar el puntero para pasar al siguiente atributo que vamos a modificar. De este modo podemos redibujar nuestros 32x16 caracteres sin tener que recalcular memaddr para cada célula, como se hacía en el caso anterior.
Línea 440: Línea 494:
 En el fichero comprimido que acompaña a este artículo están almacenadas las 2 versiones de zxlife: la versión 1 (zxlife.c y zxlife.tap) que es la versión original del programa, y la versión 2 (zxlife2.c y zxlife2.tap) que es la versión optimizada con los cambios que hemos explicado en esta sección. En el fichero comprimido que acompaña a este artículo están almacenadas las 2 versiones de zxlife: la versión 1 (zxlife.c y zxlife.tap) que es la versión original del programa, y la versión 2 (zxlife2.c y zxlife2.tap) que es la versión optimizada con los cambios que hemos explicado en esta sección.
  
-En conclusión+ 
 +===== En conclusión ===== 
  
 Con el ejemplo de esta entrega hemos pretendido mostrar un ejemplo completo y práctico de cómo z88dk nos puede ayudar a implementar cualquier tipo de programa o algoritmo que deseemos fácilmente (zxlife.c tiene apenas 200 líneas de código contando comentarios y ha sido programado en apenas 30 minutos). Además, se ha podido ver cómo lo importante no es el lenguaje de programación utilizado, sino los algoritmos que se empleen. Por supuesto, zxlife puede optimizarse más aún: no se ha hecho porque el objetivo es que el programa fuera comprensible para los lectores, pero podemos combinar la potencia de C con funciones en ensamblador en aquellos puntos donde se considere oportuno, o utilizar una implementación diferente del algoritmo para calcular las generaciones de células, obteniendo mejores resultados. Con el ejemplo de esta entrega hemos pretendido mostrar un ejemplo completo y práctico de cómo z88dk nos puede ayudar a implementar cualquier tipo de programa o algoritmo que deseemos fácilmente (zxlife.c tiene apenas 200 líneas de código contando comentarios y ha sido programado en apenas 30 minutos). Además, se ha podido ver cómo lo importante no es el lenguaje de programación utilizado, sino los algoritmos que se empleen. Por supuesto, zxlife puede optimizarse más aún: no se ha hecho porque el objetivo es que el programa fuera comprensible para los lectores, pero podemos combinar la potencia de C con funciones en ensamblador en aquellos puntos donde se considere oportuno, o utilizar una implementación diferente del algoritmo para calcular las generaciones de células, obteniendo mejores resultados.
Línea 446: Línea 502:
 En las próximas entregas comenzaremos a hablar de gráficos en el Spectrum, de forma que podamos comenzar a aplicar nuestros conocimientos de z88dk para hacer ya cosas visibles (gráficamente) en nuestro Spectrum. En las próximas entregas comenzaremos a hablar de gráficos en el Spectrum, de forma que podamos comenzar a aplicar nuestros conocimientos de z88dk para hacer ya cosas visibles (gráficamente) en nuestro Spectrum.
  
-LINKS 
  
-    * Archivos fuente del ejemplo propuesto (zxlife.zip) 
-    * Wikipedia: http://es.wikipedia.org/wiki/Juego_de_la_vida 
-    * Color Game Of Life Visual Exhibition: http://www.collidoscope.com/cgolve/ 
-    * What is the Game of Life?: http://www.math.com/students/wonders/life/life.html 
  
-Listado completo del programa 
  
 +===== Listado completo del programa =====
 +
 +
 +<code c>
 /* /*
   ZX-Life  -> Implementacion de ejemplo en C-z88dk del   ZX-Life  -> Implementacion de ejemplo en C-z88dk del
Línea 637: Línea 691:
 #endasm #endasm
 } }
 +</code>
 +
 +
 +
  
 +===== Enlaces =====
  
  
-SROMERO (NoP) +    * [[https://magazinezx.speccy.org/10/src/zxlife.zip|Archivos fuente del ejemplo propuesto]] 
-  +    * [[http://es.wikipedia.org/wiki/Juego_de_la_vida|El juego de la Vida en la Wikipedia]] 
- Volver arriba +    * [[http://www.collidoscope.com/cgolve/|Color Game Of Life Visual Exhibition]] 
-> ÍNDICE DE REVISTAS < +    * [[http://www.math.com/students/wonders/life/life.html|What is the Game of Life?]]
-2003-2007 Magazine ZX+
  
  • cursos/z88dk/z88dklife.1186662220.txt.gz
  • Última modificación: 09-08-2007 12:23
  • por sromero