The Problem
One of the great things about PICO-8 is that invites you to look for ways to work around its stringent limitations. One of the biggest limitations I've found myself repeatedly bumping up against is, quite simply, "I want to count higher than 32 thousand, dangit!"
I've used a variety of solutions in the past, all adequate for their various purposes, but none of them really satisfying–and none of them good for really ridiculously high counting. All told, I set out to find a counting system that met the following criteria:
- I want to be able to count big numbers. Just bonkers, bonkers big numbers, stupid huge numbers, thankyouverymuch
- I don't want to spend too many tokens getting there
- I want to support earning (add) and spending (subtract if less than) these numbers
- I want to be able to easily create these numbers from an integer-as-string or built-in number type
- I want to be able to display these huge numbers in a compact yet readable format
- I don't need a lot of precision; if I already have a million points, a single additional point is meaningless to me.
- I want to be able to save this huge number to a single slot of cart data (32 bits).
The Solution: Exponental Notation
Enter exponential notation. By using a significand (or mantissa) and an exponent, we can represent bonkers-high numbers in comparatively little space. Since space is always at a premium in PICO-8, we want to keep each of these numbers fairly small; in order to pack this into a single 32-bit space, we only want to use 16 bits each for the significand and exponent. (We could allocate this differently, but there are good reasons for doing it this way; see the Various Ramblings below for more details.)
With the expint() data type, we can store an almost arbitrarily large number to four significant digits. We can then represent that number in an exponential format, either in traditional scientific notation (1.234x10^4) or as a human-friendly string (12.34k). This suits PICO-8 quite well: you don't have much space to display your large numbers, and once you start getting into serious number territory, you can't even fit them on the screen anymore.
The Drawback: Also Exponential Notation
This is where the low precision comes in. Using expint()
, 10,000 + 9 = 10,000
. If you're doing important things, like flying an airplane or building a bridge, that's a very bad result! Happily, we're not doing important things; we're making video games! What's more, once you have ten thousand of something, another nine of that thing isn't going to be that big of a deal, especially if you're displaying that number as an approximation–for example, 10.13B
instead of 10,132,449,018
.
Other Limitations Of This Implementation
- Whole numbers only. This isn't designed to work with negative numbers or non-integers, either in the significand or in the exponent. I haven't tried doing it, and I'm quite confident it'd behave badly if I did.
print()
is a little weird right now. I want to further refine the print()
function; right now, instead of showing 1.000k
, it shows 1000
, which feels...wrong. That said, it works for now, and it's still technically accurate. I think. :D
- Even big numbers have their limits. We're still using a 16-bit number to hold our exponent; we can't go any higher than that. This will conk out around
999x10^32767
.
- This code is not particularly optimized for space. I'm confident that there are folks out there who will look at this, shake their heads at my foolishness, and proceed to strip this down to half its current size and twice its current speed. As an aside, Shrinko-8 will minify the verbose variable names quite nicely.
- You can add, and you can spend, and that's it. Adding other operations should be fairly straightforward, but I'm limiting this release to the two biggies for most of the applications of this number type in PICO-8.
- There are no guardrails. In an effort to save tokens, I'm not doing any real error handling or boundary testing here. There's nothing stopping you from driving this thing off a cliff.
- I, uh, haven't really tested this all that thoroughly yet. The happy path works, but there are probably some real forehead-smackers floating around in here. Set me straight in the comments, please!
Various Ramblings
At first, I thought about doing a true single-precision floating point number implementation. This, after all, is industry standard, and there are plenty of implementations I could crib off of to make this happen. Before long, though, I stumbled across a common enemy: all of these implementations were way more token-intensive than I was looking to do. (This, I suspect, is why we don't see a bunch of floating-point custom types kicking around the forums for this purpose.)
So scratch going the IEEE route; given what I know about PICO-8, my most likely path to success would be to look for nifty, imperfect ways to leverage what the system already gives me.
Eventually, two separate but related concepts came together in my head like peanut butter and chocolate:
- PICO-8 stores numbers as 16:16 fixed point decimals, neatly splitting them down the middle
- @zep added some nice format flags to tonum() and tostr() to hack this 16:16 fixed point format into a 32-bit integer format
32 bits is nice, but still cuts you off a little over 2 billion, which for a lot of games isn't gonna cut it. What will cut it for this kind of game, though, is exponential notation: using a mantissa and an exponent, you can represent pretty much any number your heart can dream of. (Note that this is the basic concept behind standard floating-point implementations: so many bits reserved for the exponent, so many reserved for the mantissa.)
So I was kinda back where I started, but not quite: I realized I could use PICO-8's 16:16 fixed-point number format as a jumping-off point for a lot of shortcuts if I used it to store my mantissa and exponent in that same 16:16 layout. I could use some basic arithmetic and bitwise operators to cram the 16-bit significand and 16-bit exponent into the integer and fractional parts of the built-in number type for easy storage: