Curso Programación ZX SpectrumCursos

Ensamblador para ZX Spectrum – Pong: $0B cambio de dirección/velocidad de la bola al golpear la pala

Llegamos a la recta final, ya casi estamos acabando.

5
(3)

Ensamblador para ZX Spectrum – Pong: Paso 9, cambio de dirección/velocidad de la bola al golpear la pala

En este paso vamos a prescindir de parte de lo que hemos implementado en el paso anterior. La velocidad de la bola va a cambiar dependiendo de con qué parte de la pala colisione.

Creamos la carpeta Paso09 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso08.

Lo primero que vamos a hacer es quitar la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3.

Abrimos el archivo Controls.asm y en la rutina ScanKeys, borramos todas las líneas hasta la etiqueta scanKeys_ctrl, quedando el inicio de la rutina de la siguiente manera:

ScanKeys:
ld      d, $00

scanKeys_A:

Si compilamos y cargamos en el emulador, vemos que la velocidad de la bola no cambia.

Vamos a añadir nuevas constantes y variables en el archivo Sprite.asm, para poder controlar la inclinación de la bola. También vamos a cambiar los sprites de las palas; ambas van a dibujar cuatro píxeles, pero en ambos casos dibujaremos los más cercanos al centro de la pantalla.

Añadimos las constantes que indican la rotación a asignar a la bola cuando se produce la colisión con la pala:

CROSS_LEFT_ROT:     EQU $ff
CROSS_RIGHT_ROT:    EQU $01

Añadimos la posición inicial de la bola, y el número acumulado de movimientos que debe llevar la bola para cambiar la posición Y. Este último dato lo vamos a usar para cambiar la inclinación de la bola:

BALLPOS_INI:        EQU	$4850
ballMovCount:       db	$00

Cambiamos la configuración inicial de la bola y la documentación (comentarios) de la misma:

; Velocidad y dirección de la bola.
; bits 0 a 3:   Movimientos de la bola para que cambie la posición Y. 
;               Valores f = semiplano, 2 = semi diagonal, 1 = diagonal
; bits 4 y 5:   Velocidad de la bola: 1 muy rápido, 2 (rápido) y 3 (lento)
; bit 6:        Dirección X: 0 derecha / 1 izquierda
; bit 7:        Dirección Y: 0 arriba / 1 abajo
ballSetting:
db      $31     ; 0011 0001

Según la nueva configuración, la bola inicialmente se mueve hacia la derecha y hacia arriba, con una velocidad lenta, y en cada movimiento cambia la posición Y.

Añadimos distintos sprites para las palas y eliminamos el anterior:

;PADDLE:    EQU	$3c     ; # Eliminar línea #
PADDLE1:    EQU	$0f
PADDLE2:    EQU	$f0

Por último, añadimos las posiciones iniciales de las palas:

PADDLE1POS_INI:     EQU $4861
PADDLE2POS_INI:     EQU $487e

Hemos añadido sprites distintos para cada pala y eliminado la constante que usábamos para pintar las palas; si compilamos, nos dará errores.

Vamos a solucionar esos errores modificando la rutina PrintPaddle de Video.asm.

La rutina PrintPaddle recibe en el registro HL la posición de la pala. En el registro C recibirá el sprite de la pala.

Modificamos la línea justo debajo de la etiqueta printPaddle_loop:

ld      (hl), PADDLE

y la dejamos como sigue:

ld      (hl), c

Compilamos, y aunque no da ningún error, al cargar en el emulador vemos que los resultados no son los deseados:

Ensamblador para ZX Spectrum, "estas palas están raras"
Ensamblador para ZX Spectrum, «estas palas están raras»

La pala que pinta no se corresponde con el sprite que hemos definido. Esto es debido a que no hemos cargado en C cual es el sprite que debe pintar.

Abrimos el archivo Main.asm, y buscamos la etiqueta loop_continue. A partir de la línea 5 es donde imprimimos las palas, cargando en HL la posición de la pala y llamando al pintado de la misma. Antes de llamar al pintado de la pala, debemos especificar que sprite debe pintar.

Este es el aspecto una vez hecha la modificación:

ld      hl, (paddle1pos)
ld      c, PADDLE1
call    PrintPaddle
ld      hl, (paddle2pos)
ld      c, PADDLE2
call    PrintPaddle

Compilamos, abrimos en el emulador, y comprobamos que las palas se vuelven a pintar bien:

