Log In  


With no library support for PICO-8 Lua, strings become somewhat cumbersome to manipulate. Technically, I can do everything I need if I'm willing to dedicate far too many tokens to tables and support routines, but it seems like it's an unnecessary impediment to making textual apps, which probably already have space issues.

I think if we had a few things that I'm actually going to draw from BASIC, it would make string handling much simpler:

CHR(n) would return a single-char string with (presumably ASCII or UCS-8, but possibly UCS-16) character n (0-255 for ASCII/UCS-8 or 0-65535 for UCS-16). It should probably return a blank string for illegal characters (or mod n by the range), and NIL if passed nothing.

ASC(c) (or maybe UCS(c)) would complement CHR(n), taking a string and returning the character value of the first character in the string. If passed a blank string, it should probably return something descriptive like -1 or NIL. Likewise it should return NIL if passed nothing.

ASC(CHR(n)) and CHR(ASC(c)) should both be no-ops.

The only other way to do that is to have tables that convert between "\0".."\255" and 0..255, which is a waste of code space akin to what you described when you provided ATAN2() and SQRT().

The other thing that's cumbersome, while not being outright preventative, is having to use SUB(s,i,i) to get what is conceptually s[i]. Strings accept the same # length operator that tables do, so it'd be reasonable to accept [] as well, even if it's just syntactic sugar for SUB(,i,i)).

Slight can of worms on the s[] thing: It opens up an issue where a programmer might expect to be able to do s[i]="x" or even s[i]="xyz". But that could simply be documented as unsupported, or it could be accepted as shorthand for s=sub(s,1,i-1).."xyz"..sub(s,i+1).

How does this strike you? I know you want to keep the API simple, but I feel like string manipulation is more of an intrinsic to a language than an API extension.

5


There are advantages to keeping Pico-8 Lua a strict subset of full Lua, so I'm not sure about syntax extensions. (Lua does not support indexing into strings as if they were tables, afaik.)

New built-ins for converting between a one-character string and a numeric character index would be fine. To keep it clean, I'd just call them Unicode code points and limit them to the range of Pico-8 fixed width numbers (32767), which is plenty more than is covered by the font. If Pico-8 is officially an ASCII machine, then asc() or string.byte() are OK as names, but are limiting for the platform.

It's worth asking what we'd use this for. Entering names in a high score table has come up multiple times. Text compression is a thing, though I haven't seen anyone do it in a real game (other than my toy attempt). That recent thread about accessing small caps chars is also a thing, and I would agree that if that's going to be a serious feature then it needs better API support. fwiw, I think Pico-8's current API declares that Pico-8 is not a text-based machine as a matter of purpose. imo that's a fair stance for a console with a six-button controller.

Aside: It's charming that there are easter eggs throughout Pico-8 in a way that is reminiscent of old platforms. As a platform user, undocumented features make me nervous. Easter eggs in old micros could be relied upon because the eggs were in hardware and had a wide install base, so much that future iterations of the hardware had to preserve things that weren't intended to be used. For a fantasy console, some kind of declaration that everything we find will be supported would be reassuring.


It's true that accessing characters in a string with [] is nonstandard for Lua, but we already have a few nonstandard things, so I figured it was worth asking. In regular Lua you can do a lot more with the expected string library, so it's ... halfway ... reasonably to suggest a language tweak in lieu of a proper string lib.

I'm a lot less concerned about s[i] though. It's cumbersome and takes a lot of tokens, but it's tolerable. Mostly I care about getting some built-ins for num/char conversion.

As for undocumented features, yeah, they're a little worrisome, but it's an alpha product, so we should assume ANYTHING we make right now will likely break in the future. I'm pretty solid on this opinion. I know what I signed up for when I put my money down. Until it's 1.0, I'm going to forgive any and every breaking change, because I'd rather have it work ideally than work the way we're used to.


