7, 410 characters, 154 bytes in 7's encoding, 0 letters = score 154
55104010504200144434451510201304004220120504005434473340353241135014335450302052254241052253052244241052335452241114014241310052340435303052335442302052335500302052335430302052313340435303135014243241310335514052312241341351052302245341351525755102440304030434030421030442030424030455733413512410523142410523030523112411350143355142410523414252410523102410523002410523413342411145257551220304010420030455741403
Try it online!
In a challenge that dislikes using letters, what better language to use than one consisting only of digits?
This is a full program that exits via crashing, so there's extraneous output to stderr, but stdout is correct.
Explanation
A 7 program, on its first iteration, simply pushes a number of elements to the stack (because out of the 12 commands that exist in 7, only 8 of them can be represented in a source program, and those 8 are specialised for writing code to push particular data structures to the stack). This program does not use the 6
command (which is the simplest way to create nested structures, but otherwise tends not to appear literally in a source program), so it's only the 7
commands that determine the structure; 7
pushes a new empty element to the top of stack (whereas the 0
…5
commands just append to the top of stack). We can thus add whitespace to the program to show its structure:
551040105042001444344515102013040042201205040054344 7
33403532411350143354503020522542410522530522442410523354522411140142413100523
40435303052335442302052335500302052335430302052313340435303135014243241310335
514052312241341351052302245341351525 7
55102440304030434030421030442030424030455 7
33413512410523142410523030523112411350143355142410523414252410523102410523002
41052341334241114525 7
551220304010420030455 7
41403
The elements near the end of the program are pushed last, so are on top of the stack at the start of the second iteration. On this iteration, and all future iterations, the 7 interpreter automatically makes a copy of the top of the stack and interprets it as a program. The literal 41403
pushes the (non-literal, live code) 47463
(7 has 12 commands but only 8 of them have names; as such, I use bold to show the code, and non-bold to show the literal that generates that code, meaning that, e.g. 4
is the command that appends 4
to the top stack element). So the program that runs on the second iteration is 47463
. Here's what that does:
47463
4 Swap top two stack elements, add an empty element in between
7 Add an empty stack element to the top of stack
4 Swap top two stack elements, add an empty element in between
6 Work out which commands would generate the top stack element;
append that to the element below (and pop the old top of stack)
3 Output the top stack element, pop the element below
This is easier to understand if we look at what happens to the stack:
- … d c b a
47463
(code to run: 47463
)
- … d c b
47463
empty a (code to run: 7463
)
- … d c b
47463
empty a empty (code to run: 463
)
- … d c b
47463
empty empty empty a (code to run: 63
)
- … d c b
47463
empty empty "a" (code to run: 3
)
- … d c b
47463
empty (code to run: empty)
In other words, we take the top of stack a, work out what code is most likely to have produced it, and output that code. The 7 interpreter automatically pops empty elements from the top of stack at the end of an iteration, so we end up with the 47463
back on top of the stack, just as in the original program. It should be easy to see what happens next: we end up churning through every stack element one after another, outputting them all, until the stack underflows and the program crashes. So we've basically created a simple output loop that looks at the program's source code to determine what to output (we're not outputting the data structures that were pushes to the stack by our 0
…5
commands, we're instead recreating what commands were used by looking at what structures were created, and outputting those). Thus, the first piece of data output is 551220304010420030455
(the source code that generates the second-from-top stack element), the second is 3341351…114525
(the source code that generates the third-from-top stack element), and so on.
Obviously, though, these pieces of source code aren't being output unencoded. 7 contains several different domain-specific languages for encoding output; once a domain-specific language is chosen, it remains in use until explicitly cleared, but if none of the languages have been chosen yet, the first digit of the code being output determines which of the languages to use. In this program, only two languages are used: 551
and 3
.
551
is pretty simple: it's basically the old Baudot/teletype code used to transmit letters over teletypes, as a 5-bit character set, but modified to make all the letters lowercase. So the first chunk of code to be output decodes like this:
551 22 03 04 01 04 20 03 04 55
c a SP e SP n a SP reset output format
As can be seen, we're fitting each character into two octal digits, which is a pretty decent compression ratio. Pairs of digits in the 0-5 range give us 36 possibilities, as opposed to the 32 possibilities that Baudot needs, so the remaining four are used for special commands; in this case, the 55
at the end clears the remembered output format, letting us use a different format for the next piece of output we produce.
3
is conceptually even simpler, but with a twist. The basic idea is to take groups of three digits (again, in the 0-5 range, as those are the digits for which we can guarantee that we can recreate the original source code from its output), interpret them as a three-digit number in base 6, and just output it as a byte in binary (thus letting us output the multibyte characters in the desired output simply by outputting multiple bytes). The twist, though, comes from the fact that there are only 216 three-digit numbers (with possible leading zeroes) in base 6, but 256 possible bytes. 7 gets round this by linking numbers from 332₆ = 128₁₀ upwards to two different bytes; 332
can output either byte 128 or 192, 333
either byte 129 or 193, and so on, up to 515
which outputs either byte 191 or 255.
How does the program know which of the two possibilities to output? It's possible to use triplets of digits from 520
upwards to control this explicitly, but in this program we don't have to: 7's default is to pick all the ambiguous bytes in such a way that the output is valid UTF-8! It turns out that there's always at most one way to do this, so as long as it's UTF-8 we want (and we do in this case), we can just leave it ambiguous and the program works anyway.
The end of each of the 3…
sections is 525
, which resets the output format, letting us go back to 551
for the next section.
a
s--or not, depending on how many letters it would take, because 20 characters is a really big penalty (although when everything else is scored by bytes, it's not quite well defined...)! \$\endgroup\$