FreeCampus Python

Unit Challenge: Decode the Starship Signal Vault

Preserve a damaged signal archive while building keyed, unique, grouped, ranked, and ordered views that reveal the vault code NOVA.
python-foundations collections-iteration unit-challenge
Open in Colab
  • Level: Python Foundations · Unit 3 challenge
  • Estimated time: 60–120 minutes
  • Outcome assessed: Choose, traverse, compare, sort, and combine collection shapes without losing source order, record relationships, unique membership, or fixed clue positions.
  • Evidence: Twenty progressive checks, a decoded vault artifact, one boundary rerun, and one debugging record

1. Challenge outcome

The research starship Wayfinder has recovered five signals from a damaged archive. Four active transmissions contain the pieces of a vault code. One inactive transmission is interference, two signals have equal strength, several symbols repeat, and the records arrived in an order that must remain evidence.

Build several deliberate views of the archive and use the tuple clues to unlock the final word NOVA. The puzzle is solved only when the source remains intact, every progressive check passes unchanged, and the final artifact is produced from the derived collections.

NoteMission rule

Run one assertion group at a time. Repair the first failed promise before moving on. Do not edit expected values, sort the source list, replace sets with display lists, or hard-code the final vault word.

The challenge uses simple for and if scaffolding because Unit 4 owns complete loop design. Your work is to choose the collection operations, preserve their relationships, and explain the state each derived view represents.

2. Understand the acceptance example

The final artifact must be exactly:

=== WAYFINDER SIGNAL VAULT ===
Status: OPEN
Code: NOVA
Signals: 4 active / 5 total
Sectors: Lyra=2, Orion=2, Vela=1
Symbols: key, moon, star, sun

The code is not written directly in source order. Each active record contains a tuple (code_position, letter). Sort complete clue tuples by position and then unpack their letters.

Collection contracts

  1. source_signals remains the ordered source of truth.
  2. signals_by_id maps every unique signal ID to its complete record.
  3. active_ids preserves transmission order while excluding inactive records.
  4. active_symbols contains unique symbols from active records only.
  5. required_symbols remains a frozenset and determines vault_ready.
  6. sector_counts counts all records, including the inactive one.
  7. clue_pairs preserves active transmission order before sorting.
  8. ranked_signals is a new list ordered by strength descending and ID ascending for equal strengths.
  9. sorted_clues orders the tuple clues by code position.
  10. vault_word is joined from derived letters, never typed as a solution value.

Constraints

  • Keep every public variable name in the starter contract.
  • Do not mutate any dictionary, set, tuple, or the outer source list.
  • Do not use functions, comprehensions, exception handlers, classes, or nested loops.
  • One straightforward traversal may build the lookup, active views, counts, and clue list together.
  • Use tuple unpacking for every clue.
  • Use set operations for symbol readiness.
  • Use operator.itemgetter() and stable sorting for ranking.
  • Sort only derived lists, never source_signals in place.

3. Start from the contract

Copy this complete starter into a fresh cell. source_snapshot is supplied so the checks can detect accidental nested mutation.

from operator import itemgetter

source_signals = [
    {
        "id": "SIG-N",
        "sector": "Lyra",
        "strength": 88,
        "status": "active",
        "clue": (0, "N"),
        "symbols": {"star", "moon"},
    },
    {
        "id": "SIG-X",
        "sector": "Orion",
        "strength": 99,
        "status": "inactive",
        "clue": (9, "X"),
        "symbols": {"comet"},
    },
    {
        "id": "SIG-O",
        "sector": "Lyra",
        "strength": 94,
        "status": "active",
        "clue": (1, "O"),
        "symbols": {"moon", "key"},
    },
    {
        "id": "SIG-A",
        "sector": "Vela",
        "strength": 82,
        "status": "active",
        "clue": (3, "A"),
        "symbols": {"sun", "key"},
    },
    {
        "id": "SIG-V",
        "sector": "Orion",
        "strength": 94,
        "status": "active",
        "clue": (2, "V"),
        "symbols": {"star", "sun"},
    },
]

