Both Lamps Lit: Right-of-Way as a Priority Encoder
Both lamps lit. Mine and hers, near enough to the same instant that the scoring box couldn’t tell us apart — a green light, a red light, and no electrical way to say who deserved the touch. In épée that’s simple: both hits count, everyone bleeds. Foil is fussier. The box only reports that we both landed; a human referee decides who scores, and the rule they apply is right-of-way — priority.
Priority is the whole point. Whoever starts a legitimate attack owns the touch until the other fencer does something specific to take it away: a parry, which cancels the attack, immediately followed by a riposte, which hands priority to the defender. The referee isn’t judging who hit harder or first. They replay the phrase in their head and track one question: at the moment both lamps lit, who held priority?
That’s an arbiter, and hardware people have built these forever. When two signals contend for one bus, or eight interrupts arrive together, something has to impose an order. The 1970s gave us the 74148 — an 8-to-3 priority encoder. Feed it a fistful of simultaneous requests and it outputs the index of the highest-ranked one, discarding the rest. A foil referee is that chip with a moustache.
Here’s the phrase from one of my (losing) exchanges, resolved the way the referee resolves it — walk the actions, track who holds priority:
def right_of_way(phrase):
holder = None
for actor, action in phrase:
if action == "attack" and holder is None:
holder = actor
elif action == "parry":
holder = None
elif action == "riposte":
holder = actor
return holder
phrase = [("L", "attack"), ("R", "parry"), ("R", "riposte")]
print("priority:", right_of_way(phrase) or "simultaneous")
# -> priority: R
L attacks and seizes priority. R parries, which nulls it. R ripostes and takes priority for herself — so when both lights come up a beat later, the touch is hers. Which is what happened, repeatedly, to me.
const std = @import("std");
const Action = enum { attack, parry, riposte };
const Move = struct { fencer: u8, action: Action };
pub fn main() void {
const phrase = [_]Move{
.{ .fencer = 'L', .action = .attack },
.{ .fencer = 'R', .action = .parry },
.{ .fencer = 'R', .action = .riposte },
};
var holder: ?u8 = null;
for (phrase) |m| switch (m.action) {
.attack => if (holder == null) { holder = m.fencer; },
.parry => holder = null,
.riposte => holder = m.fencer,
};
std.debug.print("priority: {c}\n", .{holder orelse '-'});
}
Same walk, no allocator — the kind of thing you’d actually run inside a scoring box. Note what neither version consults: speed. The counter-attack, hitting into an attack without first dealing with it, lands a light and earns nothing. My whole problem today was starting lunges with my legs, which a referee reads as a counter-attack: a hit with no priority behind it. The lamp lights. The point goes the other way.