Ensamblador para ZX Spectrum, las palas se vuelven a pinta bien
Ensamblador para ZX Spectrum, las palas se vuelven a pintar bien

Aprovechando que estamos en Main.asm, vamos a cambiar un comportamiento del que quizás no os habéis percatado. Cuando se acaba un partido, y al iniciar otro, las palas siguen en la misma posición donde estaban al acabar el partido anterior, y la bola sale desde el campo del jugador que anotó el último punto.

Para modificar este comportamiento, vamos a añadir las siguientes líneas antes de la etiqueta Loop:

ld      hl, BALLPOS_INI
ld      (ballPos), hl
ld      hl, PADDLE1POS_INI
ld      (paddle1pos), hl
ld      hl, PADDLE2POS_INI
ld      (paddle2pos), hl

Con estas líneas situamos la bola y las palas en sus posiciones iniciales.

Si compilamos, vemos que nos da un error:

ERROR on line 68 of file Main.asm
ERROR: Relative jump out of range

Este error es debido a que, al ir añadiendo líneas, tenemos algún JR que está fuera de rango. JR solo puede saltar 127 bytes hacia adelante o 128 hacia atrás, y tenemos algún JR que salta a alguna dirección fuera de este rango.

En concreto, tenemos al final del archivo Main.asm, dos JR Main y un JR Loop. Sustituimos estos tres JR por JP, y solucionamos el error. JP ocupa un byte más que JR, por lo que nuestro programa acaba de crecer 3 bytes, pero hemos reducido 6 ciclos de reloj.

Compilamos, cargamos en el emulador y comprobamos que al acabar la partida e iniciar otra, tanto la bola como las palas, vuelven a su posición inicial.

Vamos a implementar el cambio de velocidad, inclinación y dirección de la bola al colisionar con las palas.

Abrimos el archivo Game.asm y buscamos la etiqueta checkBallCross_left. Tres líneas por encima encontramos:

ld      a, $ff

Modificamos esta línea y la dejamos como sigue:

ld      a, CROSS_LEFT_ROT

Buscamos la etiqueta CheckCrossX. Tres líneas por encima encontramos:

ld      a, $01

Modificamos esta línea y la dejamos como sigue:

ld      a, CROSS_RIGHT_ROT

Hemos cambiado los valores por constantes, para si en un futuro hay que cambiar los valores, tenerlos mejor localizados.

El siguiente paso es cambiar la configuración de la bola, dependiendo de en qué parte de la pala colisiona.

Vamos a dividir la pala en 5 partes. Dependiendo de donde colisione la bola, el comportamiento será:

Zona de golpeoDirección vericalInclinaciónVelocidad
1/5ArribaDiagonal3 (lento)
2/5ArribaSemi diagonal2 (normal)
3/5No cambiaSemi plano1 (rápido)
4/5AbajoSemi diagonal2 (normal)
5/5AbajoDiagonal3 (lento)
Ensamblador para ZX Spectrum, zonas de golpeo de las palas

Localizamos la etiqueta CheckCrossY, y nos vamos a la penúltima línea, XOR A, e implementamos justo antes de ella:

ld      a, c	
sub     $15
ld      c, a	
ld      a, b
add     a, $04
ld      b, a

Cuando llegamos a este punto, en C tenemos la posición del penúltimo scanline de la pala, y en B la posición de la bola. Ambas posiciones están en formato TTLLLLSSS.

Cargamos en A la posición del penúltimo scanline de la pala, LD A, C, nos posicionamos en el primero, SUB $15, y volvemos a cargar el valor en C, LD C, A.

Cargamos en A la posición de la bola, LD A, B, nos posicionamos en la parte baja de la bola, ADD A, $04, y volvemos a cargar el valor en B, LD B, A.

A partir de aquí implementamos el cambio de comportamiento, dependiendo de donde colisiona la bola:

checkCrossY_1_5:
ld      a, c
add     a, $04
cp      b
jr      c, checkCrossY_2_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la primera parte, ADD A, $04, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_2_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:

ld      a, (ballSetting)	
and     $40
or      $31
jr      checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 3 e inclinación diagonal, OR $31. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la primera parte de la pala, comprobamos si lo ha hecho con la segunda:

checkCrossY_2_5:
ld      a, c
add     a, $09
cp      b
jr      c, checkCrossY_3_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la segunda parte, ADD A, $09, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_3_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:

ld      a, (ballSetting)
and     $40
or      $22
jr      checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 2 e inclinación semi diagonal, OR $22. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la segunda parte de la pala, comprobamos si lo ha hecho con la tercera:

checkCrossY_3_5:
ld      a, c
add     a, $0d
cp      b
jr      c, checkCrossY_4_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la tercera parte, ADD A, $0D, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_4_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:

ld      a, (ballSetting)	
and     $c0	
or      $1f	
jr      checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal y con la vertical (ya vienen calculadas), AND $C0, y ponemos velocidad 1 e inclinación semi plana, OR $1F. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la tercera parte de la pala, comprobamos si lo ha hecho con la cuarta:

checkCrossY_4_5:
ld      a, c
add     a, $11
cp      b
jr      c, checkCrossY_5_5

Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la cuarta parte, ADD A, $11, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_5_5.

Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:

ld      a, (ballSetting)
and     $40
or      $a2
jr      checkCrossY_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 2 e inclinación semi diagonal, OR $A2. Saltamos al final de la rutina, JR checkCrossY_end.

Si la bola no ha colisionado con la cuarta parte de la pala, lo ha hecho con la quinta:

checkCrossY_5_5:
ld      a, (ballSetting)	
and     $40
or      $b1

Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 3 e inclinación diagonal, OR $B1.

Por último, justo por encima de XOR A, vamos a añadir la etiqueta de fin de función a la que hemos estado haciendo referencia, y vamos a cargar la nueva configuración de la bola en memoria:

checkCrossY_end:
ld      (ballSetting), a

Después de XOR A, vamos a poner el contador de movimientos de la bola a 0:

ld      (ballMovCount), a

El aspecto final de la rutina es el siguiente:

; – ---------------------------------------------------------------------------
; Evalúa si la bola colisiona en el eje Y con la pala.
; En el caso de colisionar, actualiza la configuración de la bola.
; Entrada:  HL -> Posición de la pala	
; Salida:   Z -> Colisiona.
;           NZ -> No colisiona.
; Altera el valor de los registros AF, BC y HL.
; – ---------------------------------------------------------------------------
CheckCrossY:
call    GetPtrY             ; Obtiene la posición vertical de la pala (TTLLLSSS)
; La posición devuelta apunta al primer scanline de la pala que está a 0
; apunta al siguiente
inc     a
ld      c, a                ; Carga el valor en C
ld      hl, (ballPos)       ; Carga en HL la posición de la bola
call    GetPtrY             ; Obtiene la posición vertical de la bola (TTLLLSSS)
ld      b, a                ; Carga el valor en B
; Comprueba si la bola pasa por encima de la pala
; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0
; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º
add     a, $04              ; Apunta la posición de la bola al 5º scanline
sub     c                   ; Resta a la posición de la bola, la posición de la pala
ret     c                   ; Si hay acarreo sale porque la bola pasa por encima
; Comprueba si la bola pasa por debajo de la pala
ld      a, c                ; Carga la posición vertical de la pala en A
add     a, $16              ; Le suma 22 para apuntar al penúltimo scanline, 
                            ; último que no es 0
ld      c, a                ; Lo vuelve a cargar en C
ld      a, b                ; Carga la posición vertical de la bola
inc     a                   ; Le suma 1 para apuntar el scanline 1, primero que no es 0
sub     c                   ; Resta a la posición de la bola, la posición de la pala
ret     nc                  ; Si no hay acarreo la bola pasa por debajo 
                            ; de la pala o colisiona en el último scanline.
                            ; En este último caso se activa el flag Z

; Dependiendo de donde sea la colisión, se asigna grado de inclinación
; y velocidad a la bola
ld      a, c                ; Carga la posición del penúltimo scanline de la pala en A
sub     $15                 ; Lo vuelve a posicionar en el primero
ld      c, a                ; Carga el valor en C

ld      a, b                ; Carga en A la posición de la bola
add     a, $04              ; Se posiciona en la parte baja de la bola
ld      b, a                ; Carga el valor en B

checkCrossY_1_5:
ld      a, c                ; Carga la posición vertical de la pala en A
add     a, $04              ; Se posiciona en el último scanline de 1/5
cp      b                   ; Lo compara con la posición de la bola
jr      c, checkCrossY_2_5  ; La bola está más abajo, salta
ld      a, (ballSetting)    ; Carga la configuración de la bola en A
and     $40                 ; Se queda con la dirección horizontal
or      $31                 ; Hacia arriba, velocidad 3 e inclinación diagonal
jr      checkCrossY_end     ; Fin de la rutina