source_snapshot = [
    {
        "id": "SIG-N",
        "sector": "Lyra",
        "strength": 88,
        "status": "active",
        "clue": (0, "N"),
        "symbols": {"star", "moon"},
    },
    {
        "id": "SIG-X",
        "sector": "Orion",
        "strength": 99,
        "status": "inactive",
        "clue": (9, "X"),
        "symbols": {"comet"},
    },
    {
        "id": "SIG-O",
        "sector": "Lyra",
        "strength": 94,
        "status": "active",
        "clue": (1, "O"),
        "symbols": {"moon", "key"},
    },
    {
        "id": "SIG-A",
        "sector": "Vela",
        "strength": 82,
        "status": "active",
        "clue": (3, "A"),
        "symbols": {"sun", "key"},
    },
    {
        "id": "SIG-V",
        "sector": "Orion",
        "strength": 94,
        "status": "active",
        "clue": (2, "V"),
        "symbols": {"star", "sun"},
    },
]

required_symbols = frozenset({"key", "moon", "star", "sun"})

# Stage A: archive views.
signals_by_id = {}
active_ids = []
active_symbols = set()
sector_counts = {}
clue_pairs = []

# Stage B: readiness, ranking, and decoding.
missing_symbols = None
vault_ready = None
ranked_signals = None
ranked_ids = []
sorted_clues = None
vault_letters = []
vault_word = None

# Stage C: final display values.
sector_text = None
symbol_text = None
artifact = None

4. Build in small stages

Stage A: create archive views in one traversal

Use this supplied structure. Replace each None with the relevant collection operation; do not add another nested loop.

for signal in source_signals:
    signal_id = signal["id"]
    sector = signal["sector"]

    # Map the stable ID to the complete signal record.
    signals_by_id[signal_id] = None

    # Count every record in its sector.
    sector_counts[sector] = None

    if signal["status"] == "active":
        # Preserve active transmission order.
        active_ids.append(None)

        # Combine the members of this record's symbol set.
        active_symbols.update(None)

        # Unpack the fixed clue tuple, then preserve it as one tuple.
        code_position, code_letter = signal["clue"]
        clue_pairs.append(None)

Run assertion group A. If a value has the wrong nesting level, inspect one signal record and the affected derived collection before changing another line.

Stage B: compare, rank, and decode

  1. Calculate symbols missing from the required frozen set.
  2. Calculate readiness with a subset relationship.
  3. Rank all signals by ID ascending first, then strength descending. The second stable pass makes strength primary while retaining ID order among ties.
  4. Traverse the ranked records to preserve their IDs in ranked_ids.
  5. Sort the clue tuples. Because their first values are unique integer positions, ordinary tuple ordering gives the intended sequence.
  6. Traverse sorted_clues, unpack each tuple, and append only its letter.
  7. Join the letters into vault_word.

Run group B before formatting any display text.

Stage C: produce the vault artifact

The expected sector order is alphabetical. The expected symbol display is also alphabetical. Build:

sector_text = (
    f"Lyra={sector_counts['Lyra']}, "
    f"Orion={sector_counts['Orion']}, "
    f"Vela={sector_counts['Vela']}"
)
symbol_text = ", ".join(sorted(active_symbols))

Use an adjacent multi-line f-string for artifact. Derive the status without an if expression by using the supplied Boolean tuple lookup:

vault_status = ("SEALED", "OPEN")[vault_ready]

Do not type NOVA anywhere in the implementation.

5. Run progressive assertions

Group A: source, lookup, active order, counts, and clues

assert source_signals == source_snapshot
assert len(signals_by_id) == 5
assert list(signals_by_id) == ["SIG-N", "SIG-X", "SIG-O", "SIG-A", "SIG-V"]
assert signals_by_id["SIG-O"]["strength"] == 94
assert active_ids == ["SIG-N", "SIG-O", "SIG-A", "SIG-V"]
assert "SIG-X" not in active_ids
assert sector_counts == {"Lyra": 2, "Orion": 2, "Vela": 1}
assert clue_pairs == [(0, "N"), (1, "O"), (3, "A"), (2, "V")]

Group B: unique membership, ranking, and decoded values

