13
\$\begingroup\$

Seeing that the current language for Learn You a Lang for Great Good is Piet, and that we don't seem to have a tips page for Piet yet, I decided to make one.

Piet is a stack-based esoteric programming language in which the programs are meant to resemble abstract drawings.

What are some general tips for golfing in Piet?

As usual, please keep the tips somewhat specific to Piet, and one tip per answer.

\$\endgroup\$

5 Answers 5

10
\$\begingroup\$

Compact encoding and tools

Use piet.bubbler.one for editing Piet!

The default scoring method for code golf is in bytes. It gives Piet a significant disadvantage, since every Piet program must be a valid image file (which can be in PNG, GIF, or PPM format, but all of them costs well over 10 bytes per pixel). Also, TIO has npiet as the Piet interpreter (Try it online!), but it is unwieldy to use because it expects the hexdump of an image file.

In this regard, DLosc has made a tool called ascii-piet, which gives a printable-ASCII-based roughly-1-byte-per-pixel encoding for valid Piet programs.

The color table for ascii-piet looks like this:

Red Yellow Green Cyan Blue Magenta
Light t v r s q u
Medium l n j k i m
Dark d f b c a e
Black (space) White ?

Using only these chars gives a plain 2D-lang-like scoring, where blanks at the end of each line can be omitted and a newline counts as 1 byte. In addition, you can change the last character of each line to uppercase (_ for white) and omit all newlines.

As an example, the program

can be written like an ordinary 2D language as

ttttli
auqasj

which can be written in one line as

ttttlIauqasj

which has the score of 12 bytes = 12 codels.

Self-contained TIO setup: Try it online! (Protip: you can pass -t to npiet to get step-by-step information.)

If you use this setup and scoring, you should specify that ascii-piet is used for the encoding, like this:

# [Piet](https://github.com/cincodenada/bertnase_npiet) + [ascii-piet](https://github.com/dloscutoff/ascii-piet), 12 bytes

MasterPiets has a nice color chooser interface and a step-by-step debugger (click "Debugger" on the right side to open it). It has a few drawbacks though: you lose your work when you resize the grid, and some users have reported that Export to PNG doesn't work.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ If I'm not mistaken, aren't Piet programs scored based on codels, not the byte size of the image? I'm not sure though. \$\endgroup\$
    – Aiden Chow
    Mar 31, 2022 at 4:51
  • \$\begingroup\$ Huh, actually, I found this meta post, the top answers (count score as image size and count score as number of codels) both have the same net vote, so I'm not sure about this. \$\endgroup\$
    – Aiden Chow
    Mar 31, 2022 at 4:56
  • 1
    \$\begingroup\$ @AidenChow That discussion is quite old and has accumulated lots of negative votes for both options. But the recent general consensus is that EVERY language must be scored in bytes (as in the size of the file in disk). One relatively recent example. \$\endgroup\$
    – Bubbler
    Mar 31, 2022 at 5:18
5
\$\begingroup\$

Basic routing and halting

Routing and halting are two of the hardest walls when starting to write nontrivial Piet programs. The pointer moves in the unit of connected areas of the same color, and it chooses the next cell using the following variables:

  • DP: Direction pointer. One of right, down, left, or up. Initially right.
  • CC: Codel Chooser. One of left or right. Initially left.

Given the current area, DP, and CC, the next cell to move into is chosen as follows (hopefully more intuitive than the official doc):

  • Imagine you are facing in the direction of DP.
  • In the area you're stepping on, choose the cell(s) that are farthest from you.
  • If there are multiple, choose the rightmost or leftmost cell out of them (in your view) based on CC.
  • Walk to the chosen cell and move front once.

For example, if your current area looks like x in the following and DP=right and CC=left,

 h g
fxxxa
exx
  xxb
  cd

Then the next cell is a. The cells marked a to h are the cells you might enter next, depending on the current DP and CC.

If the next cell is black, or out of bounds, then the pointer tries the next combination of DP and CC - first swap CC, then rotate DP once clockwise, repeat. If all 8 combinations are tried and fails, it halts. (See the previous example again: the cells marked a to h are the cells that are tried in order.) Note that, by the spec, invalid commands (stack underflow and division by zero) are simply ignored and execution continues, so the only way to halt a program is via a trapped region.

So how to construct such a trapped region? The simplest ones I found are:

      x
...abcx
      x

and

...abcx
     xx

Can you see why each of these works? (It is impossible to trap in a 1-row program, or using areas of size 1 or 2.) And since these are the only trapping configuration of size 3, you will likely use one of these in every halting golfed program.

The turn-right-when-blocked behavior can be used to route in interesting ways. Some examples I found are:

# 2-row infinite loop
abc...xyz
zyx...abc

# 3-row finite loop with tail
# use conditional DP+ on `de` to turn right to move towards exit
# also can be used for a linear program; put `Push 1` on `cd` and `DP+` on `de`
abcabc z
exyzxayz
dcbacb z

# 2-row with small loops; use conditional DP+ on `de`
abcde...bcde...az
 pool   pool   zz

# more rows with few black cells to eventually lead to infinite loop
# note that seemingly unused cells can be used to push constants
>>>>>>>v
 ......v
>>>>v .v
^<<<<<<<

A note on DP+ and CC+ commands (looping and if-else)

There is a command that can change the direction of movement (essentially DP). It is called "pointer" (at hue +3, darkness +1) in the official doc, but I like to call it DP+, as it directly adds the top value to the current DP (modulo 4). If you have a condition that evaluates to 0 or 1, you can use DP+ on that to turn right once based on a condition. This creates a branch in your program, and you can mainly use it to create a conditional loop. (If you somehow need to turn left instead, applying *3 to the condition is the shortest method I can think of, which costs 4 more cells.)

There is CC+ ("switch" in official doc, hue +3, darkness +2) command too, which adds the top value to CC (modulo 2). This can be useful in creating if-else constructs: if you place CC+ at ab in the following code

...abcdexyz...
    buvwx

then the pointer follows the cde path or the uvw path based on the resulting CC. Then the x block acts as the merging point: since CC=right path is blocked, CC is toggled and the execution continues through xyz... with CC=left in both cases.

\$\endgroup\$
2
  • \$\begingroup\$ This is interesting, didn't know you could make conditional loops in Piet. Could you elaborate more on what DP+ is, how it can be used for conditionals? thanks \$\endgroup\$
    – Aiden Chow
    Mar 31, 2022 at 6:20
  • \$\begingroup\$ @AidenChow Added. \$\endgroup\$
    – Bubbler
    Mar 31, 2022 at 6:59
2
\$\begingroup\$

If-else-bounce-merge

If you need to create control flow that splits and merges quickly, you might want to use this construction:

..xAbcd..Z
...Afgh..Zw

where xA is CC+ and bcd.. and fgh.. are two if-else paths. w must be surrounded by three blacks (or outside of the program). It is easiest to use when there's nothing to do for bcd.., in which case you can just fill that row with white codels. Otherwise, you need to check what bcd.. does when run backwards.

An example where it prints 10 (newline) based on a condition.

The bounce part can be used without an if-else too, since it has an effect of fixing CC after it bounces back. primo's Hello World uses it for the 3-row layout to minimize wasted codels.

\$\endgroup\$
1
  • \$\begingroup\$ I don't get this at all :( \$\endgroup\$
    – Aiden Chow
    Jul 7, 2022 at 20:59
2
\$\begingroup\$

Detecting if the stack is empty

If you ever run into a scenario where you need to check whether or not the stack is empty, you can simply run the following instructions:

dup, dup, minus, not

If the stack is empty, all these instructions will fall through and nothing will happen. But if the stack is non-empty, this code will essentially push a 1 on top of the pre-existing stack, without changing anything else.

You can then have a DP+ or CC+ instruction afterwards to branch your code based on whether or not the stack is empty. This can be particularly helpful in things like printing out the entire stack from the top to the bottom, where you can simply construct a loop with the check shown in this tip to exit the loop if the stack is empty.

Here's an example of that here. In this code, the stack is initialized as 6 5 4 3 5 2. Then, it passes through the white codels (which are just there for clarity) and enter into the looping construct. Here, the check is ran (dup, dup, minus, not), with a DP+ following it. If the stack is empty, then it simply passes through to the right and ends up in the halting construct (again, white codels added just for clarity). But if the stack is non-empty, there will be a 1 on the top of the stack, which will result in the DP pointing downwards after DP+ is ran. The loop then continues with printing the value on the top of the stack, and then turning back up and to the right at the front of the loop to run the check again. This process is repeated until the stack is empty, which will then cause the IP to enter into the halting construct as stated previously. With the stack initialized as 6 5 4 3 5 2, the code will result in 253456 being printed out (basically, the contents of the stack from the top to the bottom).

\$\endgroup\$
2
\$\begingroup\$

A2-escape pattern

I wrote two answers today: factorial, number to binary.

Factorial:

Number to binary:

In both programs, A1->A2 is a DP+. It is a no-op at start of a program (because the stack is empty), and is only meaningful after one iteration of the loop. When DP+ triggers, the IP escapes the loop through the 2nd column, which minimizes the wasted space on row C (in ascii-piet scoring). You can then do more stuff starting at C2 or D2 facing down.

This construct is useful when you can write the first part of the program in a single pure loop (no initialization before loop). The condition is easily met in Number to Binary because the only data needed is the input number, and it is OK to put inN in the loop (it is no-op when the input is exhausted).

It is a little harder in the case of Factorial, as you'd normally push 1 at the bottom which will be the "current product" in the loop. Fortunately with the current logic, the DP+ is somewhat in the middle:

Initialize: 1 inN
Loop: dup 1 - 3 1 roll dup 1 > ! DP+ * 2 1 roll
                                 ^^^^^^^^^^^^^^

When the marked commands are put at the start of the program, DP+ * are no-ops, 2 1 push the two numbers, and roll is again a no-op (because there aren't two numbers to roll when 2 and 1 are removed). This leaves a 1 at the top of the stack (we can safely ignore the 2 below), which connects nicely to the logical start of the loop.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.