Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Pong – 0x03 Paddles and centre line

In this ZX Spectrum Assembly chapter, we will paint the centre line, the paddles and move them.

Translation by Felipe Monge Corbalán

Table of contents

Paddles and centre line

We have already acquired enough knowledge to start the development of our ZX-Pong, we have implemented a good part of the basics of the programme.

In this step we will:

  • Change the border colour.
  • Assign the colour attributes to the screen.
  • Draw the centre line of the board.
  • Draw the paddles of both players.
  • Move the paddles up and down.

We create a folder called Step03 and inside it we create the files main.asm and sprite.asm.

This time we are not going to start from scratch, as in the previous steps we developed the code, controls.asm and video.asm files that we are going to use in this step, so we are going to copy these two files into the new directory.

Change border colour

First step. Although the colour of the final border will be the same as the rest of the screen, for these first steps we will make it red to visualise the borders of the screen.

We are going to edit the main.asm file, and the first thing to do, as always, is to specify the memory address where we are going to load the program:

ASM
org  $8000

The next step is to set the border to red:

ASM
ld   a, $02
out  ($fe), a

We load the value of the red colour into A, LD A, $02, and write this value to port $FE (254), OUT ($FE), A. We already know this port, we have used it to read the keyboard status.

Finally, we quit the programme and tell PASMO where to call it when it is loaded.

ASM
ret

end  $8000

We compile with PASMO and see the end result:

The main.asm code looks like this:

ASM
org  $8000

ld   a, $02                ; A = 2
out  ($fe), a              ; Turns the border red
ret

end  $8000

Assign the colour attributes to the screen

In our case, the attributes are white ink and black background.

We will implement a routine, Cls, which will clear the screen and set the background to black and the ink to white.

The screen attributes are contiguous with the area in which the pixels are drawn; it starts at address $5800 and is $300 (768) bytes long, 32 columns by 24 lines. On the Sinclair ZX Spectrum, the colour attributes are at the character level, one attribute affects an area of 8×8 pixels, hence the attribute clash.

The attributes of a character are defined in one byte:

Bit 7Bit 6Bits 5, 4 & 3Bits 2, 1 & 0
Blink (0/1)Brightness (0/1)Background (0 to 7)Ink (0 to 7)
ZX Spectrum Assembly, Pong

The Cls routine has two parts:

  • Clean the screen.
  • Assigning the colour and background.

Let’s edit the video.asm file and implement the routine:

ASM
Cls:
ld   hl, $4000
ld   (hl), $00
ld   de, $4001
ld   bc, $17ff
ldir
ret

The first thing our routine does is point HL to the start of VideoRAM, LD HL, $4000, and erase that byte from the screen, LD (HL), $00.

The next step is to point DE to the position next to HL, LD DE, $4001, load the number of bytes to erase into BC, LD BC, $17FF, which is the entire VideoRAM area ($1800) minus one, the position where HL points and which is already erased.

LDIR, LoadData, Increment and Repeat, loads the value into the memory location pointed to by HL, pointed to by DE. When this is done, increment HL and DE. This is repeated in a loop until BC reaches zero. Finally, we exit the routine.

In the main.asm file, we add the call to Cls before RET:

ASM
call Cls

Before END $8000, we add the line to include the video.asm file:

ASM
include "video.asm"

Compile with PASMO and load into the emulator:

As you can see in the picture, the Bytes: ZX-Pong line is no longer displayed; itshows that we have cleared the screen.

To implement the second part of the routine, the assignment of the colour attributes, we will write the following lines just before the RET instruction of the Cls routine:

ASM
ld   hl, $5800
ld   (hl), $07
ld   de, $5801
ld   bc, $2ff
ldir

The first thing this part does is set HL to the beginning of the attribute area, LD HL, $5800, sets this area to flicker-free, matte, black background and white ink, LD (HL), $07.

$07 = 0000 0111 = 0 (flicker) 0 (brightness) 000 (background) 111 (ink)

The next step is to point DE to the position next to HL, LD DE, $5801, and load into BC the number of bytes to load, LD BC, $2FF, which is the entire attribute area ($300) minus one, which is the position where HL points, and already has the attributes. LDIR is executed, and the colour is assigned to the whole screen.

The full code of the routine is:

ASM
;--------------------------------------------------------------------
; Clean screen, ink 7, background 0.
; Alters the value of the AF, BC, DE and HL registers.
;--------------------------------------------------------------------
Cls:
; Clean the pixels on the screen
ld   hl, $4000             ; HL = start of VideoRAM
ld   (hl), $00             ; Clears the pixels of that address
ld   de, $4001             ; DE = next VideoRAM position
ld   bc, $17ff             ; 6143 repetitions
ldir                       ; Clears all pixels from VideoRAM

; Sets the ink to white and the background to black.
ld   hl, $5800             ; HL = start of attribute area
ld   (hl),$07              ; White ink and black background
ld   de, $5801             ; DE = next attribute position
ld   bc, $2ff              ; 767 repetitions
ldir                       ; Assigns the value to attributes

ret

At this point, we compile and see the result:

We notice that, in addition to cleaning the screen, he has set the background to black and the ink to white, but as he has not drawn anything on the screen, we cannot see if the ink is really white.

To see different effects, change the values loaded in (HL).

This routine can be modified, saving us eight clock cycles and four bytes. We leave it up to you to find out how to do this; we will give the solution in the last chapter. Don’t worry, it’s not a critical routine, so it won’t affect the development of our game.

Draw the centre line of the field

The centre line of the field consists of a blank scanline, six scanlines with bit 7 set to one, and a final blank scanline:

00000000
10000000
10000000
10000000
10000000
10000000
10000000
00000000

In this case, we will only define the empty part and the part that draws the line. In the sprite.asm file, we add the following lines:

ASM
ZERO: EQU $00
LINE: EQU $80

The EQU directive defines constant values which are not compiled; on the contrary, the compiler replaces all references in the code to these tags with the value assigned to them.

Example: ld a, ZERO → Compiler → ld a, $00

Once we have the line sprite, let’s implement the routine to draw it. Go back to the video.asm file:

ASM
PrintLine:
ld   b, $18
ld   hl, $4010

We will paint our line sprite on twenty-four lines (all of them), LD B, $18, starting at scanline 0, line 0, third 0 and column 16, LD HL, $4010.

ASM
printLine_loop:
ld   (hl), ZERO
inc  h
push bc

We will draw the first scanline blank, LD (HL), ZERO, move to the next scanline, INC H, and finally keep the value of BC on the stack as we will be using B in the loop that will draw the visible part of the line, PUSH BC.

To change the scanline, instead of calling A NextScan, we will increment H. Why? Quite simply. Since we are going to paint the eight scanlines of the same character, we are not going to change the line or the third, so incrementing the scanline is enough, saving bytes and processing time.

We also put a value on the stack, namely BC. It is very important to remember that each PUSH must have a POP, and even if there are multiple PUSHs, there must be the same number of POPs, but in reverse order:

push af
push bc
pop  bc
pop  af

Now let’s make the loop that paints the visible part of the line:

ASM
ld   b, $06
printLine_loop2:
ld   (hl), LINE
inc  h
djnz printLine_loop2
pop  bc

First we indicate the number of iterations of the new loop, LD B, $06, draw the scanline with the visible part of the line, LD (HL), LINE, move to the next scanline, INC H, and repeat until B is zero, DJNZ printLine_loop2. When B is zero, we take the value of BC from the stack to continue the loop of the twenty-four lines of the screen, POP BC.

This brings us to the final part of the routine:

ASM
ld   (hl), ZERO
call NextScan
djnz printLine_loop
ret

Now we draw the last scanline, LD (HL), ZERO, get the next one, CALL NextScan, repeat until B is zero and we draw all twenty-four lines of the screen, DJNZ printLine_loop. This time we call NextScan as we change lines.

The final aspect of the routine is as follows:

ASM
;--------------------------------------------------------------------
; Paints the centre line.
; Alters the value of the AF, B and HL registers.
;--------------------------------------------------------------------
PrintLine:
ld   b, $18                ; Paints on all 24 lines of the screen
ld   hl, $4010             ; Starts on line 0, column 16

printLine_loop:
ld   (hl), ZERO            ; In the first scanline it paints blank
inc  h                     ; Go to the next scanline