Totally agreed on the interpretation of "alpha." Anything not mentioned in pico8.txt can break, and probably even more than that. The funny part is that Pico-8 isn't a hardware ROM developed under a deadline, so the features controlled by addressable memory don't really have much of a reason to be there except to be discovered. :)

I mention standard Lua in part because there are design advantages (can point to the Lua spec and educational materials), and in part because there are implementation advantages. Pico-8's Lua interpreter is a version of stock Lua with the std lib removed. Extending the language with new syntax is probably difficult. (As you noticed in the other thread, ?foo is supported but only as a preprocessor directive and not as a grammar extension.) I'm not opposed to Pico-8 breaking away from the Lua grammar, I just doubt it'll ever be worth the effort, on both counts.

Adding (shadowable) built-ins is fair game, and that's why we have non-standard shortname replacements for bitwise ops, string ops, etc. More string builtins is entirely plausible.


Speaking of that...

It'd be nice if the bitwise ops (BAND, BOR, BXOR, BNOT, SHL, SHR) were supported as binary operators rather than functions. There's already a place where logical ops (e.g. AND, OR) are parsed, so I figure making the bitwise ops binary/unary would just be copypasta with the actual token and operation replaced.

Mainly it troubles me because of this:

-- BAND call = 4 tokens
BAND(X,Y)

-- BAND operator = 3 tokens, judging by the AND operator
X BAND Y

What we have now is nonstandard anyway, might be nice to change it to a more readable AND tokenwise-compact form. If backcompat is an issue, the operators could just be ANDB, ORB, XORB, NOTB, SHLB, SHRB, ASHRB* while the existing functions remain.


* - Shh, I'm slipping in the missing arithmetic shift. Sign extension is useful for a whole bunch of trickery. Don't tell zep, maybe he won't notice!


Although PICO-8's still in alpha, I do provisionally consider the API and syntax tweaks to be forwards compatible at this point -- partly because of the practical issue of shipping a bunch hardware units (Pocket C.H.I.P.s) that should work out of the box, but also just because changes to the API have slowed down to the point where I felt comfortable to call it and move on.

Because of the Pocket C.H.I.P. release, I did rush a bunch of stuff in there that had been on the table for a long time (metatables, coroutines, glyphs, mouse support for tools, future unimplemented stuff). But it seems to be holding up ok so far, so it's unlikely the API will change unless there are strong reasons for it.

ASC and CHR were in the dev versions for a long time, but I couldn't find enough use for them to justify their existence. They take around 33 tokens to implement, and in typical use cases -- e.g. drawing custom fonts and doing base64-style encoding -- it is preferable to specify your own mapping anyway.

al="0123456789abcdefghijklmnopqrstuvwxyz"
c2o={} o2c={}
for i=1,#al do
 local c=sub(al,i,i)
 c2o[c]=i
 o2c[i]=c
end

print(c2o["a"])
print(o2c[11])

Originally the plan was to not include any non-standard Lua syntax at all. In general, I think there is a lot of be said for staying close to standard Lua syntax -- it's not as hard for others to implement the PICO-8 API (e.g. PicoLOVE), less confusing for users moving from/to standard Lua, and less bug-prone. The short-hand alterations were just too handy given the limited screenspace, and were independently suggested by almost every early tester! I almost caved and also changed indexing to 0-based -- something that still irks me.


I know how to write the routines myself. That isn't the issue. I'm just not clear about why something as simple as ASC/CHR would need justifying.

I really do understand why you want the API to be simple, but you also want PICO-8 to be something someone can just jump in and do stuff with.

Why is it preferable to make someone write out conversion tables by hand to do what the interpreter can do in practically atomic operations? That's just mindless transcription of alphabets and numbers--nearly what I'd call makework, and to a prohibitive degree. There's nothing clever about it, no particularly better way to do it, it's just unnecessary work and cart space to do what any other language allows inherently, including normal Lua.

Also, 33 tokens doesn't sound like a lot, but a ton of people are saying they are up against the 8192 token limit.

