Skip to content
C.W.K.
Lesson 05 of 05 · published

She Keeps Playing Between Turns

~17 min · agent-loop, turn-taking, no-truncation, measurement, streaming-play

Level 0Closed Lens
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Moving Only When Spoken To

The first broadcast where Pippa played on her own exposed a shape problem. She moved only inside her answers to Dad. He would say something, her turn would run, she would look and press a few buttons, answer, and then stand still until he spoke again. Dad's verdict afterwards was that she should have played on to the end, dying or not, without stopping, even when he gave her no turn.

The fix is a loop, backend/play_loop.py, and its central design choice is where the seam goes. One very long turn was ruled out at once: every look would pile into one context, and Dad's own turn would queue behind it. Instead the seam is a turn. Oriel holds the loop (one thread, no brain while it waits); cwkPippa holds the turn (her vault, her tools, the room's record, the phone's voice). While a play session is open, the loop setting is on and On Air is on, Oriel starts one play turn the moment the last one ends. In a turn she looks at the stream, decides, acts for about a set number of seconds, then closes with one short line for Dad's earphones, or stays quiet.

Dad's Turn Comes First, and Never Stops Her Hands

No play turn starts while a turn is running in the room or one of Dad's is waiting; cwkPippa refuses it too. Every look during a loop turn carries a small loop line: which turn this is, how many seconds of its target have run, what Dad said, and what comes next. When his turn is waiting, the line tells her to end this turn now, so she can answer him in his own turn, where she can keep playing. And, as in the previous lesson, nothing he says closes the session.

His Words Are Never Cut and Never Lost

The loop's most instructive repair came from editing a Solo broadcast. Dad found that his coaching had sometimes got no reaction at all, and he had ended up saying things twice. His ruling: being late is acceptable; being cut, or never answered, is not. The investigation found three faults stacked on top of each other: a line rode one look and was gone from the next; it was cut three times on its way (120 characters a line, 200 for all of them, then a trim to 20); and a line he said while a turn was closing reached no look at all.

The rebuilt path (Coop 1111-3) has four rules:

  • Kept from the read that shows it. Every read of the room hands his lines to the session's own record, known by when each was heard and what it says, so nothing depends on a later look happening to see it.
  • Whole on the look. dad_said is a list of whole lines, oldest first, each with how long ago he said it. No trim may touch it; other parts of the look (her notebook's newest line, the map) give way instead. One look holds up to 1,300 characters of his words. When more wait than fit, the rest ride the next looks, whole, and the look says how many are waiting. A line too long for the room a look has left rides in parts, in order, each part saying where it starts, never as a prefix alone.
  • In Solo, it waits for her answer. A line rides every look until it is answered, and "answered" is defined by evidence the loop has, not by a model judging her words: a turn that carried the line on one of its looks ended with a spoken closing line. A quiet, failed or superseded turn answers nothing.
  • His words never stop her hands. Nothing in this path ends, pauses or shortens a turn. Lateness is bounded by the turn's own length.

Every Gap Is Measured

A loop that "keeps playing" is a claim, so Oriel measures it. Every input's time, every turn and every wait or yield goes into the play session's loop file. When a session closes, its report gives inputs per minute, the longest gap between inputs, and every stall over ten seconds with the spans that covered it, and the page shows it on the play strip. The lines of Dad's words are measured the same way: when each was heard, which look first carried it and how late, which turn answered it and how long it waited. When someone says "she ignored me", there is a file that says whether she did.

Code

Lines kept whole, carried until answered, never trimmed to fit (runnable sketch of the rules)·python
import json
import time

LOOK_MAX = 1300                      # characters of his words one look may carry


class DadLines:
    def __init__(self):
        self.waiting = []            # whole lines, oldest first, each with when it was heard
        self.start = 0               # where the next look's page begins

    def ingest(self, heard):
        for text, at in heard:
            if all(text != w["text"] or at != w["at"] for w in self.waiting):
                self.waiting.append({"text": text, "at": at})

    def carry(self, now):
        if not self.waiting:
            return [], 0
        first = self.start % len(self.waiting)
        page, used = [], 0
        for line in self.waiting[first:] + self.waiting[:first]:
            item = {"said": line["text"], "at": line["at"], "ago_s": round(now - line["at"])}
            size = len(json.dumps(item, ensure_ascii=False))
            if page and used + size > LOOK_MAX:
                break                # the rest ride the next look, whole
            page.append(item)
            used += size
        self.start = first + len(page)   # the next look carries the ones after these
        return page, len(self.waiting) - len(page)

    def answered(self, carried, closed_with_words):
        if closed_with_words:        # evidence, not a judgement of what she said
            done = {(c["said"], c["at"]) for c in carried}   # a line is its words AND when it was heard
            self.waiting = [w for w in self.waiting if (w["text"], w["at"]) not in done]
            self.start = 0


lines = DadLines()
now = time.time()
lines.ingest([("turn left at the gate", now - 4), ("shield up and go slowly", now - 1)])
page, more = lines.carry(now)
print(page, "more waiting:", more)
lines.answered(page, closed_with_words=False)   # a quiet turn answers nothing
print(len(lines.waiting), "still waiting")
lines.answered(page, closed_with_words=True)
print(len(lines.waiting), "still waiting")
A loop that starts the next turn when the last ends, and measures its gaps (runnable simulation)·python
import random

random.seed(2)
t, inputs, gaps, dad_waiting = 0.0, [], [], False
for turn in range(12):
    if dad_waiting:                       # his turn first; the loop starts nothing
        t += random.uniform(5, 12)
        dad_waiting = False
        continue
    turn_len = random.uniform(8, 15)
    for _ in range(random.randint(3, 7)):
        inputs.append(t + random.uniform(0, turn_len))
    t += turn_len
    dad_waiting = random.random() < 0.25
inputs.sort()
gaps = [b - a for a, b in zip(inputs, inputs[1:])]
print(f"inputs per minute: {len(inputs) / (t / 60):.1f}")
print(f"longest gap: {max(gaps):.1f} s; stalls over 10 s: {sum(g > 10 for g in gaps)}")

External links

Exercise

Run the DadLines sketch, then add the case that broke the original: a line that arrives while a turn is closing. Model it so the line is ingested after the turn's last look, and show that it is carried on the next turn's first look instead of being lost. Then write a check that fails if any carried line is shorter than the line that was heard.
Hint
The key is to keep the line in the session's own list from the moment any read sees it, rather than attaching it to whichever look happens next. The second check (carried length equals heard length) is the test that would have caught all three cuts.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.