push bc                    ; Preserves BC value for second loop
ld   b, $06                ; Paints six times
printLine_loop2:
ld   (hl), LINE            ; Paint byte the line, $10, b00010000
inc  h                     ; Go to the next scanline
djnz printLine_loop2       ; Loop until B = 0
pop  bc                    ; Retrieves value BC
ld   (hl), ZERO            ; Paint last byte of the line
call NextScan              ; Goes to the next scanline
djnz printLine_loop        ; Loop until B = 0 = 24 lines
ret

The only thing left to do is to test it. We open the file main.asm, add the call to PrintLine after the call to Cls and include the file sprite.asm as we did with the file video.asm:

ASM
call PrintLine

include "sprite.asm"

We compile and see the result in the emulator:

Now we can see that we have put the ink in white.

Draw the paddles of both players

In this step we are going to draw the paddles of both players, which occupy 1×3 characters, one byte (eight pixels) and twenty-four scanlines.

We are going to use the definition type we used for the horizontal line definition, and we are going to do it in the sprite.asm file:

ASM
PADDLE: EQU $3c

This would be the visible part of the paddle, 00111100, as we are going to paint an empty scanline, twenty-two scanlines with this definition and the last empty scanline.

The paddles will be movable elements, so in addition to their sprite we need to know where they are and what the top and bottom margins are to which we can move them.

We continue with the sprite.asm file:

ASM
PADDLE_BOTTOM: EQU $a8     ; TTLLLSSS
PADDLE_TOP:    EQU $00     ; TTLLLLLSSSSS
paddle1pos:    dw  $4861   ; 010T TSSS LLLC CCCC
paddle2pos:    dw  $487e   ; 010T TSSS LLLC CCCC

In the first two constants, which are the limits to which we can move the paddles, we specify the Y coordinate expressed in third, line and scanline. While PADDLE_TOP points to the upper limit of the screen (third 0, line 0, scanline 0), PADDLE_BOTTOM does not point to the lower limit of the screen (third 2, line 7, scanline 7), but to third 2, line 5, scanline 0, the result of subtracting twenty-three scanlines from the lower limit ($BF) to paint the twenty-four scanlines of the paddle sprite without invading the attributes area of the screen.

paddle1pos and paddle2pos are not constant, as these values change in response to control key presses.

The initial position of the paddles is:

 Paddle 1Paddle 2
Third11
Line33
Scanline00
Column130
ZX Spectrum Assembly, Pong

Once this is defined, in the video.asm file we implement the routine that draws the paddles, which has as an input parameter the position of the paddle that it receives in HL; this is necessary because we have two paddles to print, the other alternative would be to duplicate the routine and have each one print one paddle.

ASM
PrintPaddle:
ld   (hl), ZERO
call NextScan

The first thing it does is to paint the first scanline of the paddle, LD (HL), ZERO, blank, and then it gets the next scanline, CALL NextScan.

Contrary to what happened when painting the centre line, in this routine the calls to NextScan are necessary. The movement of the paddle will be pixel by pixel, this in vertical is scanline by scanline, which means that we do not know in advance when we are going to change lines (we could know, but we are not going to).

The next step is to paint the visible part of the paddle:

ASM
ld   b, $16
printPaddle_loop:
ld   (hl), PADDLE
call NextScan
djnz printPaddle_loop

The visible part of the paddle is painted in twenty-two scanlines, LD B, $16, loading the paddle sprite at the position indicated by HL, LD (HL), PADDLE, and getting the next scanline, CALL NextScan, until B is zero, DJNZ printPaddle_loop.

Finally we paint the last scanline of the paddle in white:

ASM
ld   (hl), ZERO
ret

It is useful to paint the first and last scanline in white, so that when the paddle is moved it erases itself and leaves no trace.

The final aspect of the routine is as follows:

ASM
;--------------------------------------------------------------------
; Paint the paddle.
; Input: HL -> paddle position.
;
; Alters the value of the B and HL registers.
;--------------------------------------------------------------------
PrintPaddle:
ld   (hl), ZERO            ; Paints first byte of blank paddle
call NextScan              ; Goes to the next scanline
ld   b, $16                ; Paints visible byte of paddle 22 times
printPaddle_loop:
ld   (hl), PADDLE          ; Paints the paddle byte
call NextScan              ; Goes to the next scanline