Someone said you didn't want PICO-8 to be used as a textual game platform, but I really feel like that should be our call. If there's any connection between a seeming reluctance to have rudimentary character handling like this and an attempt to funnel us into certain genres, I'd really ask you to reconsider that way of thinking.

I don't mean to be as aggressive as I think I sound here, but "It used to be there and I took it out" was about the farthest thing from what I expected as a response and I'm a bit dismayed to hear it. I'm really having a hard time fathoming the logic here. You give us circle drawing routines for free, even though they're of very limited use outside of tweetjams and easily written by hand when needed, but builtins for char conversion are unjustifiable? :/


Plus, internal ASC/CHR could work for full binary range (0..255) where illegal symbols would be just displayed in Pico as spaces. This would allow for writing 1:1 binary stuff inside strings (such as alternate mapdata, etc.) where with base64 there's more space used than needed.


@Felice to be fair, the circle drawing function is very useful because a hand-made one in Pico would be very slow. But I'm with you for asc/chr, they could be very nice, and the 33 token version doesnt allow special characters.


Speaking of, the 1-based indexing aggravates me to no end, just due to the token limit.

For example, if I were to draw a grid of tiles, offset by x*8 or whatever, that means the for loop needs to start at 0 and I offset the indexing by 1, or else I I start the for loop at 1 and I offset the final value by one unit.

I realize it's because of how Lua does things (and that's fine), but the token limit makes adding offsets annoying because you can't do things "properly". Or maybe I'm just pedantic. I love me my C#.


Thanks zep for the clarification on backwards compatibility and alpha.

I don't have a strong opinion on asc/chr, but the "should be our call" business needs checking. Pico-8 is a commercial product and a platform that relies on a strong authorial vision. We can say that these would be convenient built-ins for certain purposes, but if those purposes are uncommon, outside the intended scope, or have inexpensive workarounds, it's fair to leave them out. In today's Pico-8, circles are easy and text is hard. That's a valid design, with potentially desirable consequences.

This discussion makes me think less about new built-ins than it does about better ways to share code snippets. If I could browse a BBS-hosted snippet library and easily find "asc/chr (33 tokens)" and add it to my cart, that might meet most of the need. That also might be an interesting way to "pave the cowpaths:" if a snippet is heavily used and has a onerous token or performance cost, maybe it justifies a built-in.


@NuSan: Actually, builtin and hand-written circ/circfill are similar in perf. Most things that do bulk operations like lines, circles, fills, etc. are timed according to the number of pixels they write.

I just did a decent implementation of circ and circfill. My circ was half the speed of the builtin, and my circfill was the same speed. That just makes me think the presumed timing for circ() may be miscalculated in the interpreter. Or that I suck at circ().

As an aside, my routines are actually better than the builtins, as they handle fractional positions and radii correctly. :)

But that's beside the point. Point is, it's kind of a specific luxury function and, in my mind, doesn't rate nearly as useful as character manipulation, so the logic still escapes me.

I'm not voting for circ's removal, by the way. It's more like this:


@Felice I didn't know it was based on pixel touched. In my experience, rectfill() is a lot faster than line() even when touching a lot more pixels. Plus builtin functions handle proper clipping for free.
P.S. If you could post your implementation of circ/circfill I would be really interested, fractional circle can be handy.


@NuSan:

PICO-8's timing is based on pixels filled, but it's also based on work needed per pixel for the given operation. A line algorithm has to do more work per pixel than a rectfill, for instance. A rectfill is typically doing one add and compare per pixel, but a line needs at least one more compare, depending on the method.

These are the subpixel-correct implementations you asked about:

function circle(xc,yc,r,c)
  xc+=0.5 yc+=0.5
  local r2=r*r
  local y=0
  repeat
    local x=sqrt(r2-y*y)
    pset(xc+x,yc+y,c);
    pset(xc+y,yc+x,c);
    pset(xc-x,yc+y,c);
    pset(xc-y,yc+x,c);
    pset(xc+x,yc-y,c);
    pset(xc-x,yc-y,c);
    pset(xc+y,yc-x,c);
    pset(xc-y,yc-x,c);
    y+=1
  until y>=x
end

--------------------------------
-- EDIT: For the -fill version, 
-- use ultrabrite's superior
-- disc() implementation.
-- See below in this thread.
--------------------------------
function circlefill(xc,yc,r,c)
  xc+=0.5 yc+=0.5
  local r2=r*r
  local y=flr(yc-r)-yc
  repeat
    local yw=y<0 and y+1 or y
    local x=sqrt(r2-yw*yw)
    rectfill(xc-x,yc+y,xc+x,yc+y,c);
    y+=1
  until y>=r
end

Note that personally I would generally prefer to let the caller do half-pixel offsets, if desired, but I wanted to match the PICO-8 circ() behavior, which meant the circle had to be quietly re-centered on the middle of the given pixel.

Edit: Hm, just noticed the fill version isn't matching the original function's outline. I need to fix that.


actually your circlefill beats the built-in for radii>11

I made another one from 'circle', to match its outline, and it gets even better, even though there's nothing fancy (that's a pretty standard way to draw a circle):

updated, flr() on each argument. benchmark still relevant.

function disc(xc,yc,r,c)
  xc=flr(xc)+0.5 yc=flr(yc)+0.5 r=flr(r)
  local r2=r*r
  local y=0
  repeat
    local x=sqrt(r2-y*y)
    rectfill(xc-x,yc+y,xc+x,yc+y,c);
    rectfill(xc-x,yc-y,xc+x,yc-y,c);
    rectfill(xc-y,yc+x,xc+y,yc+x,c);
    rectfill(xc-y,yc-x,xc+y,yc-x,c);
    y+=1
  until y>=x
end

stat(1) for 100 discs:

radius circfill circlefill disc
 5       0.2031  0.3086     0.2422
 8       0.3203  0.4102     0.332
 9       0.3711  0.4453     0.332*
 11      0.4922  0.5117     0.3945
 12      0.5625  0.543*     0.3945
 20      1.316   0.8125     0.5742
 40      4.816   1.48       0.9922

it seems most of the api consists in sub-par routines in a separate lua header file or something.
I would have expected built-ins in a fantasy console to be fantasy-hardware accelerated...


@ultrabrite: Nice! I'm replacing my circlefill() with your disc(). :)

(Nice short name, too.)


@Felice @ultrabrite thanks you, Im realy surprised it's so fast, I was so wrong thinking circlfill was native. Well your implementations are prety smart too.


ok, one last update, that square root had to go:

updated: added flr() on each argument, removed +0.5 on center coordinates, changed 'while' for 'repeat'. benchmark still applies.

function icircle(xc,yc,r,c)
	xc=flr(xc) yc=flr(yc) r=flr(r)
	local x=r
	local y=0
	local dx=1-2*r
	local dy=1
	local err=0
	repeat
		pset(xc+x,yc+y,c)
		pset(xc+y,yc+x,c)
		pset(xc-x,yc+y,c)
		pset(xc-y,yc+x,c)
		pset(xc+x,yc-y,c)
		pset(xc-x,yc-y,c)
		pset(xc+y,yc-x,c)
		pset(xc-y,yc-x,c)
		y+=1 err+=dy dy+=2
		if (2*err+dx>0) then
			x-=1 err+=dx dx+=2
		end
	until y>x
end

function idisc(xc,yc,r,c)
	xc=flr(xc) yc=flr(yc) r=flr(r)
	local x=r
	local y=0
	local dx=1-2*r
	local dy=1
	local err=0
	repeat
		rectfill(xc-x,yc+y,xc+x,yc+y,c)
		rectfill(xc-x,yc-y,xc+x,yc-y,c)
		rectfill(xc-y,yc+x,xc+y,yc+x,c)
		rectfill(xc-y,yc-x,xc+y,yc-x,c)
		y+=1 err+=dy dy+=2
		if (2*err+dx>0) then
			x-=1 err+=dx dx+=2
		end
	until y>x
