FreeCampus Python

Unit Challenge: Wake the Clockwork Observatory

Derive an observatory wake-up code by decomposing a signal puzzle into frequency, selection, window-search, and running-total algorithms.
python-foundations problem-solving-algorithms unit-challenge puzzle
Open in Colab
  • Level: Python Foundations · Unit 7 challenge
  • Estimated time: 60–120 minutes
  • Outcome assessed: Specify, decompose, implement, and defend several cooperating algorithms.
  • Evidence: Passing progressive assertions, a wake-up code, cost notes, a clean rerun, and one debugging record

1. Wake the observatory with a code you derive

The hilltop clockwork observatory has slept through three meteor showers. Its brass dome will move only when a signal calibrator produces a three-part code:

<sync position>-<power step>-<anchor symbol>

You receive a symbol signal and matching energy readings:

signal = "AABCDEFAC"
energy_readings = [2, 1, 3, 2, 4, 1, 5, 2, 1]
window_width = 4
power_target = 10

Your program must discover:

  1. where the first all-distinct symbol window ends;
  2. when accumulated energy first reaches the target; and
  3. which symbol occurs most often, with first appearance winning a tie.

For the supplied data, the finished program should reveal:

Observatory awake: 5-5-A

You will choose the state and conditions that produce that result. The page supplies names, docstrings, staged assertions, three hints, and a hidden solution for use after a serious attempt.

NoteWork independently, but not without support

Plan for one or two focused hours. Read one contract, make the matching small check pass, and rerun earlier checks before moving forward. Open a hint only after writing down the exact value or condition that blocks you.

2. Read the signal rules and acceptance examples

Frequency and anchor rules

  • Count every symbol exactly as written.
  • The anchor is the symbol with the greatest count.
  • If counts tie, the symbol that appears earlier in the signal wins.
  • An empty signal has no anchor, represented by None.
assert {"A": 3, "B": 1, "C": 2, "D": 1, "E": 1, "F": 1} == {
    "A": 3,
    "B": 1,
    "C": 2,
    "D": 1,
    "E": 1,
    "F": 1,
}

# A and B both occur twice; A appears first.
tie_signal = "ABBA"
tie_anchor = "A"

The first assertion only confirms the hand-counted mapping is well formed. Your function checks begin in Section 5.

Synchronization-window rules

A window is a consecutive slice with exactly window_width symbols. It is synchronized when every symbol inside it is distinct. Return the 1-based end position, which is also the number of symbols processed when the window ends. Return None if no such window exists.

For "AABCDEFAC" and width 4:

End position Window All distinct?
4 AABC no
5 ABCD yes

The synchronization result is therefore 5.

Power rules

Add readings from left to right. Return the 1-based step where the running total first becomes greater than or equal to the target. Return None if all readings are consumed without reaching it.

Step Reading Running total Reached 10?
1 2 2 no
2 1 3 no
3 3 6 no
4 2 8 no
5 4 12 yes

The power result is 5. Stop there because the contract asks for the first reached step.

Input and output constraints

  1. signal is a finite string.
  2. readings is a finite list of non-negative numbers.
  3. width and target are positive integers.
  4. Functions must not modify supplied inputs.
  5. Use the supplied names and returned shapes.
  6. Do not hard-code 5, 5, A, or the final code.
  7. If the anchor, synchronization, or power result is missing, the complete code is None.
  8. Explain time and extra-space growth in plain language after the checks pass.

Before implementation, add one acceptance example of your own for each:

  • a frequency tie;
  • a window that never synchronizes; and
  • an energy sequence that never reaches its target.

3. Start from the contract

Run this cell unchanged. Replace each pass while preserving the public names and docstrings.

def symbol_frequencies(signal):
    """Return a dictionary that counts every symbol in signal."""
    pass


def choose_anchor(signal, frequencies):
    """Return the most frequent symbol; the first appearance wins a tie."""
    pass


def first_sync_end(signal, width):
    """Return the 1-based end of the first all-distinct window, or None."""
    pass


def first_power_step(readings, target):
    """Return the 1-based step where the running total reaches target, or None."""
    pass


def build_wakeup_code(signal, readings, width, target):
    """Return '<sync>-<power>-<anchor>', or None if calibration cannot finish."""
    pass

Use this responsibility table before writing loops:

Function Result shape State to consider May stop early?
symbol_frequencies dictionary counts by symbol no
choose_anchor symbol or None best symbol/count yes only for empty input
first_sync_end integer or None candidate window/end yes on first match
first_power_step integer or None running total/step yes on reached target
build_wakeup_code string or None returned helper results after either failed result

You may implement choose_anchor by traversing signal and consulting the frequency mapping. That makes encounter-order tie behavior visible without sorting the dictionary.

4. Calibrate one mechanism at a time

Stage A: count symbols

Start with empty and repeated inputs. Do not work on anchor selection until both frequency checks pass.

A useful trace for "ABACA" has columns for current symbol and mapping after the update.

Stage B: select the anchor

Keep the current anchor when a candidate’s count is equal. Replace only when the candidate count is strictly greater. Repeated visits to the same symbol should not change the result.

Stage C: find synchronization

Name the meaning of end before coding. For an end-exclusive Python slice, signal[end - width:end], the same end is already the promised 1-based count of processed symbols.

Stage D: accumulate power

Use enumerate(readings, start=1) or convert a zero-based index deliberately. Add the current reading before asking whether the target has been reached.

Stage E: assemble the code

Call the four helpers. If the sync result, power result, or anchor is None, return None. Otherwise return one formatted string.

After every stage:

  1. run its smallest assertion;
  2. trace actual state if it fails;
  3. change one condition or update;
  4. rerun the stage; and
  5. rerun all earlier stages.

5. Run progressive assertions

Do not change these expected values merely to make a check pass. If code and expectation disagree, compare both with the written rules and hand traces.

Symbol-frequency evidence

assert symbol_frequencies("") == {}
assert symbol_frequencies("A") == {"A": 1}
assert symbol_frequencies("ABACA") == {"A": 3, "B": 1, "C": 1}
assert symbol_frequencies(signal) == {
    "A": 3,
    "B": 1,
    "C": 2,
    "D": 1,
    "E": 1,
    "F": 1,
}

Anchor and tie evidence

assert choose_anchor("", {}) is None
assert choose_anchor("A", {"A": 1}) == "A"
assert choose_anchor("ABACA", {"A": 3, "B": 1, "C": 1}) == "A"
assert choose_anchor("ABBA", {"A": 2, "B": 2}) == "A"
assert choose_anchor("BAAB", {"B": 2, "A": 2}) == "B"

Synchronization evidence

assert first_sync_end("Z", 1) == 1
assert first_sync_end("ABCD", 4) == 4
assert first_sync_end("AABCDEF", 4) == 5
assert first_sync_end("AAAA", 2) is None
assert first_sync_end("ABC", 4) is None
assert first_sync_end(signal, window_width) == 5

Running-power evidence

assert first_power_step([5], 5) == 1
assert first_power_step([2, 3, 1], 5) == 2
assert first_power_step([2, 3, 1], 7) is None
assert first_power_step([], 1) is None
assert first_power_step(energy_readings, power_target) == 5

Complete wake-up evidence

assert build_wakeup_code("ABCD", [2, 3], 4, 5) == "4-2-A"
assert build_wakeup_code("AAAA", [2, 3], 2, 5) is None
assert build_wakeup_code("ABCD", [1, 1], 4, 5) is None
assert build_wakeup_code(signal, energy_readings, window_width, power_target) == (
    "5-5-A"
)

Ownership and changed-input evidence

readings_before = energy_readings.copy()
signal_before = signal

code = build_wakeup_code(signal, energy_readings, window_width, power_target)

assert code == "5-5-A"
assert energy_readings == readings_before
assert signal == signal_before

Add one complete assertion with a new signal, new readings, a different width, and a different target. Calculate its expected code on paper first.

6. Use the hint ladder only when needed

Hint 1

Match each helper to a pattern: frequency table, running-best selection, fixed-size window search, running-total search, and function composition.

Hint 2

For frequencies, update frequencies.get(symbol, 0) + 1. For the anchor, scan the original signal and replace only when a symbol’s count is strictly greater than the current anchor’s count. For synchronization, compare len(set(window)) with width. For power, add before checking the target.

Hint 3

Use these incomplete shapes:

for symbol in signal:
    frequencies[symbol] = ...

for end in range(width, len(signal) + 1):
    window = signal[end - width : end]
    if ...:
        return end

for step, reading in enumerate(readings, start=1):
    total += reading
    if ...:
        return step

Every search also needs a return after the loop for the no-result path.

7. Keep debugging evidence

Keep one failure that helped you understand the puzzle. Record facts before changing code.

