While reviewing Encryption using a mirror field, I decided to write my own solution from scratch. The challenge, from Reddit's /r/dailyprogrammer, is to implement a monoalphabetic substitution cipher whose key is derived from a pictorial ×ばつ13 grid. Here's a simplified ×ばつ5 example:
abcde A \ f B g C \ h D i E / j FGHIJ
The ×ばつ5 grid above defines 20 mappings, some of which are:
A
maps toG
, because a laser beam heading right from theA
would hit the\
mirror and be deflected down toG
.- Conversely,
G
maps toA
through the reverse path.B
maps tog
, and vice versa, as the horizontal beam is uninterrupted.C
maps toE
, and vice versa, as the beam is reflected twice.I
maps toj
, and vice versa.
The actual input is provided as a ×ばつ13 grid of /
, \
, and space characters, without the row and column labels. I've implemented the option to accept the key as a filename if one is provided on the command line, else it will be read from the first 13 lines of standard input. Thereafter, the program acts as a filter for text on standard input.
I've also implemented a doctest, which you can run using python -v -m doctest programname
. The presence of backslashes has forced me to write it in an ugly way; any suggestion for a better way to write it would be appreciated.
"""
Perform the mirror encryption challenge in https://redd.it/4m3ddb
"""
from fileinput import input as fileinput
from string import ascii_lowercase, ascii_uppercase
from sys import argv, stdin
def grid_to_translation(grid):
"""
Given a mirror field, construct a translation table suitable for use with
str.maketrans(). The grid may be any iterable; only the first 13 lines
will be taken from it.
>>> # Using a string replacement as literal backslashes are hard to write.
>>> input = (line.replace('x', chr(92)) for line in [
... ' xx /x ',
... ' x',
... ' / ',
... ' x x',
... ' x ',
... ' / / ',
... 'x / x ',
... ' x ',
... 'x/ ',
... '/ ',
... ' x ',
... ' x/ ',
... ' / / ',
... ])
>>> translation = grid_to_translation(input)
>>> 'TpnQSjdmZdpoohd'.translate(translation)
'DailyProgrammer'
>>> 'Code Review'.translate(translation)
'Amrh LhOnhN'
>>> 'Amrh LhOnhN'.translate(translation)
'Code Review'
"""
RIGHT, DOWN, LEFT, UP = (0, 1), (1, 0), (0, -1), (-1, 0)
LAUNCH = dict( # Top, right, left, and bottom edges
[(c, ((-1, col), DOWN)) for col, c in enumerate(ascii_lowercase[:13])]
+ [(c, ((row, 13), LEFT)) for row, c in enumerate(ascii_lowercase[13:])]
+ [(c, ((row, -1), RIGHT)) for row, c in enumerate(ascii_uppercase[:13])]
+ [(c, ((13, col), UP)) for col, c in enumerate(ascii_uppercase[13:])]
)
EDGES = {pos: c for c, (pos, _) in LAUNCH.items()}
# REFLECT[barrier][incoming_direction] gives the outgoing direction
REFLECT = {
'/': {
DOWN: LEFT,
RIGHT: UP, LEFT: DOWN,
UP: RIGHT,
},
'\\': {
DOWN: RIGHT,
RIGHT: DOWN, LEFT: UP,
UP: LEFT,
},
' ': {
DOWN: DOWN,
RIGHT: RIGHT, LEFT: LEFT,
UP: UP,
},
}
def trace(pos, aim):
pos = (pos[0] + aim[0], pos[1] + aim[1])
while not pos in EDGES:
aim = REFLECT[grid[pos[0]][pos[1]]][aim]
pos = (pos[0] + aim[0], pos[1] + aim[1])
return EDGES[pos]
grid = iter(grid)
grid = [next(grid) for _ in range(13)]
mapping = {ord(c): trace(pos, aim) for c, (pos, aim) in LAUNCH.items()}
assert all(mapping[ord(d)] == chr(c) for c, d in mapping.items())
return str.maketrans(mapping)
if __name__ == '__main__':
# Make a translation table from the a file specified on the command line,
# or from the first 13 lines of stdin if no filename is given.
translation = grid_to_translation(fileinput() if len(argv) > 1 else stdin)
# Then use it to encrypt all subsequent input lines.
for text in stdin:
print(text.translate(translation), end='')
1 Answer 1
The logic is neat and nicely done, there is not much to say. Just a few nitpicks:
fileinput()
already defaults tostdin
if there is no arguments on the command line, no need to manage it yourself. It has been made clearer in a comment that you expect more than "no filename = stdin" : you should make it more clear in the code too; maybe changing the second line of comment with something like# since fileinput will truncate anything to the 13 lines we need, overide the 'no filename = stdin' rule to be able to pipe in the grid + some text to translate
.You can easily use
'\'
in docstrings by using raw multiline strings:r'''docstring'''
. Thus when defininginput
you can use'\\'
as in a regular Python session, or use raw strings where applicable.Or use raw strings all along and add padding characters for those that ends with a backslash, as you already ignore them; and document it with something along the lines of
# avoid ending raw strings with backslashes, extra characters are ignored anyway
.- You can extract the building of the various constants out of the function, there is no need to do it each time.
- You can define the magic number 13 as
MID_ALPHABET = len(ascii_lowercase) / 2
-
\$\begingroup\$ Try it, though, and you may see the reasons for my workarounds.
fileinput
alone won't let me read the key from a file and the plaintext from stdin. Raw strings fail if the last character is a backslash. \$\endgroup\$200_success– 200_success2016年06月11日 16:17:18 +00:00Commented Jun 11, 2016 at 16:17 -
\$\begingroup\$ When
fileinput
reads fromstdin
, either because there is no arguments or because there is a'-'
on the command line, it doesn't process lines as you type it. You have to explicitly hit Ctrl+D to close the file beforefileinput
yields the lines. But you can still read fromstdin
later on. As long as you usefileinput.input()
and thus accept that behaviour if the user enters-
as first parameters, you might as well use it as the default. But I understand that your workaround lets you pipe the grid into the program and still accessstdin
later. \$\endgroup\$301_Moved_Permanently– 301_Moved_Permanently2016年06月11日 16:52:22 +00:00Commented Jun 11, 2016 at 16:52 -
\$\begingroup\$ Included some of the comments in the answer. \$\endgroup\$301_Moved_Permanently– 301_Moved_Permanently2016年06月11日 18:29:30 +00:00Commented Jun 11, 2016 at 18:29
Explore related questions
See similar questions with these tags.