assert active_symbols == {"key", "moon", "star", "sun"}
assert type(required_symbols) is frozenset
assert missing_symbols == set()
assert vault_ready is True
assert ranked_ids == ["SIG-X", "SIG-O", "SIG-V", "SIG-N", "SIG-A"]
assert source_signals == source_snapshot
assert sorted_clues == [(0, "N"), (1, "O"), (2, "V"), (3, "A")]
assert vault_letters == ["N", "O", "V", "A"]
assert vault_word == "".join(vault_letters)

Group C: final artifact

expected_artifact = (
    "=== WAYFINDER SIGNAL VAULT ===\n"
    "Status: OPEN\n"
    "Code: NOVA\n"
    "Signals: 4 active / 5 total\n"
    "Sectors: Lyra=2, Orion=2, Vela=1\n"
    "Symbols: key, moon, star, sun"
)

assert sector_text == "Lyra=2, Orion=2, Vela=1"
assert symbol_text == "key, moon, star, sun"
assert artifact == expected_artifact
print(artifact)
WarningA plausible result can still violate the contract

Typing vault_word = "NOVA", sorting source_signals in place, or adding inactive "comet" to the active symbol set may make one visible line look plausible while earlier evidence is wrong. The unchanged progressive checks are part of the artifact.

6. Use the hint ladder only when needed

Hint 1

Match each question to one collection behavior:

  • ID lookup: dictionary assignment with signal_id as key;
  • active transmission order: list append inside the active branch;
  • unique active symbols: set update from each active symbol set;
  • sector frequency: .get(sector, 0) + 1; and
  • fixed clue meaning: unpack (code_position, code_letter) and append that pair.

Run group A before attempting ranking.

Hint 2

The central Stage B operations have these shapes:

missing_symbols = required_symbols - active_symbols
vault_ready = required_symbols <= active_symbols

ranked_signals = sorted(source_signals, key=itemgetter("id"))
ranked_signals = sorted(
    ranked_signals,
    key=itemgetter("strength"),
    reverse=True,
)

sorted_clues = sorted(clue_pairs)

Use separate straightforward traversals to append ranked IDs and unpacked clue letters.

Hint 3

One complete final assembly has this structure:

vault_letters = []
for code_position, code_letter in sorted_clues:
    vault_letters.append(code_letter)

vault_word = "".join(vault_letters)
vault_status = ("SEALED", "OPEN")[vault_ready]

artifact = (
    "=== WAYFINDER SIGNAL VAULT ===\n"
    f"Status: {vault_status}\n"
    f"Code: {vault_word}\n"
    f"Signals: {len(active_ids)} active / {len(source_signals)} total\n"
    f"Sectors: {sector_text}\n"
    f"Symbols: {symbol_text}"
)

If the text still differs, compare repr(artifact) with repr(expected_artifact) to expose hidden spaces or newlines.

7. Keep debugging evidence

Preserve one real failed assertion or exception. A useful record connects the wrong collection decision to the observed value rather than saying only “the code did not work.”

Failure Exact evidence One hypothesis Controlled change Verified rerun
What was the first blocked check? Include actual/expected values or the final exception line. Which key, position, mutation, or collection operation explains it? What one line changed? Which unchanged group now passes?

Good puzzle evidence could include:

  • appending a symbol set and creating a nested list instead of updating a set;
  • counting only active signals instead of all sector records;
  • directly traversing a dictionary and expecting complete records;
  • applying only the strength sort and losing the explicit tie policy;
  • unpacking clue fields in the wrong order; or
  • mutating a source symbol set while combining membership.

8. Test the echo-signal variation

After the ordinary challenge passes from a clean state, append this record to a new outer list—not to source_signals:

variation_signals = source_signals.copy()
variation_signals.append(
    {
        "id": "SIG-ECHO",
        "sector": "Vela",
        "strength": 70,
        "status": "inactive",
        "clue": (4, "?"),
        "symbols": {"star", "echo"},
    }
)

Copy your implementation to a new section, use variation_signals as its input, and recalculate every derived collection from empty state. Verify:

