Pawn knows which task it is walking to #116

Merged
liamjd merged 3 commits from 112-pawn-knows-its-task into main 2026-09-19 09:49:21 +01:00
Owner

Pawn.PawnState.WORKING was declared and unreachable: every arriving pawn was idled, so nothing could tell "arrived because it was wandering" from "arrived at the cell it was sent to in order to work". The rule now is:

a pawn is WORKING on arrival only when it is standing on the target cell of a task it still holds — IDLING in every other case

  • Pawn.current_task_id, defaulting to -1
  • Pawns.tasks: TaskManager, injected by game.gd beside the existing buildings.tasks
  • Pawns.assign_task() / clear_task(), and the three signal handlers that clear
  • TaskManager.task_released and TaskManager.get_task()

Two traps this hit, both worth knowing

The signals are connected from the tasks setter, not _ready(). game.gd injects the manager from its own _ready(), and child _ready() runs first — so a connection made in Pawns._ready() sees a null manager and never fires. Same shape as the re-path signal race in test_pawn_repath.gd's header.

Clearing is looked up by current_task_id, not task.assignee. cancel() and release() both set assignee = -1 before they emit, so a handler reading task.assignee searches for pawn -1 and silently finds nothing. The lookup by current_task_id is also independent of whatever the manager later does to assignee.

stop() deliberately does not clear the task. repath_pawns() is stop-then-move_to, so clearing there would drop the task of every pawn a building re-routed — the #56 scenario this issue exists to handle. Two tests pin it.

Verification

test/unit/test_pawn_task_arrival.gd, 14 tests, written before the implementation and red for the right reason at that point. Full suite headless: 536 passing, 0 failing, 1 risky/pending (pre-existing), no orphans.

Not verified visually, and does not need to be: nothing here draws. The pawn reaching WORKING has no on-screen consequence yet — that arrives with #75.

Left undone

  • remove() on a pawn holding a claimed task still leaves that task assigned to a dead id. The fix is a release() from the removal path, but it wants #111's decision on whether assignee stays single-valued first.
  • assign_task() over a pawn that already holds a task silently drops the old one, which leaves that task CLAIMED with an assignee that is no longer working on it. Orchestration belongs to #75, so it is that ticket's to guard.

Closes #112

🤖 Generated with Claude Code

`Pawn.PawnState.WORKING` was declared and unreachable: every arriving pawn was idled, so nothing could tell "arrived because it was wandering" from "arrived at the cell it was sent to in order to work". The rule now is: > a pawn is WORKING on arrival only when it is standing on the target cell of a task it still holds — IDLING in every other case - `Pawn.current_task_id`, defaulting to -1 - `Pawns.tasks: TaskManager`, injected by `game.gd` beside the existing `buildings.tasks` - `Pawns.assign_task()` / `clear_task()`, and the three signal handlers that clear - `TaskManager.task_released` and `TaskManager.get_task()` ### Two traps this hit, both worth knowing **The signals are connected from the `tasks` setter, not `_ready()`.** `game.gd` injects the manager from its own `_ready()`, and child `_ready()` runs first — so a connection made in `Pawns._ready()` sees a null manager and never fires. Same shape as the re-path signal race in `test_pawn_repath.gd`'s header. **Clearing is looked up by `current_task_id`, not `task.assignee`.** `cancel()` and `release()` both set `assignee = -1` *before* they emit, so a handler reading `task.assignee` searches for pawn -1 and silently finds nothing. The lookup by `current_task_id` is also independent of whatever the manager later does to `assignee`. **`stop()` deliberately does not clear the task.** `repath_pawns()` is stop-then-`move_to`, so clearing there would drop the task of every pawn a building re-routed — the #56 scenario this issue exists to handle. Two tests pin it. ### Verification `test/unit/test_pawn_task_arrival.gd`, 14 tests, written before the implementation and red for the right reason at that point. Full suite headless: **536 passing, 0 failing, 1 risky/pending** (pre-existing), no orphans. Not verified visually, and does not need to be: nothing here draws. The pawn reaching `WORKING` has no on-screen consequence yet — that arrives with #75. ### Left undone - `remove()` on a pawn holding a claimed task still leaves that task assigned to a dead id. The fix is a `release()` from the removal path, but it wants #111's decision on whether `assignee` stays single-valued first. - `assign_task()` over a pawn that already holds a task silently drops the old one, which leaves that task CLAIMED with an assignee that is no longer working on it. Orchestration belongs to #75, so it is that ticket's to guard. Closes #112 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fourteen tests against an API that does not exist yet: current_task_id on
the Pawn record, a TaskManager injected into Pawns, and assign_task() /
clear_task() as the two ends of holding one.

The three cases the issue asks for, plus the ones that decide the shape:
arrival with a task that was cancelled mid-walk, arrival in a registry with
no manager at all (which is how most of test_pawns.gd runs), and the
stop-then-move_to re-path round trip - stop() must not clear the task or
#56 silently drops it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
current_task_id on the Pawn record, a TaskManager injected into Pawns, and
_on_view_arrived() resolving to WORKING only when the pawn is standing on
the target cell of a task it still holds.

The signals are connected from the `tasks` setter rather than _ready():
game.gd injects the manager from its own _ready(), which runs after the
child's, so a connection made in Pawns._ready() would never see a manager.

Clearing is looked up by current_task_id rather than task.assignee, because
cancel() and release() both clear the assignee before they emit - the
handler would otherwise be searching for pawn -1 and silently finding
nothing.

Adds TaskManager.task_released so all three clearing paths are signals, and
TaskManager.get_task(), which answers null for an unknown id without a
push_error: a cancelled task is erased, so a pawn holding a stale id is the
normal case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
liamjd merged commit 25056c2ba4 into main 2026-09-19 09:49:21 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
liamjd/UntitledColonyBuilder!116
No description provided.