checkCrossY_2_5:
ld      a, c                ; Carga la posición vertical de la pala en A
add     a, $09              ; Se posiciona en el último byte de 2/5
cp      b                   ; Lo compara con la posición de la bola
jr      c, checkCrossY_3_5  ; La bola está más abajo, salta
ld      a, (ballSetting)    ; Carga la configuración de la bola en A
and     $40                 ; Se queda con la dirección horizontal
or      $22                 ; Hacia arriba, velocidad 2 e inclinación semi diagonal
jr      checkCrossY_end     ; Fin de la rutina

checkCrossY_3_5:
ld      a, c                ; Carga la posición vertical de la pala en A
add     a, $0d              ; Se posiciona en el último byte de 3/5
cp      b                   ; Lo compara con la posición de la bola
jr      c, checkCrossY_4_5  ; La bola está más abajo, salta
ld      a, (ballSetting)    ; Carga la configuración de la bola en A
and     $c0                 ; Se queda con la dirección horizontal y vertical
or      $1f                 ; Hacia arriba/abajo, velocidad 1 e inclinación semi plano
jr      checkCrossY_end     ; Fin de la rutina

checkCrossY_4_5:
ld      a, c                ; Carga la posición vertical de la pala en A
add     a, $11              ; Se posiciona en el último byte de 4/5
cp      b                   ; Lo compara con la posición de la bola
jr      c, checkCrossY_5_5  ; La bola está más abajo, salta
ld      a, (ballSetting)    ; Carga la configuración de la bola en A
and     $40                 ; Se queda con la dirección horizontal y vertical
or      $a2                 ; Hacia abajo, velocidad 2 e inclinación semi diagonal
jr      checkCrossY_end     ; Fin de la rutina

checkCrossY_5_5:
ld      a, (ballSetting)    ; Carga la configuración de la bola en A
and     $40                 ; Se queda con la dirección horizontal
or      $b1                 ; Hacia abajo, velocidad 3 e inclinación diagonal

; Hay colisión
checkCrossY_end:
ld      (ballSetting), a    ; Carga en memoria la configuración actual de la bola
xor     a                   ; Activa el flag Z y pone A = 0
ld      (ballMovCount), a   ; Pone el contador de movimientos de la bola a 0
ret

Compilamos, cargamos en el emulador y vemos los resultados.

Vemos que la velocidad sí cambia dependiendo de donde colisiona la bola, pero no la inclinación. Además, al marcar un tanto, la velocidad no se reinicia, lo cual hace que sea muy difícil seguir jugando si la bola va a la velocidad máxima.

¿Por qué cambia la velocidad, pero no la inclinación?

Si hacemos memoria, en el paso anterior implementamos la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3. De hecho, este paso lo iniciamos avisando de que íbamos a prescindir de esta implementación, pero de lo que no se ha prescindido es del cambio que hicimos en Main.asm para tener en cuenta la velocidad de la bola que marque la configuración; por eso la velocidad cambia.

Nos falta la implementación para tener en cuenta la inclinación, y para que cuando se marca un punto, velocidad e inclinación de la bola se reinicien.

Vamos a empezar con el cambio de inclinación. Seguimos en el archivo Game.asm, implementando la rutina que va a cambiar la posición Y de la bola. La vamos a implementar después del RET de la etiqueta moveBall_end:

MoveBallY:
ld      a, (ballSetting)
and     $0f
ld      d, a

Cargamos en A la configuración de la bola, LD A, (ballSetting), nos quedamos con la inclinación, AND $0F, y cargamos el valor en D, LD A, D.

ld      a, (ballMovCount)
inc     a
ld      (ballMovCount), a
cp      d
ret     nz

Cargamos los movimiento de la bola en A, LD A, (ballMovCount), lo incrementamos en 1, INC A, cargamos el valor en memoria, LD (ballCount), A, y lo comparamos con D, que contiene el número de movimientos necesarios para cambiar la posición Y de la bola, CP D. Si no son iguales, no se ha llegado al valor necesario y salimos, RET NZ.

xor     a
ld      (ballMovCount), a
ret

Si hemos llegado al valor, ponemos A = 0 y activamos el flag Z, XOR A, ponemos a 0 los movimientos acumulados de la bola, LD (ballMovCount), A, y salimos, RET. Al activar el flag Z se indica, a quien llame, que se debe cambiar la posición Y de la bola.

El aspecto final de la rutina es el siguiente:

; – ---------------------------------------------------------------------------
; Cambia la posición Y de la bola
; Altera el valor de los registros AF y D.
; – ---------------------------------------------------------------------------
MoveBallY:
ld      a, (ballSetting)    ; Carga en A la configuración de la bola
and     $0f                 ; Se queda con la inclinación
ld      d, a                ; Carga el valor en D

ld      a, (ballMovCount)   ; Carga en A los movimientos acumulados de la bola
inc     a                   ; Incrementa A
ld      (ballMovCount), a   ; Carga el valor en memoria
cp      d                   ; Lo compara con la inclinación
ret     nz                  ; Si no son iguales, sale. No se cambia la posición

; La posición debe cambiar
xor     a                   ; Pone A = 0 y activa el flag Z
ld      (ballMovCount), a   ; Pone los movimientos acumulados de la bola a 0

ret

Localizamos la etiqueta moveBall_up, y entre las líneas JR Z, movelBall_upChg y CALL PreviousScan, añadimos las siguientes líneas:

call    MoveBallY
jr      nz, moveBall_x

Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.

Localizamos la etiqueta moveBall_down, y entre las líneas JR Z, moveBall_downChg y CALL NextScan, añadimos las siguientes líneas:

call    MoveBallY
jr      nz, moveBall_x

Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.

Compilamos, cargamos en el emulador, y comprobamos que ahora cambian la inclinación y la velocidad.

Por último, vamos a hacer que cuando se marque un punto, se reinicien la velocidad y la inclinación de la bola.

Localizamos la rutina SetBallLeft, eliminamos la línea AND $BF, y la sustituimos por las siguientes:

;and    $bf             ; # Eliminar línea #
and     $80
or      $31

Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la derecha, velocidad 3 e inclinación diagonal, OR $31.

Antes de la instrucción RET, añadimos las siguientes líneas:

ld      a, $00
ld      (ballMovCount), a

Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.

Localizamos la rutina SetBallRight y eliminamos la línea OR $40 y la sustituimos por las siguientes:

or      $40
and     $80
or      $71

Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la izquierda, velocidad 3 e inclinación diagonal, OR $11.

Antes de la instrucción RET, añadimos las siguientes líneas:

ld      a, $00
ld      (ballMovCount), a

Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.

El aspecto final de ambas rutinas es el siguiente:

; – ---------------------------------------------------------------------------
; Posiciona la bola a la izquierda.
; Altera el valor de los registros AF y HL.
; – ---------------------------------------------------------------------------
SetBallLeft:
ld      hl, $4d60           ; Carga en HL la posición de la bola
ld      (ballPos), hl       ; Carga el valor en memoria
ld      a, $01              ; Carga 1 en A
ld      (ballRotation), a   ; Lo carga en memoria Rotación = 1
ld      a, (ballSetting)    ; Carga en A la dirección y velocidad de la bola
and     $80                 ; Se queda con la dirección Y
or      $31                 ; Pone dirección X a derecha, velocidad 3
                            ; e inclinación diagonal
ld      (ballSetting), a    ; Carga la nueva dirección de la bola en memoria
ld      a, $00
ld      (ballMovCount), a

ret

; – ---------------------------------------------------------------------------
; Posiciona la bola a la derecha.
; Altera el valor de los registros AF y HL.
; – ---------------------------------------------------------------------------
SetBallRight:
ld      hl, $4d7e           ; Carga en HL la posición de la bola
ld      (ballPos), hl       ; Carga el valor en memoria
ld      a, $ff              ; Carga -1 en A
ld      (ballRotation), a   ; Lo carga en memoria Rotación = -1
ld      a, (ballSetting)    ; Carga en A la dirección y velocidad de la bola
and     $80                 ; Se queda con la dirección Y
or      $71                 ; Pone dirección X a izquierda, velocidad 3 
                            ; e inclinación diagonal
ld      (ballSetting), a    ; Carga la nueva dirección de la bola en memoria
ld      a, $00			
ld      (ballMovCount), a

ret

Compilamos, cargamos en el emulador y vemos los resultados, que deben ser los esperados, aunque la bola va algo lenta, ¿o no?

En el próximo capítulo de Ensamblador para ZX Spectrum, implementaremos los efectos de sonido, optimizaremos algunos aspectos de nuestro programa y lo haremos compatible con el modelo de 16K, llegando así a la línea de meta.

Enlaces de interés

Ficheros

 

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuar!

Mostrar más

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicaciones relacionadas

Botón volver arriba