Skip to content

Build a Two-Player Buzzer Lockout for a Small TV-Format Trial

Television

Build a Two-Player Buzzer Lockout for a Small TV-Format Trial

A buzzer lockout looks like a measuring instrument and behaves, in a small trial, more like an admissions desk. It decides which of two switches is allowed to speak next. It cannot tell you who moved first, and if you ask it to, it will return an answer that looks like a measurement and isn't one.

The build below is one local controller — a micro:bit, two player switches, one display — with three phases and two ways to leave a round: an admitted player, or an unresolved observation. Both terminal outcomes need the operator to reset before the next round. Everything else about the round, including whether the answer was right and what it earned, stays with the humans.

Define what admission does in this round

Two eligible players, one switch each: player 1 on pin 1, player 2 on pin 2. The operator's opening action is a deliberate arm request made on the controller board itself, using a button the players can't reach.

An admitted response does exactly one thing: it names the player who now has the next answer attempt, and it holds that name on the display until the operator resets. The controller doesn't judge the answer, start a clock, award a point, or decide whether the question was fair. If your format hasn't settled what an admitted input changes — the next attempt, the chance to answer, a point, a penalty — settle that before you wire anything. The controller can hand over a name; what the name is worth is a rules decision and it has to exist before the trial, not after.

Here is the whole behaviour as a table. Treat it as the specification you'll test against.

Phase Operator asks to arm A fresh press appears A switch already held Operator resets
Waiting (w) Opens the window only if both switches read released, twice, 20 ms apart. Otherwise refused; stays waiting. Ignored. Waiting admits nothing. Blocks arming until released. Stays waiting; current switch levels become the baseline.
Armed (r) Ignored; the window is already open. One player fresh: latched, display shows 1 or 2. Both fresh in the same observation: latched unresolved, display ?, no player admitted. Cannot occur — the window only opens with both released. Returns to waiting.
Latched (1, 2, ?) Ignored. Ignored, and not recorded anywhere. Ignored. On reset, a held switch becomes the baseline. Returns to waiting.

Two things follow from that table. A response window is not a score state; it's an open question about who gets to talk. And both terminal outcomes put the display in front of the operator rather than the players, which is where the next decision lives.

One local controller, and read both inputs before you decide

The route: a micro:bit running MicroPython, two momentary push-to-make switches each wired from its pin to ground, the internal pull-up enabled so an open switch reads 1 and a pressed switch reads 0, and the 5×5 display as the state and player indicator. The operator's controls are the board's own A and B buttons — arm and reset, both physically on the controller, out of the players' reach.

Write your board revision and your MicroPython version into your notes the first time you run it, next to the results. Pin behaviour and the display API are not identical across micro:bit revisions, and a result you can't attribute to a version is a result you'll have to take again. One more piece of housekeeping: don't call your players A and B if your operator buttons are A and B. By the third test case your own notes will betray you.

Now the part that matters. Read both pins into variables, then decide what to do. A branch structure like this looks fine and quietly makes a decision for you:

if pin1.read_digital() == 0:
    admitted = '1'
elif pin2.read_digital() == 0:
    admitted = '2'

When both inputs read active in the same pass, that code names player 1 and says nothing about having done so. The Micro:bit Educational Foundation's Reaction game project has the same shape — its "How it works" section and the visible Python listing, lines 4–15, which I read on 18 September 2026 (the page had also been inspected earlier, on 13 September), check pin 1 before pin 2 with an if/elif. That is a reading of the published listing, not a hardware test, and for stipulated active inputs the first checked branch wins.

Reading both pins into variables first does not make the reads simultaneous. They are still separated by however long the interpreter takes between them, and that gap is real: an input that begins and ends inside it can be missed entirely. What storing both reads buys you is that priority becomes a decision you made in the open instead of one your branch order made on your behalf.

Require release before arming, and a fresh press afterward

Waiting holds while either switch is down, and an arm request made while a switch is held is refused — the operator asks again once the players let go. A fresh press is one thing only: pressed now, and not pressed in the previous pass.

Why refuse instead of deferring? If the arm request sits pending and the window opens by itself when the last switch is released, then the moment the window opens is chosen by a player's hand rather than the operator's. That defeats the point of an explicit opening action.

The published listing has no release step before its gate opens. A player who holds the switch down before the gate opens is already active when the gate opens, so the gate itself is what triggers their press. The Foundation's description of the game is more confident about early presses than its listing supports; release-before-arm is the addition that makes that description true of your build, and it costs you one extra button press per round.

Latch the result, and make ambiguity visible

One fresh input latches, displays its player's number, and closes the round to everything else until reset. Two fresh inputs in one observation latch as unresolved: display ?, admit neither. Reset returns to waiting, and a switch that is still held at that moment becomes the baseline rather than a press.

