Image by timlewisnm, cropped.

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:
        db      'A', 'Y'
        db      'B', 'X'
        db      'C', 'Z'
        db      0

        .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:
        db      'A', 'Y'
        db      'B', 'X'
        db      'C', 'Z'
        db      0

        .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 |