*edit 2: 0.2.3 has a built-in way to do this with tostr

*edit: see downthread for a better function*

I was thinking about high scores in PICO-8 a while ago and it occurred to me that they'd make more sense as unsigned 32 bit integers than 16b.16b fixed point decimals. The easy part in that case is adding points - simply increment in units of 0x0.0001 instead of units of 1 - but if it's a high score, I'd also like to be able to display it.

Thus:

function tostr_u32(n) -- return n as a 32-bit uint -- 92 tokens, ~1/780 of a 30 FPS CPU per call -- " " as thousands divider -- calculate ones -- (0x.03e8 = 1000 * 0x0.0001) local s=tostr(shl(n%0x.03e8,16)) if n>0 then n/=1000 else -- if not-actually-a-sign-bit is set -- have to be a little tricksy -- splitting in half -- n&0x0.ffff lower -- lshr(n,16) upper -- upper half unit = 65 536 -- so within thousands: local m=536*lshr(n,16) m+=n&0x0.ffff -- originally used -- local m=536*lshr(n,16)+n&0x0.ffff -- but that returned wrong results s=tostr(shl(m%0x.03e8,16)) -- and doing the division by -- 1000 in two steps: n=lshr(n,1) n/=500 end while n~=0 do while #s%4~=3 do -- pad with zeros s="0"..s end s=tostr(shl(n%0x.03e8,16)) .." "..s n/=1000 end return s end |

I'm sure this could be minimized further and/or optimized further and/or made more general, but I'd be willing to use it as is so I figured I'd share.

simpler version (not using a string conversion on top): https://www.lexaloffle.com/bbs/?pid=22677

This version from downthread looks like it's solving more-or-less the problem I was thinking of, yeah - signed int instead of unsigned int, but yeah

Thanks for the link!

*Edit: for a better u32 version, see the revised one I did further below in this thread after @packbat had a better idea.*

Hm, this is my current solution for u32. I nip off the top bit, convert it, and then if the top bit was set, I use longhand addition to add 2147483648 (0x80000000) to the output string.

function u32_tostr(u) local s,v="",u&0x7fff.ffff repeat s=(v%0x0.000a<<16)..s v/=10 until v==0 if u<0 then local a,b="2147483648",sub("000000000"..s,-10) s,v="",0 for i=10,1,-1 do v=sub(a,i,i)+sub(b,i,i)+v\10 s=(v%10)..s end end return s end |

I'd like to think there's a more elegant solution, but I haven't thought of one.

At 83 tokens and roughly double the work for upper-range numbers, it's definitely heavier than the most current version of my s32 function that you linked above, at 39 tokens.

I like that method! Thinking about the addition step: how about doing the addition during the first divide-by-ten?

function u32_tostr(u) local s,v="",u&0x7fff.ffff local w=v%0x.000a v/=10 if u<0 then w+=0x.0008 s=(w%0x.000a<<16)..s v+=w/10 v+=0xccc.cccc else s=(w<<16)..s end while v>0 do s=(v%0x.000a<<16)..s v/=10 end return s end |

It looks like it cuts tokens to 74 and speeds things up substantially, and it means that the s=[digit]..s lines can be replaced with printing directly to screen if desired.

*Edit: Improved AGAIN in the next post.*

Wait, I have a better one that builds off of *your* idea! Since 10 is 5x2, we can do a logical shift right as part of the first /10 and then divide by 5, letting us evade the problem with the top bit:

function u32_tostr(u) local v=u>>>1 local s=(v%0x0.0005<<17)+(u<<16&1) v/=5 while v!=0 do s=(v%0x0.000a<<16)..s v/=10 end return s end |

47 tokens! :D

Also, a lesson for the day: You can concatenate two number variables into a string, even though you can't concatenate two number literals. Weird but true. I wonder if this is a parser bug, since the runtime obviously allows it.

Wait. Now that I know how to do an unsigned /10, I can just put that in the original function and not have any excess!

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! :D

Added to my original post in the other thread.

Edit: Just to be comprehensive, so someone who lands on this thread from a search can easily get both versions, here's the signed version also:

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.

Edit 2: And if someone needs *both* functions, 20 tokens can be saved by using the u32 version to support the s32 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 |

Total 61 tokens.

@freds72: `so 4,294,967,295 maximum score now? ;)`

...oh, wow, someone ought to make a clicker game that has a game mechanic that kicks in when you hit 4,294,967,296 and the number of picos or whatever drops back near zero. That would be extremely appropriate.

Also thanks! It was quite delightful and validating to see someone else looking at PICO-8 numbers and going, "yes, reverse-engineering 32-bit unsigned integers out of these is a good idea and I like it". :D

Heads up that if you need both functions in one app, I added a (fairly obvious) version that uses one to support the other in my post above. It saves 20 tokens vs. having both independent functions.

About this detail:

> You can concatenate two number variables into a string, even though you can't concatenate two number literals

We get 'syntax error malformed number' because a dot is used for fixed-points literals (like '20.1'), so two dots ('20..10') is rejected (which seems a good decision, could be a typo). This can be worked around by adding parens ('(20)..10' → string '"2020"') or a space ('20 ..10' — idea taken from Python, where you can call methods on number literals like this: '20 .bit_length()').

Oo, good to know. And yeah, I should have realized it was because of decimal points in number literals. Mind you, I think the parser ought to be able to detect the difference between a decimal point and a concatenation, same as it can tell the difference between a greater-than operator and a shift-right operator.

[Please log in to post a comment]