Fast decimal scoring system

By Grim.

In games where every cycles count, the score is one of the many tiny things to take care of. The most cycle-consuming score-operations usually are converting the score value from binary to decimal and displaying that decimal value on screen.

To convert a binary number to its decimal string representation, using generic ITOA (Integer to ASCII) routines will be too slow and, more often than not, limited to 16-bit range. Specialized routines will hopefully perform better but still take precious CPU-cycles. Bummer!

This article presents an example of somewhat fast decimal scoring routines for arbitrary large numbers that work directly on decimal values, because the fastest conversion is no conversion at all! Technically, all the numbers will be stored in little-endian (LSB to MSB) and BCD form (2 digits per byte).

To keep the source-code simple, we will deal with only two numbers, the score and the points (to be added to or subtracted from the score), both with separately configurable sizes. Also, no checks are made to prevent under/overflows.

Why not using the same size for all numbers you may ask? Well, you can if you want to. But in some cases, for example shooter games, using very large score (10 or more digits), you almost never add to it any point values with more than 4 or 5 digits. So it would be a waste of memory and CPU-cycles to use the same size on everything, score and points. And in practice, you might even need differently sized points!

Little-Endian?

In little-Endian, the values are stored with the least significant byte first (exactly like the Z80 already does for 16-bit values). This endianness was chosen because it simplifies a bit the ADD/SUB operations on the score.

BCD?

Binary-Coded-Decimal will store 2 decimal digits in a single byte, 1 digit (0 to 9) per nibble (4-bit). The BCD values will obviously take a little bit more memory-space (since a single byte can only represent values from 0 to 99 instead of the usual 0 to 255). This trade-of allows avoiding slow conversions from binary to decimal and our high-end Z80 CPU has the necessary instruction (DAA) to quickly handle arithmetic operations on BCD values! Yay!

Score display?

There won’t be any optimized score display presented here since optimized means tailored for a very specific context only you know. However, here are a few hints:

πŸ’‘ #1: The score does not need to be displayed at full frame rate! Only once every, say, 20 video-frames will be more than enough.

πŸ’‘ #2: Also, to avoid cycle-spikes when displaying very large score numbers, you can display only one or two digits per frame instead of the whole thing at once.

πŸ’‘ #3: Captain Obvious, no need to display the score if it did not change.

The score

You can configure the size of the score to any length, but the larger you set it to, the more memory and CPU-cycles it will take to store, update and display.

;; Configure the size of the score in byte (2 digits per byte)
CNF_SCORE_SIZE EQU <size between 1 and 255>

API: score_reset

  • Description : Reset the score to zero
  • Input : None
  • Output : HL, B, AF modified
;; Reset the score to zero
call score_reset

The points

Again, configurable size. Obviously it should not be greater than the score‘ size (OR ELSE! :).

;; Configure the size of the points in byte (2 digits per byte)
CNF_POINT_SIZE EQU <size between 1 and 255>

If you do not need the subtraction routine, you can disable it at compile time with:

;; Enable (1) or Disable (0) compilation of the score subtraction function
CNF_SCORE_WITH_SUB EQU <0 or 1>

API: score_add and score_sub

  • Description: Add or Sub operations on the current score value
  • Input:
    • DE = Pointer to point number
  • Output : HL, DE, AF modified
  • Execution Time:
    • Min = 7 + CNF_POINT_SIZE * 10
    • Max = 19 + CNF_POINT_SIZE * 10 + (CNF_SCORE_SIZE - CNF_POINT_SIZE) * 12
    • Times given in NOPs when the score buffer does not cross a &xx00 address boundary.
  ;; Usage example of score_add and score_sub routines
 
  ;; Add points when a zombie is killed
  ld de,pt_zombie_killed
  call score_add

  ;; Sub points when a zombie bit you!
  ld de,pt_zombie_bite
  call score_sub

...

pt_zombie_killed DB &00,&10,&00 ; 1000 pts
pt_zombie_bite   DB &50,&00,&00 ; 50 pts