djnz printPaddle_loop      ; Loop until B = 0

ld   (hl), ZERO            ; Paints last byte of blank paddle

ret

Finally, we need to test that the routine works. We open the main.asm file and add after the PrintLine call:

ASM
ld   hl, (paddle1pos)
call PrintPaddle
ld   hl, (paddle2pos)
call PrintPaddle

With LD HL, (paddle1pos) we load the position of paddle one in HL and with CALL PrintPaddle we print it. The same with paddle two.

We compile and look at the results:

Move the paddles up and down

We tackle the last part of step 3.

We have declared constants with lower and upper limits. Now we implement the check whether a memory location on the screen has reached or is outside a certain limit.

The set of routines to be implemented receives the limit in TTLLLLSSS format in A, and the current position in 010TTSSS LLLCCCCC format in HL. These routines return Z if the limit has been reached and NZ otherwise (we continue in video.asm):

ASM
CheckBottom:
call checkVerticalLimit
ret  c

We call the checkVerticalLimit routine, CALL checkVerticalLimit, and if there is a carry it exits, RET C, with NZ. If there is a carry, the memory location is above (on the screen) the lower limit.

ASM
checkBottom_bottom:
xor  a
ret

If it gets this far, it is because it has reached the lower limit, activate the Z flag, XOR A, and exit, RET.

This routine doesn’t do much, so we can assume that most of the logic is in checkVerticalLimit.

Let’s implement the upper limit routine:

ASM
CheckTop:
call checkVerticalLimit
ret

As in the previous routine, checkVerticalLimit is called. In this case the limit has not been reached if the result of checkVerticalLimit is not zero, so we leave with the correct result, RET.

Most of the upper and lower limit detection is done in the checkVerticalLimit routine, which receives the vertical limit in A (TTLLLLSSS) and the current position (010TTSSS LLLCCCCC) or position to compare with in HL.

Because of the different format we have in HL and A, the first step is to convert the content in HL to the same format as the content in A.

ASM
checkVerticalLimit:
ld   b, a
ld   a, h
and  $18
rlca
rlca
rlca
ld   c, a

First we keep the value of A, LD B, A, and then we load the value of H into A, LD A, H, and keep the third, AND $18. We rotate register A to the left three times, RLCA, to put the third in bits 6 and 7, and load the value into C, LD C, A. Now C contains the third of the position we received in HL.

ASM
ld   a, h
and  $07
or   c
ld   c, a

We load the value of H again into A, LD A, H, but this time we keep the scanline, AND $07. Now we have in A the scanline that comes in HL, and we add the third that we have stored in C, OR C, and we load the result in C, LD C, A. Now C has the third and the scanline that we received in HL, but with the same format as the value that we received in A (TT000SSS).

ASM
ld   a, l
and  $e0
rrca
rrca
or   c

Now we are going to put the value of the line where it belongs, loading the value of L into A, LD A, L, keeping the bits where the line came from, AND $E0, and rotating the bits twice to put the line in bits 3, 4 and 5, RRCA. Finally, we add the third and the scanline that we have stored in C, OR C, so that in A we now have the third, the line and the scanline that came in HL, with the format we need (TTLLLLLSSS).

ASM
cp   b
ret

The last step is to compare what we now have in A with what we have in B, which is the original value of A (vertical limit), CP B.

This last operation will, among other things, change the carry and zero flags:

ResultZC
A = B1 – Z0 – NC
A < B0 – NZ1 – C
A > B0 – NZ0 – NC
ZX Spectrum Assembly, Pong

Depending on the flags and whether we are evaluating the lower or upper limit, we will know whether the limit has been reached or exceeded.

The full code for this set of routines is as follows:

ASM
;--------------------------------------------------------------------
; Evaluates whether the lower limit has been reached.
; Input:  A  -> Upper limit (TTLLLLSSS).
;         HL -> Current position (010TTSSS LLLCCCCCCC).
; Output: Z  =  Reached.
;         NZ =  Not reached.
;
; Alters the value of the AF and BC registers.
;--------------------------------------------------------------------
CheckBottom:
call checkVerticalLimit    ; Compare current position with limit
; If Z or NC has reached the ceiling, Z is set, otherwise NZ is set.
ret c
checkBottom_bottom:
xor a                      ; Active Z
ret

