Reply To: Challenge 10B
A Tale of 2 Secrets › Forums › T.E.M.P.E.S.T. › Challenge 10B › Reply To: Challenge 10B
@AES_of_spades, as @Puzzling_Pelican is the undisputed winner, I’ll explain their code. It uses a few Python-specific things:
0. An iterable is roughly an object that has a defined iteration order over some values. Or, anything you can iterate over in a for loop. Examples: [1,2,3] or (1,2,3) or range(42) or “iterable”
1. List range syntax: list[start:stop:step] extracts the part of the list between start & stop, in steps of step. list[::-1] reverses the list.
2. The spread operator (*), is applied to an iterable and spreads out its values as multiple (function) arguments. It can also be used in lists (and possibly other places).
x = [1,2,3]
print(*x) # same as print(1, 2, 3)
print(*x, *x) # same as print(1, 2, 3, 1, 2, 3)
y = [2, *x, 3] # same as y = [2, 1, 2, 3, 3]
def cool_function(thing, *args):
print(thing) # <-- first argument, mandatory
print("variable args", args) # <-- 0 or more optional arguments
cool_function("one", 2, "three", 4)
3. Not used in @Puzzling_Pelican’s program (because it’s quite expensive in characters), lambda defines an anonymous function.
def double(x):
return x * 2
double_anom = lambda x: x * 2
assert(double(4) == double_anom(4))
add = lambda x, y: x + y # <-- anonymous function which takes two arguments
print(add(1, 2))
In Python, functions are first-class: they can be stored in variables (or lists and other data structures), passed as arguments to other functions, and returned from functions.
4. map(function, iterable) and zip(*iterables) are built-in functions. map() applies a function to each element in an iterable, producing a generator. zip() takes a variable number of iterables and groups the first element of each iterable, then the second, and so on, in tuples (parenthesised lists).
x = list(map(lambda x: x*2, [1,2,3])) # <-- x = [2,4,6]
y = list(zip(x, x[::-1])) # <-- y = [(2,6), (4,4), (6,2)]
5. List comprehension lets you concisely manipulate the contents of an iterable (such as a list) and convert it into a list, dictionary or generator.
x = [1,2,3]
y = (y*2 for y in x) # <-- list comprehension that doubles each element in x, producing a generator
y = [y*2 for y in x] # <-- same thing, but producing a list
z = {y:y*2 for y in x} # <-- similar thing, but producing a dictionary
q = [u + v for u, v in zip(x, x[::-1])] # <-- with two bindings: u & v
6. The walrus operator, :=, allows you to assign a value and then use it within the same line. The assignment expression (x := value) evaluates to the value as well. So print((x := 1) * 2 + x) gives you 3.
7. Semicolons can be used to separate multiple statements on the same line. x=1;print(x);print(x*2).
This should be everything needed to understand @Puzzling_Pelican’s program. I’m going to break it down into a few subexpressions:
ALPHABET0 = "A23456789XJQKCDHS"
CIPHERTEXT = "7CXS3 H6S7C KSXDA S2CKS"
MAP_EXPR = map(ALPHABET0.find, CIPHERTEXT.replace(" ",""))
ZIP_EXPR = zip(*[MAP_EXPR]*4)
ALPHABET1 = "SHADOWBCEFGIKLMNPQRTUVXY"
ALPHABET2 = "FULHEARTBCDGIKMNOPQSVWXY"
PLAINTEXT_EXPR = [ALPHABET1[(y:=c+d%13*13)//4*6+(x:=a+b%13*13)%6-54] + ALPHABET2[x//6*4+y%4] for a,b,c,d in ZIP_EXPR]
print(*PLAINTEXT_EXPR)
1. MAP_EXPR: we are applying the ALPHABET0.find function (basically the same as lambda x: ALPHABET0.index(x)) to each element in CIPHERTEXT (with spaces removed). This is later taken mod 13 (%13) to convert the suit & number to integers.
2. ZIP_EXPR: this is a subtle trick. The list [MAP_EXPR] is multiplied by 4, which creates a list of 4 elements containing 4 MAP_EXPR values. These elements are not duplicates; they all refer to the same value. The spread operator is then used so zip() gets 4 arguments which all refer to MAP_EXPR. Equivalently written as zip(MAP_EXPR, MAP_EXPR, MAP_EXPR, MAP_EXPR). zip() essentially works by calling the built-in function next() on each iterable sequentially, in a loop. Because all the arguments reference the same thing, this has the effect of collecting 4 sequential input elements (from MAP_EXPR) into a tuple, as each value of ZIP_EXPR. So if MAP_EXPR = iter((1,2,3,4,5,6,7,8)), list(ZIP_EXPR) = [(1,2,3,4), (5,6,7,8)].
3. PLAINTEXT_EXPR: this uses a list comprehension, binding the ciphertext cards in every group of 4 from ZIP_EXPR to a, b, c & d. b and d are taken mod 13 (%13) to give suit numbers between 0 and 3. a & c are card numbers between 0 and 12. The walrus operator is used to introduce x and y, which map the first and second card in the pair to the 0 to 51 range (there are 52 cards). x & y are then combined to index into the two cipher alphabets.
4. print(*PLAINTEXT_EXPR): this uses the spread operator to provide print() with one argument for each element of PLAINTEXT_EXPR (a list of strings). print() automatically inserts spaces between each argument and outputs them on the same line, so this just helps to cut a few more characters compared to print(“”.join(PLAINTEXT_EXPR)).
See if you can figure out how this works:
q=lambda c:[*{k:0 for k in[*c,*range(65,74),*range(75,90)]}.keys()]
print(*map(chr, q(b"SHADOW")))