The Flytrap Counts to Two
The flytrap on my windowsill will not close if you poke it once. I tried. It sat there, smug, lobes open. The trap fires only on the second bend of its trigger hairs — and only if that second touch lands within roughly twenty seconds of the first. A single raindrop or a bit of blown grit isn’t worth the enormous energy of snapping shut and re-opening. Two touches in quick succession, though, means something is walking around in there.
That’s a counter with a timeout, which is to say a finite state machine. Three states: OPEN, PRIMED (one touch registered, clock running), and SHUT. A touch moves you forward. Silence moves you back — the plant literally forgets. Electrophysiologists have measured this: the first mechanical stimulus fires an action potential, and the memory of it decays as calcium levels fall back down. Miss the window and you’re back to OPEN, waiting.
Here it is with the twenty-second window baked into the transitions:
enum class Trap { OPEN, PRIMED, SHUT }
fun step(s: Trap, touch: Boolean, sincePrime: Int) = when {
s == Trap.OPEN && touch -> Trap.PRIMED
s == Trap.PRIMED && touch && sincePrime <= 20 -> Trap.SHUT
s == Trap.PRIMED && sincePrime > 20 -> Trap.OPEN // memory fades
else -> s
}
fun main() {
var s = Trap.OPEN
val events = listOf(Triple(true, 0, "hair 1 bent"),
Triple(true, 8, "hair 2 bent"))
for ((touch, t, note) in events) {
s = step(s, touch, t)
println("t=${t}s $note -> $s")
}
}
t=0s hair 1 bent -> PRIMED
t=8s hair 2 bent -> SHUT
The same machine in Forth, which is the language I reach for whenever something is really just one variable and a handful of guarded transitions. This is the shape a lot of 1980s embedded control code took — a state cell, and words that poke it:
variable trap 0 trap ! \ 0 open 1 primed 2 shut
: touch ( elapsed -- )
trap @ 0 = if drop 1 trap ! exit then
trap @ 1 = if 21 < if 2 trap ! then exit then
drop ;
: .trap trap @ case
0 of ." OPEN" endof
1 of ." PRIMED" endof
2 of ." SHUT" endof
endcase cr ;
0 touch .trap \ first sensory hair bent
8 touch .trap \ second hair, 8 s later
Both versions treat elapsed the way the plant does: a value that only matters while PRIMED, ignored once the trap is shut. The interesting wrinkle is that the flytrap’s timeout isn’t a clean timer at all — it’s a chemical concentration bleeding away, so the real window stretches and shrinks with temperature. My Kotlin <= 20 is a lie of convenience. A warm afternoon buys the plant a longer memory; a cold morning, a shorter one. I haven’t decided yet whether to model that decay properly or leave the machine honest-but-crude. For now the trap on the sill and the trap in the code agree on the important part: one poke is nothing, two is lunch.