;--------------------------------------------------------------------
; Evaluates whether the upper limit has been reached.
; Input:  A  -> Upper margin (TTLLLLSSS).
;         HL -> Current position (010TTSSS LLLCCCCCCC).
; Output: Z  =  Reached.
;         NZ =  Not reached.
;
; Alters the value of the AF and BC registers.
;--------------------------------------------------------------------
CheckTop:
call checkVerticalLimit    ; Compare current position with limit
ret                        ; checkVerticalLimit is enough

;--------------------------------------------------------------------
; Evaluates whether the vertical limit has been reached.
; Input: A  -> Vertical limit (TTLLLLSSS).
;        HL -> Current position (010TTSSS LLLCCCCCCC).
;
; Alters the value of the AF and BC registers.
;--------------------------------------------------------------------
checkVerticalLimit:
ld   b, a                  ; Stores the value of A in B
ld   a, h                  ; A = value of H (010TTSSSSSS)
and  $18                   ; Keeps the third
rlca
rlca
rlca                       ; Sets the value of the third in bits 6 and 7
ld   c, a                  ; Load the value in C
ld   a, h                  ; A = value of H (010TTSSSSSS)
and  $07                   ; Keeps the scanline
or   c                     ; Add the third
ld   c, a                  ; Load the value in C
ld   a, l                  ; A = value of L (LLLCCCCCCC)
and  $e0                   ; Keeps the line
rrca
rrca                       ; Puts the line on bits 3, 4 and 5
or   c                     ; Adds third and scanline. A = TTLLLSSS
cp   b                     ; Compare with B = original value 
                           ; A = boundary
ret

Using these routines, we can now implement the movement of the paddles and prevent them from leaving the screen.

We will edit main.asm and include controls.asm:

ASM
include "controls.asm"

We are going to implement an infinite loop in which we evaluate whether any control key has been pressed, in which case we move the corresponding paddle. The loop is implemented immediately after the PrintLine call:

ASM
loop:
call ScanKeys

The first thing the loop does is to check whether any of the control keys, CALL ScanKeys, have been pressed.

ASM
MovePaddle1Up:
bit  $00, d
jr   z, MovePaddle1Down
ld   hl, (paddle1pos)
ld   a, PADDLE_TOP
call CheckTop
jr   z, MovePaddle2Up
call PreviousScan
ld   (paddle1pos), hl
jr   MovePaddle2Up

After evaluating the controls, we check if the control key was pressed to move paddle one up, BIT $00, D, and if not, we jump to the next check, JR Z, MovePaddle1Down.

To move the paddle up, we need to see if the paddle leaves the upper limit, for which we need to know the current position of the paddle, LD HL, (paddle1pos), we need to get the upper limit, LD A, PADDLE_TOP, and check if it has been reached, CALL CheckTop.

When CheckTop triggers the Z flag, we have reached the limit, so we jump to check paddle two, JR Z, MovePaddle2Up.

If the Z flag is not set, we get the position to paint the paddle, CALL PreviousScan, load into memory, LD (paddle1pos), HL, and jump to check paddle two, JR MovePaddle2Up.

If the control key above paddle one has not been pressed, a check is made to see if the control key below has been pressed:

ASM
MovePaddle1Down:
bit  $01, d 
jr   z, MovePaddle2Up
ld   hl, (paddle1pos)
ld   a, PADDLE_BOTTOM
call CheckBottom
jr   z, MovePaddle2Up
call NextScan
ld   (paddle1pos), hl

Evaluates whether the control key was pressed to move paddle one down, BIT $01, D, and if not, JR Z, MovePaddle2Up, jumps.

To move the paddle downwards, before moving it, we must check that it does not leave the lower limit, for which we must know the current position, LD HL, (paddle1pos), get the lower limit, LD A, PADDLE_BOTTOM, and check that we have not already reached it, CALL CheckBottom.

When CheckBottom triggers the Z flag it means we have reached the limit, so we jump to check the movement of paddle two, JR Z, MovePaddle2Up.

If the Z flag is not activated, we get the position where the paddle is to be painted, CALL NextScan, and load it into memory, LD (paddle1pos), HL. This time we don’t jump, the next instruction starts to check the movement of paddle two.

