Score, etc

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Score, etc
by on (#160358)
I've seen lots of posts on this. Trying to keep it very simple. Was wondering if anyone had any thoughts.

Trying to essentially keep a score tallying (no problem with drawing or using a digit-based solution). I can make it work by seeding the additive...like, if I'm adding a three digit number to a three digit number, if I:

Code:

    LDA #$0
    STA add
    LDA #$5
    STA add+1
    LDA #$1
    STA add+2


...and run the routine, I can successfully appear to add 150 to the score, no problem there.

The problem I'm having is using this simple, quick method with variable numbers. So for instance, if I had a table with point-values for various objects (I'm ok with everything being worth <256, so I only need a byte), what would be the simplest way to get a hex value broken into 'places'?

For instance...hex value $64 = 100. What would be the best method or converting $64 to seed a 1 in the 'hundreds' additive, a zero in the 'tens' additive, and a zero in the 'ones' additive?

I can imagine creating three separate 256 byte LUTs to handle this, but that seems dreadfully inefficient and wasteful. Surely, there has to be some better method, and maybe I'm just looking at it too close?

Thanks!
Re: Score, etc
by on (#160359)
There's a small, reasonably fast (80 cycles) byte-to-digits converter in the source code for Zap Ruder.
Re: Score, etc
by on (#160363)
This is how its done in TSB. But probably not the most efficient way possible. All of these variables are temporary variables.

Code:

INPUT:  A= hex number to convert

RESULT:
$45=  high digits
$44=  low digits

hex_to_bcd_8_bit                  ; CONVERT HEX NUMBER TO BCD(A= HEX)
   STA $45
   LDA #$0A                  ; SETS TO BASE 10
   STA $44
   JSR divide_8_bit               ; 8-BIT DIVIDE ($45/$44) QUOTIENT =$45 REMAINDER =$42
   LDA $42                                             ;
   STA $43                                             ;
   JSR divide_8_bit               ; 8-BIT DIVIDE ($45/$44) QUOTIENT =$45 REMAINDER =$42
   LDA $42                  ;
   ASL                                                   ;
   ASL                                                   ;
   ASL                                                   ;
   ASL                                                   ;
   ORA $43                                             ;
   STA $43                                             ;
   JSR divide_8_bit               ; 8-BIT DIVIDE ($45/$44) QUOTIENT =$45 REMAINDER =$42
   LDA $42                  ; SAVE HIGH DIGIT IN $45
   STA $45                                             ;
   LDA $43                                             ; SAVE LOW DIGIT IN $44
   STA $44                                             ;
   RTS                     ; RETURN

divide_8_bit:                  ; 8 BIT DIVIDE ($45/$44) QUOTIENT $45 REMAINDER $42
   LDA #$00
   STA  $42
   LDX #$08                                            ; do 8 bits
@div:
   ASL $45
   ROL $42
   LDA $42
   SEC
   SBC $44
   BCC @next
   STA $42
   INC $45
@next:
   DEX
   BNE @div
   RTS   



Re: Score, etc
by on (#160366)
Hackfresh - I'm guessing those JSRs are supposed to be to divide_8_bit, and there's not some other divide_8 routine being called, correct?

And I'm guessing $42-$45 are just your memory locations for the 'score' variable or whatever? I'm a bit confused by that part of it.

Thanks!
Re: Score, etc
by on (#160385)
Yes, I posted this fast so yes the JSR's should have been divide_8_bit. I edited my post. In this game $3E-$45 are used as local variables for routines.

The 'A" register holds the hex number that is to be converted. The result of the conversion gets saved in $44,$45 where 44 is the low digit and $45 is the high digit.

Ex

Input to hex_to_bcd_8_bit A= #$82 = 130 decimal

Result
$44 = #$30
$45 = #$01
Re: Score, etc
by on (#160386)
Perfect - that makes it a lot easier to read. Thanks for the clarification...I'll try to implement this. It's simple enough to navigate and should work just fine for my purpose.
Re: Score, etc
by on (#160389)
Do you really need 256 unique score values? If you're willing to settle for less (which is very, very common in games), your lookup table doesn't have to be large. E.g. you could decide that possible scores are 5, 10, 15, 30, 50, 100, 200, 350, 500, 800 => only 30 bytes of lookup tables.
Re: Score, etc
by on (#160390)
JoeGtake2 wrote:
Perfect - that makes it a lot easier to read. Thanks for the clarification...I'll try to implement this. It's simple enough to navigate and should work just fine for my purpose.



Here is a full example. Sorry about the formatting.

EXAMPLE A= #$80 = 128 DECIMAL

Code:
hex_to_bcd_8_bit      ;
   STA $45                 ; $45=#$80
   LDA #$0A                      ; SETS TO BASE 10

                            ; GET ONES PLACE
   STA $44                             ; $44= #$0A
   JSR divide_8_bit                   ; = #$80/#$0A   RESULT = QUOTIENt $45= #$0C, REM $42 = #$08
   LDA $42                             ;
   STA $43                             ; $43= #$08
            
                           ; GET 10'S PLACE
   JSR divide_8_biT                 ; =#$0C/#$0A    RESULT = QUOTIENt $45= #$01, REM $42 = #$02
   LDA $42                     ; A= $42= #$02
   ASL                                  ;
   ASL                                  ;
   ASL                                  ;
   ASL                                  ; A= #$20
   
                           ; COMBINE 10'S AND 1'S PLACE   
   ORA $43                            ; = #$20 + #$08
   STA $43                           ; $43 =#$28
   
                           ; GET 100'S PLACE
   JSR divide_8_bit                  ; = #$01/#$0A   RESULT = QUOTIENT $45= #$00, REM $42 = #$01
   
                           ; SAVE RESULT IN $44,$45
   LDA $42                         ; SAVE HIGH DIGIT IN $45 = #$01
   STA $45                           ;
   LDA $43                            ; SAVE LOW DIGIT IN $44 = #$28
   STA $44                            ;
   RTS                        ; RETURN
Re: Score, etc
by on (#160395)
It depends on how fast the division by 10 is.

For reference, here's a copy of the one I was referring to:
Code:
.macro bcd8bit_iter value
  .local skip
  cmp value
  bcc skip
  sbc value
skip:
  rol highDigits
.endmacro

;;
; Converts a decimal number to two or three BCD digits
; in no more than 80 cycles.
; @param a the number to change
; @return a: low digit; 0: upper digits as nibbles
; No other memory or register is touched.
.proc bcd8bit
highDigits = 0

  ; First clear out two bits of highDigits.  (The conversion will
  ; fill in the other six.)
  asl highDigits
  asl highDigits

  ; Each iteration takes 11 if subtraction occurs or 10 if not.
  ; But if 80 is subtracted, 40 and 20 aren't, and if 200 is
  ; subtracted, then 100, 80, and one of 40 and 20 aren't.
  ; So this part takes up to 6*11-2 cycles.
  bcd8bit_iter #200
  bcd8bit_iter #100
  bcd8bit_iter #80
  bcd8bit_iter #40
  bcd8bit_iter #20
  bcd8bit_iter #10
  rts
.endproc
Re: Score, etc
by on (#160397)
I'm sure I'd just keep it decimal the whole time. I wouldn't bother to convert, and I probably wouldn't use a lookup table. It doesn't seem terribly inefficient to me at all to just do it one digit per byte. Probably something along the lines of this:

Code:
.segment "BSS"
score: .res SCORE_SIZE

.segment "CODE"

; decimal addition code

score_add100: ; add A * 100 to score (A = 0-9)
   ldx #2
   jmp score_add_loop

score_add10: ; add A * 10 to score (A = 0-9)
   ldx #1
   jmp score_add_loop

score_add1: ; add A to score (A = 0-9)
   ldx #0
score_add_loop:
   clc
   adc score, X
   cmp #10
   bcc :+
      sec
      sbc #10
      sta score, X
      inx
      cpx #SCORE_SIZE
      bcs :++ ; overflow, maybe set all digits to 9 instead?
      lda #1
      jmp score_add_loop
   :
   sta score, X
   :
   rts

; example use

goomba_kill:
   ; add 300 to score
   lda #3
   jsr score_add100
   rts

power_up:
   ; add 150 to score
   lda #1
   jsr score_add100
   lda #5
   jsr score_add10
   rts
Re: Score, etc
by on (#160403)
Rain - that's pretty simple, but now if I had 50 possible 'increase amounts', this would require 50 mini routines, right? KillMonster0-KillMonster50. Again, that seems a little bloated...like the LUTs. Not that it's a huge issue, just wondering what method makes more sense.
Re: Score, etc
by on (#160405)
Whether or not to use a table of values to add is kind of a completely independent question from how to store/display/work with decimal numbers.

If all of your monsters share the same code for dying, sure, use tables? If each has unique code for it, there might not be an advantage. I've got no advice on that part without knowing what you're working with. Doesn't seem like a difficult decision, though.
Re: Score, etc
by on (#160406)
Thanks for the input (and that's to everyone!).

Point taken, Rain. I think maybe the best way to handle it in this scenario would be to use your method, make a LUT table for all point-yielding objects (since they all have an identifying object type number anyway) that will point to different 'amounts' to increase, and then just set up routines that are "addOne", "addFive", "addTen", "addTwentyFive" and so on.

Definitely helped me sort that out in my head and simplified it a good bit.
Re: Score, etc
by on (#160407)
Maybe my example wasn't clear. The functions score_add1/add10/add100 were just meant to add a value from A (0 to 9) starting at a specific digit (i.e. multiplied by 1/10/100). I wasn't suggesting to add a subroutine entry for every possible value, just one for each digit you want to add to.
Re: Score, etc
by on (#160408)
No, I got it. But if Monster1 adds 5 points, monster2 to adds 250 points, monster3 adds 27 points...etc...I'd need subroutines for each to populate those values, and then feed them into the routine.

ie:
Code:
add5:   ;; for monster one and any other thing that adds five points
    LDA #$05
    JSR score_addOnes
   RTS

add250:  ;; for monster two and any other thing that adds two hundred and fifty points
   LDA #$5
   JSR score_addTens
   LDA #$2
   JSR score_addHundreds
   RTS


etc.
Re: Score, etc
by on (#160411)
Ah, I didn't think you meant that, because I can't think of a situation where that kind of code would be an advantage. If you're concerned about the space used on such a detailed level, I might suggest a few things:

1. Every tail call (JSR + RTS) can be replaced with a JMP for a savings of 1 byte (and several cycles).
2. If you use the first subroutine less than 3 times, you're increasing the size of your code instead of saving anything. (LDA imm + JMP = 5 bytes, JSR = 3 bytes.)
3. Just making one subroutine that looks up from a table would almost certainly be smaller than creating a subroutine for each value.

Personally, if I was using it a lot I'd probably go the opposite route, not caring about space much and using macros to automate generation of code so that I don't have to think about tables or subroutines:
Code:
.macro SCORE_ADD pval
   .if (pval % 10) > 0
      lda #(pval % 10)
      jsr score_add1
   .endif
   .if ((pval / 10) % 10) > 0
      lda #((pval / 10) % 10)
      jsr score_add10
   .endif
   .if ((pval / 100) % 10) > 0
      lda #((pval / 100) % 10)
      jsr score_add100
   .endif
.endmacro

; usage then becomes a one-liner
SCORE_ADD 300
SCORE_ADD 15

Of course, this is mildly wasteful of space, but it's all a matter of proportion. There are a hundred other ways to optimize for space too, but I tend to defer optimization until I have a problem that I need to solve with it.