Failure Actual evidence Single-cause hypothesis Controlled change Verified rerun
What failed? Exact returned value, state, or message Which one condition/update fits? What one thing changed? Which focused and earlier checks pass?

Useful failures include:

  • returning a zero-based start instead of a 1-based end;
  • replacing the anchor on an equal frequency and therefore choosing the last tied symbol;
  • checking power before adding the current reading;
  • returning None from inside a loop after the first non-match; or
  • constructing a code containing the text None instead of returning None.

Restart the notebook or Python process and run every cell from the contract through the complete assertions. A result that depends on hidden state is not a finished solution.

8. Compare with a complete solution

Show the complete implementation after attempting every stage
def symbol_frequencies(signal):
    """Return a dictionary that counts every symbol in signal."""
    frequencies = {}
    for symbol in signal:
        frequencies[symbol] = frequencies.get(symbol, 0) + 1
    return frequencies


def choose_anchor(signal, frequencies):
    """Return the most frequent symbol; the first appearance wins a tie."""
    anchor = None
    for symbol in signal:
        if anchor is None or frequencies[symbol] > frequencies[anchor]:
            anchor = symbol
    return anchor


def first_sync_end(signal, width):
    """Return the 1-based end of the first all-distinct window, or None."""
    for end in range(width, len(signal) + 1):
        window = signal[end - width : end]
        if len(set(window)) == width:
            return end
    return None


def first_power_step(readings, target):
    """Return the 1-based step where the running total reaches target, or None."""
    total = 0
    for step, reading in enumerate(readings, start=1):
        total += reading
        if total >= target:
            return step
    return None


def build_wakeup_code(signal, readings, width, target):
    """Return '<sync>-<power>-<anchor>', or None if calibration cannot finish."""
    frequencies = symbol_frequencies(signal)
    anchor = choose_anchor(signal, frequencies)
    sync_end = first_sync_end(signal, width)
    power_step = first_power_step(readings, target)

    if anchor is None or sync_end is None or power_step is None:
        return None
    return f"{sync_end}-{power_step}-{anchor}"

Why the main decisions work:

  • choose_anchor scans in encounter order and replaces only for a strictly larger count, so an equal count keeps the earlier symbol.
  • end begins at width; signal[end - width:end] has the requested width, and end is already the 1-based number of processed symbols.
  • first_power_step adds the current reading before comparing with the target, so the returned step includes the reading that reaches it.
  • The composition function returns None rather than formatting an incomplete code.

For a signal of length n, frequency counting and anchor selection each grow linearly. With fixed window width w, the window search checks up to about n windows and builds a set of up to w symbols for each, so its time is O(n × w) and its temporary window space is O(w). Power search is linear in its number of readings in the worst case. The frequency dictionary uses space proportional to the number of distinct symbols.

9. Predict a changed calibration rule

The observatory engineer proposes two changes:

  1. on an anchor-frequency tie, choose the later symbol; and
  2. return the zero-based window start instead of the 1-based end.

Before editing code, answer:

  • Which acceptance examples change?
  • Which comparison changes from strict to inclusive, and why can repeated visits complicate that shortcut?
  • How is a start index calculated from the current end and width?
  • Does the wake-up code for the original input change?
  • Which docstrings and cost statements remain valid?

A robust later-wins anchor strategy should compare distinct candidates or track the last position deliberately; blindly replacing on every equal count also replaces an anchor when the loop sees the same symbol again. Write a small tie trace before implementing the changed rule.

10. Check your understanding

11. Decide whether the challenge is complete

Evidence rubric

Evidence Ready to record when
Behavior All supplied assertions and one learner-created case pass unchanged.
Decomposition Each public function has one named responsibility and returned shape.
Reasoning Tie, index, stopping, no-result, time, and extra-space decisions can be explained.
Debugging One failure record connects exact evidence to one change and verified reruns.
Reproducibility The complete observatory program works from a clean notebook state.

This button stores a self-reported marker only in this browser. It does not submit or grade the program.

Not yet recorded.

Key points

  • The observatory puzzle becomes manageable when counting, selection, window search, accumulation, and composition have separate contracts.
  • Progressive assertions reveal which responsibility is wrong without revealing the implementation.
  • Tie rules, index meanings, stopping behavior, and no-result values belong in examples before they become conditions.
  • Correct behavior, a clean rerun, and a plain-language cost explanation are all part of the finished solution.
Back to top