end

thanks Jack Elton Bresenham, circa 1975. (retro enough?)

stat(1) for 100 calls:

radius	circfill	disc	circ	 circle
 5		 0.2031		0.2344	0.1641	0.2617
 7		 0.2774		0.2852	0.2070	0.3359
 8		 0.3203		0.2852* 0.2305	0.3359
 9		 0.3711		0.3047	0.2578	0.3711
 11		0.4922		0.3633	0.3203	0.4375
 12		0.5625		0.3633	0.3555	0.4375
 20		1.3160		0.5195	0.7461	0.6484*
 40		4.8160		0.8828	2.5270	1.133	
 60		10.5940	 1.2500	5.4450	1.625

for some reason circle is slower than circ until radius 17, so not worth the tokens unless you really need lots of huge circles. disc is better from radius 8 but not far behind on smaller radii.

edit: Jack, not John :-)


@ultrabrite:

The reason I rewrote it was to see the perf, but it turned out the rewrite was subpixel-correct, unlike the builtin, and that's the main reason I'd use it in the future.

BTW the inner part of sqrt() is only about six cycles on a PICO-8. Don't be too afraid of it. :) The whole thing, including the function call, is 11, but about 5 of that is just call+return overhead.


@ultrabrite:

Also by the way, you should probably use repeat-until for this instead of while-do-end. No matter what, you're always doing at least one iteration, even for a zero-radius circle. You should skip that first guaranteed compare by putting the condition at the bottom.

Pedantic, I know, but I yam what I yam. :)


Oh, actually, there are issues with this last iteration. It messes up with fractional values. Here's an example in my test bench:

The upper right and lower left in the white 3x3 of circles are your disc() and the upper left and lower right are your circle(). Note the gaps.

On the bright side, the functions do produce identical results to the API for integer inputs. Otherwise one or more of the green circles would have red pixels showing.

If you want to try to repro the problem, radius is the blue value at the top, in fixed-point hex. Try disc(64,64,0x9.a) in your own app, presumably you'll get similar results.


ahem, yes, i only considered integers, so there's no subpixel accuracy.
center and radius have to be floored for this to work correctly (even sqrt_disc yields ugly dissymetrical discs on a fractional center)

with r==0 a 'repeat' would still draw one pixel. that's an edge case so you might be better off with if(r==0)return if that's a problem.

not sure what you use subpixel accuracy for, as there's not much room for antialising at this resolution and with this palette. but your circlefill allows for even diameter discs, which can be quite useful (fit a 16x16 square in a puzzle game for instance).


You want a pixel for a 0-radius circle, since that's what the builtin does. A radius of 1 isn't a pixel, it's a diamond.

Subpixel accuracy just lets me get circles that appear overall to be at the position and radius I specify, even if the results are jaggy. Here's an example:


misread your comment somehow, you're totally right about repeat-until.
nice gif, couldn't be clearer! thanks!


That GIF is super cool. Nicely done!


Cheers. :)

You know, though, I was thinking... it would be a shame to lose the integer-only version. It was really handy in the tweet jam, e.g. in those fire demos, for being able to render little 4-pixel diamonds at random positions:

CIRC(RND(128),RND(128),1,C)

If it were replaced with something like what I'm using, you could still get the same effect, but you'd need to do this:

CIRC(FLR(RND(128)),FLR(RND(128)),1,C)

And that's 10 characters lost. :(

I think I'd want both in my arsenal. Maybe have CIRC()+CIRCFILL() and RING()+DISC().

(At this point, I would look hopefully towards zep, but my hopes for that sort of thing seem to have been crushed, sigh.)



[Please log in to post a comment]