(2025-05-26) Herding the cattle better with advanced HP 12C techniques
----------------------------------------------------------------------
This surely is not my last 12C-related post here, but probably my final post
about porting Bulls and Cows onto this surprisingly capable pocket machine.
As you might have told, the previous version was quite slow, especially on
the initial target number generation phase, and required rather complicated
setup. Within the previous week, I discovered a bunch of "hidden" 12C
functionality that not only allowed me to make that phase a bit faster, but
also to simplify the setup and to compress the entire program code. Let's
dig into that a bit, shall we?

If you remember, I had written in my previous posts that the 12C doesn't have
indirect memory addressing and indirect jumps. The latter, as far as I know,
still holds true (unfortunately), but the former turned out to be pretty
much false. The 12C, in fact, DOES have indirect memory addressing. What's
more, it even has some special-purpose registers that can only be accessed
indirectly! But let's begin from the very beginning.

Among other financial-specific functions, two of them pose a particular
interest: NPV (net present value) and IRR (internal rate of return). Not
digging into much specifics, I'll only tell you that the NPV function
computes the value of a polynomial (with the argument transformed in a
simple way) and the IRR function finds _roots_ of that polynomial. Yes, the
12C has a built-in polynomial solver, although it's not documented as such.
I have added all the detailed info on how to make use of this in my "Tricks"
section of the document ([1]), so feel free to read that section if you're
interested in polynomials in general, but here, I'm gonna focus on how the
_coefficients_ for those polynomials are input and stored, because that's
what's important for understanding the indirect addressing feature.
Officially, the manual says that you start entering the coefficients (in the
Horner order, from the free coefficient A0 to the highest degree coefficient
An) with the "cash flow" keys: CFo for A0 and then subsequent presses of CFj
for Aj. The manual also says you can recall and edit the coefficients by
pressing RCL CFj. What's really happening behind the scenes is:

1. Whenever you press CFo, the number in the X register is stored into R0 and
the number 0 is stored into Rn.
2. Whenever you press CFj, the contents of Rn is autoincremented and the
number in the X register is stored into whatever register Rn is pointing to
after this increment. If the register index is just 1 more than what's
available, the machine writes the number into RFV.
3. Whenever you press RCL CFj, the number stored into whatever register Rn is
pointing to gets copied into the X register, and THEN Rn gets
autodecremented. Again, if Rn was pointing to a register index just 1 more
than what's available, the contents of RFV would be returned.

So yeah, that's your indirect register addressing for ya. Just put an index
one less than you need into Rn and you can write to the register under that
index using the CFj command. Yes, this works for R0 too if you say 1 CHS STO
n, for instance, putting -1 into Rn: it will pre-increment and write to R0
when you press CFj. When you need to indirectly read from a register, just
put its unaltered index into Rn and issue RCL CFj, and you're good to go.
But wait, there's more! There's much more.

You see, the NPV/IRR functions also allow you to specify _repeating_ cash
flows with a separate Nj function. Essentially, that repeats the last
polynomial coefficient CFj Nj times (up to 99), allowing you to also
increase the degree of the polynomial without having to waste extra storage
space. What's interesting for us is that the Nj registers to not overlap
with the main register area. In fact, physically, they are stored in three
separate 7-byte blocks of memory. Why so? Because every Nj register can only
hold integers, and those integers can only be from 0 to 99 (yes, whoever
tells you it's 1 to 99 is wrong, it's 0 to 99). So, how do we access those
special registers? Same way but the operations don't alter the contents of
Rn: you first set the index to Rn and then use Nj for writing and RCL Nj for
reading. Now, how many Nj registers are available? Alas, despite this memory
area is independent, the amount of available Nj registers is strictly equal
to the amount of available coefficients, which is the amount of available
main area registers plus one (because of the RFV register). This means that,
for instance, in order to have 10 Nj registers available, you need r-09 to
be displayed on the g MEM screen, which means 85 program steps at most. Oh,
and one more thing: they all get initialized to 1, not 0. And there is some
inconvenience when trying to reset them all to 1 because they survive the f
FIN command, and f REG cannot be called programmatically.

But why would I ever need ten extra registers that can only hold integers
from 0 to 99? Well, for starters, this makes a perfect flag array for
generating numbers with unique digits. Just like in... you know... Bulls and
Cows. Having a set of flags for marking which digits already have been used
means a much faster and cleaner algorithm. Now, only three "minor"
challenges needed to be overcome for the ultimate Bulls and Cows version for
the 12C to appear:

1) switching to a PRNG that wouldn't depend on Rn,
2) incorporating a mechanism to reset the Nj flags after the target number
has been generated,
3) squeezing all the code to the hard limit of 85 steps.

The first challenge was rather easy to tackle because, besides the shortcut
12x function that multiplies by 12 and automatically saves the result into
Rn, we also have the 12/ function that divides by 12 and automatically saves
the result into Ri. After some testing, I had come up with the formula S[n]
= frac(exp(S[n-1] + 7) / 12) which takes the same 7 steps and achieves
equally good statistical results. The second challenge, on the other hand,
was tightly intertwined with the third one. Every time I came up with a nice
flag clearing algorithm, I found that I fully ran out of steps. Finally, I
decided that I would calculate the outer digit as a single digit (as opposed
to the the entire integer part of whatever remained of the target) and use
the digit as the index to reset the flag as well. Still several steps more
than necessary though. And then it dawned on me: why not just use Rn as the
outer digit intermediate storage, since I had to populate it anyway? And
after that, everything finally clicked into place and, after using the
CL.STAT trick to clear the registers R1 to R6 at once (and reallocating the
constants 10 and 10000 to R7 and R8), the new Bulls and Cows version takes
exactly 85 steps and has been published in the document under the
UltimateMoo12 name. Because this, in my opinion, is the pinnacle of digital
cattle herding on a purely financial calculator. But the real value is
contained in all those techniques I have learned throughout this
optimization process. I have been capturing them all in the document's
"Tricks" section as well for everyone to see and try out. Because people
deserve to know what's really possible even on the original 884 KHz model.

And you know what? I have a feeling that all that still is just a tip of the
iceberg.

--- Luxferre ---

[1]: gopher://hoi.st:70/0/docs/own/hp-12c-soft.txt