WYSIWYG control code editor
Hey there!
Tired of manually writing that long, one-line print statement, filled with control codes in order to layout static text like in a title screen, painstakingly modifying \^j
and \+
values to position the text just right, adjusting \^x
and \^y
values of solid-filled text to draw background elements instead of using rectfill
to save precious tokens, and then deciphering all of that when you need to go back and edit something? ...no? just me?
Did you know that with P8SCII control codes, you can change things like text color, size, screen position and more? Meaning, for static (and relatively positioned – camera
offsets will apply) text, you never really need more than 1 argument to print
, and no more than one print
per layout!
For example, print("hello",8,8,5); print("world",16,16)
can be written as ?"\^j22\f5hello\^j44world"
, saving tokens and characters.
However, there's a lot to remember with P8SCII, several potential snags, and it can get pretty unwieldy pretty quickly. That's why I made this WYSIWYG tool!
tool usage
To load in immediate mode, run load #wysiwyg
, or run the cart above.
Hopefully the UI is fairly intuitive. See a demo below:

Basic usage:
- Click the plus in the layers tab to add a new text box
- With a layer selected (by default new layers are selected, but you can also select by clicking on it in the preview or in the layers tab):
- click the edit icon (pencil icon) to edit it
- use arrow keys (when not in the editing tab) or drag it with your mouse to move it
- Press the tab key to show/hide the sidebar
- Scroll through the layers list
- Click on a selected background color again to set it to none
- Before clicking "load from clipboard", paste (ctrl-v)
Note that there is no undo, so be careful and save (copy, then paste somewhere) often!
features
Everything on the right half of the screens below was done using this editor, meaning that printing each layout is just 2 tokens (although quite a few characters)! The demo print commands are given below, which you can load into the editor to inspect the different layers and see how some of the effects were done.

--effects ?"⁶jf4⁵ih⁶hᶜ2²2⁶xo⁶ye \n ⁶jg4⁴j⁶h²1⁶xm⁶yc \n ⁶jf4⁵ji⁶h⁶xn⁶yd \n ⁶jg5³h⁶h²2⁶xl⁶yb \n ⁶jg8⁵ijᶜa⁶x4⁶y6 2 tokens ⁶jl7³i only⁶jg7³iᶜ7⁶-#text:⁶jg5⁵ihany static⁶jff⁵iiᶜ0heavy outline⁶jff⁵ihheavy outline⁶jff³iheavy outline⁶jff³hheavy outline⁶jffheavy outline⁶jff⁴hheavy outline⁶jff⁴iheavy outline⁶jff⁵hiheavy outline⁶jff⁵hhᶜ7heavy outline⁶jfh⁵hiᶜ0light outline⁶jfh⁵ijlight outline⁶jfh⁴jlight outline⁶jfi³hlight outline⁶jfh⁵hjᶜ7light outline⁶jfk⁵jiᶜ0basic shadow⁶jfk⁵jhᶜ7basic shadow⁶jem⁵ijᶜ9multi-color (y)⁶jem⁵ijᶜa⁶y2multi-color (y)⁶jep⁵ihᶜ1²d⁶y6multi-color (x)⁶jep⁵ihᶜ2⁶-#⁶x2m u l t i - c o l o r ( x )⁶jgsᶜ7²9⁶y8 ⁶jfs⁵jh⁶x4⁶y6 containers \0" --wysiwyg logo ?"⁶jif⁵ijᶜ1editor⁶jif⁵iiᶜdeditor⁶jhd⁵ijᶜ1ctrlcode⁶jhd⁵iiᶜdctrlcode⁶jia⁶tᶜ0wysiwyg\n⁶jia⁶=ᶜ8wysiwyg\n⁶jiaᶜ9⁶y4wysiwyg\n⁶jiaᶜa⁶y3wysiwyg\n⁶jiaᶜb⁶y2wysiwyg\n⁶jiaᶜc⁶y1wysiwyg\n\0" --age of ants title screen ?"⁶jd7⁴i⁶w⁶tᶜ0age ants\n⁶jf6⁴j⁶-w⁶-t⁶y7. ⁶x3 .⁶x2 .⁶jt8⁵ji⁶x4⁶y6.⁶jc7⁵jh⁶w⁶tᶜ7age ants\n⁶jt8⁵ih⁶-w⁶-t.⁶je6⁵ji⁶y7. ⁶x3 .⁶x2 .⁶jj8³jᶜ0⁶x4⁶y6⁶:0060123515120800⁶jj7⁵jjᶜ7⁶:0060123515120800⁶jm6⁵ihᶜ5⁶x2.⁶x3 ⁶x2.⁶jc6⁵ih.⁶x3 ⁶x2.⁶jgc⁵jjᶜ0⁶x4difficulty:⁶jgc⁵jiᶜcdifficulty:⁶jdj⁵hjᶜ0press ❎ to start⁶jdj⁵hiᶜ9press ❎ to start⁶jqt⁵ihᶜ0v1.7⁶jqt³iᶜ6v1.7⁶jct³iᶜ0eeooty⁶jcs⁵ijᶜ6eeooty⁶jdm⁵hjᶜ0pause for options⁶jdm⁵hiᶜapause for options⁶jkf⁴hᶜ6\0" |
Support for:
- Various control code effects from the UI (tall, wide, stripey, inverted, border, x-width, y-height, fg color, optional bg color)
- Text printed near the bottom of the screen (adds
\0
to the end of the string so the console doesn't scroll) - Multi-line textboxes
- Maintains the first line's x-position for subsequent new lines using the cursor home control code
- Accounts for this tall mode quirk
- Newlines show up as ■ in the editor input
- As much character efficiency as sanely possible (will carry over as many effects as possible from textbox to textbox instead of resetting them each time)
- Easy 𝘱𝘶𝘯𝘺𝘧𝘰𝘯𝘵 insertion
- Note: pasting saved output containing punyfont into a pico-8 code editor won't retain punyfont, you will have to paste it into the p8 file from an external editor
- Saving (to a 2-token print command) and loading via clipboard
- Inline custom control codes via an "insert" menu
- Useful for one-off effects on characters within a textbox, e.g. the gif above shows using wide mode for just one character, but can also be useful to make a space a littler bigger or smaller using x-width
- Warning: This is an advanced feature! Subsequent text boxes may break if you don't "undo" the one-off effect (or not, test it yourself!)
- Warning: Constructing invalid control code sequences (even as you edit) can have unexpected results - save before trying stuff!
Hi! Think I ran into a bug when using the tall mode control code as part of a longer string (that then undoes it).
Seems like tall mode retains the doubled line height even after it's turned off in the same string. The following code produces this output:
cls() ?"\n ⁶t hi \n\n ⁶-t l1 \n l2 \n" |

...when I'd expect it to produce this ("l1 \n l2"
is no longer in tall mode, so the height of the line break shouldn't be doubled):

It seems like the only way to revert to normal line height is to start a new print command, but that's another 2 tokens! This is affecting a WYSIWYG-style static text editor I'm planning on releasing.
Age of Ants

send your worker ants to gather resources…
amass an army to defend your queen…
build your ant empire and conquer the lawn!
Age of Ants is a demake of Age of Empires II (with an ant/bug retheme, why not), featuring:
- 1 map with 4 possible starting locations
- Up to 2 allied AI opponents with 3 difficulty modes
- 9 units, 8 buildings, 12 tech upgrades (most are repeatable)
- Up to 99 active units per player
- AoE2 controls & behaviors (let me know if something seems off, it's been a while)
- Savefiles¹ (save in the pause menu to generate a screenshot, drag and drop to load)
- Original soundtrack
- wololoooo
Defeat the enemy queen to win!
It's playable on:
- desktop with mouse (best experience, download here)
- mobile/tablet (on bbs the page might scroll around - for a better experience try playing on itch)
- handheld console
When the game first launches, open the pause menu and select the appropriate controls mode.
¹there are some caveats with savefiles, see the note on itch
controls
I tried to stay as faithful to AoE2 controls as possible, but if you've never played or you need a refresher, check out the gifs below. If you prefer, there are also video versions embedded on itch (that way you can pause etc).
desktop tutorial (gif)
mobile tutorial (gif)
handheld tutorial (gif)
advanced controls
tech tree

credits
thank you to:
- @morgan3d's p8pathfinder (modified for caching and approximating unreachable paths)
- @musurca's fast dist()
- @Gruber's "explosion 18" sound effect
- @carlc27843's pxaviz for compression hints
- @thisismypassword's shrinko8 for code compression (mostly whitespace removal)
- @Wolfe3D for the idea of a screenshot-based savefile
- siege engineers' aoe2techtree.net for some baseline unit stats
- the p8 community!
here's the uncompressed and highly commented sourcecode if anyone is curious. lots of token hacks and pretty tersely named variables, so hopefully the comments help demystify what's happening (i was originally attempting trying not to use shrinko but i caved once i added in helper text)
itch link for downloads and stuff: https://eeooty.itch.io/age-of-ants
have fun! if you'd like, share savefiles of your games (they will still work even on the win/lose screen)
changelog
hello!
sorry if this is "yet another" boring token-saving post (haven't been around long enough to know how common they are), but here's a list of (fairly) generalizable token saving tricks i've compiled while working on my upcoming game, in case it might be helpful to anyone. i think there are some on here that i haven't seen elsewhere on bbs, but also some of these may be pretty basic :)
this list does not include unpack(split())
tricks, or _ENV
tricks, which can both have enormous token windfalls.
also, by far the best way to save tokens is not using these tricks, but to stare at your code hard... really hard... until something clicks and you realize you can convey 3 variables' worth of information with one, that you can reuse functions, that you're doing the same computation throughout your code many times and can use a variable, etc. or you know, to delete stuff.
these are kind of last-resort, please god just 3 more tokens sorts of things (although they do add up). hope they are of some use. with that aside, let's begin!
2 tokens: flr to \
this one is basic but it took me a while to learn about \
. flr(a/b)
can be written as a\b
, saving 2 tokens. if flooring the result of a multiplication, assuming that one side is a constant, you can just invert: flr(a*2.5)
to a\0.4
2 tokens: ceil to \ !?
this one is situational, but interestingly floor "decreases" a number even if it's negative, making it kind of function a bit like ceil
in the opposite direction. the token save here only works if you can already negate the expression for free:
a+=ceil(p*2) --to: (note += changes to -=) a-=p\-.5 |
of course you can use order of ops to fix any precedence issues this might cause (this time moving the negation to a constant in case there's no +=
/-=
):
a=2*ceil(p*2) --to: a=p\-.5*-2 |
1 token: calling apis with string params
since lua lets you call functions without parens if the only argument is a string/table literal, you can save a token on many standard api calls, e.g. sfx(5)
to sfx"5"
, stat""
, rnd""
, btnp""
, btn""
, dget""
, fillp""
(for this one, see the decimal value for your pattern in the console e.g. fillp"23130.5"
for fillp(▒)
), or even poke""
if you want to set that value to 0.
this will also work on your own functions too! if you call functions with a single truthy value, you can do fn"1"
instead of fn(true)
, you can even do this if you call a function with a single numeric argument, but keep in mind this won't work if you do any comparisons with that argument. "3"+3
is allowed in lua, but not "3">3
!
1 token: foreach
foreach(a, function(x) end)
saves a token over for x in all(a) do
. note that this means you can't break
or return
early, and it does come at the cost of performance (since you are introducing the overhead of function calls). it's not worth the token for nested loops / huge arrays!
1 token: next, inext
easy one:
for k,v in pairs(tbl) do end --to: --unordered for k,v in next,tbl do end --ordered but only monotonically increasing past 1 are guaranteed: for k,v in inext,tbl do end |
1 token: use return value of add/del/deli
if you can guarantee that the item being added/removed is present, you can replace a reference with the function:
add(a,b) b.x=y --to: add(a,b).x=y |
1 token: hacking 'if' - add/del/deli
if you call add
, del
, or deli
, the call will only "go through" if the first argument (the list) is a list. if it's falsey, it's a no-op. so you can save a token with:
if (x) add(arr, i) --to: add(x and arr,i) |
note also that these functions are nil
-forgiving for the second argument, too - add(arr,nil)
does nothing, so you don't need if(x) add(arr,x)
1 token: hacking 'if' - camera/pal
if you want to call camera
with x
under condition c
, save a token with:
if (c) camera(x) --to: camera(c and x) |
note that this resets the camera if not c
(which is actually sometimes what you want!)
the same applies to pal:
pal(c and unspl"5,6,7") |
1 token: hacking 'if' - function call next to assignment
if you want to call any function under a condition, if there is assignment (or a function call that can take an extra argument) nearby, you can remove the if and save a token:
if (x) f() k=v --to: k=v,x and f() |
note that in this case the order matters! if it was the other way around:
k=v if (x) f() |
you can't make the same conversion if f()
relies on the new value of k
.
function call next to fully-loaded function call
note that this same trick works if you are calling another function nearby with all of its parameters passed:
if (x) f() pset(50,50,9) --to: --pset doesn't take a 4th argument, but who cares! pset(50,50,9,x and f()) |
same caveat applies here with ordering - f()
will be called before pset()
essentially we are forcing lua to have a floating expression without an assignment.
not
as or
in all of the 'hacking if' cases above, if your condition x
is of the form not y
, you can usually do or y
instead of and not y
!
1-3 tokens: hacking 'if' - function calls in ternary
finally, this is kind of specific, but let's say you have an expression that should go one way under one condition, and another way otherwise. but let's also say that in one of those cases, you also need to make a function call.
if the call returns truthy, it can be prefixed with and
to the value associated with the condition under which you want it to run:
---- pset returns truthy, so prefix with `and` ---- -3 tokens (-2 if on "truthy" side of ternary) a=c and x or y if(not c) pset(3,3,5) --to: a=c and x or pset(3,3,5) and y |
if the call returns falsey, it can be prefixed with or
, but you'll have to add parens:
---- sfx returns falsey, so prefix with `or` and add parens ---- -1 token (-2 if on "falsey" side of ternary) func(c and x or y) if(c) sfx"3" --to: func(c and (sfx"3" or x) or y) |
1 token: max to coerce to num
you can use max()
to coerce a potentially non-number value into 0: max(nil)=>0; max(false)=>0; max(5)=>5
. this saves a token over x or 0
because if you are doing math you will pay an extra token for the parens:
5+(x or 0) --to 5+max(x) |
it does have limits: be aware that max(true)=>0
, and max("5")=>5
2 tokens per argument: local functions
if a function is only used by one other function, you can define it inside the other (locally) and remove any arguments already in scope:
function g(a,x) ... end function f() local a,b,c=... g(a,b) g(a,c) end --to: function f() local a,b,c=... local function g(x) ... end g(b) g(c) end |
1 token: table length checks
you can replace the expression #tbl>x
with tbl[x+1]
(which saves a token if x
is a literal), or the expression #tbl>=x
with tbl[x]
(which saves a token):
if (#tbl>0) do_x() if (#tbl>=threshold) do_y() --to: if (tbl[1]) do_x() if (tbl[threshold]) do_y() |
note: this trick won't work if the table can contain false
!
??? tokens: practical bit ops
sometimes bit ops can be useful to save a bunch of tokens. pardon the somewhat specific examples, hopefully these get you thinking:
x!=0 and y!=0
to x&y!=0
(-2 tokens)
x==0 and y==0
to x|y==0
(-2 tokens)
(these also have the advantage of not using a boolean operator, so if you invert one of these (e.g. x|y!=0
instead of x!=0 or y!=0
), you won't need parens if you and
it with something)
x^^=0xf0
: if x starts at 0, this toggles between 0 and a high number. if passed into e.g. camera(x)
, can be used to toggle showing/hiding something
0!=~0
, so if you want to toggle something, say a controls option that you want to store in cartdata, you can do it pretty tersely with the binary not operator ~
:
menuitem(1,"toggle foo",function() dset(0,~dget"0") end) ... if dget"1"==0 then --default, since cartdata defaults to 0 else --toggled end |
1 token: pal color 1
another highly specific one, but if you're using pal
on color 1, save a token with pal(1,x)
to pal{x}
1 token: mid for range-checking
mid()
can sometimes save a token when doing a range check of the form x >= 0 and x < y
(or negated):
x>=0 and x<8192 --to: mid(x,8191)==x |
one extra edge case with this not involving 0 that i thought was cool:
dt<.1 or dt>.25 --to: mid(dt,.1,.25)!=dt --wait it's the same number of tokens... --but the first one has an `or`, so if it needs --to be combined with `and`, it'll need parens! |
(context here is flashing a selection once, dt
is the time since the selection)
2 tokens: do you really need cls()?
it's common to add cls()
when starting a new project, but if you fill the whole screen each frame with e.g. map()
you might not need it!
??? tokens: control codes
a LOT can be done with control codes, this is a large topic. but in general, i've found that if you are printing to the screen you generally don't ever need to pass (constant) args to print (that is, ?
) since you can move the cursor with \^j
and \^+
, and set color with \f
.
moreover, if you are printing a bunch of hard-coded text, e.g. a title screen, you generally only need a single call to print (?
) since you can jump around on the screen so easily. need a shadow? first print your text in a dark color, then use \^j
or \+
to jump backwards and slightly up and re-print your text in the foreground color.
i've spent so much time tweaking horrible controlcode-ridden strings, i kind of want to write a utility cart that is a kind of WYSIWYG editor for text that will spit out a single horrible string to print. EDIT: i've done this!
1 token: return value of ?
this one is a little hacky, but since ?
is an alias for print that can be called without parens, you can still capture its return value if you need to get the max x-drawn. just make sure you add a newline after the last argument to ?
:
-- line x1 will be max_x-3, the -3 has to be on the next line line( ?text_of_unknown_length -3,80,8,80,9) x+=?text_of_unknown_length |
1 token: .. operator with numbers
the ..
string concat operator can be weird with numbers. if it's placed directly after a number, the parser will complain, expecting a fractional component following the first .
. but if you add a space...:
?"minutes: "..t\60.." seconds: "..t%60 --malformed number near '60..'! --i thought this meant that you needed parens: ?"minutes: "..(t\60).." seconds: "..t%60 --but you can save that token if you just add a space! ?"minutes: "..t\60 .." seconds: "..t%60 |