The unresolved outcome is the part people want to argue about, so here is the reasoning. If both switches read active in one sample, the controller has exactly one fact: at the moment of sampling, both were down. Whether player 1 was three milliseconds earlier is precisely what the sample doesn't contain. Naming player 1 because pin 1 is checked first is a preference written into branch order, not a finding.

And the honest caveat, which belongs in your trial notes: "together" is not a physical event the controller can detect. Two presses a millisecond apart may land in one observation; two presses ten milliseconds apart may land in two consecutive observations. The controller reports observation patterns, not physical gaps, and the report depends on where the sampling boundary fell. There is more on this in the last section.

When ? appears, the operator applies whatever rule the format already decided — a tiebreak question, a coin, replay the round. If the format has no such rule, that's a gap in the format, and a tabletop is a much cheaper place to find it than a studio. What the controller must not do is pick a player, and what it must not do is hide the ambiguity by letting the earlier press through as if the later one had never happened.

Here is the whole controller.

from microbit import *

# Player switches on pins 1 and 2, each wired from its pin to GND.
pin1.set_pull(pin1.PULL_UP)
pin2.set_pull(pin2.PULL_UP)

WAITING = 0
ARMED = 1
LATCHED = 2

state = WAITING
admitted = None      # None, '1', '2', or '?'


def s1():
    return pin1.read_digital() == 0     # pressing pulls the pin low


def s2():
    return pin2.read_digital() == 0


def draw():
    if state == WAITING:
        display.show('w', wait=False)
    elif state == ARMED:
        display.show('r', wait=False)
    else:
        display.show(admitted, wait=False)


prev1 = s1()
prev2 = s2()
draw()

while True:
    now1 = s1()
    now2 = s2()

    if button_b.was_pressed():                 # operator reset
        state = WAITING
        admitted = None
        prev1 = now1                           # a held switch is a baseline, not a press
        prev2 = now2
        draw()

    elif state == WAITING:
        if button_a.was_pressed() and not now1 and not now2:
            sleep(20)                          # let a dirty contact settle
            if not s1() and not s2():
                state = ARMED
                admitted = None
                prev1 = False                  # both just read released
                prev2 = False
                draw()

    elif state == ARMED:
        fresh1 = now1 and not prev1
        fresh2 = now2 and not prev2
        if fresh1 or fresh2:
            state = LATCHED
            if fresh1 and fresh2:
                admitted = '?'                 # both seen in one observation
            elif fresh1:
                admitted = '1'
            else:
                admitted = '2'
            draw()

    # LATCHED: no admission path. The display holds until reset.

    prev1 = now1
    prev2 = now2

A few notes on it. The prev values are updated at the end of every pass, so "fresh" always means "new since the last observation." Both baseline writes above — the one at reset and the one at arm time — are inert in this listing, and you can satisfy yourself of that from the code rather than from a board. For the reset write: now1 and now2 are that same pass's reads, so the end-of-pass update re-writes the identical values. For the arm write: the gate has just required both switches to read released, so setting prev1 = False / prev2 = False sets exactly what now1 and now2 already are. Keep them if they help you see the intent at the point of the state change, or delete them for tidiness; the trace below comes out the same either way.

What is load-bearing at arm time is the gate itself — button_a.was_pressed() and not now1 and not now2, followed by the 20 ms confirm — because that pair is what makes arming mean "both switches released, twice, 20 ms apart." That pair you should not delete. If you ever move the end-of-pass update, say so that it runs only while ARMED, then the arm-time baseline stops being inert, since the pass that opens the window goes through the WAITING branch and never reaches the ARMED one; nothing else would set the baseline for the following pass. The reset-time write stays inert under that change too: arming always rewrites the baseline from current reads.

display.show(..., wait=False) keeps a display write from stalling the loop. Check that on your build: if the display call blocks, the window opens late by however long it blocks, and you'd rather discover that with a stopwatch than on a shooting day. Reset wins if the operator somehow presses both buttons in the same pass; that ordering is a choice, and it's worth writing down.

Walk the trace before you touch hardware

Everything below uses stated input levels. The switches are invented for the example; I'm telling you what they do, and nothing here is a recording of anything.

Round 1

  1. Waiting, both switches released. Display w.
  2. Player 1 holds their switch down. Still waiting. Display w.
  3. The operator asks to arm. Refused: player 1's switch reads pressed. The window stays closed.
  4. Both switches released.
  5. The operator asks again. Both read released, twice, 20 ms apart. Armed. Display r. Baseline: both released.
  6. Player 2 presses. Observed: switch 1 released, switch 2 pressed. Fresh on switch 2 only. Latched. Display 2.
  7. Player 1 presses. The round is latched, so it's ignored. Display stays 2.
  8. Reset. Back to waiting. Display w.

Player 2's input is the admitted input for round 1. Notice what the branch-order question did here: nothing. Player 1 was not active at step 6, so a first-checked-pin rule and a read-both-then-decide rule agree. The two rules only part company when both switches read active in the same observation, which is why round 2 is the interesting one.

