
Advent of Z80 - Day 2, 2022


It’s day two of the Advent of Z80. As with day 1 the post will focus more on the Z80 assembly than on the puzzle, and is a “literate assembly” document that will execute in your browser.
JP DAY02
The second puzzle is based on the venerable rock, paper, scissors game. The puzzle input is a turn by turn list of the gameplay, with the opponent’s move given as one of A, B, or C and the player’s move given as X, Y, or Z. The required output is a score based on the player’s chosen move plus and whether the game was a win, draw, or loss.
For every round I am told what move I will make and what move my opponent makes, and I simply need to translate that into two score values and add that in to a running tally. The first score value is 1, 2, or 3 for rock, paper, or scissors. The second score value will be a lookup into a 3x3 matrix based on the two moves. If I assign 0 to rock, 1 to paper, and 2 to scissors the first score is the move plus one and the second score is a lookup.
When using the lookup table I’ll load the address of the table into HL
then add the player’s move three times and the opponent’s move once. The maximum total offset is 8, and I’d really like to be able to use 8-bit additions without worrying about carry into H
, so I will align the lookup table’s address in memory such that the address plus 8 never overflows.
Most assemblers have a .align
directive for this purpose. The directive inserts padding into the program until the address modulo the alignment equals zero. For this to prevent overflow when adding the alignment needs to be a power of two - the smallest alignment that will ensure safety is 16. Because I only have 9 bytes of data this means up to 7 bytes of padding will be added even if there’s no chance of overflow.
Some assemblers - including the one used here - allow extra arguments to .align
to specify what fill value will be inserted into the output for the padding, and to specify the maximum offset that will be allowed. I can specify that I want my table aligned to 256 bytes, but not to add any padding if the padding to add is greater than 9 bytes - in most cases this will simply leave the table where it is.
.block
.align 256, 0, 9
WINLOSS:
; oppt: R, P, S
defb 3, 0, 6 ; me: R
defb 6, 3, 0 ; me: P
defb 0, 6, 3 ; me: S
; IN: B opponent's move (0: Rock, 1: Paper, 2: Scissors)
; C player's move (0: Rock, 1: Paper, 2: Scissors)
; OUT: A score
@SCORE: push hl
ld a, WINLOSS & 0ffh
add a, c ; my move * 1
add a, c ; my move * 2
add a, c ; my move * 3
add a, b ; + your move
ld l, a
ld h, WINLOSS >> 8
; score = my move (rock = 1, paper = 2, scissors = 3) ...
ld a, c
inc a
; plus win/loss score
add a, (hl)
pop hl
ret
.endblock
Now that I can score a round all I need to do is score all the rounds and sum the scores up.
.block
@DAY02: ld hl, STRATEGY
ld de, 0
ROUNDS: ld a, (hl) ; load opponent's move
sub a, 'A'
jr c, DONE ; finish when < 'A'
ld b, a
inc hl
ld a, (hl) ; load my move
inc hl
sub a, 'X'
ld c, a
call SCORE
add a, e ; add A into DE
ld e, a
ld a, 0
adc a, d
ld d, a
jr ROUNDS
DONE: call PRINT16
halt
STRATEGY:
'A', 'Y'
db 'B', 'X'
db 'C', 'Z'
db 0
db
.endblock
Day 1 required printing a 32-bit number. Day 2’s results fits into a 16-bit number on real input - and an 8-bit number for the sample input - which is simpler to print:
.block
; IN DE a 16-bit unsigned number to print
; OUT A clobbered
@PRINT16:
push bc
push de
push hl
ex de, hl
ld c, '0' ; the 'non-zero' flag
ld de, -10_000
call DIGIT
ld de, -1_000
call DIGIT
ld de, -100
call DIGIT
ld de, -10
call DIGIT
; force last digit to print
ld c, 1
ld de, -1
call DIGIT
pop hl
pop de
pop bc
ret
DIGIT: ld a, '0'-1
DLOOP: inc a
add hl, de
jr c, DLOOP
sbc hl, de
cp c
jr z, NOPRINT
out (1), a
ld c, 1
NOPRINT:
ret
.endblock
Let’s fire it up and see what it does.
jp DAY02
Huzzah and hurrah, 15 is printed.
Star 2
For star 2, the meaning of the second column has changed: an ‘X’ means to lose, a ‘Y’ to draw, and a ‘Z’ to win. The score is still calculated the same way, so all I need is an extra step to convert the ‘X’, ‘Y’, and ‘Z’ into the appropriate move, then just repeat the first star’s computations.
I can use the same table-based approach as for scoring to translate to a move: the move to be made depends on both the opponent’s move and the desired outcome. Once again ensuring the table is aligned will prevent overflow and allow 8-bit arithmetic to be used.
.block
.align 256, 0, 9
MOVES:
; oppt: R, P, S
defb 2, 0, 1 ; lose
defb 0, 1, 2 ; draw
defb 1, 2, 0 ; win
; IN: B opponent's move (0: Rock, 1: Paper, 2: Scissors)
; C win/loss/draw goal (0: Loss, 1: Draw, 2: Win)
; OUT: A score
@SCORE2:push hl
push bc
ld a, MOVES & 0ffh
add a, c ; goal * 1
add a, c ; goal * 2
add a, c ; goal * 3
add a, b ; + opponent move
ld l, a
ld h, MOVES >> 8
; load the move to make
ld c, (hl)
; score it
call SCORE
pop bc
pop hl
ret
.endblock
The remainder of the star 2 implementation is exactly the same as the star 1 implementation save for calling SCORE2
instead of SCORE1
.
.block
@STAR2: ld hl, STRATEGY
ld de, 0
ROUNDS: ld a, (hl) ; load opponent's move
sub a, 'A'
jr c, DONE ; finish when < 'A'
ld b, a
inc hl
ld a, (hl) ; load my move
inc hl
sub a, 'X'
ld c, a
call SCORE2
add a, e ; add A into DE
ld e, a
ld a, 0
adc a, d
ld d, a
jr ROUNDS
DONE: call PRINT16
halt
STRATEGY:
'A', 'Y'
db 'B', 'X'
db 'C', 'Z'
db 0
db
.endblock
And executing this should print 12.
jp STAR2
The series
Day 1 | Day 2 | Day 3 | Day 4 | Day 5 | Day 6 | Day 7 | Day 8 | Day 9 | Day 10 | Day 11 | Day 12 | Day 13 | Day 14 | Day 15 | Day 16 | Day 17 | Day 18 | Day 19 | Day 20 | Day 21 | Day 22 | Day 23 | Day 24 | Day 25 |