Jelly, score 40, tiebreak score 61 bytes
Try it online!
Jelly, score 40, tiebreak score 58 bytes, in collaboration with @Arnauld and @ChartZ Belatedly
Try it online!
I found an almost asymptotically optimal algorithm for solving this sort of problem (mapping a set of given inputs into specific numbers, not caring about other inputs) a while ago. The algorithm takes a few bytes to express in Jelly, but the savings in not having any wasted bytes actually describe the outputs are worthwhile, making this the currently shortest non-builtin-based solution. (As a side note, it may well be worth adding the algorithm in question as a Jelly builtin, which would save 11-13 bytes from this program depending on how it was implemented; it seems likely to come up in the future.)
The algorithm basically generates programs that work by running a large number of hash functions on the input to produce a vector of hashes, then taking the dot product of that vector with a hardcoded vector over a finite field. (When implementing this in Jelly, we use a finite field of prime order, because it doesn't have builtins for dealing with other sorts of finite field.) It turns out that the minimum length of a hardcoded vector that solves this problem is almost always equal to the number of input/output pairs, and the maximum size of the vector elements is the order of the finite field, so the amount of storage used by this is equal to the amount of storage needed to store the possible outputs (thus asymptotically optimal). Adding a new HTTP status code to this program would typically cost less than a byte on average per code, unless it was outside the range of the existing codes (e.g. here's how to add "I'm a teapot" to the original program at the cost of only one more byte).
I've since automated a program generator that generates Jelly programs to solve these programs. So this codegolf submission was pretty much entirely automatically generated. This is the generation program I used (configured to generate a solution to this problem). The Sage program it generates, when run, generates this Jelly program which generates the program written above.
The reason I've described the algorithm as "almost asymptotically optimal" is that it has two failure modes. One is mathematical; the algorithm involves solving a set of simultaneous equations, which can almost always be solved using a vector whose length is equal to the number of input/output pairs, but rarely and randomly have no solution (the odds of this become smaller as the size of the codomain becomes larger, and when it happens you can "reroll" by adding an arbitrary extra input/output pair, leading to a slightly larger program). The other is a deficiency of Jelly; the shortest way in Jelly to describe a list of integers drawn from a uniform random distribution is to use a large constant integer and base-convert it, but this mechanism is incapable of expressing lists with a leading 0 (the program generator I've written above doesn't attempt to fix this problem itself, but the odds of it happening are again just 1 in the size of the output domain).
Explanation (original version)
Here's how this program is implemented in Jelly:
ɠ a line taken from standard input
€ each of the following hash configurations:
ȷ € each number from 1 to 1000 (the salt)
; concatenated with
ȷ 1000 (the codomain of the hash function);
ḋ take the dot product of the resulting hashes and
¤ a constant calculated by
“…’ converting a large constant integer
b123 to a list of base 123 digits;
%127 take the resulting dot product modulo 127,
b28 convert to a list of base 28 digits,
ḅ³ interpret as base 100 digits,
+³ and add 100
Everything up to the %127 is just the implementation of our general-purpose input→output mapper. The
b28ḅ³+³ after that implements the inverse of a function that maps HTTP status codes from 100…505 onto integers in the range 0…117; producing a smaller codomain allows for a smaller hardcoded vector. (The basic idea is to note that the last two digits of the status code are never greater than 27, so the double base conversion "closes some of the gaps" in the range of possible outputs.)
As it happens, our hardcoded vector didn't contain any of the values 123, 124, 125, or 126, so it was possible to use 123 as the base for that base conversion. (The gain from this is minimal; if Jelly had a builtin for this algorithm, you'd hardcode that the "123" and "127" were the same number.)
There's no particular algorithmic reason for the codomain of the hash function to be 1…1000 (any sufficiently large codomain would do), and calculating 1000 hashes is likewise not algorithmically useful because we only use the first 40 of them. This is just a tiny byte saving: 1000 has a 1-byte representation, whereas most other numbers can't be written in a single byte.
Incidentally, the reason we take input from standard input is that 100 (the lowest HTTP status code) can be represented in 1 byte in Jelly if the program has no command-line arguments, but takes 3 bytes if there are any command-line arguments. There wasn't a byte cost to doing anything because I needed to take explicit input anyway (attempting to take implicit input would run into a parse ambiguity that would need a byte to fix, so the extra byte for explicit input doesn't cost).
Explanation (improved version)
@Arnauld suggested, instead of the base-28 calculation used in the previous version, to generate a list of the 41 possible (return values + 306) (trying to omit 306 from the list would cost more bytes than simply just adding it; and including it also gives us a prime number of possible outputs, which we need to be able to do finite field operations over the range of outputs in Jelly).
We can generate a list of the values like this:
Ḷ range from 0 to n-1, i.e. [[0,1],[0,1,…,6],…]
Ɗ group the three preceding bultins together
×³ 100 times
J the index of
" each sublist to every element of the sublist
In other words, we're adding 1 to each element of the first list, 2 to each element of the second list, etc., 100 times, thus effectively adding
[100,200,300,400,500] to each element of the corresponding sublist of [[0,1],[0,1,…,6],…] to produce [[100,101],[200,201,…,206],…], which can be flattened to produce the list of status codes we want.
I originally generated the list using slightly different code (doing the addition 100 times rather than adding 100 times the index), and connected the list to the original program using a newline and
ị is wrapping indexing into a list (thus contains an implicit "modulo 41"; 41 is the size of the finite field we're using in this version and also the number of status codes in the list), and
¢ tells Jelly to look at the previous line to find the list to index into. However, @ChartZ Belatedly realised that rearranging the list generation like this, although it still takes the number of bytes, makes the list generation into a nilad followed by a sequence of monads. This makes it possible to treat the entire list generation like a literal constant by using a single
¤ byte, which is a less general method of specifying the grouping than the a newline and
¢ were, but worth it as it's a byte shorter overall.
This approach, where almost all the outputs are used, costs several bytes for the more complex post-processing, but makes the hardcoded vector require substantially less storage because the numbers in it now only go up to 40 rather than 122, so it saves more bytes than it costs.