As to the fact that the checking of the movement of paddle two is very similar to that of paddle one, the memory positions change to obtain the position of paddle two and the jump positions, we do not detail it:

ASM
MovePaddle2Up:
bit  $02, d
jr   z, MovePaddle2Down
ld   hl, (paddle2pos)	
ld   a, PADDLE_TOP
call CheckTop
jr   z, MovePaddleEnd	
call PreviousScan
ld   (paddle2pos), hl
jr   MovePaddleEnd

MovePaddle2Down:
bit  $03, d 
jr   z, MovePaddleEnd
ld   hl, (paddle2pos)
ld   a, PADDLE_BOTTOM
call CheckBottom
jr   z, MovePaddleEnd
call NextScan
ld   (paddle2pos), hl

MovePaddleEnd:

The last line, MovePaddleEnd, is a tag to jump to the area where the paddles will be painted.

Finally, after painting the paddles, we will replace RET with JR loop to stay in an infinite loop.

The final code in the main.asm file is as follows:

ASM
; Draw the two paddles and the centre line.
; Moves the paddles up and down in response to pulsation
; the control keys.
org  $8000

ld   a, $02                ; A = 2
out  ($fe), a              ; Turns the border red

call Cls                   ; Clear the screen
call PrintLine             ; Paints the centre line

loop:
call ScanKeys              ; Scan for keystrokes

MovePaddle1Up:
bit  $00, d                ; Evaluates whether A has been pressed
jr   z, MovePaddle1Down    ; If not pressed, it skips
ld   hl, (paddle1pos)      ; HL = position of paddle one
ld   a, PADDLE_TOP         ; A = top margin
call CheckTop              ; Evaluates whether the margin
                           ; has been reached
jr   z, MovePaddle2Up      ; If reached, skip
call PreviousScan          ; Gets scanline before paddle
ld   (paddle1pos), hl      ; Loads new paddle position into memory
jr   MovePaddle2Up         ; Jump

MovePaddle1Down:
bit  $01, d                ; Evaluates if the Z has been pressed       
jr   z, MovePaddle2Up      ; If not pressed it skips
ld   hl, (paddle1pos)      ; HL = position of paddle one
ld   a, PADDLE_BOTTOM      ; A = bottom margin
call CheckBottom           ; Evaluates whether the margin
                           ; has been reached
jr   z, MovePaddle2Up      ; If reached, skip
call NextScan              ; Gets scanline next to the paddle
ld   (paddle1pos), hl      ; Loads new paddle position into memory

MovePaddle2Up:
bit  $02, d                ; Evaluates if 0 has been pressed
jr   z, MovePaddle2Down    ; If not pressed, it skips
ld   hl, (paddle2pos)      ; HL = paddle two position
ld   a,PADDLE_TOP          ; A = top margin
call CheckTop              ; Evaluates whether the margin
                           ; has been reached
jr   z, MovePaddleEnd      ; If reached, skip
call PreviousScan          ; Gets scanline prior to the paddle
ld   (paddle2pos), hl      ; Loads new paddle position into memory
jr   MovePaddleEnd         ; Jump

MovePaddle2Down:
bit  $03, d                ; Evaluates whether the O has been pressed
jr   z, MovePaddleEnd      ; If not clicked, skips
ld   hl, (paddle2pos)      ; HL = paddle two position
ld   a, PADDLE_BOTTOM      ; A = bottom margin
call CheckBottom           ; Evaluates whether the margin
                           ; has been reached
jr   z, MovePaddleEnd      ; If reached, skip
call NextScan              ; Gets scanline next to the paddle
ld   (paddle2pos), hl      ; Loads new paddle position into memory

MovePaddleEnd:
ld   hl, (paddle1pos)      ; HL = position of paddle one
call PrintPaddle           ; Paint the paddle
ld   hl, (paddle2pos)      ; HL = paddle two position
call PrintPaddle           ; Paint the paddle
jr   loop                  ; Infinite loop

include "controls.asm"
include "sprite.asm"
include "video.asm"

end  $8000

We compile and see the results in the emulator:

ZX Spectrum Assembly, Pong

In the next ZX Spectrum Assembly chapter, we will start moving the ball.

Download the source code from here.

ZX Spectrum Assembly, Pong by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.



Deja una respuesta

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo