Group a turn's takes by their parent, not by where they sit
SP7 shipped a tree nobody could use. Driving it by hand found three things, and the schema is what the third one needs. A take forked onto its own branch leaves the coordinate its siblings are still at. `attempts.group` filtered on (branch_id, depth), so that take read as the only one of its turn -- 1/1 where the player is owed 1/3, with the other takes unreachable from the line they were taken on. Nesting has the same shape from the other side: takes under C1 and takes under C2 share a depth, and only the parent says a pager under C2 reads 2/2 rather than counting C1's three too. So `actions.parent_id`, and `group` keys on it. The alternative -- pointing a branch's fork at a node instead of a depth, so a promoted take never moves -- was rejected: `lineage` exists so a read is an OR-clause per branch rather than a walk up parent pointers, and moving the fork point changes path resolution itself, which drags in cursors, memory depths and both bundle formats. This column is read to group takes and for nothing else. One indexed lookup, never a walk, and no read of the story changes. Two writers had to learn it. `add_attempt` places a sibling by hand rather than through `place_action`, and copies the parent, because a take belongs to the turn it is a take *of*. `place_action` resolves it from the path for a genuinely new node, which is the honest default. And one reader had to be kept out of it. `delete_turn` meant "every attempt at this coordinate"; the group spans branches now, so undoing a turn would have deleted a take that another branch is telling. `attempts.on_branch` scopes it back. 409 tests, unchanged from main and green -- a linear story has one take per parent, so none of this is reachable until a second one exists. The migration adds one column, backfills the linear case, and leaves NULL where it cannot honestly place a row; `group` falls back to the coordinate there, which is the rule those rows were written under. One `VACUUM FULL actions;` owed after deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7
This commit is contained in:
committed by
Parth
co-authored by
Claude Opus 5
parent
e692780f08
commit
6fa213f6db
@@ -182,6 +182,7 @@ def place_action(
|
||||
adventure: models.Adventure,
|
||||
action: models.Action,
|
||||
branch: models.Branch | None = None,
|
||||
parent: models.Action | None = None,
|
||||
) -> models.Branch:
|
||||
"""Put `action` on the head branch and move the head to it.
|
||||
|
||||
@@ -191,17 +192,52 @@ def place_action(
|
||||
|
||||
`branch` is the head, already resolved, for a caller placing several nodes
|
||||
at once — see `place_new_nodes` for why that is worth a parameter.
|
||||
|
||||
`parent` is the take this node was played after (SP9), and is what groups a
|
||||
turn's takes. Resolved from the path when the caller does not say, which is
|
||||
the honest default: a node written now follows whatever the player is
|
||||
reading now. A caller placing several nodes in one flush should chain it —
|
||||
the second node's parent is the first, and the database has not seen either.
|
||||
"""
|
||||
branch = branch or head_branch(db, adventure)
|
||||
action.branch_id = branch.id
|
||||
if action.depth is None:
|
||||
action.depth = action.index
|
||||
if parent is not None:
|
||||
action.parent_id = parent.id
|
||||
elif action.parent_id is None and action.depth:
|
||||
action.parent_id = _preceding_id(db, adventure, branch, action.depth)
|
||||
adventure.head_branch_id = branch.id
|
||||
if action.depth is not None and action.depth > adventure.head_depth:
|
||||
adventure.head_depth = action.depth
|
||||
return branch
|
||||
|
||||
|
||||
def _preceding_id(
|
||||
db: Session, adventure: models.Adventure, branch: models.Branch, depth: int
|
||||
) -> int | None:
|
||||
"""The id of the live node one step back along `branch`'s path.
|
||||
|
||||
Asked of the whole lineage rather than of `branch` alone, because a branch
|
||||
borrows the story before its fork point: the node in front of a forked
|
||||
branch's first turn lives on an ancestor, and that is exactly the parent a
|
||||
pager needs to find its siblings through.
|
||||
"""
|
||||
path = lineage.Path(lineage.entries_of(branch))
|
||||
return (
|
||||
db.query(models.Action.id)
|
||||
.filter(
|
||||
models.Action.adventure_id == adventure.id,
|
||||
models.Action.depth == depth - 1,
|
||||
models.Action.live.is_(True),
|
||||
path.clause(),
|
||||
)
|
||||
.order_by(models.Action.id)
|
||||
.limit(1)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
|
||||
def place_memory(
|
||||
db: Session,
|
||||
adventure: models.Adventure,
|
||||
|
||||
Reference in New Issue
Block a user