assert source_signals == source_snapshot
assert len(variation_signals) == 6
assert "SIG-ECHO" in signals_by_id
assert active_ids == ["SIG-N", "SIG-O", "SIG-A", "SIG-V"]
assert active_symbols == {"key", "moon", "star", "sun"}
assert sector_counts == {"Lyra": 2, "Orion": 2, "Vela": 2}
assert sorted_clues == [(0, "N"), (1, "O"), (2, "V"), (3, "A")]
assert vault_word == "NO" + "VA"
assert ranked_ids[-1] == "SIG-ECHO"

Explain why the lookup, total record count, Vela count, and ranking change while the active order, required-symbol readiness, clue order, and decoded word do not.

9. Compare with a solution path

Reveal after your checks pass or all three hints have been used
from operator import itemgetter

# Use source_signals, source_snapshot, and required_symbols from the starter.
signals_by_id = {}
active_ids = []
active_symbols = set()
sector_counts = {}
clue_pairs = []

for signal in source_signals:
    signal_id = signal["id"]
    sector = signal["sector"]

    signals_by_id[signal_id] = signal
    sector_counts[sector] = sector_counts.get(sector, 0) + 1

    if signal["status"] == "active":
        active_ids.append(signal_id)
        active_symbols.update(signal["symbols"])

        code_position, code_letter = signal["clue"]
        clue_pairs.append((code_position, code_letter))

missing_symbols = required_symbols - active_symbols
vault_ready = required_symbols <= active_symbols

ranked_signals = sorted(source_signals, key=itemgetter("id"))
ranked_signals = sorted(
    ranked_signals,
    key=itemgetter("strength"),
    reverse=True,
)

ranked_ids = []
for signal in ranked_signals:
    ranked_ids.append(signal["id"])

sorted_clues = sorted(clue_pairs)
vault_letters = []
for code_position, code_letter in sorted_clues:
    vault_letters.append(code_letter)

vault_word = "".join(vault_letters)
vault_status = ("SEALED", "OPEN")[vault_ready]

sector_text = (
    f"Lyra={sector_counts['Lyra']}, "
    f"Orion={sector_counts['Orion']}, "
    f"Vela={sector_counts['Vela']}"
)
symbol_text = ", ".join(sorted(active_symbols))

artifact = (
    "=== WAYFINDER SIGNAL VAULT ===\n"
    f"Status: {vault_status}\n"
    f"Code: {vault_word}\n"
    f"Signals: {len(active_ids)} active / {len(source_signals)} total\n"
    f"Sectors: {sector_text}\n"
    f"Symbols: {symbol_text}"
)

print(artifact)

The source list provides authoritative order. The lookup and counts include all records; the active list, symbol set, and clue tuples include only active records. Two stable ranking passes preserve an explicit ID tie rule. The word is derived from sorted fixed-position clues, and display sorting is applied only to a new symbol list.

10. Check your understanding

Answer these questions before recording the challenge. The quiz runs directly in your browser.

11. Decide whether the challenge is complete

Evidence rubric

Evidence Ready to record when
Behavior All twenty progressive checks pass unchanged and the artifact displays exactly.
Collection design You can explain why each derived view is a list, tuple, dictionary, set, or frozenset.
Source preservation The source snapshot passes before and after ranking and variation work.
Debugging One failure record connects exact evidence, one hypothesis, one change, and a passing rerun.
Reproducibility The ordinary challenge and variation work from separately initialized clean state.

Record completion only when every statement is true:

This button stores a self-reported marker only in this browser. It does not submit the artifact, grade it, verify identity, or issue a certificate.

Not yet recorded.

Key points

TipKey points
  • One ordered source can support keyed, grouped, unique, ranked, and display views when every view has a named purpose.
  • Progressive assertions reveal the first broken collection contract before a plausible final string hides it.
  • Stable sorting and complete tuple/record movement preserve relationships among tied or repositioned values.
  • Sets answer unique-membership questions; frozen sets can state immutable requirements; lists preserve the orders the puzzle actually needs.
  • A clean rerun, boundary variation, and debugging record are part of the solved artifact.
Back to top