Source-code

            ;; Micro-optimization utility
            MACRO SCORE_INC_HL
              LET _a = _score_data AND &FF00
              LET _b = _score_data + CNF_SCORE_SIZE AND &FF00
              IF _a XOR _b
                 inc hl
               ELSE
                 inc l
               ENDIF
             MEND

             ;; Reset score to zero
score_reset:
             ld hl,_score_data
             ld b,CNF_SCORE_SIZE
             xor a
 _score_reset_loop
                 ld (hl),a
                 SCORE_INC_HL
                 djnz _score_reset_loop
             ret

             ;; Add points
 score_add:
             ld hl,_score_data
             ; Clear carry
             or a
             ; Add points to the score
             REPEAT CNF_POINT_SIZE
                 ld a,(de)
                 adc a,(hl)
                 ; Adjust ADC result to BCD
                 daa
                 ld (hl),a
                 SCORE_INC_HL
                 inc de
             REND
             ; Exit immediately if there is no carry left
             ret nc
             ; Carry propagation
             REPEAT CNF_SCORE_SIZE - CNF_POINT_SIZE
                 ld a,(hl)
                 adc a,0
                 daa
                 ld (hl),a
                 ret nc
                 SCORE_INC_HL
             REND
             ; Overwrite the useless last RET NC:INC
             ORG $-2
             ; Exit
             ret

             IF CNF_SCORE_WITH_SUB
                 ;; Substract points (optional)
 score_sub:
                 ld hl,_score_data
                 ; Clear carry
                 or a
                 ; Sub points to the score
                 REPEAT CNF_POINT_SIZE
                     ld a,(de)
                     sbc a,(hl)
                     daa
                     ld (hl),a
                     SCORE_INC_HL
                     inc de
                 REND
                 ; Exit immediately if there is no carry left
                 ret nc
                 ; Carry propagation
                 REPEAT CNF_SCORE_SIZE - CNF_POINT_SIZE
                     ld a,(hl)
                     sbc a,0
                     daa
                     ld (hl),a
                     ret nc
                     SCORE_INC_HL
                 REND
                 ; Overwrite the useless last RET NC:INC
                 ORG $-2
                 ; Exit
                 ret
             ENDIF

Usage example

;; Compilation options (must be defined _before_ including score.asm)
CNF_SCORE_WITH_SUB EQU 0  ; Disable subtract function (not needed)
CNF_SCORE_SIZE     EQU 10 ; Score is 20 digits long
CNF_POINT_SIZE     EQU 3  ; Points are 6 digits long

        ORG &1000
        RUN $

        di

        ; reset score
        call score_reset

        ; add some points
        ld de,score_pt_drone
        call score_add_point

        ld de,score_pt_turet
        call score_add_point

        ld de,score_pt_zombie
        call score_add_point

        ld de,score_pt_boss
        call score_add_point

        ; ...

;; Example of a Right To Left (RTL) ASCII conversion, all at once score display.
;; (for an LTR conversion, you must read the score from MSB to LSB instead)
print_score:
        ld hl,_score_data
        ld b,CNF_SCORE_SIZE
_print_score_loop
            ld a,(hl)
            SCORE_INC_HL
            ; first digit
            ld c,a
            call print_score_digit
            ; second digit
            ld a,c
            rra
            rra
            rra
            rra
            call print_score_digit
            djnz _print_score_loop
        ret

;; Display a single digit (0 to 9)
print_score_digit:
        and &0F
        add a,"0"
        ; A = ASCII code to display
        push hl
        push bc
        ;call to some chr out routine here
        pop bc
        pop hl
        ret

;; Define the point values that will be added to the score
;; LSB to MSB and BCD form.

score_pt_drone   DB &50,&00,&00 ; 50 pts
score_pt_turet   DB &00,&01,&00 ; 100 pts
score_pt_zombie  DB &00,&10,&00 ; 1000 pts
score_pt_boss    DB &00,&00,&01 ; 10000 pts

;; Score buffer, ideally located within a 256-bytes address boundary so that
;; the INC L speed-optimization can be used
_score_data      DS CNF_SCORE_SIZE, 0

         ;;Include the score routines
         READ "score.asm"

Changelog

  • 2020-11-07 – Recycled by MrEarwig