Challenge
We're given secret.txt, a single line of comma-separated tuples wrapped
in what looks like braces and underscores:
(4, 17), (2, 16), (2, 15), (4, 9), { , (3, 2, 1), (5, 3), _ , (2, 17), ...
$ file secret.txt
secret.txt: ASCII text, with very long lines (479), with no line terminators
No further context is given — the challenge is to figure out the encoding
scheme from the data alone.
Recon
The tuples are almost all 2- or 3-element groups of small integers, plus
three literal tokens scattered through the stream: {, }, and _.
Small integer pairs, a {/} wrapper, and an obvious "flag format" hint
(bronco{...}) strongly suggested a coordinate lookup cipher — each
tuple indexes into some 2D reference grid, and the underscore/braces are
literal characters meant to survive the decode untouched.
The first four tuples are the key to spotting the scheme:
(4, 17), (2, 16), (2, 15), (4, 9)
Reading these as (period, group) coordinates on the periodic table:
| Tuple | Period | Group | Element | Symbol |
|---|---|---|---|---|
| (4, 17) | 4 | 17 | Bromine | Br |
| (2, 16) | 2 | 16 | Oxygen | O |
| (2, 15) | 2 | 15 | Nitrogen | N |
| (4, 9) | 4 | 9 | Cobalt | Co |
Concatenating the symbols: Br + O + N + Co = BrONCo →
"Bronco" — matching the expected flag prefix exactly. Scheme confirmed.
Refining the scheme
Most tuples are 2-element (period, group) pairs and decode to a full
element symbol (1 or 2 letters). But a number of tuples have a third
element, e.g. (4, 17, 2). Decoding these as full symbols produced
garbled output, so the third number was tested as a letter index into
the 2-letter symbol:
(4, 17, 2) -> Br, take letter #2 -> "r"
(2, 1, 2) -> Li, take letter #2 -> "i"
(3, 13, 1) -> Al, take letter #1 -> "A"
This let single letters be pulled out of two-letter element symbols —
necessary because plain English text needs individual letters, not
2-character blocks, at most positions.
The literal tokens map directly:
-
_→ space -
{/}→ literal brace (flag wrapper)
Decoding
Running the full tuple stream through this scheme (see decode.py)
produces:
BrONCo{MY FAVOriTe MeSSAGeS HAVe AT eleMeNT OF S[?9,6?]PriSe}
Nearly the entire message resolves cleanly into a coherent, on-theme
sentence — fitting, since the challenge itself hinges on the "surprise"
of finding a message hidden in periodic table coordinates:
MY FAVORITE MESSAGES HAVE AN ELEMENT OF SURPRISE
Two positions didn't resolve automatically:
-
(9, 6)— there is no period 9 on the periodic table (periods only go 1–7), so this tuple has no valid lookup. Context makes the intended letter obvious: the word isS_PRISE, which can only sensibly be SURPRISE, so the missing letter isU. - The word decoded as
ATreads awkwardly; grammatically the sentence wantsAN("have an element of surprise"). This points to a likely transcription slip in the source tuple for that word (a digit that should differ from what was copied), rather than an error in the decoding scheme itself — every other tuple in the file resolves without issue.
Both anomalies are localized to specific known tuples and don't affect
confidence in the rest of the decode, which is fully self-consistent
across ~50 other tuples.
Final Flag
bronco{MY_FAVORITE_MESSAGES_HAVE_AN_ELEMENT_OF_SURPRISE}
(Spaces converted to underscores to match standard flag formatting
conventions.)
Tools
decode.py — standalone Python decoder implementing the scheme above.
Usage:
python3 decode.py secret.txt
Full source:
#!/usr/bin/env python3
"""
Periodic Table Cipher Decoder
==============================
Encoding scheme discovered from secret.txt:
- Each token is either:
* A literal character: { } _
* A tuple (period, group) -> full element symbol (1 or 2 letters)
* A tuple (period, group, index) -> a single letter from that symbol,
where index is 1-based (1st or 2nd letter)
- "_" represents a space / underscore separator in the final message
- "{" and "}" are literal brace characters (used for the flag wrapper)
Example:
(4,17) (2,16) (2,15) (4,9) -> Br O N Co -> "BrONCo" -> "Bronco"
Usage:
python3 decode.py secret.txt
"""
import re
import sys
# Standard periodic table symbols by (period, group), IUPAC 1-18 group numbering.
# Only positions that are actually populated on a real periodic table are included.
PERIODIC_TABLE = {
(1, 1): 'H', (1, 18): 'He',
(2, 1): 'Li', (2, 2): 'Be', (2, 13): 'B', (2, 14): 'C', (2, 15): 'N',
(2, 16): 'O', (2, 17): 'F', (2, 18): 'Ne',
(3, 1): 'Na', (3, 2): 'Mg', (3, 13): 'Al', (3, 14): 'Si', (3, 15): 'P',
(3, 16): 'S', (3, 17): 'Cl', (3, 18): 'Ar',
(4, 1): 'K', (4, 2): 'Ca', (4, 3): 'Sc', (4, 4): 'Ti', (4, 5): 'V',
(4, 6): 'Cr', (4, 7): 'Mn', (4, 8): 'Fe', (4, 9): 'Co', (4, 10): 'Ni',
(4, 11): 'Cu', (4, 12): 'Zn', (4, 13): 'Ga', (4, 14): 'Ge', (4, 15): 'As',
(4, 16): 'Se', (4, 17): 'Br', (4, 18): 'Kr',
(5, 1): 'Rb', (5, 2): 'Sr', (5, 3): 'Y', (5, 4): 'Zr', (5, 5): 'Nb',
(5, 6): 'Mo', (5, 7): 'Tc', (5, 8): 'Ru', (5, 9): 'Rh', (5, 10): 'Pd',
(5, 11): 'Ag', (5, 12): 'Cd', (5, 13): 'In', (5, 14): 'Sn', (5, 15): 'Sb',
(5, 16): 'Te', (5, 17): 'I', (5, 18): 'Xe',
(6, 1): 'Cs', (6, 2): 'Ba', (6, 3): 'La', (6, 4): 'Hf', (6, 5): 'Ta',
(6, 6): 'W', (6, 7): 'Re', (6, 8): 'Os', (6, 9): 'Ir', (6, 10): 'Pt',
(6, 11): 'Au', (6, 12): 'Hg', (6, 13): 'Tl', (6, 14): 'Pb', (6, 15): 'Bi',
(6, 16): 'Po', (6, 17): 'At', (6, 18): 'Rn',
(7, 1): 'Fr', (7, 2): 'Ra', (7, 3): 'Ac', (7, 4): 'Rf', (7, 5): 'Db',
(7, 6): 'Sg', (7, 7): 'Bh', (7, 8): 'Hs', (7, 9): 'Mt', (7, 10): 'Ds',
(7, 11): 'Rg', (7, 12): 'Cn', (7, 13): 'Nh', (7, 14): 'Fl', (7, 15): 'Mc',
(7, 16): 'Lv', (7, 17): 'Ts', (7, 18): 'Og',
}
# Matches: {, }, _, or a tuple like (4, 17) / (4, 17, 2)
TOKEN_RE = re.compile(r'\{|\}|_|\(\s*\d+\s*,\s*\d+\s*(?:,\s*\d+\s*)?\)')
TUPLE_RE = re.compile(r'\(\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d+)\s*)?\)')
def decode(text: str) -> str:
output = []
for match in TOKEN_RE.finditer(text):
token = match.group(0)
if token in ('{', '}'):
output.append(token)
continue
if token == '_':
output.append(' ')
continue
tm = TUPLE_RE.match(token)
period, group, index = tm.groups()
period, group = int(period), int(group)
symbol = PERIODIC_TABLE.get((period, group))
if symbol is None:
output.append(f'[?{period},{group}?]')
continue
if index is None:
output.append(symbol) # full symbol
else:
idx = int(index)
if 1 <= idx <= len(symbol):
output.append(symbol[idx - 1]) # single letter
else:
output.append(f'[?{symbol}#{idx}?]')
return ''.join(output)
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <secret.txt>")
sys.exit(1)
with open(sys.argv[1], 'r') as f:
raw = f.read()
result = decode(raw)
print("Decoded message:")
print(result)
if __name__ == '__main__':
main()
Key Takeaways
- When a cipher's tuples don't line up with an obvious 1:1 substitution, try mapping them onto a real-world reference table (periodic table, keyboard layout, ASCII table, book/page/line, etc.) rather than assuming a purely mathematical transform.
- Confirming a scheme against a small, checkable prefix (here, the
bronco{flag wrapper) before decoding the whole message saves a lot of wasted effort chasing the wrong theory. - When a decode is 95% clean and self-consistent, isolated garbage characters are usually a transcription/OCR artifact in the source data rather than a flaw in the scheme — worth verifying against the raw file before assuming the cipher logic itself is wrong.
Top comments (0)