Round 2

  1. Both switches held down. The operator resets. Waiting, and both switches are recorded as already down, so a held switch can't be mistaken for a press.
  2. Both released. The baseline updates to both released.
  3. The operator arms. Display r.
  4. Both switches are pressed as close together as the players can manage. If the controller's two reads both see them down in the same observation: latched unresolved, display ?, no player admitted. If the first press lands in one observation and the second in the next, the earlier one is admitted and shown as 1 or 2.
  5. Reset.

Same stipulated intent from both players, two different reported outcomes, depending on where the observation boundary fell. Where the Foundation's listing would name the pin-1 player in the both-active case, this build reports ?, and the price of that is that presses a few milliseconds apart sometimes come back unresolved and the operator has to reach for a tiebreak rule they may never have written down. That's the trade. It is the right one for a trial, where a wrong name is worse than an unresolved round.

Run the failure cases on the build

The expected column below comes from the state table, not from a board. Everything in this section is a script for your bench; it isn't a report from anyone's.

Case How to produce it Expected What to record
Held before arm Hold player 1's switch, ask to arm Display stays w, no window opens; release, ask again → r Whether the first request was refused and whether the second opened the window
Ordinary admission Arm, then player 2 presses 2, held until reset The display, and whether later presses changed it
Later input With 2 latched, player 1 presses Display stays 2 That nothing changed and no record appeared
Both observed together Reset, release both, arm, press both as close to simultaneous as you can, ten times ? when both appear in one observation; 1 or 2 when they appear in successive ones How many of the ten gave ?. That count is your sampling-gap number.
Contact bounce Release slowly; try a light, scraping press during the window Usually the same as an ordinary press; occasionally a latch nobody intended Any latch that arrived without a deliberate press
Reset while held Latch a player, keep both switches held, reset, then release, arm, and press one Waiting at reset, r after the second arm, then that player's number That reset returned to w and the held switch did not latch

Keep two columns for the duration of the trial: what the table says, and what the board did. When they disagree, the bug is almost always in your input reading or your reset path, not in the game — inspect the controller before you start rewriting the round's rules. Case four is the one worth running more than ten times, because it's the only case whose outcome is genuinely a property of your sampling rate rather than your state machine.

What this demonstrates at the end is narrow, and useful: for each round, which switch the controller admitted, when the window opened, and when it needed the operator's hand. What it does not demonstrate is who reacted first, how large the physical gap between two presses was, whether the round was fair, or how the thing would behave in front of cameras. No reaction-time figure and no fairness claim survives contact with a polled input, so keep them out of the write-up entirely.

Say instead what is true. The controller samples both switches once per pass, decides from stored readings, and reports the input it admitted. Two inputs within one sampling gap come back unresolved, and an input that begins and ends inside the gap between the two reads can be missed. Contact bounce can produce a press nobody intended, and the 20 ms settle at arming reduces that without removing it. The display shows the admitted input, not the fastest contestant. Put that paragraph in the same document where you explain the round to your producers, not in an appendix — if the format can't survive a round that comes back unresolved, better to know that before anyone builds a set around it.

Frequently asked questions

What does the buzzer lockout actually decide?

It decides which of two switches is allowed to speak next. An admitted response names the player who has the next answer attempt and holds that name on the display until the operator resets. The controller does not judge the answer, start a clock, award a point, decide fairness, or tell you who moved first; what an admitted input changes must be settled as a rules decision before the trial.

Why require release before arming, and what counts as a fresh press?

Waiting holds while either switch is down, and an arm request made while a switch is held is refused; the operator asks again after release. Arming opens only when both switches read released, twice, 20 ms apart. A fresh press is pressed now and not pressed in the previous pass. If an arm request waited and the window opened when the last switch was released, a player's hand, not the operator's deliberate action, would choose the opening moment.

Why does the controller show ? instead of picking player 1 when both switches are pressed?

If both switches read active in one observation, the controller has exactly one fact: both were down at the sampling moment. Whether one player pressed three milliseconds earlier is precisely what the sample does not contain. Naming player 1 because pin 1 is checked first is a preference written into branch order, not a finding. The controller reports observation patterns, not physical gaps, so the operator must apply the format's tiebreak rule.

What should be recorded in the failure-case run?

Keep two columns for the duration of the trial: what the state table says and what the board did. Cases include a switch held before arm, ordinary admission, a later input while latched, both switches observed together ten times, contact bounce, and reset while held. The both-observed-together count of ? outcomes is the sampling-gap number. When table and board disagree, inspect the input reading or reset path before rewriting the round's rules.

What can and cannot be claimed after the build is tested?

It can show which switch the controller admitted, when the window opened, and when it needed the operator's hand. It cannot show who reacted first, the size of a physical gap between presses, whether the round was fair, or behavior in front of cameras. Two inputs within one sampling gap come back unresolved, an input can begin and end inside the gap between reads, and contact bounce can produce an unintended press. The display shows the admitted input, not the fastest contestant.

More in Television Browse all articles