DAEDALUS
LOCAL TIME --:--:--LOCATION NOVGOROD STATIONWEATHER 15°C · CLEAR ☼

PROJECT 10 // A KEY WHOSE SHAPE IS A WAVEFORM

SINE

A ten-level puzzle game where the player reconstructs a key by tuning sine waves: amplitude, frequency, and phase do not decorate the key, they cut it. Written entirely in ArcoBASIC and compiled by ArcoFission to a self-contained WebAssembly page.

STATUS ActivePROGRESS 100%

WEB CAPSULE // RUNS ENTIRELY IN YOUR BROWSER, NO SERVER INVOLVED

PLAY SINE

OPEN FULL SCREEN ↗

The waveform is the object, not a picture of it

SINE's key is not a graph with a key drawn next to it. Its actual cut silhouette is the combined waveform: `profile(x) = sum(amplitude[n] * sin(frequency[n] * x + phase[n]))`, sampled once per pixel column and rendered as solid key material below that sampled curve. Drag a slider, the sample values change, the silhouette changes, and whether the key fits the lock changes with it -- the same causal chain, always, never a game wearing a graph-matching exercise as a costume.

The lock's required profile is generated by the exact same function, just with the level's correct oscillator values instead of the player's. Target and player are never two different systems that happen to look similar; they are one system evaluated twice, which is what makes the puzzle physically honest.

Filling a curve with no polygon-fill primitive

ArcoBASIC's GUI drawing primitives cover rectangles, rounded rectangles, circles, lines, and text -- there is no general filled-polygon call. A solid key body with a wavy cut top is rendered as many adjacent one-pixel-wide vertical columns, each running from the sampled curve down to the blade's fixed bottom edge: the same bar-under-a-curve technique a spreadsheet uses to shade the area under a line chart, just applied at pixel resolution to produce a sharp, physical-looking silhouette instead of a plotted curve.

ArcoBASICArcoBASIC: the waveform IS the cut, sampled per pixel column
' The key's cut profile is not a decoration of the waveform. It IS the waveform,
' sampled once per position and turned directly into a cut depth.
FUNCTION SampleProfile(oscillators, position)
    total = 0
    FOR i = 0 TO LEN(oscillators) - 1
        osc = oscillators[i]
        phaseRadians = osc.Phase * 3.14159265 / 180
        total = total + osc.Amplitude * SIN(osc.Frequency * position * 6.2831853 + phaseRadians)
    NEXT i
    RETURN total
END FUNCTION

' A key blade cut from two combined signals: a broad wave and a fine one.
blade = [
    {Amplitude: 0.6, Frequency: 2.0, Phase: 0, Enabled: TRUE},
    {Amplitude: 0.25, Frequency: 7.0, Phase: 90, Enabled: TRUE}
]

FOR sample = 0 TO 4
    position = sample / 4
    depth = ROUND(CLAMP(SampleProfile(blade, position), -1, 1) * 1000) / 1000
    PRINT "position " + STRING(position) + " -> cut depth " + STRING(depth)
NEXT sample

One function, asked the same question twice

The match percentage compares the target profile and the player's profile at identical sample points and reports how close the average deviation is to zero. Because both curves come from the same `SampleProfile` function, a "wrong" key is never wrong for an arbitrary or hidden reason -- it is wrong by a specific, visible amount, at specific positions along the blade.

ArcoBASICArcoBASIC: target and player scored by the same function
' The lock and the key are built from the same math, so the puzzle is physically
' consistent: matching the wave is the only way to match the cut.
FUNCTION SampleProfile(oscillators, position)
    total = 0
    FOR i = 0 TO LEN(oscillators) - 1
        osc = oscillators[i]
        phaseRadians = osc.Phase * 3.14159265 / 180
        total = total + osc.Amplitude * SIN(osc.Frequency * position * 6.2831853 + phaseRadians)
    NEXT i
    RETURN total
END FUNCTION

FUNCTION ComputeMatch(target, player, sampleCount)
    totalError = 0
    FOR sample = 0 TO sampleCount - 1
        position = sample / (sampleCount - 1)
        targetDepth = CLAMP(SampleProfile(target, position), -1, 1)
        playerDepth = CLAMP(SampleProfile(player, position), -1, 1)
        totalError = totalError + ABS(targetDepth - playerDepth)
    NEXT sample
    meanError = totalError / sampleCount
    RETURN MAX(0, 1 - MIN(1, meanError / 0.5))
END FUNCTION

target = [{Amplitude: 0.7, Frequency: 3.0, Phase: 0, Enabled: TRUE}]

' A key cut with the wrong amplitude does not fit the lock.
tooShallow = [{Amplitude: 0.2, Frequency: 3.0, Phase: 0, Enabled: TRUE}]
PRINT "Wrong amplitude, match = " + STRING(ROUND(ComputeMatch(target, tooShallow, 200) * 1000) / 10) + "%"

' The same amplitude cuts a key that actually fits.
correct = [{Amplitude: 0.7, Frequency: 3.0, Phase: 0, Enabled: TRUE}]
PRINT "Matching amplitude, match = " + STRING(ROUND(ComputeMatch(target, correct, 200) * 1000) / 10) + "%"

Ten levels teaching one parameter at a time

The first three levels lock two of a single oscillator's three parameters and leave only amplitude, then only frequency, then only phase, adjustable -- the player learns what each control does in isolation before ever having to combine them. Later levels introduce a second and third signal, tighten the tolerance, and in one case lock an entire oscillator as a correct anchor the player has to work around rather than adjust.

A shared `Slider` widget -- drag to set a value coarsely, scroll to nudge it finely -- was added to ArcoBASIC's GUI standard library to build this, the first control of its kind there. It is available to any future ArcoBASIC GUI program now, not just this one.

One function, asked twice, is a design pattern

SINE's deepest idea is not sine waves. It is scoring a player's answer against a target by running both through the exact same function, so a mismatch is never ambiguous about its cause. That pattern is worth recognizing anywhere two things are supposed to match.

  • Whenever you need to compare a target and an attempt, generate both from one shared function instead of writing separate code paths for "the correct answer" and "the player's answer." Two independently written paths can silently drift out of agreement in ways a single shared function structurally cannot.
  • Teach one variable at a time before combining them. Locking two of three parameters in the earliest levels here, so only amplitude, then only frequency, then only phase changes, is the same instructional move as isolating one variable in any tutorial, code walkthrough, or onboarding flow.
  • Build the small reusable primitive the current feature needs, the Slider widget, as a real addition to the shared library rather than a one-off hack local to this game. The next feature that needs the same control gets it for free instead of reinventing it.

DAEDALUS_OS TERMINAL

DAEDALUS_OS v3.8.0

CONNECTED.

How can I help?

Technology should adapt to people.

Choose a perspective above or type help for commands.