Since P8 has a 32k limit on numbers, what's the best way (or a way) to have numbers in the 100 thousands or millions and so on?
I mean, I guess I could super fake it by just printing out zeros after a low score integer but is that really the best way to deal with it?
Well, you've got two ways...
- Fake it!
Example:
print(score.."00",0,0,7) |
- Use a combination of 2 variables that can preferably calculate to 9999.
Example:
print(score1..score2,0,0,7) |
This is from my Pico Racer game (though it actually uses tens of thousands):
function addscore(points_to_add) ones += points_to_add while ones >= 1000 do ones -= 1000 thousands += 1 end end |
It will of course overflow if you have e.g. 999 in ones and try to add 32000 points. If you want to avoid the overflow with huge scores you need to deal with the thousands as well when adding.
Then you have to print the thousands and ones so that the ones are zero padded (000, 001, 002 etc.). Didn't test the following but it should work:
local score_string = "00"..ones print(thousands..sub(score_string, #score_string - 2,#score_string)) |
EDIT: fixed a stupid typo in addscore()
In combo pool, I tried to simply store the score divided by 100, so using the fractionnal part to have more values. It's not totaly accurate but it was working good enough. I also added a 0 to make the score more impressive. Here is the code to display it, but there is probably a better way :
mulint = 100 -- when adding score : score += addscore/mulint function getscoretext(value) local fracscore = flr((value%1)*mulint+0.5) local textscore = (fracscore%mulint).."" local floorscore = flr(value) + flr(fracscore/mulint) if floorscore > 0 then if(#textscore<2) textscore = "0"..textscore textscore = floorscore..textscore end return textscore end -- then displaying the score : print(getscoretext(score)) |
In theory you could divide by 2^16 I think, but retrieving the number to display it may become hard, if someone as a simple code for that, it would be great.
Ok, I did some more tests with only one value to store the score, and I have something working prety well until about 300 millions. I check accuracy as well as I could. You must never add more than 32k to the score at a time obviously.
Here is the code :
mulint = 2^14 -- to add an integer value to the score: score += addscore/mulint -- convert a number to a string, padding with 0 to reach size n: function tostr(v,n) local s = ""..v local t = #s for i=1,n-t do s="0"..s end return s end -- get the text from a score value: function getscoretext(val) local low = (val * mulint)%mulint local high = flr(val) local begin = flr(high*1.6384) local last = flr(((high * 1.6384)%1)*10000+0.5) local nlow = low+last local rest = nlow%10000 local nhigh = begin+flr(nlow/10000) return tostr(nhigh,5)..tostr(rest,4) end -- print a score: print(getscoretext(score)) |
Tell me if you find any errors
@kometbomb: that's a really simple yet elegant solution, I like it! Might actually have to steal that one for my games :P
@kometbomb I didnt try that, but the precision displayed by a print of a fractionnal is not great. And asking a decimal version of a fixed point encoded value will always give an approximation.
I also quickly made a small "library" following your method, and it maybe easyer to use. Here it is :
function newscore() local s = {u=0,k=0} s.add = function (this,p) this.u += p%10000 this.k += flr(p/10000) while this.u>=10000 do this.u -= 10000 this.k += 1 end while this.u<0 do this.u += 10000 this.k -= 1 end end s.text = function (this) local vunit = this.u local kilo = ""..this.k if this.k<0 then kilo = "-"..(abs(this.k)-1) vunit = abs(((this.u)-10000)) end local units = "000"..vunit return ""..kilo..sub(units, #units-3,#units) end return s end -- here some sample use : score = newscore() score:add(1000) score:add(-30000) print(score:text()) |
@NuSan: Yeah, I did some testing and adding 0.01 for one point gets inaccurate really quick.
I was just thinking over the morning coffee, remembering programming in 6502 assembler that there could be a function that updates a number and returns the carry like this:
function adc(number,add) number += add local carry = 0 -- could probably get the carry -- with flr(number/1000) but I -- am not 100 % sure so let's -- be naive while number >= 1000 do carry += 1 number -= 1000 end return carry end |
And now you can cascade it. E.g.:
ones = 0 thousands = 0 millions = 0 billions = 0 adc(billions, adc(millions, adc(thousands, adc(ones, p)))) |
And you can similarly come up with a function that prints with zero padding (it's the same for the ones, thousands, millions etc.). This wastes a lot of space compared to your magic, might be relevant when saving e.g. a TOP 50 list in the cart memory.
Edit: Left for education/posterity, but use this one further below, it's better.
Just use all 32 bits of the S15.16 fixed-point numbers. Under the hood, they're just integers with an implied decimal point between bits 15 and 16.
If you enjoy retro programming, you should definitely learn more about fixed-point numbers. I'll give a quick overview, but I'm a terrible teacher and I strongly suggest learning about fixed-point math on your own time.
I'll have a TL;DR at the end for those who just want to know how to solve this particular problem for now.
PICO Lua holds values where the integer portion is shifted left 16 so it can rest in the upper 16 bits of a native 32-bit integer on the real cpu. Let me give you an example...
Let's say you want to assign 10 to x:
x = 10
You're actually saying something like this to the Lua interpreter:
x.internal = 10 << 16
Or, if we write it directly in hex:
x.internal = 0x000a0000
The lower 16 bits are the fraction. For integer values in lua, it's always going to come out 0x????0000. For non-integers, you simply multiply the real fraction portion by 65536 (0x10000) and those are the bits to put in the lower 16.
For instance, if you want to encode 10.5, the fractional portion is .5, and .5 * 65536 is 32768, or 0x8000. So...
x = 10.5
...is...
x.internal = 0x000a8000
As it happens, multiplying by 65536 IS shifting left 16. You're not really doing two operations, one for the integer and one for the fraction. It's all just one shift by 16.
Helpfully, Lua lets us write non-integers with hexadecimal. So this:
x = 10.5
Could actually be written like this:
x = 0x000a.8000
If we simply take those bits and shift them left 16 bits, which is the same thing as shifting the decimal point right 16 bits, we can see how simple the underlying operation really is:
x.internal = 0x000a.8000 << 16 = 0x000a8000
So if you want to store something in those bottom bits, you just have to do as a fraction. Hex fractions are easiest to deal with for literals, so I suggest you use them. If you don't know hex... well, you should learn hex. :)
TL;DR:
We need to store integer values in the bottom 16 bits, in the fraction. We can do this by (conceptually) shifting the integer portion right 16:
-
Think: need to add 100 to massive score
- Do:
score += 100 >> 16 --or, using a hex literal directly score += 0x0000.0064 |
And...
-
Think: need to print score
- Do:
local s = "" local v = score while (v!=0) do -- modulo by the underlying version of 10 local r = v % 0x0000.000a -- bring it up where lua will stringify it correctly r <<= 16 -- prefix the new digit onto the score string s = r..s -- move to the next digit v /= 10 end print(s) |
This math shouldn't lose any precision because it's all integers under the hood, no underflows or anything.
You guys were having issues with using tens and hundreds and thousands because they aren't powers of two, but shifting is all about powers of two and it can always be done cleanly. The only time we use tens is to convert the value to a human-readable decimal string.
Addendum: If you tried that code before I wrote this post, I missed the SHL() in the printing code, sorry it didn't work without that. It's fixed now.
Nice one Felice! I was just in the middle of writing up the same approach, but struggling over how to do the conversion back to string, and you nailed it!
@kometbomb : that's a prety simple way to extend the number as high as you want, wich can be usefull.
Felice : thanks, I knew there was a clean way to extract the string. Once you think about extracting each digits separatly, it become way easyer.
I tried changing it to handle negative score, tell me if (or when ^^) you see a cleaner version :
function getscoretext(val) local s = "" local v = val while (v!=0) do -- modulo by the underlying version of 10 local r = v % 0x0000.000a -- bring it up where lua will stringify it correctly r = shl(r, 16) -- prefix the new digit onto the score string if val<0 then s = ((10-r)%10)..s else s = r..s end -- move to the next digit v /= 10 end if val<0 then s = "-"..s end return s end |
Edit: Use this one further below, it's better.
This is the smallest I can make it, for the sake of tokens+mem:
function getscoretext(val) local s = "" local v = abs(val) while (v!=0) do s = (v % 0x0.000a << 16)..s v /= 10 end if (val<0) s = "-"..s return s end |
Note that you could obviously just draw your score to screen during the loop, to avoid making half a dozen temporary strings, since Lua annoyingly insists strings must be immutable and constantly makes new copies when you manipulate them.
@Felice : the code is so short and clean now, thanks. I think I will reuse that each time I need to store score in a game now.
So I just got around to trying all of this out and I think it's working...thanks to everyone that contributed. Lord knows I would have never come up with such a elegant solution. All that math and Lua knowledge is beyond me.
Here's how I'm using it.
I use the shr() function to add to my score. So here, adding 15,000 points.
score += shr(15000,16) |
Then I use Felice's function to display it
print(getscoretext(score), 0,0, 7) |
The only change I made to the function was so that it would display a zero score. I just changed the val check to be val<=0 and instead of the result being the dash, it's just a 0 character. Seems to work fine.
The only thing I did notice is that if you add a number greater than 32,000 it gets wonky...which makes sense. As long as I don't give any single thing a point value greater than 32k, it's all good.
Thanks again, everyone. I know this will be the defacto thread for anyone looking to have nice, big, fat scores.
val<0 is for negative scores.
Felice function needs a special case for val==
Actually I think they should be v not val?
@matt
You're right that it needs a 0 condition. However, the loop ends when v==0, so you can't use v to check for 0 or sign. That's why it refers back to the original val to find the sign to prefix. It should also use val for the 0 condition.
Edit 2020-10-13: Updated to my most recent versions.
The fix would be to use a repeat..until loop instead of while..do. E.g.:
function s32_tostr(v) local s,t="",abs(v) repeat s=(t%0x0.000a<<16)..s t/=10 until t==0 return v<0 and "-"..s or s end |
(39 tokens)
That way it always does one iteration and that covers the 0 case.
Caution: this s32 version fails on the single so-called "wrap", "outpost", or "weird", value: 0x8000.0000, but that's not something most people need to worry about. If you do need to handle that value, you could simply add if(val==0x8000) return "-2147483648" to the start of the function. Alternatively, you could spend about 15 tokens more and use the u32+s32 combo code in the final section of this post, which does not have this issue.
Here's a u32 version. It does its divide-by-10 in two operations, rather than one. It starts with a logical shift right by 1, which is effectively an unsigned divide by 2. This ensures the top bit is 0 and the number is now unquestionably positive. That means we can safely divide by 5 using the normal (signed) divide operator., producing a number we can safely assume is positive. Second, it divides by 5 using the normal (signed) divide operator, which , effectively producing an unsigned divide by 10:
function u32_tostr(v) local s="" repeat local t=v>>>1 s=(t%0x0.0005<<17)+(v<<16&1)..s v=t/5 until v==0 return s end |
(41 tokens)
And finally, for those who need both, around 20 tokens can be saved by having the s32 version rely on the u32 version:
function u32_tostr(v) local s="" repeat local t=v>>>1 s=(t%0x0.0005<<17)+(v<<16&1)..s v=t/5 until v==0 return s end function s32_tostr(v) if(v<0) return "-"..u32_tostr(-v) return u32_tostr(v) end |
(61 tokens total)
As a bonus, this s32 version doesn't suffer from the 0x8000.0000 wrap-point problem.
keywords: unsigned 32-bit int unsigned int unsigned 32-bit integer unsigned integer signed 32-bit int signed int signed 32-bit integer signed integer u32 s32 i32 int32 uint32 int32_t uint32_t dword long
I've just realized that I basically failed to implement itoa() correctly due to missing an obvious edge case. I have brought shame on my CS104 professor.
Speaking of edge cases, this function will also fail if your player has -2147483648 points. So be aware of that. Just in case it happens.
FYI this breaks in picolove
Sure, I'm using an array to store the value of each digit [0-9], and an array to store the sprite index (it's mandatory).
digits={} sprites={} |
Next, i'm using a function that add a value to the score. It increment each digit until it reach (1)0, then increase the next digit, from the lowest to the greatest (right to left):
function add_score(h) local s,i,d=sprites,9,digits if(h<0)printh("ERR: add_score >32787")return while h>0 do local a=h%10 if(s[i]+a>9)h+=10 s[i]=(s[i]+a)%10 d[i]=64+s[i] h=flr(h/10) i-=1 end end |
Take care, if the added value is too big, it will be a negative value.
Hope it helps.
Based on the digits idea, I made a score control where you can add large numbers:
function _init() score_ctrl = score_control() -- =================================================== -- scores can be added as a number if they are <=32767 -- =================================================== score_ctrl.add(12) score_ctrl.add(32767) score_ctrl.add(100) -- ===================== -- otherwise use strings -- ===================== score_ctrl.add("250000") score_ctrl.add("32768") score_ctrl.add("1000000") print(score_ctrl.score) -- result 1315647 end function score_control() local digits={} sc={score=0} sc.add=function(points) local str = tostr(points) if (#digits<#str) then for i=1,#str-#digits do add(digits,0,1) end end while (#str<#digits) do str="0"..str end for i=1,#str do digits[i]+=tonum(sub(str,i,i)) end local score,overflow="",0 for i=#digits,1,-1 do local c,h=digits[i]%10,digits[i]\10 digits[i]=c score=c..score if (i>1) then digits[i-1]+=h else overflow=h end end if (overflow>0) then add(digits,0,1) digits[1]=overflow score=overflow..score end sc.score = score end return sc end |
we have a much easier way now! https://www.lexaloffle.com/dl/docs/pico-8_manual.html#TOSTR
score=0 -- smallest number possible, represents 1 point score+=0x.0001 score+=0x.0001 -- 100 points for boss defeat! score+=0x.0064 -- show big number print(tostr(score,0x2)) |
After seeing threads like this, I knew I needed to put this post up.
STRDSUM()
https://www.lexaloffle.com/bbs/?tid=51451
I think you can add the number of digits up to the limit of the strings that can be combined.
?strdsum('900109','123456') -- 1023565 |
[Please log in to post a comment]